Files
MBTI_wang/api/docs/distribution-design.md
2026-03-17 12:00:31 +08:00

17 KiB
Raw Blame History

分销系统设计文档

版本v1.4
日期2026-03-06
作者AI 设计稿(待确认)


一、功能概述

本系统分销分为两个维度:

维度 配置来源 佣金来源 管理入口
个人版分销 超管后台配置 平台微信支付订单收款 超管端
企业版分销 企业管理员后台配置 企业账户余额 管理端admin

二、核心业务规则

2.1 绑定规则

规则 说明
绑定方式 用户 A 分享专属链接/海报,用户 B 通过链接进入小程序,系统自动建立 A → B 的绑定关系
有效期 30 天,从绑定时刻起计算,到期自动解除
续期机制 B 在绑定有效期内再次点击 A 的链接(同一推荐人),有效期从点击时刻重置为 30 天;举例:昨天 A→B 绑定剩余 29 天,今天 B 再次点击 A 的链接,有效期重新变为 30 天
抢绑机制 支持覆盖绑定A 已绑定了 BC 再分享给 B 后B 的推荐人变更为 C新绑定覆盖旧绑定重新计算 30 天)
禁止互相绑定 仅当 A→B 在有效期内时B 再分享给 AA 扫码不能绑定 B静默忽略若 A→B 已过期,则 A 扫码可以绑定 B
绑定时机 用户点击推广链接进入小程序 onLoad 时,识别 uid推荐人ID写入/更新绑定记录
例外情况 用户已付款订单对应的绑定关系不会因抢绑而撤销(付款时的绑定快照保留)

2.2 佣金规则(付款触发)

规则 说明
触发时机 被邀请用户完成付款时触发佣金计算
佣金比例 优先使用与订单 scope 匹配的配置;回退匹配时使用个人版(超管)配置
计算基数 订单实付金额(分)
佣金归属 按付款时刻的绑定关系确认推荐人,快照写入佣金记录,后续抢绑不影响已有佣金
跨 scope 回退 若订单 scope 下无匹配绑定(如企业版订单但推荐人仅有个人版绑定),则回退查找该用户任意有效的 personal 绑定;命中后以个人版佣金配置结算,但只要命中企业上下文,佣金资金仍优先从该企业余额扣除

2.3 测试完成佣金(免付款触发)

规则 说明
触发时机 用户完成指定类型测试并提交结果时触发,无需付款
适用类型 人脸分析face、MBTI、DISC、PDP各自独立开关
佣金金额 每种类型单独配置固定金额(元),非比例
佣金来源 无企业上下文时由平台发放;若命中企业上下文,则优先扣减企业余额,余额不足时冻结,待企业补余额后解冻
防重机制 同一 testResultId 只结算一次,重复提交不产生多条佣金
配置入口 超管后台 → 分销设置 → 测试完成佣金
全局开关 超管可一键关闭整个「测试完成佣金」功能
前提条件 用户须有有效的 personal scope 绑定关系invitee → inviter企业版绑定不触发

2.4 企业版特有规则

规则 说明
佣金资金来源 只要佣金记录命中企业上下文,都扣除企业账户余额(enterprises.balance
余额充足 触发佣金时立即从企业余额扣款,佣金状态置为 paid,推荐人可提现
余额不足 佣金状态置为 frozen(冻结),企业补余额后系统扫描待解冻记录,余额充足则自动解冻并扣款
解冻顺序 按佣金记录的 createdAt 升序(先冻先解)

三、数据库设计

3.1 新增表:distribution_bindings(绑定记录)

CREATE TABLE `mbti_distribution_bindings` (
  `id`              int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '绑定ID',
  `inviterId`       int(11) NOT NULL COMMENT '当前推荐人ID (wechat_users.id)',
  `inviteeId`       int(11) NOT NULL COMMENT '被推荐人ID (wechat_users.id)',
  `scope`           varchar(20) NOT NULL DEFAULT 'personal' COMMENT '分销维度: personal|enterprise',
  `enterpriseId`    int(11) NULL DEFAULT NULL COMMENT '企业ID企业版时非空',
  `expireAt`        int(11) NOT NULL COMMENT '绑定过期时间戳(绑定时间+30天',
  `status`          varchar(20) NOT NULL DEFAULT 'active' COMMENT '状态: active/expired/overridden被抢绑覆盖',
  `prevInviterId`   int(11) NULL DEFAULT NULL COMMENT '被抢绑前的推荐人ID抢绑时记录上一任推荐人',
  `overriddenAt`    int(11) NULL DEFAULT NULL COMMENT '被覆盖时间(抢绑时记录)',
  `createdAt`       int(11) NULL DEFAULT NULL COMMENT '绑定创建时间',
  `updatedAt`       int(11) NULL DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_invitee_scope_ent` (`inviteeId`, `scope`, `enterpriseId`),
  INDEX `idx_inviterId`     (`inviterId`),
  INDEX `idx_inviteeId`     (`inviteeId`),
  INDEX `idx_prevInviterId` (`prevInviterId`),
  INDEX `idx_expireAt`      (`expireAt`),
  INDEX `idx_status`        (`status`)
) ENGINE=InnoDB COMMENT='分销绑定记录表';

说明

  • (inviteeId, scope, enterpriseId) 联合唯一索引保证同一被推荐人在同一维度下只有一条 active 记录。
  • 抢绑时:将当前 inviterId 写入 prevInviterId,再将 inviterId 更新为新推荐人,同时记录 overriddenAt,保留完整的历史推荐链路,便于管理端溯源。
  • 续期时(同一推荐人再次点击):仅更新 expireAtupdatedAtprevInviterId 不变。

3.2 改造表:distribution_commission_records(佣金记录)

在现有 mbti_commission_records 基础上新增字段:

ALTER TABLE `mbti_commission_records`
  ADD COLUMN `scope`        varchar(20)  NOT NULL DEFAULT 'personal' COMMENT '分销维度: personal|enterprise',
  ADD COLUMN `enterpriseId` int(11)      NULL DEFAULT NULL COMMENT '企业ID企业版佣金',
  ADD COLUMN `inviterId`    int(11)      NOT NULL COMMENT '推荐人 wechat_users.id',
  ADD COLUMN `inviteeId`    int(11)      NOT NULL COMMENT '付款用户 wechat_users.id',
  ADD COLUMN `bindingId`    int(11)      NOT NULL COMMENT '触发此佣金的绑定记录ID',
  ADD COLUMN `orderAmount`  int(11)      NOT NULL DEFAULT 0 COMMENT '订单金额(分)',
  ADD COLUMN `commissionFen` int(11)     NOT NULL DEFAULT 0 COMMENT '佣金金额(分)',
  ADD COLUMN `frozenAt`     int(11)      NULL COMMENT '冻结时间(企业余额不足时)',
  ADD COLUMN `unfrozenAt`   int(11)      NULL COMMENT '解冻时间',
  MODIFY COLUMN `status` varchar(20) DEFAULT 'pending' COMMENT 'pending/paid/frozen/cancelled',
  ADD INDEX `idx_scope`        (`scope`),
  ADD INDEX `idx_enterpriseId` (`enterpriseId`),
  ADD INDEX `idx_inviterId`    (`inviterId`),
  ADD INDEX `idx_status`       (`status`);

佣金状态流转

pending → paid          (个人版:订单付款成功后自动结算)
pending → paid          (企业版:企业余额充足,扣款成功)
pending → frozen        (企业版:企业余额不足)
frozen  → paid          (企业充值后,系统解冻扫描)
pending/frozen → cancelled (订单退款时撤销)

3.3 新增:推荐人钱包余额字段(wechat_users

ALTER TABLE `mbti_wechat_users`
  ADD COLUMN `walletBalance`    int(11) NOT NULL DEFAULT 0 COMMENT '钱包余额(分)',
  ADD COLUMN `walletTotalEarned` int(11) NOT NULL DEFAULT 0 COMMENT '历史累计佣金(分)',
  ADD COLUMN `walletPending`    int(11) NOT NULL DEFAULT 0 COMMENT '待入账佣金(分)';

3.4 新增表:distribution_withdrawals(提现记录)

CREATE TABLE `mbti_distribution_withdrawals` (
  `id`          int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `userId`      int(11) NOT NULL COMMENT '推荐人 wechat_users.id',
  `amountFen`   int(11) NOT NULL COMMENT '申请提现金额(分)',
  `realNameInfo` varchar(255) NULL COMMENT '收款实名信息JSON: name+idcard/wechat',
  `status`      varchar(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/approved/rejected/transferred',
  `auditNote`   varchar(500) NULL COMMENT '审核备注',
  `auditAt`     int(11) NULL COMMENT '审核时间',
  `transferAt`  int(11) NULL COMMENT '打款时间',
  `createdAt`   int(11) NULL,
  `updatedAt`   int(11) NULL,
  PRIMARY KEY (`id`),
  INDEX `idx_userId`  (`userId`),
  INDEX `idx_status`  (`status`)
) ENGINE=InnoDB COMMENT='分销提现记录表';

四、核心流程

4.1 绑定流程

用户点击分享链接进入小程序
    ↓
onLoad(options) 识别 uid=推荐人ID, eid=企业ID可选
    ↓
调用 POST /api/distribution/bind  { inviterId: uid, scope, enterpriseId }
    ↓
【前置校验】
    ├── inviterId == 当前登录用户自身 → 忽略,静默返回(不能自绑)
    ├── 企业版scope=enterprise
    │       校验推荐人 wechat_users.enterpriseId === enterpriseId
    │       不符合(推荐人非该企业成员)→ 忽略,静默返回
    ├── 禁止互相绑定(仅有效期内):存在未过期的 A→BinviterId=当前用户, inviteeId=本次推荐人, 同 scope, status=active, expireAt>now
    │       → 忽略,静默返回;若 A→B 已过期则不拦截,允许建立 B→A
    └── 校验通过 → 继续
    ↓
系统查询 distribution_bindings 中该 inviteeId 是否有 active 且未过期的记录
    │
    ├── 无记录(首次绑定):
    │       新建绑定status=activeexpireAt=now+30天prevInviterId=NULL
    │
    ├── 有记录 且 inviterId 相同(同一推荐人再次点击 → 续期):
    │       更新 expireAt = now+30天重置有效期
    │       updatedAt = now
    │       ⚠️ prevInviterId 不变,不新建记录
    │
    └── 有记录 且 inviterId 不同(抢绑):
            记录 prevInviterId = 旧 inviterId保留上一任推荐人
            更新 inviterId = 新推荐人expireAt=now+30天overriddenAt=now
            ⚠️ 历史绑定快照保留在同一行,不新建记录
    ↓
返回绑定成功(含新的 expireAt 供前端展示倒计时)

4.2 佣金结算流程(付款成功后触发)

订单支付成功回调 (Payment/notify)
    ↓
确定订单 scope订单有 enterpriseId → enterprise否则 → personal
    ↓
【第一步:精确匹配】
查询付款用户 (inviteeId) 与订单 scope + enterpriseId 完全匹配的 active 未过期绑定
    ├── 命中 → 使用该绑定进入佣金计算scope 保持订单 scope
    └── 未命中 → 进入【第二步:回退匹配】
    ↓
【第二步:回退匹配(跨 scope】
查询付款用户任意 active 未过期的 scope='personal' 绑定
    ├── 命中 → 使用该绑定,强制 scope='personal'(以个人版配置+平台资金结算)
    └── 未命中 → 跳过,无佣金产生
    ↓
读取对应 scope 佣金比例配置,计算 commissionFen = orderAmount * rate
    ↓
写入 commission_records记录实际使用的 scope 与 enterpriseId
    ↓
if scope == 'personal':
    直接结算 → 推荐人 walletBalance += commissionFen
    commission status → paid
if scope == 'enterprise':
    查询 enterprises.balance
    ├── 余额 >= commissionFen:
    │       enterprises.balance -= commissionFen
    │       推荐人 walletBalance += commissionFen
    │       commission status → paid
    └── 余额不足:
            commission status → frozenfrozenAt=now

4.3 企业充值后解冻流程

企业补余额成功(充值接口回调 / 超管上调余额)
    ↓
enterprises.balance += 充值金额
    ↓
查询该企业所有 status=frozen 的 commission_records按 createdAt ASC
    ↓
循环:
    if 当前 balance >= commissionFen:
        balance -= commissionFen
        commission status → paidunfrozenAt=now
        推荐人 walletBalance += commissionFen
    else:
        break余额耗尽剩余记录保持冻结
    ↓
更新 enterprises.balance

五、API 接口规划

5.1 小程序端(需登录)

方法 路径 说明
POST /api/distribution/bind 绑定推荐人(进入小程序时调用)
GET /api/distribution/stats 获取我的推广中心统计(余额、绑定数、佣金等)
GET /api/distribution/bindings 我邀请的用户列表(分页,支持 tab绑定中/已付款/已过期)
GET /api/distribution/commissions 我的佣金记录(分页)
POST /api/distribution/withdraw 申请提现
GET /api/distribution/withdrawals 我的提现记录

5.2 管理端admin/enterprise_admin

方法 路径 说明
GET /api/v1/admin/distribution/overview 分销数据概览
GET /api/v1/admin/distribution/bindings 所有绑定记录(管理员查看)
GET /api/v1/admin/distribution/commissions 所有佣金记录
GET /api/v1/admin/distribution/withdrawals 提现申请列表
POST /api/v1/admin/distribution/withdrawals/:id/approve 审核通过提现
POST /api/v1/admin/distribution/withdrawals/:id/reject 拒绝提现
GET /api/v1/admin/distribution/settings 获取分销配置
PUT /api/v1/admin/distribution/settings 更新分销配置

5.3 超管端superadmin

方法 路径 说明
GET /api/v1/superadmin/distribution/settings 获取个人版分销全局配置
PUT /api/v1/superadmin/distribution/settings 更新个人版分销全局配置
GET /api/v1/superadmin/distribution/overview 全平台分销数据

六、分销配置结构

个人版(超管配置,存 system_config 表)

{
  "enabled": true,
  "commissionRate": 90,
  "bindingDays": 30,
  "minWithdrawFen": 1000,
  "withdrawFee": 0
}

企业版(企业管理员配置,存 enterprise_distribution_config 表 或 system_config 带 enterpriseId

{
  "enabled": true,
  "commissionRate": 80,
  "bindingDays": 30,
  "minWithdrawFen": 1000
}

七、已确认问题

# 问题 确认结论
1 被抢绑的旧推荐人已产生的佣金是否撤销? 已确认:不撤销,已产生佣金归旧推荐人
2 企业版佣金冻结期间,推荐人的 walletPending 是否展示冻结金额? 已确认:展示 pending+frozen 合计
3 个人版提现是否需要人工审核,还是自动打款? 已确认:人工审核
4 企业版的被邀请用户invitee是否需要是该企业成员 已确认:不限制,任何用户均可被企业推广链接绑定
5 推广分享链接的落地页是否区分个人版和企业版? 已确认:企业版链接携带 eid,个人版不带,系统根据是否携带 eid 自动判断 scope
6 企业版分销的推荐人是否必须是该企业内部成员? 已确认:必须限制。绑定接口需校验推荐人的 wechat_users.enterpriseId === 请求中的 enterpriseId,非该企业成员的分享链接不产生绑定关系,静默跳过
7 A 分享给 BB 购买个人版或企业版A 都能得佣金吗? 已确认v1.3:是。结算时先精确匹配订单 scope未命中则回退查找 personal 绑定。无论 B 购买哪个版本,只要存在 A→B 的有效绑定(任意 scopeA 均可获得佣金。跨 scope 回退时使用个人版配置,佣金来源为平台
8 测试完成佣金适用于企业版测试吗? 已确认v1.4:不适用。测试完成佣金仅适用于 personal scope 绑定,企业版测试(testScope=enterprise)不触发此规则
9 commission_records 需要哪些新字段支持测试完成佣金? 已确认v1.4:需新增 testResultId INT NULL(关联 test_results.idcommissionSource VARCHAR(20) DEFAULT 'payment'payment | test_completion字段并在 testResultId + commissionSource 上加唯一防重索引

八、数据库迁移v1.4

测试完成佣金需要对 commission_records 表新增两个字段,上线前需执行

ALTER TABLE `mbti_commission_records`
  ADD COLUMN `testResultId`     int(11)      NULL DEFAULT NULL COMMENT '测试结果ID测试完成佣金使用关联 test_results.id',
  ADD COLUMN `commissionSource` varchar(20)  NOT NULL DEFAULT 'payment' COMMENT '佣金来源: payment|test_completion',
  ADD UNIQUE KEY `uk_test_commission` (`testResultId`, `commissionSource`);

九、现有代码与新功能的关系

现有内容 状态 处理方式
mbti_distribution_agents 旧设计,基于 users 废弃,改为直接用 wechat_users
mbti_commission_records 部分字段可复用 按第三节 ALTER 改造
admin/Distribution.php 骨架存在,逻辑空 在新设计基础上重写
miniprogram/pages/promo/index 推广中心UI已完成 对接 /api/distribution/stats/api/distribution/bindings
utils/share.js 中携带 uid 参数 已实现 直接复用,uid 即推荐人ID
小程序 onLoad 解析 uid 参数 尚未对接绑定 API 需在各落地页调用 POST /api/distribution/bind