74 KiB
7月迭代技术方案(标准评审稿)
状态:待评审
最后更新:2026-07-15
分支:Iteration/7-11
系统:junhong_cmp_fiber及配套后台、代理端、C 端前端
负责人:待指定
评审人:后端、前端、产品、验收负责人、运维
关联需求:7月迭代需求 01~22(需求16已移出)及新增的数据同步、企微审批、站内通知、全局审计、代理钱包扫码充值
说明:本文是技术评审主文档;完整版和专项文档保留为设计素材,不作为会议逐页评审内容。
一、评审摘要
1.1 背景与目标
当前退款、线下充值审批各自维护,卡实名、流量和网络状态又被轮询、手动刷新及业务操作分别写入,容易出现同一业务动作只更新部分状态。现有账号和资产审计也没有统一模型,无法按人员、资源、请求、资金链路和外部交互追踪。复杂资金和状态业务仍集中在旧 Service 中,配置、批量任务、通知和导出权限缺少统一契约。
本次评审覆盖原需求 01~22(需求16移出)及五项新增技术需求,主要解决以下问题:
- 退款和平台员工线下充值直接接入企业微信审批,本系统只维护业务快照、审批状态镜像和终态处理。
- 将卡实名、流量和网络状态写入收口到统一 DDD 用例,以轮询兜底、业务事件阶梯触发和运营商回调提高同步及时性。
- 一次性切换全局 Audit Event、Integration Log 和多视角审计中心,旧审计表停止新写入。
- 补齐代理后台微信/支付宝扫码充值,在线支付成功后直接幂等入代理主钱包,不进入审批。
- 将信用额度、批量订购、退款和充值中的资金规则收敛为可并发校验的不变量。
- 补齐异步任务、站内消息、动态配置、导出字段权限和 Gateway 限速等基础能力。
- 统一后台、代理端和 C 端的页面状态、异步反馈和异常展示。
- 在不一次性重构旧系统的前提下,按完整用例渐进迁移 DDD。
1.2 非目标
本期明确不做:
- 本地通用审批流引擎、审批节点配置、审批人解析、待我审批和本地审批动作页面。
- BPMN 全规范、拖拽流程设计器、部门组织模型和本地直属领导计算。
- 套餐临期企业微信消息推送;本期先完成站内通知和防重记录。
- 全仓 MVC/贫血模型一次性重构。
- 为旧退款和线下充值审批接口建设长期兼容层。
- 后端提供 Excel 模板下载接口;模板由前端静态资源随版本发布。
- 微信/支付宝自动退款,以及基于套餐规则的自动限速。
- 运营商解除实名后自动回滚本地实名状态;第一版只保留防腐层入口和集成记录。
1.3 已确认决策
| 编号 | 决策 | 影响范围 |
|---|---|---|
| D-01 | 采用触碰式渐进 DDD,以完整用例为迁移单位,不以整个模块为单位重构 | 全局 |
| D-02 | 复杂写进入 Domain;简单写使用 Application 事务脚本;复杂查询走独立 Query 通道 | 全局 |
| D-03 | 不建设本地审批流;节点、审批人、意见和审批附件全部由企微模板及审批详情负责 | 18、20、21 |
| D-04 | 本系统保存提交业务快照、本地业务资料和企微审批详情快照,审批人能够看到审批对象、备注和附件 | 18、20、21 |
| D-05 | 平台员工不建立钱包信用额度,信用额度只属于代理主钱包 | 17、19 |
| D-06 | 批量订购支付方式整批统一,Excel 不包含支付方式 | 19 |
| D-07 | 角色级导出字段权限本期落地,服务端取角色授权与用户选择的交集 | 14 |
| D-08 | Gateway 只按 cardNo 手动设置/取消限速;设备入口先解析当前绑定卡,不做套餐自动限速 |
10 |
| D-09 | 需求16整体移出本期,不创建相关表、API、流程、页面和迁移 | 16 |
| D-10 | 退款金额在发起时固定;非代理钱包退款由财务人工处理,代理钱包仅回退原扣款主钱包;线下充值企微通过后自动入账 | 20、21 |
| D-11 | 采用停机发布,同时切换数据库、API、Worker 和前端,旧审批接口同步下线 | 20、21 |
| D-12 | 企微模板 ID 和控件 ID 均按不可变版本映射;编辑模板前暂停场景,发布新映射后原子切换 | 企微审批 |
| D-13 | 设备批量分配代理与套餐系列拆为两个独立命令 | 08 |
| D-14 | 排队套餐使用购买时长快照推算预计最后到期时间;临期页面实时查询,定时任务仅发送通知 | 06、22 |
| D-15 | 行业卡是否需要实名由运营商 realname_link_type 决定,card_category 不参与实名、复机和轮询判断 |
01、02、数据同步 |
| D-16 | 数据同步保留轮询兜底,关键业务事件按立即、3 分钟、5 分钟触发;超频不建立退避状态 | 数据同步 |
| D-17 | 企微账号由已登录平台员工扫码自助绑定,普通运营不录入 userid |
企微审批 |
| D-18 | 全系统审计本次一次性切换到 Audit Event + Integration Log,多视角 API 和前端同时发布 | 全局 |
| D-19 | 代理在线充值最低 100 元,支持微信 Native 和支付宝 PreCreate,支付成功直接入主钱包且不审批 | 21、新增充值 |
1.4 本期边界
本期包含企业微信审批、账号扫码绑定、回调与轮询补偿;不再保留“先做站内审批、以后切企微”的中间态。套餐临期的企业微信消息推送仍属于后续扩展,本期只实现站内通知及外部渠道可扩展边界。
二、系统架构与渐进式 DDD
2.1 系统上下文
flowchart LR
Admin[后台管理端] --> API[Junhong API]
Agent[代理端] --> API
Client[C端 H5/公众号] --> API
API --> PG[(PostgreSQL)]
API --> Redis[(Redis)]
API --> Gateway[Gateway]
API --> Outbox[(Outbox)]
Outbox --> Relay[Outbox Relay]
Relay --> Asynq[Asynq]
Asynq --> Worker[Worker]
Worker --> Notice[站内消息]
Worker --> Biz[退款/充值/批量任务]
Worker --> WeCom[企业微信]
Worker --> Pay[微信/支付宝]
Worker --> Gateway[Gateway]
Carrier[运营商回调] --> API
Pay --> API
API --> Audit[(Audit/Integration Log)]
边界约束:
- API 负责同步校验、数据库事务和返回可追踪状态。
- Worker 负责通知、导出、批量处理及审批后的业务动作。
- Redis/Asynq 不参与核心业务事实的唯一持久化;关键事件先写 Outbox。
- Gateway、运营商回调、微信/支付宝和企业微信均通过 Infrastructure Adapter 接入,外部字段不得直接进入领域模型。
2.2 渐进迁移规则
复杂写:Handler -> Application -> Domain -> Repository/Infrastructure
简单写:Handler -> Application -> Store/GORM
复杂查询:Handler -> Query -> GORM/DTO
旧用例未触碰:继续 Handler -> Service -> Store -> Model
| 场景 | 选择 | 判断依据 |
|---|---|---|
| 钱包扣款、授信、卡状态应用、退款状态转换 | Domain | 有不变量、状态机、并发或跨表一致性 |
| 单字段修改、简单开关、局部 CRUD | Application 事务脚本 | 规则少且不会形成复杂状态 |
| 列表、报表、导出、跨聚合详情 | Query | 只读、字段多、需要 JOIN/聚合/分页 |
| 未被本次用例触碰的旧逻辑 | 保持旧架构 | 避免扩大认知和回归范围 |
站内消息只是幂等写入和已读状态管理,采用轻量 Application/Infrastructure 即可,不为满足目录形式强行创建空洞聚合。
迁移单位必须是一个完整用例。例如修改“钱包扣款”时,应同时迁移该用例的规则、事务、仓储和事件,不要求顺带迁移所有钱包查询或后台 CRUD。
2.3 查询侧规则
- Query 可以直接使用 GORM、JOIN、聚合和只读 DTO,不要求经过聚合根。
- Query 不修改状态、不发布领域事件、不承担业务不变量。
- 列表默认分页 20、最大 100;导出和批量详情使用批量查询,禁止 N+1。
- 企微审批摘要、审计多视角、导出字段和套餐临期列表均属于 Query 场景。
2.4 跨需求技术约定
| 项目 | 约定 |
|---|---|
| 管理端 API | /api/admin/* |
| C 端 API | /api/c/v1/* |
| 金额 | 数据库与接口使用分,前端显示元 |
| 时间 | 后端返回 ISO 8601,前端统一本地化显示 |
| 状态 | 生命周期使用 int,类型/方式使用 string |
| 创建幂等 | 稳定业务键或 request_id + 唯一索引 |
| 状态幂等 | WHERE status = expected 条件更新 |
| 余额并发 | 版本号或行锁,并在同一事务写资金流水 |
| 异步事件 | 业务事务内写 Outbox,Relay 投递 Asynq,消费者按至少一次设计 |
| 权限 | 后端强校验,前端隐藏按钮不能作为权限边界 |
三、跨需求基础设施
3.1 系统配置
目标
将支付方式等受控配置从代码迁移到数据库,平台超管修改后无需重新部署;H5 实名流程继续由资产自身字段决定,不增加全局默认 Key。
数据模型
tb_system_config 关键字段:
| 字段 | 说明 |
|---|---|
config_key |
唯一键,格式 module.group.name |
config_value |
字符串形式的配置值 |
value_type |
string/int/bool/json |
module |
前端分组和查询维度 |
description |
中文说明 |
is_readonly |
只读配置不能从后台修改 |
updater/updated_at |
审计信息 |
唯一约束:UNIQUE(config_key)。
首批配置:
| Key | 默认值 | 用途 |
|---|---|---|
c2b.payment.card_allowed_methods |
["alipay","wallet"] |
卡资产 C 端支付方式 |
c2b.payment.device_allowed_methods |
["wechat","wallet"] |
设备 C 端支付方式 |
更新流程
sequenceDiagram
actor Admin as 平台超管
participant Web as 系统配置页
participant API as Config Application
participant DB as PostgreSQL
participant Redis as Redis
Admin->>Web: 修改受控表单
Web->>API: PUT /api/admin/system/config/{key}
API->>API: 按 key 校验允许值
API->>DB: 更新并记录操作人
API->>Redis: 删除 sys:config:{key}
API-->>Web: 返回最新配置
- 配置读取缓存 5 分钟,更新后立即失效。
- 不能只按
value_type校验;每个已注册 Key 必须限制枚举和值域。 - 未注册 Key 默认只读,禁止通用接口写入任意配置。
- 配置解析失败、Redis/数据库异常时使用最近一次有效值或代码安全默认值并告警,禁止静默放开未知支付方式。
- 前端使用复选框、单选或开关,不向运营人员暴露 JSON 文本框。
API
| 方法 | 路径 | 权限 |
|---|---|---|
| GET | /api/admin/system/config?module= |
系统配置查看 |
| PUT | /api/admin/system/config/{config_key} |
平台超管 |
3.2 站内消息
设计
sequenceDiagram
participant Biz as 业务事务
participant Outbox as Outbox
participant Relay as Relay
participant Worker as Notification Worker
participant DB as tb_notification
participant Web as 前端
Biz->>Outbox: 同事务写领域事件
Relay->>Worker: 至少一次投递
Worker->>DB: event_id + recipient 幂等插入
Web->>DB: 经 API 轮询和标记已读
tb_notification 关键字段:event_id、recipient_kind、recipient_id、category、type、severity、title、body、ref_type、ref_id/ref_key、is_read、read_at、expires_at。
关键约束:
- 唯一索引:
event_id + recipient_kind + recipient_id,Outbox/Asynq 重试不得重复建消息。 - 未读和分类索引均以
recipient_kind + recipient_id开头,所有 Query 和已读更新从登录上下文固定接收人。 - 正文使用受控纯文本模板,不保存任意 HTML、密钥、操作密码、完整个人信息或长期附件 URL。
- 企微审批待办由企业微信负责,本系统只发送审批结果、通过后撤销异常、模板失效和业务处理结果通知。
- 前端每 30 秒轮询未读数,不使用 WebSocket;页面不可见时暂停,恢复时立即刷新。
ref_type/ref_id/ref_key只用于受控目标解析,跳转后仍需业务权限校验。
通知类型:
| 类型 | 接收人 |
|---|---|
wecom.approval.approved/rejected/cancelled |
申请人账号 |
wecom.approval.revoked_after_approved |
申请人和财务角色账号 |
wecom.template.invalid |
平台超管 |
package.expiring |
代理/企业相关账号 |
card_sync.failed |
平台运维角色 |
carrier_callback.card_not_found |
平台运维角色 |
system.alert |
平台超管或指定运维角色 |
API:
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/admin/notifications/unread-count |
未读总数 |
| GET | /api/admin/notifications/unread-summary |
分类未读数 |
| GET | /api/admin/notifications |
按分类、类型、级别和已读状态分页 |
| PUT | /api/admin/notifications/{id}/read |
幂等标记单条已读 |
| PUT | /api/admin/notifications/read-all |
全部或按分类已读 |
| GET | /api/admin/notifications/{id}/target |
返回受控目标类型,不返回任意 URL |
| GET | /api/c/v1/notifications/unread-count |
C 端当前客户未读数 |
| GET | /api/c/v1/notifications |
C 端简化消息列表 |
| PUT | /api/c/v1/notifications/{id}/read |
C 端幂等已读 |
| PUT | /api/c/v1/notifications/read-all |
C 端全部已读 |
3.3 企业微信审批接入
3.3.1 系统边界
企业微信负责模板、审批节点、审批人、会签/或签、通过、驳回、撤销、审批意见、审批附件和企业微信端待办。本系统不保存本地审批任务,也不提供审批按钮,只负责:
- 创建退款和平台员工线下充值业务单。
- 保存提交时业务快照和本地业务资料,上传企微附件副本。
- 以稳定场景码映射企微模板版本和控件 ID。
- 保存
sp_no、审批状态、审批人/意见/附件详情快照。 - 接收回调并以
getapprovaldetail查询作为状态权威来源。 - 审批终态后幂等执行退款或线下充值业务处理。
Viper 配置包含 corp_id、agent_id、agent_secret、回调 Token/EncodingAESKey、审批回调路径、账号绑定回调 URL、2 分钟审批轮询和 10 分钟模板验证间隔。Secret 只允许环境变量覆盖,后台只返回“是否配置”和最近连通结果。Access Token 缓存在 Redis,TTL 使用 expires_in-300秒 并通过锁避免并发刷新。
sequenceDiagram
actor Staff as 平台员工
participant App as Refund/Recharge Application
participant DB as PostgreSQL
participant Outbox as Outbox
participant Worker as WeCom Worker
participant WeCom as 企业微信
participant Sync as Approval Sync
participant Biz as 业务终态用例
Staff->>App: 创建退款/线下充值
App->>DB: 业务单+企微实例(submitting)
App->>Outbox: SubmissionRequested
Worker->>WeCom: 上传附件并applyevent
WeCom-->>Worker: sp_no
WeCom->>Sync: 加密状态回调
Sync->>WeCom: getapprovaldetail
Sync->>DB: 保存详情快照并幂等更新状态
Sync->>Outbox: 首次终态写业务事件
Outbox->>Biz: 退款/充值终态处理
3.3.2 模板版本与暂停切换
业务代码只引用稳定场景码:
scene_code |
业务 |
|---|---|
refund_approval |
退款审批 |
offline_recharge_approval |
平台员工线下充值审批 |
tb_wecom_approval_scene 保存场景启用、暂停中、已暂停和当前模板版本;tb_wecom_approval_template_version 保存不可变的 template_id、控件映射、模板快照、fingerprint、验证状态和发布时间。
企微编辑模板可能更换模板 ID,删除再添加控件也会更换控件 ID,因此发布流程必须是:
暂停业务场景并等待提交租约清空
→ 在企微编辑模板
→ 输入新的 template_id 并读取模板
→ 将稳定业务字段映射到当前控件 ID
→ 后端重新调用 gettemplatedetail 校验
→ 原子发布新版本并恢复场景
退款至少映射店铺、退款单号、申请金额、原因、附件和提交人;线下充值至少映射店铺、充值单号、金额、备注、附件和提交人。后台每 10 分钟验证启用版本,模板不可访问或 fingerprint 变化时暂停场景并发送系统告警。
3.3.3 系统账号扫码绑定企微成员
普通运营不录入 userid。员工先登录本系统,再点击“绑定企业微信”,后端生成一次性 state 和企微 Web 登录 URL;扫码回调后使用自建应用 Access Token 调用 auth/getuserinfo 获取 userid。
系统登录账号
→ 创建5分钟绑定会话
→ 打开企微Web登录二维码
→ 回调code+state
→ 原子消费state
→ 换取userid并校验企业成员
→ 保存一对一绑定
约束:
state与前端查询session_id分开存 Redis;state单次使用,绑定结果会话保留 10 分钟。- 回调不接受
account_id,目标账号只能来自服务端绑定会话。 account_id和wecom_userid均唯一;成员已绑定其他账号时拒绝覆盖。- 自助解绑和管理员强制解绑不影响历史审批快照,但会阻止新审批。
- 管理员只查看绑定状态和强制解绑,不提供
userid输入框。 - 企微自建应用可信域名和可见范围必须覆盖需要发起审批的平台员工。
tb_account_wecom_mapping 保存账号、userid、成员名称、CorpID、AgentID、绑定来源和验证时间。
3.3.4 审批实例与状态同步
tb_wecom_approval_instance 保存:业务类型/ID/编号、轮次、场景、模板版本和映射快照、创建人本地账号与企微 userid、sp_no、提交业务快照、企微详情和审批人快照、业务处理结果、轮询时间、乐观锁版本。
状态:
| 本地状态 | 企微状态 |
|---|---|
| 提交中/提交失败/提交结果未知 | 尚未取得明确审批单结果 |
| 审批中 | sp_status=1 |
| 已通过 | sp_status=2 |
| 已驳回 | sp_status=3 |
| 已撤销 | sp_status=4 |
| 通过后撤销 | sp_status=6 |
| 已删除 | sp_status=7 |
applyevent 请求已发送但响应超时必须标记“提交结果未知”,禁止盲目重试产生重复审批。管理员在企微核对后选择“确认未创建并重新提交”或手工绑定已有 sp_no,后者记录高风险审计。
回调只负责验签、解密、保存 Integration Log 并触发统一 SyncApprovalStatus。审批中实例每 2 分钟兜底查询,回调和轮询共用状态同步用例;首次进入终态时同事务写业务 Outbox,business_processed_at 保证资金动作只执行一次。
3.3.5 DDD 边界与 API
domain/wecomapproval 场景、模板版本、审批状态和终态幂等
application/wecomapproval 发布模板、提交审批、回调、同步和异常恢复
domain/wecomidentity 系统账号与企微成员一对一绑定
application/wecomidentity 创建绑定会话、完成绑定和解绑
infrastructure/adapter/wecom Token、模板、附件、审批、身份和回调加解密
query/wecomapproval 审批运行列表、详情和业务摘要
主要 API:
| 方法 | 路径 | 用途 |
|---|---|---|
| GET | /api/admin/wecom/status |
查询连接配置状态 |
| PUT | /api/admin/wecom/approval-scenes/{scene_code}/status |
暂停或恢复场景 |
| POST | /api/admin/wecom/approval-templates/inspect |
读取企微模板 |
| POST | /api/admin/wecom/approval-templates/publish |
发布控件映射版本 |
| GET | /api/admin/wecom/approval-templates |
模板版本列表 |
| POST | /api/admin/wecom/account-binding/sessions |
当前账号创建扫码绑定会话 |
| GET | /api/admin/wecom/account-binding/sessions/{session_id} |
查询扫码绑定结果 |
| GET/DELETE | /api/admin/wecom/account-binding/me |
查询或解绑自己 |
| GET | /api/admin/wecom/account-bindings |
超管查询绑定列表 |
| DELETE | /api/admin/wecom/account-bindings/{account_id} |
超管强制解绑 |
| GET | /api/callback/wecom/account-binding |
Web 登录回调 |
| GET/POST | /api/callback/wecom/approval |
企微审批回调校验和事件 |
| GET | /api/admin/wecom/approvals* |
审批运行列表和详情 |
| POST | /api/admin/wecom/approvals/{id}/sync |
立即同步详情 |
3.4 数据同步触发与轮询优化
3.4.1 三条自动通道
轮询、业务事件触发和运营商回调相互隔离,但最终都进入统一的卡状态应用用例:
flowchart LR
Polling[活跃/不活跃轮询] --> Request[RequestCardObservation]
Event[关键业务埋点] --> Series[立即/3分钟/5分钟]
Series --> Request
Manual[现有手动刷新] --> Request
Request --> Gateway[Gateway Adapter]
Gateway --> Apply[ApplyCardObservation]
Callback[运营商实名回调] --> ACL[Carrier ACL]
ACL --> Apply
Apply --> Card[卡状态聚合]
Apply --> Outbox[套餐/停复机等事件]
Request --> Integration[Integration Log]
ACL --> Integration
- 轮询始终兜底,只保留
enable_polling总开关,按活跃/不活跃使用不同间隔。 - 业务事件不是新接口,而是在查询资产、获取实名链接、停复机、支付、套餐激活、切卡、重启等用例成功边界埋点。
- 每个事件序列默认创建立即、3 分钟、5 分钟三个无自动重试任务;达到预期状态后剩余任务提前完成。
- 现有手动刷新接口保持响应契约并直接同步,不再额外创建阶梯任务。
- Gateway 超频只记录当前
rate_limited,不维护blocked_until或 30/60/120 秒退避,后续阶梯任务和轮询保持原计划。
3.4.2 实名判断与状态应用
行业卡不能统一跳过实名。运营商 realname_link_type 决定实名要求和链接方式:
| 值 | 规则 |
|---|---|
none |
不需要实名,不创建实名查询任务 |
template |
需要实名,按模板生成链接 |
gateway |
需要实名,通过 Gateway 获取链接 |
card_category 只用于分类和展示,不参与实名、复机、套餐激活和轮询资格。资产 realname_policy 只决定实名与购买顺序;realname_link_type=none 时有效策略统一视为 none。
ApplyCardObservation 是唯一允许写入实名、流量和网络状态的入口:加载并锁定卡聚合,应用标准观测值,状态变化时同事务保存领域事件和 Audit Event,外部调用始终写 Integration Log。旧轮询 Handler、手动实名修改和刷新 Service 必须改为调用该用例,不保留双实现。
3.4.3 活跃调频和请求协调
卡维护活跃级别、最后活跃时间/场景和最后上游变化时间。C 端/后台/OpenAPI 查询、实名、停复机、支付、套餐、切卡、回调和轮询发现变化均标记活跃;持续无业务活动且轮询无变化后转为不活跃。
第一版建议 inactive_after=30m;活跃卡沿用现有实名/流量/状态默认间隔,不活跃卡三类查询初始均为 15 分钟。上线前按生产卡量只读测算 QPS 后再调整,数值进入轮询配置而不是写死代码。
重复控制拆为:
- 同场景、同资源、同同步类型合并未结束的
0/3/5序列。 - 单卡、运营商、接口类型只在一次 Gateway 请求执行期间互斥。
- 运营商最小请求间隔按接入和接口类型配置,默认兜底 10 秒,不使用统一五分钟冷却。
3.4.4 运营商实名回调防腐层
移动、联通、电信分别实现报文 Translator,把 JSON/XML/表单、状态码和成功应答转换为标准实名观测。业务结论百分百信任,不验证来源真实性,也不反查 Gateway。
ICCID 必须按原始长度精确路由:19 位只查 iccid_19,20 位只查 iccid_20,禁止截断、补位或跨列降级。发布前检查 iccid_19 重复并建立未删除数据范围内的部分唯一索引。解除实名回调第一版只写 Integration Log 并返回约定成功报文,不修改卡实名状态。
本需求不新增同步按钮和显式同步 API。资产详情只增加自动轮询状态和“查看同步轨迹”,跳转 /operations/audit?tab=integrations&resource_type=iot_card&resource_key=...。
3.5 全局多视角审计
3.5.1 四类记录边界
| 类型 | 权威存储 | 用途 |
|---|---|---|
| Access Log | 日志文件/平台 | HTTP 调试、性能和 request_id |
| Audit Event | tb_audit_event |
谁对哪些资源做了什么及结果 |
| Domain Ledger | 钱包流水、订单、退款、充值、企微实例等业务表 | 领域事实 |
| Integration Log | tb_integration_log |
Gateway、运营商、企微、支付渠道的外部交互 |
tb_audit_event 保存不可变 event_id、操作注册编码、操作者快照、来源、结果、风险、前后数据、request_id/correlation_id/parent_event_id、请求摘要和内容哈希。tb_audit_event_resource 支持一个事件关联主要、影响和引用资源,解决退款同时影响订单、钱包、流水和资产的问题。
tb_integration_log 保存 provider、方向、operation、外部单号、资源、触发来源/场景/序列/尝试、结果、脱敏请求响应摘要、耗时和关联链路。数据同步的合并、限流、提前完成即使没有真正请求上游,也必须写可解释结果。
3.5.2 写入可靠性和脱敏
- 钱包余额、人工退款结果、代理钱包回溯、线下充值入账、角色权限和关键配置变更必须与业务事务同事务写 Audit Event;审计失败则业务回滚。
- 外部轮询和系统任务写 Integration Log;状态变化、连续失败、人工触发或高风险异常再追加 Audit Event。
- 失败和拒绝在业务回滚后用独立短事务记录,不使用裸 goroutine。
- 密码、操作密码、Token、Secret、企微密钥、支付私钥、验证码、完整身份证、签名 URL 和
media_id永不进入审计。 - Access Log 请求体和响应体使用同一递归脱敏;登录、Token、回调和文件路由只记录字段摘要/哈希。
3.5.3 多视角 API 与前端
主要 Query:
GET /api/admin/audit/events
GET /api/admin/audit/events/{event_id}
GET /api/admin/audit/actors* 人员行为
GET /api/admin/audit/resources/search
GET /api/admin/audit/resources/{type}/{id}/timeline
GET /api/admin/audit/requests/{request_id}/timeline
GET /api/admin/audit/correlations/{correlation_id}/timeline
GET /api/admin/audit/risks/*
GET /api/admin/audit/integrations*
GET /api/admin/audit/finance/timeline
POST /api/admin/audit/exports
前端 /operations/audit 使用工作台式 Tab:全局事件、人员行为、资源轨迹、请求/业务链路、资金审计、风险事件、外部集成。资源搜索先返回候选资源再打开时间线;Gateway 同步序列按立即、3 分钟、5 分钟连续展示。敏感字段默认脱敏,查看完整敏感值的动作本身再次审计。
本需求在一次停机发布中全局切换:新功能和现有敏感写操作全部改用统一 Audit Writer,旧账号/资产/轮询日志表停止新写入,不做双写;历史数据通过 Query UNION ALL 只读投影到新审计页面。
保留策略:资金、权限、审批和关键配置 Audit Event 保留 5 年,普通业务审计 2 年,Integration Log 默认 180 天,Gateway 无变化成功记录 30 天,Access Log 30 天;达到单表维护阈值后按月分区和归档。
四、统一前端技术方案
当前仓库不包含前端源码。本节定义框架无关的页面、状态和接口契约;实际目录、状态库和组件名称以各前端仓库现状为准。
4.1 页面与模块
| 模块/页面 | 建议路由 | 主要能力 |
|---|---|---|
| 企微审批运行 | /operations/wecom-approvals |
状态、业务单号、模板版本、异常恢复和详情抽屉 |
| 企微配置 | /system/wecom |
连接状态、模板映射版本、账号绑定和异常审批 |
| 账号企微绑定 | 当前用户个人中心 | 扫码绑定、查看成员名称、重新绑定和解绑 |
| 站内消息 | /notifications |
未读数、列表、已读和受控业务跳转 |
| 全局审计 | /operations/audit |
全局、人员、资源、链路、资金、风险和外部集成多视角 |
| 系统配置 | /settings/system-config |
受控单选、复选和开关 |
| 异步任务 | 复用各业务页面 | 导入、批量订购、导出进度和失败明细 |
全局状态只保存登录用户、权限和通知未读数。列表、详情和表单草稿保留在页面或模块级状态,避免复制服务端状态后长期失真。
顶部铃铛固定宽度显示 0/1~99/99+,点击打开最近 10 条通知抽屉,按全部、审批、临期、同步/系统分类;通知中心提供分类、已读和严重级别筛选及全部已读。点击通知先幂等标记已读,再解析受控目标,未知目标只展示正文不跳转。
4.2 通用显示规则
- 前端使用
status判断逻辑,使用后端返回的status_name展示中文。 - 金额接口使用分,金额组件显示元;转换结果必须是整数。
- 时间按后端 ISO 8601 和现有时区工具展示。
- 无权限和资源不存在使用统一页面语义,避免泄露资源存在性。
- 操作成功后重新请求服务端详情,不在本地自行推进审批或资金状态。
4.3 异步任务交互
stateDiagram-v2
[*] --> Editing
Editing --> Submitting: 提交文件和参数
Submitting --> Processing: 创建任务成功
Submitting --> Editing: 参数或上传失败
Processing --> Processing: 轮询进度
Processing --> Completed: 全部或部分完成
Processing --> Failed: 任务失败
Completed --> [*]
Failed --> Editing: 修正后创建新任务
- 创建成功后立即请求一次详情,再按 2、3、5 秒退避,最大间隔 10 秒。
- 页面不可见时暂停轮询,恢复可见时立即刷新。
- 网络错误不等于业务失败,展示“状态获取失败,点击重试”。
- 必须展示总数、成功数、失败数和部分成功状态。
- 页面刷新后根据任务 ID 恢复进度。
- Excel 模板由前端静态资源提供;后端仍严格校验表头、版本和内容。
4.4 企业微信审批交互
- 删除本系统待我审批、流程节点配置、审批人配置、通过/驳回/退回按钮,审批操作全部在企业微信完成。
- 退款和线下充值创建成功后先显示“正在提交企业微信审批”,取得
sp_no后显示“企业微信审批中”。 - 业务详情的企微审批区块展示审批单号、状态、模板版本、申请人、审批人、意见、审批附件、状态时间线和本地业务处理结果。
- “立即同步”只调用
getapprovaldetail,不提供任何本地审批动作。 - 通过后撤销且资金动作已执行时使用高风险异常提示,明确说明不会自动冲正。
- 未绑定企微账号时,创建页原地展示“绑定企业微信”按钮;扫码成功后继续当前表单,不要求用户先跳转配置页。
- 模板发布使用业务字段与企微控件的可视化映射,不向运营暴露 JSON;场景暂停时创建页提前展示维护原因。
4.5 审批与业务处理状态
退款和充值详情必须分别展示审批状态、企微状态更新时间、业务处理状态,以及失败摘要和“系统重试中/联系管理员”提示。
approval_source 展示规则:
| 值 | 前端行为 |
|---|---|
none |
不显示审批区域 |
wecom |
展示只读企微详情和本地业务处理结果 |
legacy |
只读展示历史摘要,不伪造节点,不提供审批按钮 |
4.6 权限与敏感数据
- 企微配置、账号绑定、导出字段、审计视角和数据范围均以后端为准。
- 通知跳转使用前端受控
ref_type路由表,不接受任意 URL。 - 退款凭证、充值凭证、身份证、营业执照和审批附件均走对象存储,不保存永久公开地址。
- 角色导出字段配置只控制列,不扩大已有店铺或企业数据范围。
- 审计前后数据和外部交互摘要默认脱敏;敏感查看权限和审计导出权限分开控制。
五、需求 01~15、22 专项方案
5.1 简单改动汇总
| 需求 | 业务规则 | 后端/数据 | 前端与验收 |
|---|---|---|---|
| 需求 01 行业卡复机 | 是否要求实名只看运营商 realname_link_type;none 可未实名复机,template/gateway 仍需实名 |
删除按 card_category=industry 放行或跳过实名的逻辑,复用卡状态领域规则 |
验证同为行业卡但不同运营商实名能力时行为不同,卡类别不再影响结果 |
| 需求 03 联系电话搜索 | 店铺联系电话 11 位精确查询 | 店铺列表 Query 增加 contact_phone |
增加搜索框;空参数不影响原查询,非法号码返回参数错误 |
| 需求 04 退款中禁止换货 | 企微审批中、历史已退回、企微已通过但退款终态未完成的资产禁止换货;已拒绝、已撤销或退款完成后放行 | 创建换货前批量校验活跃退款,返回“该资产存在退款申请” | 前端直接展示后端错误,不自行判断退款状态 |
| 需求 06 最后到期时间 | 返回当前生效及排队主套餐全部接续后的预计最终到期时间 | 资产详情 Query 按队列顺序使用购买时长快照推演;无套餐返回 null |
文案使用“预计最后到期时间”,实时查询,不保存冗余结果 |
| 需求 07 实名筛选 | 卡按自身实名状态;设备任一有效绑定卡实名即视为设备实名 | 设备增加 real_name_status 快照;实名变化、绑定、解绑、换卡均刷新旧/新设备 |
卡和设备列表增加全部/已实名/未实名筛选 |
| 需求 11 当前套餐到期 | 统一并入需求 22 的剩余天数和颜色规则 | 不再单独维护第二套计算 | 资产详情直接使用需求 22 字段 |
| 需求 12 换货显示与搜索 | 卡的新旧资产标识统一快照 ICCID;设备维持设备号;历史数据不回填 | 创建快照逻辑修正;列表增加 old_asset_keyword/new_asset_keyword,支持 ICCID、接入号、虚拟号 |
搜索框拆为旧资产和新资产;验收大结果集查询性能 |
| 需求 13 列表字段 | 提交人写业务快照;企微审批摘要动态读取 | 换货、退款、充值增加 submitter_name;按本页企微实例批量查询状态和审批人摘要,禁止 N+1 |
展示企微状态、当前审批人摘要、处理状态和历史审批标识 |
| 需求 15 下架套餐续费 | 禁用套餐始终不可购买;下架套餐只允许资产所有人基于有效历史使用记录续费,禁止代理代购 | 复用统一套餐可售策略;后端根据资产和登录主体判定续费资格 | C 端当前套餐旁展示“续费”,复用购买流程;新购入口隐藏,后台代购禁用 |
5.2 需求 02:H5 实名与充值顺序配置
规则
| 资产视角 | 策略来源 |
|---|---|
| 独立卡 | 卡的 realname_policy |
| 设备 | 设备的 realname_policy |
| 设备下的卡 | 设备的 realname_policy,卡自身值不参与 H5 流程 |
资产顺序策略:none、before_order、after_order,默认 after_order。运营商能力先于资产顺序策略:realname_link_type=none 时有效策略固定为 none;其他值下如果资产策略为 none,属于待修复冲突数据,禁止运行时静默放行。
flowchart TD
Login[用户进入 H5] --> Resolve[解析资产视角]
Resolve --> Policy{realname_policy}
Policy -->|none| Pay[直接进入充值/购买]
Policy -->|before_order| Realname[先实名]
Realname --> Pay
Policy -->|after_order| PayFirst[先充值/购买]
PayFirst --> Prompt[完成后提示实名]
现有字段和单条接口继续使用:
PATCH /api/admin/assets/{identifier}/realname-mode
新增批量接口:
POST /api/admin/iot-cards/batch-update-realname-policy
POST /api/admin/devices/batch-update-realname-policy
- 单次最多 500 条;事务内全成全败,任一资产不合法则返回明确失败原因并不更新任何记录。
- 设备下的卡在后台修改卡策略时,前端提示“实际 H5 流程由设备策略决定”。
- C 端只使用初始化接口返回的有效策略,不自行判断资产类型;本期不增加全局默认配置,新建卡/设备继续使用模型默认
after_order。
5.3 需求 05:套餐分配生效条件
完整生命周期:
套餐默认规则
-> 代理分配可选覆盖
-> 客户购买时计算有效值并写入 PackageUsage 快照
-> 套餐激活、排队接续和最后到期时间只读取快照
覆盖的含义是“该代理未来购买此套餐时采用的规则”,不是修改套餐本身。客户购买后,PackageUsage 同时快照 expiry_base、周期类型和时长;之后修改套餐或分配配置都不影响已经购买的套餐。旧记录缺少快照时仅兼容回退到当前套餐值,不能把回退逻辑用于新订单。
数据变更:
| 表 | 字段 | 说明 |
|---|---|---|
tb_shop_package_allocation |
expiry_base_override nullable |
NULL 表示跟随套餐默认 |
tb_package_usage |
expiry_base_snapshot、calendar_type_snapshot、duration_months_snapshot、duration_days_snapshot |
新订单写入有效值和时长;用于激活和排队到期推算 |
API:
POST /api/admin/shop-package-allocations
PATCH /api/admin/shop-package-allocations/{id}/expiry-base
- 修改分配覆盖值只影响后续新订单,不修改已经形成的使用记录快照。
- 前端提供“跟随套餐默认、购买即生效、实名即生效”三选一。
- PATCH 必须能区分“字段缺失”和“显式恢复默认”;实现时使用可识别 JSON
null的 DTO,不能用普通指针静默混淆。
示例:套餐默认“实名即生效”,代理 A 覆盖为“购买即生效”。代理 A 的客户购买后,使用记录写入 from_purchase;后来将套餐默认改为“实名即生效”或删除代理覆盖,已购买记录的计时方式和预计到期时间均不变化。
5.4 需求 08:设备批量分配
“分配代理”和“分配套餐系列”是两个业务命令,不允许一个任务同时修改两个字段。两者复用 Excel 解析、任务轮询和失败明细基础设施,但每个任务只携带一种 operation_type 和一个目标值。
sequenceDiagram
actor User as 平台员工
participant Web as 设备管理页
participant API as Batch Allocation API
participant DB as PostgreSQL
participant Worker as Asynq Worker
User->>Web: 选择一种分配操作并上传 Excel
Web->>API: POST batch-assign-shop 或 batch-assign-series
API->>DB: 保存任务和文件 Key
API-->>Web: task_id
Worker->>DB: 分批查询、条件更新、记录失败
Web->>API: GET /api/admin/devices/batch-allocation/{id}
任务字段:任务号、源文件 Key、operation_type=assign_shop|assign_series、目标 ID、操作人、总数、成功数、失败数、状态、失败明细、开始/完成时间。
处理规则:
- 设备号去重后批量查询,禁止逐行查询。
assign_shop:已属于其他代理的设备失败并提示先回收;已属于目标代理的按幂等成功。assign_series:已属于目标套餐系列的按幂等成功;不修改shop_id。- 临时本地路径和文件字节不得进入 Asynq 载荷。
- 模板由前端提供,后端不实现下载接口。
- 限制:文件最大 10MB、最多 1000 行、Worker 每批 200 条、失败明细最多保存 1000 条。
- 前端提供两个独立入口和两个表单:批量分配代理、批量分配套餐系列。
5.5 需求 09:C 端支付方式限制
卡和设备的允许支付方式读取系统配置,钱包始终允许。初始默认值:
| 资产 | 默认允许方式 |
|---|---|
| 卡 | 支付宝、钱包 |
| 设备 | 微信、钱包 |
- C 端支付页只展示接口返回的允许方式。
- 创建订单和支付接口必须再次校验,不能依赖前端隐藏。
- 配置异常时使用上述安全默认值并记录错误,不得放开全部方式。
- 后台配置使用复选框,不直接编辑 JSON。
5.6 需求 10:Gateway 限速
本期只提供统一的手动卡限速能力,不在套餐上配置固定限速,也不因套餐激活、到期、切卡或停机自动调用 Gateway。
限速最终对象始终是卡 ICCID:
flowchart TD
Trigger[后台手动设置或取消] --> Asset{资产类型}
Asset -->|卡| ICCID[读取卡 ICCID]
Asset -->|设备| Binding[查询 is_current=true 的当前卡]
Binding --> Exists{当前卡有效?}
Exists -->|否| Reject[拒绝操作并记录原因]
Exists -->|是| ICCID
ICCID --> Gateway[SetSpeedLimit cardNo, speedKbps]
Gateway --> Audit[记录操作审计和 Gateway 结果]
数据和契约:
- 应用层只接收
speed_kbps:正数为设置,0为取消;内部单位固定为kbps。 - 统一接口:
POST /api/admin/assets/{identifier}/speed-limit。资产为设备时解析当前绑定卡,不存在当前卡则拒绝,绝不把设备 IMEI 传给 Gateway。 - Gateway 端口只有
SetSpeedLimit(cardNo, speedKbps);取消仍调用同一端口,适配器按上游最终契约转换取消参数。 - 不新增
tb_package.speed_limit_kbps、SpeedLimitApplyRequestedOutbox 或自动补偿 Worker。本期也不宣称能展示 Gateway 当前实际限速,除非上游另提供查询接口。 - 卡详情和设备详情都提供设置/取消入口;设备入口明确显示“当前使用卡 ICCID”。每次操作记录资产、最终
cardNo、目标值、操作人、请求结果和错误摘要。
5.7 需求 14:统一导出与字段权限
复用现有 tb_export_task + DataSource + Asynq,新增或扩展以下场景:
| Scene | 内容 |
|---|---|
iot_card |
增加套餐名称、已用流量、剩余流量 |
agent_wallet_transaction |
代理主钱包流水 |
package |
套餐基础、流量、价格、状态和审计字段 |
refund |
退款业务字段、动态审批摘要和凭证 |
exchange |
换货新旧资产、收货和物流字段 |
agent_recharge |
充值、余额、状态、审批摘要和凭证,不含支付通道 |
expiring_asset |
临期资产、套餐、到期和流量字段 |
统一创建接口:
POST /api/admin/export-tasks
body: scene + format + query + fields
字段权限:
resolved_fields = 用户申请字段
∩ 角色授权字段并集
∩ 场景支持字段
新增 tb_role_export_field_permission(role_id, scene, field_key),三列唯一。超级管理员拥有全部字段;普通角色从授权并集计算。数据行范围继续使用现有权限,字段授权不能扩大数据范围。
API:
GET /api/admin/export-fields?scene={scene}
GET /api/admin/roles/{role_id}/export-fields
PUT /api/admin/roles/{role_id}/export-fields
- 创建任务时快照最终字段和表头,Worker 不能因后续角色变化扩大字段。
- 权限解析失败时拒绝导出,不能回退到全字段。
- Count 和 Fetch 必须使用相同权限及查询条件。
- 退款和充值审批摘要按本批实例批量查询,禁止 N+1。
- 导出审批附件只输出数量,不输出对象 Key 或永久 URL。
5.8 需求 22:套餐临期提醒
临期定义:按 Asia/Shanghai 自然日计算,当前生效套餐剩余 0~15 天,且资产没有可接续排队套餐。已过期资产不按 0 天计入临期。需求 06、11、22 共用查询侧日期计算,但需求06另行推算排队套餐接续后的预计最后到期时间。
| 剩余天数 | 展示颜色 | 通知节点 |
|---|---|---|
| 8~15 天 | 粉红色 | 15 天 |
| 4~7 天 | 紫色 | 7 天 |
| 0~3 天 | 黄色 | 3 天 |
flowchart TD
Schedule[每日定时任务] --> Query[批量查询实时临期候选]
Query --> Queue{存在排队套餐?}
Queue -->|是| Skip[不临期、不通知]
Queue -->|否| Days[计算剩余自然日]
Days --> Node{存在最近未发送阈值?}
Node -->|是| Notify[按使用记录+节点+接收人幂等通知]
Node -->|否| End[结束]
后端:
- 卡/设备列表和详情增加
days_until_expiry、is_expiring。 - 列表、详情、临期页、代理首页和 C 端均实时 SQL 计算,不读取每日任务快照,也不需要前端轮询临期状态。
- 新增
GET /api/admin/expiring-assets,支持资产、套餐、店铺、到期范围和剩余天数筛选。 - 代理首页增加临期卡/设备数量。
- C 端
GET /api/c/v1/asset/info增加临期字段并提供续费入口。 - 临期导出复用
scene=expiring_asset。 tb_expiry_push_record从本期起按package_usage_id + recipient + channel + node防重;漏跑后只补发当前最近且尚未发送的阈值,避免一次补发多条旧通知。
六、需求16移出记录与需求17~21方案
6.1 需求 16:代理分销码与佣金提现(已移出本期)
状态:2026-07-14 移出 7 月迭代
本期不建设分销码、H5 代理申请、代理申请审批、自动开店、发展人关系和佣金提现材料。本需求后续独立立项时重新评审数据模型、H5 安全、审批接入、开店幂等、提现材料和工时。
本期审批流仅接入退款和平台员工线下充值;迁移、API、前端、发布及工时均不包含需求 16。
6.2 需求 17:代理主钱包信用额度
平台员工不是结算和负债主体,不建立员工钱包或信用额度。角色只表达权限,不能承载财务余额和债务。
不变量
effective_credit = credit_enabled ? credit_limit : 0
available = balance - frozen_balance + effective_credit
available >= 0
- 信用额度只作用于代理主钱包,不作用于佣金钱包和资产钱包。
- 余额允许为负,但不能突破可用额度边界。
- 关闭信用时额度必须为 0;开启信用时额度必须大于 0。
- 存在欠款或冻结金额导致修改后可用金额小于 0 时,禁止降低额度或关闭信用。
- 扣款、冻结、解冻、充值、退款回充、调额、版本和资金流水必须维护同一不变量。
数据约束
tb_agent_wallet 增加:
credit_enabled BOOLEAN NOT NULL DEFAULT FALSE
credit_limit BIGINT NOT NULL DEFAULT 0
数据库 CHECK 至少保证:
CHECK (frozen_balance >= 0),
CHECK (credit_limit >= 0),
CHECK (
(credit_enabled AND credit_limit > 0)
OR (NOT credit_enabled AND credit_limit = 0)
),
CHECK (
balance - frozen_balance
+ CASE WHEN credit_enabled THEN credit_limit ELSE 0 END >= 0
)
迁移前必须核对生产库现有约束名和历史异常数据,不能仅依赖 DROP CONSTRAINT IF EXISTS。
用例与 API
| 用例 | 规则 |
|---|---|
| 修改额度 | 校验权限、加载主钱包、校验新可用金额、按 version 条件更新、写信用变更审计 |
| 钱包扣款 | 使用有效额度计算可用金额,同事务更新余额、版本和资金流水 |
| 查询/导出 | Query 返回 credit_enabled、credit_limit、available_balance、is_in_debt、debt_amount |
POST /api/admin/shops 创建代理时可初始化信用配置
PUT /api/admin/shops/{id}/credit-limit 独立调整信用额度
GET /api/admin/shops/fund-summary 返回信用和可用金额
前端只展示接口返回的可用金额,不自行重新计算。额度调整弹框显示修改前后金额预览;并发冲突时刷新最新钱包版本。
6.3 需求 18:多人审批业务映射
多人审批由企业微信模板负责,本系统不解析审批角色、账号、部门或会签规则:
| 需求 | 方案 |
|---|---|
| 多级、会签、或签 | 在企微模板中配置,本系统使用 use_template_approver=1 |
| “部门领导→财务” | 可以作为企微模板中的组织规则,本系统不建立部门模型也不推导审批人 |
| 当前节点待办 | 由企业微信自身提醒,本系统不重复生成站内待审批任务 |
| 通过/驳回/撤销通知 | 状态同步后向申请人生成站内结果通知 |
| 意见和附件 | 从 getapprovaldetail 保存审批详情快照并在业务详情只读展示 |
| 历史版本 | 本地保存模板 ID、控件映射和提交快照,历史实例不受新模板影响 |
平台员工必须先完成系统账号与企微成员扫码绑定,发起审批使用绑定的 userid;普通运营不查询或录入成员 ID。
6.4 需求 19:批量订购套餐
规则和流程
- 支付方式整批选择
offline或agent_wallet,Excel 不包含支付方式。 - 混合支付必须拆成不同批次。
- 模板字段:资产类型、资产标识、套餐编码、套餐名称;实际匹配使用套餐编码。
- 任务允许部分成功,每行是独立、可重试、可审计的业务单元。
- 文件最大 10MB、最多 1000 行;失败明细最多保存 1000 条。
flowchart TD
Submit[选择代理、支付方式、凭证并上传] --> Task[创建任务]
Task --> Parse[解析并持久化逐行明细]
Parse --> Item{处理下一行}
Item -->|钱包| Wallet[锁钱包并校验有效可用金额]
Item -->|线下| Voucher[校验整批凭证快照]
Wallet --> Order[单行事务:订单+扣款+流水+明细]
Voucher --> OfflineOrder[单行事务:已支付订单+明细]
Order --> More{还有明细?}
OfflineOrder --> More
More -->|是| Item
More -->|否| Summary[从明细汇总任务结果]
数据模型
| 表 | 关键字段 |
|---|---|
tb_bulk_purchase_task |
任务号、request_id、文件 Key、代理、支付方式、凭证快照、金额和数量汇总、状态、处理租约 |
tb_bulk_purchase_item |
行号、资产、套餐快照、金额、状态、订单 ID、错误码、错误原因、幂等键 |
任务状态:1=待处理, 2=处理中, 3=完成, 4=部分成功, 5=失败。
明细状态:1=待处理, 2=处理中, 3=成功, 4=失败。
必须具备:
- 创建接口使用
request_id唯一约束,重复提交返回原任务。 - Worker 通过状态条件和处理租约领取任务/明细,防止并发消费者重复执行。
- 行幂等键:
bulk_purchase:{task_id}:{row_no}。 - 钱包可用金额统一使用
credit_enabled后的有效信用额度,禁止直接无条件加credit_limit。 - 钱包余额、版本、订单、资金流水和明细成功状态在同一事务。
- 单行失败不回滚其他成功行,任务统计从明细表重新聚合。
- 批量订购必须复用需求 15 的套餐可售策略,不能绕过下架续费限制。
API:
POST /api/admin/bulk-purchases
GET /api/admin/bulk-purchases/{task_id}
GET /api/admin/bulk-purchases/{task_id}/items
线下凭证仅作为本批次业务资料和审计快照,本期不校验跨批次唯一性或建立财务核销规则。
6.5 需求 20:退款审批
退款金额在申请时完成合法性校验并固定,企微审批不允许修改金额。退款凭证先保存本地对象存储,再上传企微副本;审批人可在企微看到业务字段、提交备注和附件。
退款状态在历史枚举基础上追加 5=已撤销/审批已删除,保留历史 4=已退回 语义但新企微审批不再产生退回状态。独立保存业务处理结果,避免“企微已通过但代理钱包回溯或资产处理失败”被展示为全部完成。
sequenceDiagram
actor User as 平台员工/财务
participant Refund as Refund Application
participant WeCom as 企业微信审批
participant Sync as WeCom Approval Sync
participant DB as PostgreSQL
participant Worker as Refund Terminal Worker
User->>Refund: 创建退款申请
Refund->>DB: 保存退款单、业务快照和企微实例
Refund-->>WeCom: 异步提交申请和附件
User->>WeCom: 审批;非代理钱包先完成人工退款
WeCom-->>Sync: 回调/轮询同步终态
Sync->>DB: 保存审批详情并写终态Outbox
DB-->>Worker: 首次通过时处理业务终态
Worker->>DB: 代理钱包回溯、订单/佣金/资产处理
关键规则:
- 微信、支付宝、线下等非代理钱包支付由财务在系统外人工退款后再通过企微;企微通过即表示人工退款已确认,本系统不调用渠道退款 API,也不再提供本地
manual-complete二次确认。 - 代理钱包支付订单在企微通过后按原扣款流水定位原代理主钱包,幂等回溯并写退款流水;可自然冲减负余额。
- 个人客户或资产钱包不自动回款,现有
BuyerTypePersonal自动资产钱包退款分支在迁移时删除或隔离。 - 驳回时本地状态改为已拒绝;撤销/删除时改为已撤销,可修改业务资料后重新申请。
- 通过后撤销且资金已执行时不自动冲正,记录
critical审计、站内告警并人工处理;资金尚未执行时终止后续任务。 - 原退款业务单级
approve/reject/return路由下线,不存在本地审批动作 API。
重新申请:
POST /api/admin/refunds/{id}/resubmit
只允许已驳回、已撤销或已删除且尚未完成退款的申请按原退款规则修改申请金额、凭证和原因,并创建 round_no + 1 的企微实例;订单、资产快照、提交人和历史审批不可修改。
前端只读展示企微审批状态、意见/附件和业务处理结果,不显示本地审批或人工退款确认按钮。
6.6 需求 21:代理在线充值与员工线下充值审批
本需求分为完全隔离的两条路径:代理在线扫码充值不审批;平台员工线下代充值走企业微信审批。
代理在线扫码充值
- 代理只能为当前店铺主钱包充值,最低
10000分(100 元)。 - 支持微信 Native 和支付宝
alipay.trade.precreate,创建本地充值单和tb_payment(order_type=agent_recharge)后再预下单。 - 后端统一返回
qr_content和过期时间,前端使用二维码组件渲染,不生成后端图片文件。 - 微信/支付宝回调按支付单类型分发,校验渠道、支付配置、金额、第三方交易号和业务单关联。
- 支付单、充值单、代理主钱包、钱包版本、唯一钱包流水和 Audit Event 在同一事务推进;重复回调只返回渠道成功,不重复加钱。
- 支付成功但钱包事务失败时保持已支付并由可靠任务补齐;二维码过期后关闭原充值单,重新生成必须创建新支付单。
GET /api/admin/agent-recharges/payment-methods
POST /api/admin/agent-recharges
GET /api/admin/agent-recharges/{id}/payment-status
代理在线充值详情固定 approval_source=none,前端每 3 秒轮询轻量支付状态,页面不可见时暂停。
平台员工线下代充值
创建线下充值后立即创建企微审批实例,不再暴露本地确认入账和驳回按钮,也不再校验操作密码。充值状态沿用现有 1~6:
| 状态 | 语义 |
|---|---|
| 1 | 待支付/线下待审批 |
| 2 | 已支付或审批通过后正在入账 |
| 3 | 已完成 |
| 4 | 已关闭 |
| 5 | 已退款 |
| 6 | 已驳回 |
充值处理状态:0=未触发, 1=处理中, 2=成功, 3=失败。
sequenceDiagram
actor Staff as 平台员工
participant Recharge as Recharge Application
participant WeCom as 企业微信审批
participant Sync as WeCom Approval Sync
participant DB as PostgreSQL
participant Worker as Recharge Worker
Staff->>Recharge: 创建线下充值
Recharge->>DB: 保存充值单、业务快照和企微实例
Recharge-->>WeCom: 异步提交审批
WeCom-->>Sync: 回调/轮询同步终态
Sync->>DB: 首次通过时写入账Outbox
DB-->>Worker: RechargeApproved
Worker->>DB: 同事务更新钱包、版本、流水和充值单
关键规则:
- 企微通过后自动增加代理主钱包余额并写钱包流水,无操作密码。
- Worker 使用
recharge:{recharge_no}幂等键和处理租约。 - 钱包余额、版本、充值单和资金流水必须在同一事务更新。
- 若资金流水已存在但充值单状态未完成,重试只补齐状态,不再次入账。
- 驳回改为已驳回;撤销/删除改为已关闭,可重新创建充值申请。
- 通过后撤销且已经入账时不自动扣回,记录严重异常并通知财务;尚未入账时终止任务。
旧接口在停机发布后不再注册:
POST /api/admin/agent-recharges/{id}/offline-pay
POST /api/admin/agent-recharges/{id}/reject
七、数据迁移、发布与回滚
7.1 迁移前检查
- 核对钱包现有 CHECK 约束名、负余额、冻结金额和异常数据。
- 检查同一设备多条
is_current=true绑定并先修复。 - 检查未删除卡
iccid_19重复,解决冲突后才能建立部分唯一索引。 - 检查
realname_link_type!=none但资产realname_policy=none的冲突数据并明确修正结果。 - 使用生产卡量测算活跃/不活跃轮询间隔对应的 Gateway QPS 和最长兜底延迟。
- 核对存量退款的支付方式和买家类型;仅将代理钱包订单接入自动回退,个人资产钱包订单不得误入该处理器。
- 统计待审批退款、平台员工线下充值和历史终态记录数量。
- 轮换用户 demo 中泄露的企微 Secret、Token 和 EncodingAESKey,配置可信域名、应用可见范围和回调地址。
- 发布并验证退款、线下充值企微模板控件映射;要求会发起审批的平台员工完成扫码绑定。
- 验证微信 Native、支付宝 PreCreate 配置和回调地址,确认代理充值支付渠道可用。
- 盘点旧账号、资产、轮询日志的写入口和查询入口,确认统一审计切换清单。
- 初始化角色导出字段的最小权限集,禁止默认放开敏感字段。
7.2 迁移清单
| 类型 | 表/对象 | 变更 |
|---|---|---|
| 新建 | tb_system_config |
受控动态配置 |
| 新建 | tb_notification |
分类、级别、受控跳转和接收人幂等 |
| 新建 | tb_wecom_approval_scene、tb_wecom_approval_template_version |
稳定场景和不可变模板映射版本 |
| 新建 | tb_wecom_approval_instance、tb_account_wecom_mapping |
企微审批镜像和账号扫码绑定 |
| 新建 | tb_audit_event、tb_audit_event_resource |
全局不可变业务审计和多资源关联 |
| 新建 | tb_integration_log |
Gateway、运营商、企微和支付渠道交互 |
| 新建/确认 | tb_outbox_event |
可靠事件投递 |
| 新建 | tb_device_batch_allocation_task |
设备批量分配任务 |
| 新建 | tb_role_export_field_permission |
角色导出字段权限 |
| 新建 | tb_expiry_push_record |
15/7/3 天临期通知防重 |
| 新建 | tb_bulk_purchase_task、tb_bulk_purchase_item |
批量订购任务和逐行明细 |
| 修改 | tb_device |
实名状态快照 |
| 修改 | tb_iot_card/轮询调度状态 |
活跃级别、最后活跃/上游变化信息;高频调度心跳不写核心表 |
| 修改 | tb_polling_config |
收口为全局活跃/不活跃间隔,不再按卡类别形成轮询资格 |
| 修改 | tb_iot_card.iccid_19 |
未删除数据范围 Partial Unique Index |
| 修改 | tb_shop_package_allocation、tb_package_usage |
生效条件、周期和时长快照 |
| 修改 | tb_exchange_order |
提交人快照;新建记录修正资产标识快照 |
| 修改 | tb_agent_wallet |
信用开关、额度和 CHECK 约束 |
| 修改 | tb_refund_request |
提交人、企微实例轮次、撤销状态和终态处理结果 |
| 修改 | tb_agent_recharge_record |
提交人、企微实例、支付状态和入账处理结果 |
| 修改 | tb_payment |
支持 order_type=agent_recharge 和创建时支付配置快照 |
迁移必须拆成可回滚的增量文件;同一表和同一索引只由一个迁移创建。旧账号、资产和轮询日志表保留只读,不回填新表、不再产生新写入。
7.3 发布顺序
flowchart LR
M[进入维护模式并停止写入] --> DB[执行增量迁移]
DB --> Deploy[同时发布 API、Relay/Worker、前端]
Deploy --> WeCom[发布企微模板映射和扫码绑定]
WeCom --> Backfill[提交存量待审批单到企微]
Backfill --> Verify[人工验证核心链路]
Verify --> Open[解除维护模式]
详细步骤:
- 停止订单、退款、充值、资产状态和配置相关写入,暂停旧 Worker。
- 创建企微、审计、通知、批量任务、权限和同步调度所需表/索引,并增加业务关联字段。
- 同时发布 API、Relay/Worker 和前端,统一切换 Audit Writer、Integration Log 和 Access Log 脱敏。
- 发布两个企微模板映射版本,验证连接、回调、状态查询和员工扫码绑定。
- 使用一次性 Application 命令为存量待审批退款和平台员工线下充值创建企微实例;未绑定创建人的记录进入迁移待处理列表。
- 历史终态记录保留
approval_source=legacy,不得伪造企微实例或审批时间线。 - 验证数据同步三通道、19/20 位 ICCID 回调、代理微信/支付宝扫码充值和统一审计查询。
- 确认旧退款审批、充值
offline-pay/reject、旧审计写入口和重复卡状态写逻辑不可访问。 - 完成人工验证后恢复 Worker 并解除维护模式。
7.4 回滚边界
- 开放访问前验证失败:可以回滚应用版本和可逆迁移。
- 已提交到企业微信的审批无法通过回滚本地版本撤销;必须继续保留实例并同步终态。
- 已形成的企微实例、审计、Integration Log、资金流水、Outbox 和通知不得清表回滚。
- Worker 暂停时保留 Outbox,恢复后继续消费。
- 信用额度启用并产生负余额后,不能删除字段或直接关闭信用;必须先处理欠款或继续保留新资金逻辑。
- 已部分成功的批量任务不能整体回滚;通过明细和资金流水逐条追踪。
- 支付渠道已经成功但钱包尚未入账的充值单必须由恢复任务完成,不能要求代理重新支付。
八、风险与已确认约束
本轮评审结论已经收口,以下约束直接进入实施,不再要求实施人员自行猜测:
| 编号 | 已确认结论 |
|---|---|
| C-01 | 限速内部单位固定 kbps;本期仅人工设置/取消,Gateway 取消参数仅由适配器处理。 |
| C-02 | 实名策略批量单次最多 500 条,事务内全成全败。 |
| C-03 | 设备批量分配文件最大 10MB、最多 1000 行、每批 200 条、失败明细最多 1000 条,且一任务只做一种分配操作。 |
| C-04 | 批量订购线下凭证仅保存业务资料快照,不校验跨批次唯一性或财务核销。 |
| C-05 | 套餐导出固定采用实际列举的 25 个稳定字段 Key。 |
| C-06 | 钱包流水套餐名称优先使用订单项不可变快照,历史缺失时才读取 metadata;禁止关联可修改的当前套餐名称。 |
| C-07 | 临期按 Asia/Shanghai 自然日计算,0~15 天为临期;漏跑后仅补发当前最近且未发送的阈值通知。 |
| C-08 | 本系统不校验线下充值审批操作密码;审批人与流程规则由企微模板决定。 |
| C-09 | 退款、充值业务资料以本地对象存储 Key 为权威,企微 media_id 仅作为提交副本且不进入日志审计。 |
| C-10 | 企微 applyevent 请求已发送但结果未知时禁止盲目重试,必须人工核对后恢复。 |
| C-11 | 第三方退款本期人工处理;不增加渠道退款号、渠道结果或自动退款适配器。代理钱包退款仅回退原扣款代理主钱包。 |
| C-12 | H5 不增加全局实名策略默认值,新建卡/设备继续默认 after_order。 |
| C-13 | 行业卡实名由 realname_link_type 决定;19/20 位 ICCID 精确查对应列,不截断、不补位、不跨列降级。 |
| C-14 | 事件同步固定立即、3 分钟、5 分钟三次;Gateway 超频不退避,不删除后续任务。 |
| C-15 | 企微账号只允许扫码绑定;一个 userid 不能覆盖绑定到第二个系统账号。 |
| C-16 | 代理在线充值最低 100 元,支付成功直接入主钱包,不因金额大进入审批。 |
| C-17 | 全局审计本次停止旧表新写入;历史只读投影,不双写、不在线回填。 |
Gateway 的取消限速具体报文不是产品决策:上线前由上游接口契约确定,业务层始终只传 speed_kbps=0。
8.1 主要运行风险
| 风险 | 触发条件 | 应对 |
|---|---|---|
| 企微模板失效 | 模板或控件 ID 编辑后变化 | 场景暂停、模板版本、定期验证和原子切换 |
| 企微提交重复 | applyevent 超时后盲目重试 |
提交结果未知状态和人工绑定 sp_no |
| 审批回调漏失 | 网络或企微回调异常 | 审批中实例每 2 分钟查询详情兜底 |
| 账号绑定冲突 | 同一企微成员绑定多个账号 | userid 唯一约束、扫码会话和强制解绑审计 |
| 重复代理钱包回退或入账 | Outbox/Asynq 至少一次投递 | 稳定业务幂等键、资金流水唯一业务号、处理租约 |
| 通过后撤销 | 企微通过且资金已执行后撤销 | 不自动冲正,critical 审计、通知和人工处理 |
| 支付重复回调 | 微信/支付宝多次通知 | 支付单状态条件、钱包流水唯一键和业务幂等键 |
| 同步请求放大 | 查询、轮询和业务事件同时请求同一卡 | 同场景合并、单卡互斥、运营商最小间隔和活跃调频 |
| ICCID 错配 | 20 位截断或 19 位补位命中错误卡 | 双列精确路由和 iccid_19 部分唯一索引 |
| 审计数据量过大 | 高频轮询和全局写操作持续增长 | 时间索引、分批清理、Integration Log 短保留和后续月分区 |
| 批量重复下单 | 重复提交或并发 Worker | 创建请求幂等、任务/明细条件领取、行级唯一键 |
| 限速对象错误 | 设备无当前卡或错误把 IMEI 当卡号 | 统一解析当前卡 ICCID;无卡拒绝调用并记录审计 |
| 导出敏感字段泄露 | 权限缺失或 Worker 回退全字段 | 服务端字段交集、任务快照、失败关闭 |
| 临期重复通知 | 定时任务重试 | 稳定事件 ID 和接收人维度唯一约束 |
九、可观测性与人工验收
9.1 日志和查询维度
统一记录:request_id、correlation_id、event_id、integration_id、task_id、wecom_approval_instance_id、sp_no、biz_type、biz_id、operator_id。
关键监控:
- Outbox 待投递数、最早积压时间和失败次数。
- 企微 Token 刷新、模板失效、提交结果未知、回调失败、待审批轮询积压和业务处理失败。
- 账号扫码绑定成功/失败/冲突和未绑定申请人数量。
- 活跃/不活跃卡数量、各同步类型 QPS、Gateway 超频、连续失败和事件序列完成率。
- Audit Event/Integration Log 写入失败、增长速度、清理积压和敏感读取次数。
- 微信/支付宝预下单、回调失败、已支付未入账和重复回调数量。
- 批量任务成功/失败/部分成功数量。
- 钱包版本冲突、可用金额不足和数据库约束失败。
- Gateway cardNo、目标限速、请求结果和错误摘要。
- 导出字段快照、数据范围和文件生成失败。
9.2 人工验收矩阵
| 范围 | 必测场景 |
|---|---|
| 企微审批 | 模板发布/失效、扫码绑定、提交、回调、2分钟轮询、驳回/撤销/删除、提交未知恢复、意见附件和业务快照 |
| 数据同步 | 活跃调频、0/3/5 阶梯、同场景合并、单卡互斥、超频不退避、19/20位回调、解除实名忽略、旧入口收口 |
| 全局审计 | 关键事务失败回滚、人员/资源/请求/资金/风险/集成视角、历史投影、脱敏、旧表停止新写入 |
| 钱包 | 普通余额扣款、信用扣款、额度降低失败、并发版本冲突、负余额回充 |
| 批量订购 | 钱包/线下两种支付、部分成功、重复提交、Worker 重试、失败明细、模板错误 |
| 退款 | 金额发起时固定、财务人工退款企微确认、代理钱包回退原主钱包、个人资产钱包不误入、通过后撤销不冲正 |
| 充值 | 微信/支付宝最低100元扫码、重复回调不重复入账、已支付恢复、在线不审批、线下企微通过自动入账且无操作密码 |
| 导出 | 普通角色、敏感字段角色、超级管理员、权限为空、审批摘要批量查询 |
| 限速 | 单卡、设备当前卡、无当前卡、设置、取消、Gateway 失败和审计记录 |
| 临期 | 有/无排队套餐、15/7/3 天、漏跑补发、重复定时任务、不同接收人、列表/详情/C端一致 |
| 发布 | 存量退款和充值回填、历史 legacy、旧路由不可访问、Worker 暂停后恢复 |
验收使用接口调用、PostgreSQL 数据核对、日志检查和页面操作,不以自动化测试作为本项目交付前提。
十、实施顺序
1. 增量迁移、事务管理、Outbox、统一审计模型和 Access Log 脱敏
2. 站内通知与审计多视角 Query/前端
3. 卡状态领域、轮询活跃调频、事件阶梯和运营商回调防腐层
4. 企微连接、模板版本、账号扫码绑定、提交/回调/轮询
5. 退款和平台员工线下充值接入企微,代理微信/支付宝扫码充值
6. 钱包信用额度、批量订购、导出权限、设备批量分配、限速和临期提醒
7. 其他查询、字段和显示修复、存量回填和停机发布演练
每个复杂用例按“迁移 → Application/Domain → Infrastructure → Handler → 前端 → 人工验收”形成完整闭环,不在同一任务中顺带重构未触碰模块。
十一、工时估算与排期
11.1 估算口径
- 1 人日按 8 小时计算。
- 当前固定投入为 1 名后端和 1 名前端,两人并行开发,目标自然周期为 11~15 个工作日。
- 包含本期企业微信审批、数据同步重构、全局审计一次性切换、代理微信/支付宝扫码充值及原 7 月迭代范围。
- 不包含套餐临期企业微信消息推送、自动第三方退款、运营商解除实名回滚和需求16。
- 包含后端、前端、第三方联调、人工接口验证、PostgreSQL 数据核对、存量回填和停机发布;不单独预留长期缓冲。
- 假设对象存储、ExportTask、Asynq、钱包、退款、充值、微信支付和支付宝 SDK 基础代码可以复用,但企微和支付渠道仍需要真实环境联调。
- 采用快速交付口径:优先复用现有 Store、支付、Asynq、导出和页面组件,DDD 只迁移本次触碰的复杂写用例,不追加非必要重构。
- 全局审计仍一次性切换写入口,但第一版页面和 Query 只实现评审稿列出的核心视角,不增加额外分析报表。
- AI 用于代码生成、批量迁移和文档核对;验收优先覆盖资金、企微、回调、同步和权限主链路。
11.2 人日估算
| 工作包 | 后端人日 | 前端人日 | 快速交付说明 |
|---|---|---|---|
| 迁移、Outbox、全局审计和站内通知 | 2 | 2 | 复用现有日志、列表和抽屉组件,先完成核心多视角 |
| 数据同步领域收口、活跃轮询和运营商回调 | 2~3 | 1 | 后端为主,前端只补状态和审计跳转 |
| 企微模板、扫码绑定、提交/回调/轮询 | 2~3 | 2~3 | 复用企微官方页面和现有业务详情布局 |
| 退款、线下充值终态和代理扫码充值 | 2~3 | 2~3 | 复用现有钱包、支付回调和充值页面 |
| 信用、批量、导出、限速、临期及其他需求 | 2~2.5 | 2~3 | 多个小需求并行处理,避免额外抽象 |
| 联调、数据核对、回填和停机发布 | 1~1.5 | 1~2 | 随开发持续联调,最后集中验证核心链路 |
| 合计投入 | 11~15 | 10~14 | 总投入约 21~29 人日,两人并行 11~15 个工作日 |
排期基线按 13 个工作日,对外承诺范围为 11~15 个工作日。如真实企微/支付配置无法按时提供、生产 ICCID 或审计旧入口存在大量异常,问题记录为外部阻塞,不在开发阶段扩展额外重构范围。
11.3 当前团队排期
当前团队固定为 1 后端 + 1 前端,两人从第一天开始并行,预计 11~15 个工作日。推荐节奏:
第 1~2 天:迁移、Outbox、审计/通知骨架,前端同步搭建页面框架
第 3~5 天:数据同步收口、活跃轮询、回调防腐层和审计外部集成视角
第 4~7 天:企微模板、扫码绑定、审批提交/回调/轮询及前端配置页面
第 6~9 天:退款、线下充值终态、代理微信/支付宝扫码充值
第 8~11 天:信用、批量、导出、限速、临期和其他小需求
第 11~13 天:全链路联调、数据核对、旧入口清理和存量回填演练
第 14~15 天:问题修复和停机发布缓冲;进度稳定时可提前
十二、评审结论记录
| 评审项 | 结论 | 调整项 | 负责人 |
|---|---|---|---|
| 本期范围与移出项 | 待评审 | ||
| 渐进式 DDD 边界 | 待评审 | ||
| 数据同步三通道与运营商回调 | 待评审 | ||
| 企业微信审批、模板版本与账号绑定 | 待评审 | ||
| 全局多视角审计与历史投影 | 待评审 | ||
| 站内通知与受控跳转 | 待评审 | ||
| 资金与信用额度 | 待评审 | ||
| 代理微信/支付宝扫码充值 | 待评审 | ||
| 批量任务和导出权限 | 待评审 | ||
| Gateway 限速和临期提醒 | 待评审 | ||
| 前端交互和权限 | 待评审 | ||
| 停机发布和回滚 | 待评审 |
评审通过条件:第八章约束全部纳入实施任务;除 Gateway 上游取消参数和真实第三方联调结果外,不存在需要实施人员自行猜测的数据模型、接口、状态语义或资金规则。