简介草莓CDMS内容分销系统v1.0是一套基于ThinkPHP5.1、EasyWeChat4.0、Swoole和Redis开发的原创内容分销系统主要面向需要搭建微信小说、漫画、有声等内容平台的技术人员与运营团队。系统提供微信开放平台扫码授权与公众平台手动对接两种模式内置平台方、版权商、合作商、渠道商、代理商五种角色并配有订单统计、用户统计、下线统计、扣量统计等完整数据看板可支撑多层级分销运营。资源包共5个文件包含系统源码压缩包、框架依赖包、安装必读txt、使用说明html及Swoole扩展loaderso整体约37.86MB便于快速部署和二次开发。目前已有147人学习下载适合具备一定PHP开发经验、希望快速落地内容分销业务的读者参考使用。1. 草莓CDMS到底是什么先解决一个反直觉的问题先说结论这套微信小说平台系统最难的从来不是小说阅读器而是内容方的结算诉求和微信生态的种种隐性边界。草莓CDMS这套所谓的“独创内容分销系统”拆开看其实是一套围绕微信生态的“内容分销管理系统”CDMS它把小说内容、渠道推广、用户付费和分账结算串在同一条链路上核心价值不在“阅读体验”而在“每一笔钱都能算清楚、每一分利润都能分明白”。我带过好几个类似的模拟项目X也接手过某团队做到一半的跨平台系统坦白说80%的翻车都发生在系统边界上微信内置浏览器里登录态莫名丢失、H5拉起支付时报错、渠道推广链接带上参数后用户打开却识别不到来源。这些都是CDMS这类系统真正值得提前设计的地方。这篇笔记面向的读者是三类人手里有小说版权想做分销的内容方有微信流量想做付费阅读变现的渠道团队以及帮别人搭建这类平台的开发者。按照标题里给的“v1.0”这个版本定位我从CDMS的模块拆解讲到微信生态的具体落地再把踩过的坑逐条列出来最后给一套能实际用起来的对账验证方法。整个方案看下来你会明白为什么市面上那么多小说分销平台最后拼的都是“系统能不能撑住账目”和“微信域名拦截检测有没有容灾”。2. CDMS的核心模块拆解内容、分销、账务三位一体2.1 从小说入库到H5阅读页内容分销的数据流一套微信小说平台系统最前面的模块是内容管理。CDMS要处理的不是单本小说而是批量导入的章节内容和对应的付费规则。我在搭建模拟项目X的时候第一件事就是把内容的数据流捋直版权方提供小说全文系统按章节拆分后入库运营后台配置免费章节数和单章定价然后把小说发布到H5阅读页或者微信小程序里。这个流程看起来简单但有两个细节做不好就会埋雷。第一个是章节内容里的特殊字符小说文本经常带段落缩进、换行、敏感词过滤后的占位符入库时要统一清洗否则读者在微信内置浏览器里看到的是整段文字挤在一起。第二个是付费规则要跟着“分销”走不是所有渠道的定价都一样。常见做法是把定价配置做成可覆盖的全局默认定价之外每个渠道可以单独设置折扣比例这样渠道方自己推广时有议价空间系统里也能追溯每一笔订单归属哪个定价策略。CDMS在这里的核心设计是“内容只存一份规则单独映射”。小说章节表只做纯粹的文本存储定价、可见性、分销比例都放在独立的配置表里按渠道ID去匹配。用户访问H5阅读页时服务端根据来源渠道动态渲染价格。这样的好处是内容方更新章节时不需要碰任何渠道配置渠道方也不会因为内容调整而产生分账误差。数据流的最后一步是阅读页埋点页面打开、试读结束、支付成功这些事件都要回传到CDMS的订单中心形成后面分账模块的输入。2.2 订单与分账表设计钱不落地分销就是空谈CDMS和普通小说阅读器的分水岭就在这里。普通阅读器只关心“用户付没付钱”内容分销系统要关心“这笔钱怎么分、分给谁、什么时候结算”。我见过太多项目在订单表里只放一个金额字段分销比例写在代码常量里一到月底结算就变成手工从Excel里数——那不是系统是定时炸弹。我一般会用四张表来承载整个账目链路订单表、订单明细表、分账规则表、结算单表。订单表记录用户每笔支付的主单信息包括订单号、用户ID、渠道ID、支付金额、支付平台单号。订单明细表把一笔订单拆成商品维度比如某本书的第12章单价多少。分账规则表配置内容方、渠道方、平台方各自的分成比例这个比例可以按书配置也可以按渠道配置。结算单表则按结算周期通常是T7或T15生成汇总记录标记是否已打款。ORDER_DETAIL表的关键字段设计我直接给出一个能落地的DDLCREATE TABLE order_detail ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, order_id varchar(32) NOT NULL COMMENT 订单号业务生成, user_id bigint(20) NOT NULL COMMENT 读者用户ID, channel_id bigint(20) NOT NULL COMMENT 分销渠道ID0表示平台自营, book_id int(11) NOT NULL COMMENT 小说ID, chapter_id int(11) NOT NULL COMMENT 章节ID, price decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 实付金额元, original_price decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 原价元, pay_platform tinyint(4) NOT NULL DEFAULT 0 COMMENT 支付平台0微信支付1小程序支付, pay_status tinyint(4) NOT NULL DEFAULT 0 COMMENT 支付状态0待支付 1已支付 2已退款, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, paid_at datetime DEFAULT NULL COMMENT 支付成功时间分账以此为准, PRIMARY KEY (id), KEY idx_channel_id (channel_id, paid_at), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;字段说明要解释几点price字段用decimal而不是float这是分账系统最基础的要求浮点数在累加时会出现0.10.2不等于0.3的精度问题而结算恰好又依赖大量的金额累加用decimal(10,2)可以保证两位小数的精确计算。channel_id单独建索引是因为所有渠道对账、渠道收益查询都会按这个字段过滤不加索引的话百万订单量级下查询会明显变慢。paid_at是支付成功时间CDMS的分账周期按它计算不能用created_at因为created_at包含待支付状态的脏数据。2.3 微信生态的技术选型H5优先还是小程序优先做微信小说分销第一个要拍板的问题是主战场放在哪。微信内置浏览器打开的H5页面还是微信小程序。从CDMS的分销角度讲H5是绝大多数团队的起步选择原因很实际H5链接可以在微信群、公众号菜单、朋友圈文本里直接传播分享路径短用户点击即读。微信小程序虽然体验更好、支付流程更顺但小程序的分销分享链路受平台规则限制而且每个渠道方都要走“生成小程序码-推广”的流程对中小渠道方的门槛明显高一些。我自己的习惯是“H5起步小程序作为复购增强”。H5页面负责拉新和首单转化用户阅读到一定量级之后再引导其打开微信小程序用小程序承载书架、续读、充值这类高频操作。CDMS的技术实现要在这两端都预留入口H5用微信内置浏览器的JSAPI拉起支付小程序端用微信的登录和支付能力。两边共用同一套订单和分账表通过openid做用户唯一标识关联。需要特别注意的是微信开发者工具里调试H5页面和真实手机上的表现经常不一致。我在某图像处理Demo项目里就踩过一次开发工具里分享卡片正常拉起真机上却提示“支付已关闭”。这往往不是代码问题而是微信对未认证域名的拦截策略和IP白名单校验在真机上才生效。所以选型定了之后一定要第一时间去微信支付商户平台把JSAPI支付授权目录配好同时确认分享链接的域名已经完成ICP备案并在后台登记。3. 在微信生态把小说平台跑通关键代码路径3.1 请求入口拦截与UA识别为什么要校验微信内置浏览器微信小说H5平台的第一道门面是请求入口识别。系统收到一个访问请求首先要判断这个请求是不是真的来自微信内置浏览器同时还要识别用户使用的微信版本和操作系统。这个判断直接影响两件事一是决定要不要进入JSAPI支付流程二是拦截掉大量的恶意抓取和刷量请求。我在做模拟项目X期间某团队还专门提到过一个词“伪造微信浏览器头信息”。用PHP伪造微信UA的事情在技术上确实可行因为服务端判断用户是否来自微信最常见的方案就是检查User-Agent字符串里有没有“MicroMessenger”。但这套判断有一个明显的盲区UA可以被任何人伪造。所以CDMS在v1.0里不能只做UA字符串包含判断还要加一层二级校验。一个相对可靠的PHP校验逻辑长这样?php /** * 微信内置浏览器识别与校验 * 返回数组: [is_wechat bool, is_mm bool, openid string|] */ function checkWechatBrowser($appid, $secret, $code ) { $ua $_SERVER[HTTP_USER_AGENT] ?? ; // 一级校验UA包含MicroMessenger关键字 $isWechat (strpos($ua, MicroMessenger) ! false); // 二级校验通过微信OAuth换取openid确认用户身份真实 $openid ; if ($isWechat !empty($code)) { $url https://api.weixin.qq.com/sns/oauth2/access_token?appid{$appid}secret{$secret}code{$code}grant_typeauthorization_code; $resp file_get_contents($url); $data json_decode($resp, true); if (isset($data[openid])) { $openid $data[openid]; } } return [ is_wechat $isWechat, is_mm $isWechat, openid $openid ]; }这段代码的逻辑是分级校验。第一级用UA做快速过滤因为绝大多数正常访问都会带MicroMessenger标记这样能省掉很多无效的OAuth请求。第二级才是关键通过用户从微信环境访问时自动携带的code参数去微信开放接口换openid。这个code是微信OAuth流程签发的临时凭证只有微信浏览器里重定向才会带上伪造UA的人拿不到有效的code自然过不了第二级。参数上要注意$appid和$secret来自微信公众号平台的开发配置code是一次性的有效期只有5分钟不能缓存复用。如果接口返回errcode常见原因是appid和secret不匹配或者公众号没有配置网页授权域名。3.2 用JSAPI拉起微信支付H5小说付费的最小实现H5阅读页最关键的用户动作是“买这一章”CDMS在支付这一步采用的是微信JSAPI支付方案这是微信内置浏览器里唯一的支付方式。它跟扫码支付的区别在于JSAPI支付需要一个预先获取的openid然后在微信内置环境中调起支付收银台。在小说场景里用户点“支付”按钮后流程是前端把章节ID发给后端后端查询图书和章节价格生成商户订单号再调用微信统一下单接口拿到prepay_id最后前端用wx.chooseWXPay拉起收银台。后端拉起支付的PHP代码核心片段开篇第一句话先写结论让用户知道这个函数的作用。public function createJsapiOrder($openid, $bookId, $chapterId, $totalFee) { $orderNo $this-generateOrderNo(); // 业务订单号用日期随机数生成 $params [ appid $this-appid, // 公众号appid mch_id $this-mchId, // 微信支付商户号 description 小说章节购买-{$bookId}-{$chapterId}, out_trade_no $orderNo, notify_url $this-notifyUrl, // 支付回调地址 amount [total intval($totalFee * 100)], // 金额单位转为分 payer [openid $openid], // 用户身份 trade_type JSAPI ]; // 请求微信支付v3统一下单接口 $client new \GuzzleHttp\Client(); $resp $client-post(https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi, [ json $params, headers [ Authorization $this-buildAuthHeaders($params), Content-Type application/json ] ]); return json_encode([orderNo $orderNo, prepayJson $this-buildJsapiParams($resp)]); }这里有个非常重要的参数amount.total必须转成整数分微信支付接口不接受元为单位的小数。很多团队在联调阶段一直报“订单金额无效”就是在这个地方直接用10.5传给了接口。另外trade_type固定为JSAPI这是微信内置浏览器支付的类型标识。buildAuthHeaders是签名逻辑v3版本要用商户私钥对请求做RSA-SHA256签名这块代码比较长但核心就是拉取微信平台证书公钥后对请求体做签名。前端收到prepayJson后用微信提供的js-sdk里的wx.chooseWXPay就能拉起支付收银台。需要注意的一个实际细节是用户在微信里打开了H5付费页但微信版本过低时会提示“不支持JSAPI支付”。CDMS系统里应该对这种版本做一次探测版本低于某个阈值时给用户一个提示页告诉他们升级微信客户端而不是让支付环节直接崩掉。3.3 微信域名拦截检测站点被屏蔽之前的容灾准备微信对域名有一套自己的检测和拦截机制小说分销平台尤其容易触碰到这个敏感点——支付域名、分享域名、阅读域名如果共用同一个主域一旦被微信风控系统判定异常整个站的访问都会被拦。CDMS系统v1.0里不能没有域名容灾方案常见做法是域名分置加上一个独立的“微信域名拦截检测”定时任务。我实现过的方案是三条检测路径第一从代理服务器向微信公众平台的接口检测URL发送一个包含自己域名的请求观察返回的errCode第二模拟H5请求访问自己域名下的一个探针文件看响应头里是否出现了微信的拦截标识第三周期性让测试号去真实微信环境打开自己的链接记录是否出现“已停止访问该网页”的提示。三条路径命中任何一个就要自动切换域名。切换域名这件事不能等到用户投诉了再做那已经晚了。我通常会用一个配置表存多个可用域名前端页面本身固定不动后端在返回HTML前根据当前域名状态决定要不要做302跳转到备用域。支付回调地址、分享链接里的URL参数全部跟着主域名走切域名时同时改写。这样做的好处是即使主域名被拦用户在微信里收到新的分享链接时打开的还是同一个系统只是域名换了。需要坦白说域名拦截检测本身只是一个软性判断微信的口径和风控逻辑不透明这套检测只能做到“提前感知异常”做不到“保证不被拦”。所以CDMS在业务设计上还要做一层纵深分享链接不要直接挂可交互的付费页而是挂一个落地中转页中转页只有一张封面图和一段引导文案真正的阅读页和支付页放在下一层。这样即使中转页域名被拦损失的可替换成本会低很多。4. 分销渠道追踪与绑定从URL参数到长期用户关系4.1 渠道码的生成与透传追踪每个用户的来源CDMS的分销能力落地在最基础的一环每个渠道方发出的推广链接必须带着自己的唯一标识。用户点进来的那一瞬间系统要把这个标识存下来并且在整个用户生命周期里都能追溯到。市面上常见的参数叫channel_id我更喜欢用share_tag因为它能表达两层含义——这个用户是通过哪个渠道来的以及这个渠道里的哪个推广员。在H5小说平台里URL参数是这么设计的渠道方拿到的链接是https://read.example.com?stCH8888-UIDCH8888是渠道编号UID是渠道方内部某个推广员的编号。用户打开链接后前端把st参数原样传给后端后端验证这个渠道编号有效后给用户种一个Cookie或者写入用户表。这里有个容易犯的错只把渠道参数放在跳转URL里用户一旦离开再回来参数就丢了。所以正确的做法是用户打开落地页后就立即用接口把来源写入用户记录后续的所有页面访问都从用户表里取渠道再也不依赖URL上的参数。用户表加字段的思路ALTER TABLE user ADD COLUMN channel_id bigint(20) NOT NULL DEFAULT 0 COMMENT 首次来源渠道ID, ADD COLUMN promoter_id bigint(20) NOT NULL DEFAULT 0 COMMENT 推广员ID0表示无, ADD COLUMN bind_time datetime DEFAULT NULL COMMENT 绑定时间, ADD COLUMN user_source tinyint(4) NOT NULL DEFAULT 0 COMMENT 0H5 1小程序 2公众号菜单;这样设计的用意是channel_id是首访来源一旦绑定就不变它决定了用户后续每一笔订单的分账归属。bind_time用来做结算时的首次购买时间窗口判断比如有些渠道方的结算周期是从用户绑定时间开始算的这个字段能直接对账。user_source记录用户从哪个入口进来方便观察H5和小程序两端各自的转化率。4.2 从一次性用户到长期关系关注公众号与模板消息唤醒小说分销和普通商品分销最大的不同是复购属性。一套小说几十上百章读者看完了前几章大概率还会继续买但如果每次都要靠用户自己想起回来读流失率会非常高。CDMS在这个环节的标准做法是支付完成后引导用户关注公众号然后利用公众号的模板消息或客服消息做触达。技术上要解决的是微信网页授权和公众号关注的绑定。用户在H5阅读页里点“关注公众号获取继续阅读权限”按钮时系统通过微信公众号的网页授权拿到用户的openid然后利用“公众号关注事件”把这个openid和内部用户ID关联起来。顺带提醒一句微信的网页授权和公众号的模板消息用的是同一套openid体系微信小程序登录和获取手机号是另一套体系如果你同时做H5和小程序用户表的openid字段要区分odh5_和odmini_前缀不要混着存。后续的唤醒逻辑一般是这样的用户购买第10章后系统通过定时任务检查他是否还有未购买的第11-12章如果有三天后给他发一条模板消息内容是“某小说已更新至第30章你追到了第10章”附上阅读链接。这个链接必须带上他个人的标识这样即使他从公众号里点进来CDMS也能识别他的身份和渠道归属不会让他变成“无来源用户”。4.3 分润计算与结算冻结先算后结还是先结后算分润是CDMS里最敏感的模块。写小说分销系统之前我一直觉得分账不过就是“订单金额乘以比例”真正做起来才发现有好几个边界情况用户支付后申请退款怎么办渠道方自己的推广员要求日结怎么办内容方在结算周期内调了分成比例已经产生的订单要不要跟着改我采用的方案是分润计算时点做“冻结快照”。每一笔订单支付成功并过了退款保护期比如7天之后系统才把这笔订单的分润明细算出来并冻结这个明细按照下单时的分账规则执行后续规则改动不影响历史订单。结算周期上“先算后结”是底线——先把每个渠道商的订单汇总和分润金额算清楚再进入打款流程整个过程都要留痕。分润计算的核心逻辑是按“订单所属渠道”匹配分账规则表再按内容方和渠道方各自的比例拆分。里面最关键的一步是每个渠道方看到的收益统计口径要跟实际结算口径一致。很多团队给的推广后台里写的是“预估收益”结算时按“实际收益”两者差了退款扣减和冻结期渠道方一问就容易产生信任危机。所以我做的推广后台里只有“已结算收益”和“待结算金额”两个数字待结算金额固化为“支付成功超过7天且未退款”的订单这样渠道方随时看后台都和自己手里的结算单一对就对上。5. 微信小说平台避坑指南现象、原因、解决5.1 微信内置浏览器里登录态一天丢三次现象用户白天在H5页面买了小说晚上打开同一链接又要重新登录阅读进度和历史购买记录全部“消失”。原因微信内置浏览器的Cookie管理策略比普通浏览器严格得多CDMS如果沿用传统Web的Session机制很容易因为Cookie被清或过期而导致用户身份丢失。解决登录态不能只靠Cookie我后来的做法是把用户ID和openid做双重确认用户打开页面时先通过微信OAuth静默授权拿openid用openid去查用户表直接恢复登录态Cookie只做缓存不做唯一凭证。这样即使Cookie被清用户点进页面后依然会被自动识别。5.2 伪造微信浏览器头信息导致的支付风控现象某个渠道的推广链接突然大面积支付失败微信支付回调里提示“当前商户涉嫌交易异常”。原因排查后发现那个渠道方的推广页在服务器端强制改写了User-Agent把非微信浏览器的请求伪装成微信内置浏览器用来绕过系统的入口限制。这种行为触发了微信支付侧的风控模型连带整个商户号都受了影响。解决CDMS干脆在服务端统一把非MicroMessenger标识的请求直接拦截到“请在微信中打开”的提示页不做任何UA改写也不给前端伪造的空间。这个坑给我们的教训是伪造UA这种“小聪明”在微信支付链路里不止是技术问题它会直接毁掉整个商户的信誉。5.3 支付回调延迟导致的重复发货现象用户支付成功后章节内容没有立即解锁用户又点了一次支付结果扣了两次钱系统也发了两次章节权限。原因微信支付回调存在网络延迟用户在前端看到的是“支付成功但未解锁”但其实回调通知还在路上。系统在收到回调前已经把预支付订单标记成了待确认用户重复点击时没有做去重。解决下单前先做幂等校验——用用户ID和章节ID查一下是否已有支付成功的订单如果有且未解锁直接补发章节权限不再生成新订单。同时在前端支付成功回调里做轮询等待每2秒查一次后端订单状态最多查10次。5.4 分享链接域名被拦整个渠道流量归零现象某渠道方的推广链接在微信里被提示“已停止访问该网页”该渠道的所有用户都打不开系统。原因该渠道方为了让用户“分享得书币”在文案里用了诱导分享的话术被用户举报后域名被微信侧处置。解决一是将所有带营销文案的页面都迁到独立二级域名阅读和支付保持在主域这样被拦的只是营销页二是在CDMS里增加域名状态自检一旦发现主域有被拦迹象自动把分享链路由中转页跳转到备用域名。这件事让我养成了习惯任何小说分销项目上线前必须准备至少两个已完成备案且不在同一IP段的域名。5.5 分账金额有分差月底对账对出“幽灵账”现象月底结算时系统里的分润汇总比实际打款金额多了十几块钱查遍订单也对不上。原因数据库中历史订单表里有个字段被误用过早期测试环境生成的少量测试单没清干净这些测试单一样走了分润计算。解决CDMS在分润计算时增加“排除标记”逻辑任何非正式渠道或测试渠道的订单都不进分润池同时每个月生成结算单时自动跑一次对账把订单表里的有效订单金额、分润明细表的累加金额、结算单表的打款金额三方比对任何一个对不上就在结算后台单独标记出来不进入打款流。6. 进阶技巧用一套对账脚本让系统半夜不“闹鬼”CDMS这类带资金流的系统跑得稳不稳不看功能多不多看的是“无人值守时能不能自己发现问题”。我在v1.0上线后做的最值钱的一件事是写了一套对账用的PHP脚本每天凌晨跑一次把系统里所有账目过一遍任何异常直接在后台列表里标红。这个脚本的价值在于它让分销系统从“用户不投诉就没事”变成“系统自己发现异常”。脚本的核心逻辑是三重验证。第一重统计当天所有支付成功且超过退款保护期的订单金额总和与分润明细表里的待结算金额总和做差差值必须为0否则说明有订单没进分润池。第二重按渠道ID分组汇总订单金额再和渠道方后台展示的“待结算金额”对比一旦发现某渠道两边数字不一致说明该渠道的订单归属有问题多半是用户来源绑定丢了。第三重用微信支付商户平台的对账单拉取当日实际入账金额与本系统order表的支付成功金额做比对这一步能抓出支付回调漏掉但实际扣了款的单子。写脚本时有个细节要提醒微信支付商户平台的对账单是一天一个文件的TXT里面包含当天的交易记录和退款记录解析时要注意编码是GBK不是UTF-8。public function reconcileDaily($date) { $list $this-fetchWechatBill($date); // 拉取平台对账单 $systemTotal $this-getSystemTotal($date); // 查本系统订单汇总 $diff 0; foreach ($list as $bill) { if ($bill[trade_type] JSAPI $bill[status] SUCCESS) { $diff $bill[amount]; // 平台侧入账累加 } } $systemTotal[pay_amount] $systemTotal[pay_amount] ?? 0; if ($diff - $systemTotal[pay_amount] 0) { // 差额大于0说明系统漏记了订单立即告警 $this-alertPendingPayment($date, $diff - $systemTotal[pay_amount]); return [status diff, diff $diff - $systemTotal[pay_amount]]; } return [status ok, diff 0]; }这段逻辑的用意是拿微信侧的实际入账作为基准本系统记录再全也可能漏掉回调丢失的情况两边一比就能看出漏洞。核心是diff变量累加每天的入账金额再用平台侧总和减去系统侧总和能精确锁定差额。跑定时任务时的建议是每天凌晨4点执行因为微信对账单在当天凌晨三点前才生成完整数据跑早了只会得到前一天的不完整账单。执行完成后把结果推送到企业微信机器人或者邮件组这样团队早上打开消息就能看到昨天的账目状态。这套对账脚本做完之后我的系统从“没人说有问题”变成了“有问题早上就知道”有一次还真抓到一笔因支付回调网络故障而漏掉的订单金额不大但胜在能让渠道方信任这个CDMS平台。做这种带资金分配的系统能让你睡得着觉的不是代码写得漂亮而是异常能被自动发现账目对不上时能精确到单。最后想说的是一句这几年的习惯任何小说分销平台上线前先把对账脚本写好再谈流量和分成账目立住了后面的运营才有资格谈增长。希望帮到你。本文还有配套的精品资源点击获取