# 需求15/16/18/19/20/21 技术方案 --- ## 需求15:套餐下架后允许续费 ### 业务规则 - 下架套餐(`shelf_status=2`)不可被**新购** - 下架套餐**可以续费**(已在使用该套餐的客户) - 续费仅支持**客户自己购买**(不允许代理代购下架套餐给新客户) ### 后端 **当前逻辑**:下架套餐在购买时被拦截。 修改:在订单创建校验中,区分"新购"和"续费"场景: ```go // internal/service/order/service.go 或 client_order/service.go func (s *Service) validatePackageAvailability( ctx context.Context, pkg *model.Package, assetID uint, isRenewal bool, ) error { if pkg.Status == constants.StatusDisabled { // 0=禁用,定义在 pkg/constants/constants.go return errors.New(errors.CodeForbidden, "套餐已禁用") } if pkg.ShelfStatus == constants.ShelfStatusOff && !isRenewal { // 2=下架 return errors.New(errors.CodeForbidden, "套餐已下架,不可新购") } return nil } ``` **isRenewal 判断**:查当前资产是否有该套餐的历史生效记录: ```go func (s *Service) isRenewal(ctx context.Context, assetType string, assetID uint, packageID uint) bool { count, _ := s.packageUsageStore.CountByAssetAndPackage(ctx, assetType, assetID, packageID) return count > 0 } ``` ### 前端 C端续费页面:下架套餐不在"新购"列表中展示,但在"续费"入口中仍可展示。 后台代购时:下架套餐的"代购"按钮禁用,tooltip 提示"套餐已下架,不可代购"。 --- ## 需求16:代理分销码与佣金提现 ### 业务规则 **DST-001**:新建代理时自动建立分销归属关系(指定发展人) **DST-002**:员工可作为代理发展人进行标识 **二维码**:代理或员工生成推广二维码 → 扫码进入H5填写信息 → 提交成为代理申请 → 平台审批 ### 数据库变更 ```sql -- 1. Shop 表新增发展人字段 ALTER TABLE tb_shop ADD COLUMN referrer_type VARCHAR(20) DEFAULT NULL COMMENT '发展人类型 shop=代理介绍 admin=员工介绍', ADD COLUMN referrer_id BIGINT DEFAULT NULL COMMENT '发展人ID(shop_id 或 tb_account.id)', ADD COLUMN referrer_name VARCHAR(50) DEFAULT NULL COMMENT '发展人姓名快照'; -- 2. 分销码表 CREATE TABLE tb_distribution_code ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ, creator BIGINT NOT NULL DEFAULT 0, updater BIGINT NOT NULL DEFAULT 0, code VARCHAR(32) NOT NULL, owner_type VARCHAR(20) NOT NULL, -- shop=代理 | admin=员工 owner_id BIGINT NOT NULL, owner_name VARCHAR(50) NOT NULL DEFAULT '', qr_code_url TEXT, use_count INT NOT NULL DEFAULT 0, status INT NOT NULL DEFAULT 1, -- 1=有效 0=禁用 expires_at TIMESTAMPTZ ); CREATE UNIQUE INDEX idx_distribution_code ON tb_distribution_code(code) WHERE deleted_at IS NULL; -- 3. 代理申请表(H5扫码提交) CREATE TABLE tb_agent_application ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ, creator BIGINT NOT NULL DEFAULT 0, updater BIGINT NOT NULL DEFAULT 0, apply_no VARCHAR(30) NOT NULL, distribution_code VARCHAR(32) NOT NULL, referrer_type VARCHAR(20) NOT NULL, referrer_id BIGINT NOT NULL, applicant_name VARCHAR(50) NOT NULL, applicant_phone VARCHAR(20) NOT NULL, shop_name VARCHAR(100) NOT NULL, province VARCHAR(50), city VARCHAR(50), district VARCHAR(50), address VARCHAR(255), status INT NOT NULL DEFAULT 1, -- 1=待审批 2=已通过 3=已拒绝 reject_reason TEXT, reviewed_by BIGINT, reviewed_at TIMESTAMPTZ ); CREATE UNIQUE INDEX idx_agent_application_no ON tb_agent_application(apply_no) WHERE deleted_at IS NULL; ``` ### API 设计 **生成分销码**: ``` POST /admin/distribution-codes body: { owner_type: "admin"|"shop", owner_id: 123 } ``` **H5 扫码获取分销码信息**: ``` GET /app/distribution-codes/{code} ``` **H5 提交代理申请**: ``` POST /app/agent-applications body: { distribution_code, applicant_name, applicant_phone, shop_name, ... } ``` **后台代理申请列表/审批**: ``` GET /admin/agent-applications?status=1 POST /admin/agent-applications/{id}/approve POST /admin/agent-applications/{id}/reject body: { reject_reason: "..." } ``` 审批通过时:自动创建 `tb_shop` + `tb_account`,设置 `referrer_type` 和 `referrer_id`。 **佣金提现(DST-003~007)**: 现有 commission 框架基础上新增材料上传字段,提现申请时一次性上传所有材料(对象存储 Key 列表)。 --- ## 需求18:多人审批(APR-001~009) ### 依赖 基于 [审批流基础设施](./基础设施/审批流.md) 和 [站内消息](./基础设施/站内消息.md)。 APR-009(企微审批对接)= Phase 2。 ### 实现要点 | 编号 | 需求 | 实现 | |------|------|------| | APR-001~003 | 充值/退款多级审核 | 见需求20/21 | | APR-004 | 审核环节:部门领导→财务 | 审批流固定两步:step=1 部门领导,step=2 财务 | | APR-005 | 待审核有消息提示 | 站内消息 `NotifyTypeApprovalPending` | | APR-006 | 上一级完成后才提示下一级 | `ApprovalFlowAggregate.Approve()` 触发 `ApprovalStepAdvancedEvent` | | APR-007 | 通过后通知申请人 | `ApprovalCompletedEvent` handler | | APR-008 | 驳回/退回后通知申请人含原因 | `ApprovalRejectedEvent` / `ApprovalReturnedEvent` handler | | APR-009 | 对接企微 | **Phase 2** | ### 业务表与审批流的关联 充值单、退款单接入审批流,各自新增 `approval_flow_id` 字段: ```sql ALTER TABLE tb_refund_request ADD COLUMN approval_flow_id BIGINT DEFAULT NULL COMMENT '当前审批流ID(已退回后重新提交会更新为新流程ID)'; ALTER TABLE tb_agent_recharge_record ADD COLUMN approval_flow_id BIGINT DEFAULT NULL COMMENT '当前审批流ID(已退回后重新提交会更新为新流程ID)'; ``` 接入协议详见 [审批流文档 - 接入协议](./基础设施/审批流.md#六接入协议业务层如何接入审批流)。 --- ## 需求19:批量订购套餐(BPO-001~008) ### 业务流程 1. 员工进入批量订购页面 2. 选择代理 + 上传 Excel(字段:资产类型/资产标识/套餐编码/套餐名称/支付方式) 3. 系统解析并校验每一行 4. 支付方式:`offline=线下支付(默认成功)` / `agent_wallet=代理钱包支付` 5. 代理钱包支付时从代理余额扣款(含信用额度) 6. 线下支付时标记为已支付(不走钱包) 7. 校验失败的行展示在页面,带失败原因 8. **线下支付需上传整批次的支付凭证** ### 数据库变更 ```sql CREATE TABLE tb_bulk_purchase_task ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ, creator BIGINT NOT NULL DEFAULT 0, updater BIGINT NOT NULL DEFAULT 0, task_no VARCHAR(30) NOT NULL, shop_id BIGINT NOT NULL, operator_id BIGINT NOT NULL, payment_method VARCHAR(20) NOT NULL, -- offline | agent_wallet voucher_keys JSONB, -- 线下支付凭证列表 total_count INT NOT NULL DEFAULT 0, success_count INT NOT NULL DEFAULT 0, fail_count INT NOT NULL DEFAULT 0, status INT NOT NULL DEFAULT 1, -- 1=处理中 2=已完成 failed_items JSONB -- 失败明细 ); CREATE UNIQUE INDEX idx_bulk_purchase_task_no ON tb_bulk_purchase_task(task_no) WHERE deleted_at IS NULL; ``` > 失败明细存 JSONB(`failed_items`),与现有 DeviceImportTask 模式一致,无需单独明细表。 ### API 设计 **下载 Excel 模板**: ``` GET /admin/bulk-purchases/template ``` **上传并提交**: ``` POST /admin/bulk-purchases Content-Type: multipart/form-data 字段: shop_id: 123 payment_method: offline | agent_wallet voucher_keys: ["key1","key2"] (offline 时必填) file: ``` **查询任务状态**: ``` GET /admin/bulk-purchases/{task_id} ``` --- ## 需求20:退款审批 ### 依赖 基于 [审批流基础设施](./基础设施/审批流.md)。 ### 退款单现有状态(不变) ``` 1=待审批 2=已通过 3=已拒绝 4=已退回 ``` 退款单本身的状态含义不变,审批进度由 `tb_approval_flow` 管理,两者通过 `approval_flow_id` 关联。 ### 流程 ``` 提交退款申请(POST /admin/refund-requests) → 创建 RefundRequest(status=1 待审批) → 调 SubmitApprovalUseCase 创建 ApprovalFlow(biz_type=refund) → 把 approval_flow_id 写入 RefundRequest 审批人操作(POST /admin/approvals/{flow_id}/approve|reject|return) → ApprovalCompletedEvent → RefundRequest.status=2(已通过)→ 触发实际退款 → ApprovalRejectedEvent → RefundRequest.status=3(已拒绝) → ApprovalReturnedEvent → RefundRequest.status=4(已退回)→ 通知提交人可修改 ``` ### 退回后重新提交 ``` PUT /admin/refund-requests/{id} 仅 status=4 时可编辑退款单内容 POST /admin/refund-requests/{id}/resubmit → 校验 status=4 → 新建 ApprovalFlow → 更新 approval_flow_id,status 回到 1(待审批) ``` --- ## 需求21:充值审核流程 ### 充值单现有状态 ``` tb_agent_recharge_record:1=待支付 2=已支付 3=已完成 4=已关闭 5=已退款 ``` 员工线下充值走审批流,需在现有状态基础上**新增"已退回"状态**: ```sql -- status 说明更新(不改原有值语义,追加新状态) -- 6=已退回(审批人退回给提交人修改) ``` 充值业务的状态语义: - `1=待支付`:创建未支付(线下充值等待审批时也停在这里,由 approval_flow_id 判断是否在审批中) - `2=已支付`:支付成功(线下充值审批通过后跳到已完成,不经过此状态) - `3=已完成`:充值到账 - `4=已关闭`:取消/超时 - `5=已退款`:退款 - `6=已退回`:审批人退回给提交人修改 ### 流程 **代理自行充值(不走审批)**: ``` 代理提交充值申请 → 系统生成收款码 → 代理扫码支付 → 回调自动充值到钱包 ``` **员工线下代充值(走审批)**: ``` POST /admin/agent-recharge-records(payment_method=offline) → 创建 AgentRechargeRecord(status=1) → 调 SubmitApprovalUseCase 创建 ApprovalFlow(biz_type=recharge) → 把 approval_flow_id 写入记录 审批通过 → AgentRechargeRecord.status=3(已完成)→ 实际充值到钱包 审批驳回 → AgentRechargeRecord.status=4(已关闭) 审批退回 → AgentRechargeRecord.status=6(已退回)→ 通知提交人可修改 ``` ### 退回后重新提交 ``` PUT /admin/agent-recharge-records/{id} 仅 status=6 时可编辑 POST /admin/agent-recharge-records/{id}/resubmit → 校验 status=6 → 新建 ApprovalFlow → 更新 approval_flow_id,status 回到 1(待支付/待审批) ```