VWCG-1121 充值赠送账户 MVP 技术方案
版本:V0.9(评审稿)
日期:2026-07-27
适用仓库:store、ai_api、ai_app
风险等级:高
结论状态:有条件可实施,7 项产品/范围决策确认后进入开发
1. 方案摘要
本需求不是单纯增加一个 gift_balance 字段。要完整满足“充值赠送、不可提现、全渠道消费、可配置扣款顺序、原路退款”,必须同时改造账户、充值、消费、退款和对账链路。
推荐方案:
- 在
yoshop_user增加赠送余额,并把现有现金余额内部拆为“可提现现金 + 活动本金”。 - 新建充值赠送规则表与赠送流水表,充值订单复用三金额字段并保存命中规则快照。
- 新建账户结算单作为消费/退款幂等锚点,统一 PC、消费设备和小程序扣款算法。
- 在
ydy_meal_order增加赠送支付、赠送退款、赠送剩余金额及扣款顺序快照。 ydy_config保存全局扣款顺序,暂定默认值为“餐补 → 现金 → 赠送”;只影响新订单。- 跨账户库与业务库采用“账户侧幂等提交 + 业务侧重试同步 + 对账补偿”,不依赖伪跨库事务。
- 充值订单、消费订单及相关人员/部门/设备统计同步调整异步导出。
2. 关键冲突与开发前决策
| 编号 | 问题 | 暂定方案 | 是否阻断开发 |
|---|---|---|---|
| D1 | 云效 PRD 默认“餐补→赠送→现金”,本轮输入默认“餐补→现金→赠送” | 以本轮输入为暂定值:subsidy,cash,gift |
是 |
| D2 | 命中赠送活动的充值本金不可提现,但现有 balance 没有本金属性 |
新增 withdrawable_balance、activity_principal_balance,且两者之和等于 balance |
是 |
| D3 | 部分退款如何恢复账户 | 建议按该订单实际扣款逆序恢复,确保每次退款可复算、可追溯 | 是 |
| D4 | 旧充值套餐赠送与新赠送活动关系 | 同一订单只能命中一种赠送机制;建议新活动表成为统一入口,旧套餐迁移后停用 | 是 |
| D5 | PC/小程序之外的旧 H5 充值是否改造 | 本期默认不含旧 H5,但上线前必须确认是否仍有真实流量 | 是 |
| D6 | source=10 是否直接产生扣款 |
先确认主/聚合订单是否只是父单;父单不得重复扣款 | 是 |
| D7 | PRD 的 scanPay.vue 在当前 ai_app 分支不存在 |
确认目标分支或真实页面后再排期 | 是 |
3. 范围与非范围
3.1 本期范围
- PC 机构人员列表显示赠送余额。
- PC 账户设置新增赠送活动、扣款顺序设置及操作日志。
- PC 和小程序充值展示并记录支付金额、赠送金额、实际到账金额。
- 充值成功后现金与赠送余额、流水分别入账。
- PC、小程序、绑盘机、消费机、虚拟订单、外部订单、闸机、点餐机共用账户结算规则。
- 消费订单展示和保存现金、餐补、赠送拆分。
- 全额/部分退款按原订单实际来源恢复。
- 相关统计、流水、导出与人工验收。
3.2 本期非范围
- 赠送余额有效期、冻结、转赠、提现。
- 按商品、餐次、人群、部门等复杂赠送活动条件。
- 多活动叠加。
- 对历史订单重算赠送金额。
- 把两套数据库合并为一个数据库。
4. 系统边界与目标架构
4.1 设计原则
- 账户库是余额事实源,业务订单库保存业务状态与账户拆分快照。
- 所有金额使用
decimal(10,2),服务层使用字符串/定点运算,禁止浮点比较。 - 客户端预览只用于展示,创建充值订单时必须由服务端重新匹配规则。
- 消费与退款都以业务订单号作为幂等键。
- 当前配置只用于新订单;历史订单退款只使用订单快照。
- 新接口字段采用增量兼容,旧客户端可忽略。
5. 账户与金额模型
5.1 账户定义
| 账户 | 用户可见 | 可消费 | 可提现 | 有效期 | 说明 |
|---|---|---|---|---|---|
| 餐补 | 是 | 是 | 否 | 沿用现状 | 继续以有效补贴明细合计为事实值 |
| 个人现金 | 是 | 是 | 部分可提现 | 永久 | 展示值仍为 balance |
| 可提现现金 | 可在提现页展示 | 是 | 是 | 永久 | withdrawable_balance |
| 活动本金 | 可在明细中标识 | 是 | 否 | 永久 | 命中赠送活动时的 pay_price |
| 赠送余额 | 是 | 是 | 否 | 永久 | gift_balance |
5.2 核心不变量
actual_money = pay_price + gift_money
balance = withdrawable_balance + activity_principal_balance
usable_total = valid_subsidy + balance + gift_balance
meal_order.pay_price = subsidy + cash + gift
cash = withdrawable_cash_payment + activity_principal_payment
累计退款金额 <= 原订单各账户实际扣款金额建议现金账户消费内部顺序为“活动本金 → 可提现现金”,避免用户先消耗可提现资金而留下不可提现本金。这个内部顺序不改变前台显示的“现金”一级账户顺序,但须保存内部拆分以支持提现与退款。
6. 数据库设计
以下为逻辑设计,正式 DDL 应按项目数据库版本、索引长度和发布时间另行生成。
6.1 账户库 yoshop_*
yoshop_user
| 字段 | 类型 | 默认值 | 用途 |
|---|---|---|---|
gift_balance |
decimal(10,2) |
0.00 |
赠送余额 |
withdrawable_balance |
decimal(10,2) |
迁移确定 | 可提现现金 |
activity_principal_balance |
decimal(10,2) |
0.00 |
活动本金 |
约束:
- 所有余额字段非负。
- 每次现金变动后检查
balance = withdrawable_balance + activity_principal_balance。 subsidy_balance继续兼容展示,但有效补贴以补贴明细合计为准。
yoshop_recharge_gift_rule(新增)
| 字段组 | 建议字段 | |
|---|---|---|
| 主键与租户 | rule_id、store_id |
|
| 展示 | rule_name |
|
| 匹配条件 | <<<<<<< HEADpay_method、min_money |
=======
platform、pay_method、min_money |
>>>>>>> 5d05833ef476571c794ef3016336bc136526872c
| 奖励 | gift_money |
|
| 状态 | status、is_delete |
|
| 审计 | create_user_id/name、update_user_id/name、create_time、update_time |
匹配规则:
- 只匹配当前门店、启用且未删除的规则。
pay_price >= min_money。
<<<<<<< HEAD
- 支付方式仅允许
cash、alipay_offline、wechat_offline、online_scan,规则不设置适用端。
=======
- 平台、支付方式匹配;“全部”用明确枚举值表达。 >>>>>>> 5d05833ef476571c794ef3016336bc136526872c
- 多条命中时依次取:
min_money最大、gift_money最大、rule_id/create_time最新。 - 同一充值订单最多命中一条规则。
推荐索引:
-
<<<<<<< HEAD
(store_id, status, is_delete, pay_method, min_money)
=======
(store_id, status, is_delete, platform, pay_method, min_money)
>>>>>>> 5d05833ef476571c794ef3016336bc136526872c
- 列表页
(store_id, is_delete, create_time)
yoshop_recharge_order(复用并扩展)
继续复用:
pay_pricegift_moneyactual_money
建议增加快照字段:
| 字段 | 用途 | |
|---|---|---|
gift_rule_id |
命中规则 ID,未命中为 0 | |
gift_rule_snapshot |
<<<<<<< HEAD
规则名、门槛、赠送金额、支付方式 JSON 快照 | =======规则名、门槛、赠送金额、平台、支付方式 JSON 快照 | >>>>>>> 5d05833ef476571c794ef3016336bc136526872c
withdrawable_amount |
本次进入可提现现金的金额 | |
activity_principal_amount |
本次进入活动本金的金额 |
规则:
- 普通充值:
withdrawable_amount = pay_price,activity_principal_amount = 0。 - 命中赠送:
withdrawable_amount = 0,activity_principal_amount = pay_price。 gift_money只进入gift_balance。- 旧订单四个新增字段默认 0/空,维持历史含义。
yoshop_user_gift_log(新增)
| 字段组 | 建议字段 |
|---|---|
| 主键与租户 | log_id、store_id、user_id |
| 变动 | scene、money、balance_before、balance_after |
| 业务定位 | biz_type、biz_order_id、biz_order_no |
| 幂等 | idempotency_key |
| 审计 | describe、remark、operator_id/name、create_time |
场景建议:
- 10:充值赠送
- 20:消费扣款
- 30:后台调整(本期若无入口则保留枚举、不开放)
- 40:消费退款
- 50:充值退款冲正
对 idempotency_key 建唯一索引,避免回调/消费/退款重复入账。
yoshop_account_settlement(新增)
用于跨库消费和退款的幂等、重试与对账,不取代现有账户流水。
| 字段组 | 建议字段 |
|---|---|
| 业务键 | settlement_id、biz_type、biz_order_no、refund_no |
| 主体 | user_id、store_id、order_source |
| 金额 | total_money、subsidy_money、cash_money、gift_money |
| 现金内部 | withdrawable_money、activity_principal_money |
| 快照 | deduction_order_snapshot、account_before_snapshot、account_after_snapshot |
| 状态 | status:处理中/账户已提交/业务已同步/失败待处理 |
| 重试 | retry_count、last_error、next_retry_time |
| 时间 | create_time、update_time、completed_time |
唯一键:
- 消费:
(biz_type, biz_order_no) - 退款:
(biz_type, biz_order_no, refund_no)
6.2 业务库 ydy_*
ydy_config
新增配置:
key: account_deduction_order
value: ["subsidy","cash","gift"]
desc: 消费账户扣款顺序校验规则:
- 必须恰好包含
subsidy、cash、gift三项。 - 不允许重复或未知账户。
- 缺失、关闭或非法时回退到
["subsidy","cash","gift"]并告警。 - 保存后使相关缓存失效。
建议新增 ydy_account_config_log,记录配置键、变更前、变更后、操作人、时间、IP,满足账户设置审计要求。
ydy_meal_order
| 字段 | 用途 |
|---|---|
gift |
本单赠送余额支付金额 |
gift_refund_amount |
已退赠送金额 |
remain_gift |
可退赠送金额 |
withdrawable_cash_payment |
本单消耗的可提现现金 |
activity_principal_payment |
本单消耗的活动本金 |
withdrawable_cash_refund |
已恢复的可提现现金 |
activity_principal_refund |
已恢复的活动本金 |
deduction_order_snapshot |
本单实际使用的一级账户顺序 |
account_settlement_id |
账户侧结算单 ID |
兼容规则:
- 新增金额字段默认
0.00,历史订单视为gift=0。 - 历史订单保留已有现金/餐补值,不回填虚构的扣款顺序。
- 历史订单退款按旧逻辑兼容;新订单以是否存在结算快照区分。
- 不修改
source既有枚举值。
ydy_meal_order_refund
增加:
giftwithdrawable_cashactivity_principalaccount_settlement_id
退款单保存本次实际恢复金额,不只保存申请金额。
7. 充值流程
7.1 服务端规则
- 预览结果不作为入账依据。
- 创建订单时重算并保存规则快照,后续规则修改不影响该订单。
- 支付回调按订单快照入账,不能再次匹配当前规则。
- 使用订单号/支付回调号做幂等,重复回调返回已有成功结果。
- 订单、用户余额、现金流水、赠送流水在同一账户库事务中提交。
- 回调失败必须向上抛出并进入渠道重试或内部补偿,禁止吞异常后返回成功。
7.2 三金额与流水
示例:支付 100 元,赠送 20 元。
| 项目 | 变动 |
|---|---|
pay_price |
100.00 |
gift_money |
20.00 |
actual_money |
120.00 |
balance |
+100.00 |
activity_principal_balance |
+100.00 |
gift_balance |
+20.00 |
| 现金流水 | +100.00,标记活动本金 |
| 赠送流水 | +20.00,场景为充值赠送 |
8. 消费结算流程
8.1 统一算法
输入:
- 用户、门店、业务订单号、订单来源、应付金额。
- 有效补贴明细、现金内部余额、赠送余额。
- 当前合法扣款顺序。
输出:
- 餐补、现金、赠送拆分。
- 现金内部的活动本金、可提现现金拆分。
- 扣款顺序、账户前后快照和结算单 ID。
伪代码:
remaining = payable
for account in configured_order:
used[account] = min(available(account), remaining)
remaining -= used[account]
if remaining > 0:
fail("账户余额不足")
if used.cash > 0:
used.activity_principal = min(activity_principal_balance, used.cash)
used.withdrawable = used.cash - used.activity_principal8.2 跨库一致性
规则:
- 账户扣款前锁定用户汇总行及需要使用的补贴明细。
- 同一业务订单只允许一个成功消费结算单。
- 账户库提交后,业务库更新失败时不得重复扣款;重试只同步原结算结果。
- 对账任务按“已提交但未同步”“业务已支付但无账户结算”“金额拆分不等”三类检测。
- 异常订单在设备/客户端返回“处理中”或明确失败,不能返回虚假成功。
8.3 所有下单入口接入
| 来源 | 当前入口 | 改造要求 |
|---|---|---|
| 1/2/3/6/7 | store/application/api/service/Consume.php |
同步与队列路径调用同一结算服务,删除重复算法 |
| 4 | store 与 ai_api 的 meal/PaySuccess.php |
统一调用结算服务;只保留渠道编排 |
| 4 的直接支付 | ai_api/app/api/service/meal/DirectPay.php |
移除独立 deductBalance() 规则 |
| 5 | 外部订单入口 | 纳入相同幂等键、余额检查与拆账 |
| 10 | 主/聚合订单 | 确认是否父单;父单不得与子单重复扣款 |
设备协议新增 gift_payment、remain_gift 等字段应保持可选,旧设备继续读取既有字段。
9. 退款与提现
9.1 消费退款
- 全额退款:按原订单各账户实际扣款金额全部恢复。
- 部分退款:建议从实际扣款顺序的末端向前恢复。例如订单按“餐补→现金→赠送”扣款 10/20/30,退款 35 时先恢复赠送 30,再恢复现金 5。
- 现金恢复时继续按原订单保存的
activity_principal_payment、withdrawable_cash_payment逆序恢复,不把活动本金误恢复为可提现现金。 - 重复退款以退款单号幂等;累计恢复不能超过原订单拆分。
- 配置修改不影响历史订单退款。
9.2 充值退款/提现
- 用户可提现金额上限为
withdrawable_balance,不能再仅依据balance或充值订单原始refundable_money。 - 命中赠送活动的充值:支付本金进入活动本金,赠送进入赠送余额,二者均不可提现。
- 普通充值:支付本金进入可提现现金。
- 充值退款如仍需支持,应先明确“已消费后的退款”规则,并在账户余额足够时按原充值来源冲正。
9.3 历史现金迁移
建议初始迁移:
withdrawable_balance = balance
activity_principal_balance = 0
gift_balance = 0该方案保持历史现金可提现能力,不倒推历史活动属性。若产品要求按 365 天或旧活动记录重建可提现金额,必须另立数据治理任务并做差异报表,本期不能凭现有余额字段可靠推导。
10. API 与页面改造
10.1 PC 后台
| 模块 | 改造 |
|---|---|
| 人员管理-机构人员 | 列表返回并展示 gift_balance;总余额口径明确为餐补+现金+赠送 |
| 账户设置-赠送活动 | 规则列表、新增、编辑、启停、删除、命中预览 |
| 账户设置-扣款顺序 | 三账户拖拽/排序、保存、默认值、操作日志 |
| 充值订单 | 展示/筛选/统计 pay_price、gift_money、actual_money 及命中规则 |
| 消费订单 | 展示现金、餐补、赠送及退款拆分 |
| 流水 | 增加赠送流水页或在统一流水中增加账户类型筛选 |
权限要求:
- 新页面和接口必须同时配置菜单、功能权限与数据权限。
- 控制器不能只因继承基类就假设权限完整;需核对
beforeActionList。 - 规则新增/编辑/启停、扣款顺序变更必须记录操作人、前后值和时间。
10.2 小程序 ai_app / ai_api
| 页面/接口 | 改造 |
|---|---|
| 用户资产接口 | 返回现金、可提现现金、活动本金、餐补、赠送及总可用余额 |
| 余额页 | 展示赠送余额和不可提现说明 |
| 充值页 | 输入金额后调用预览;展示支付、赠送、到账三金额 |
| 充值记录 | 展示三金额和命中活动 |
| 收银台 | 展示预计/实际账户拆分 |
| 订单详情 | 展示现金、餐补、赠送支付与退款 |
| 流水页 | 支持赠送账户流水与账户类型筛选 |
创建订单和支付成功响应建议增加:
{
"pay_price": "100.00",
"gift_money": "20.00",
"actual_money": "120.00",
"account_payment": {
"subsidy": "10.00",
"cash": "20.00",
"gift": "30.00"
},
"deduction_order": ["subsidy", "cash", "gift"]
}金额字段统一使用两位小数字符串,避免前端浮点误差。
11. 统计与异步导出
该列表页关联异步导出功能,需同步确认/修改异步导出逻辑。
| 关联列表 | 必须调整的口径 |
|---|---|
| 消费订单 | 增加赠送支付、赠送退款、赠送净额 |
| 人员消费明细 | 个人现金、餐补、赠送分列并校验合计 |
| 部门消费明细 | 汇总增加赠送支付与退款 |
| 设备消费明细 | 按设备/来源汇总增加赠送 |
| 充值统计 | 支付金额、赠送金额、实际到账分别统计,不能继续只汇总 actual_money |
菜品销量、餐厅经营汇总、档口营收结算是否受影响,取决于其收入口径是否只看订单实付总额。若只看订单总额且不展示账户来源,可不改金额口径,但仍需回归证明。
12. 安全、并发与审计
- 所有查询、规则、流水和配置按
store_id做租户隔离。 - 账户行、补贴批次按确定顺序加锁,降低死锁概率。
- 充值回调、消费、退款均有唯一业务幂等键。
- 后端拒绝负数、超过金额精度、非法账户顺序、越权门店和超额退款。
- 不信任客户端传入的赠送金额、扣款拆分或可提现金额。
- 流水不允许物理修改;冲正使用相反方向的新流水。
- 配置与规则变更写审计日志,记录操作前后值。
- 错误日志不记录支付密钥、数据库口令和完整个人敏感信息。
13. 兼容、上线与回滚
13.1 上线顺序
- 在影子环境执行 DDL 并验证表锁、耗时、索引和回滚脚本。
- 先部署后端兼容代码,新增字段读取为空时按 0/默认顺序处理。
- 执行用户现金属性迁移并产出迁移前后余额对账报告。
- 部署 PC 与小程序,保持赠送规则全部停用。
- 对内部测试门店启用一条小额规则,覆盖充值、消费、退款、导出。
- 观察对账无差异后逐门店启用。
13.2 监控
- 充值三金额不相等计数。
- 账户余额不变量异常数。
- 重复回调拦截数。
- 账户已提交但业务未同步数量与最长滞留时间。
- 订单实付与账户拆分差额。
- 退款超额/幂等拦截数。
- 各账户余额负数告警。
13.3 回滚
- 业务回滚:先停用全部赠送活动,将扣款顺序恢复默认;旧客户端继续使用原字段。
- 代码回滚:保留新增字段和表,不在有数据后直接删列;回滚版本应忽略赠送规则并阻止产生新赠送。
- 数据回滚:对已产生的赠送与消费不得直接覆盖余额,必须用冲正流水和结算单逐笔恢复。
- 跨库未同步:先停止新交易,完成结算单对账和补偿,再决定代码回滚。
- DDL 回滚:只在确认新增表/列无业务数据时执行;生产已入账后采用前向修复。
14. 测试覆盖矩阵
| 测试域 | 必测场景 | 自动化层级 |
|---|---|---|
| 规则匹配 | 门槛前/等于/超过;平台/方式;禁用;多规则排序;规则修改后订单快照不变 | 单元 + 接口 |
| 充值 | 无赠送/有赠送;三金额;现金/赠送双流水;重复回调;并发回调;回调异常不返回假成功 | 服务 + 集成 |
| 现金属性 | 普通本金可提现;活动本金不可提现;现金内部优先级;余额不变量 | 单元 + 集成 |
| 扣款顺序 | 6 种排列;单账户/多账户;临界余额;余额不足;非法配置回退 | 参数化单元 |
| 订单来源 | 1、2、3、4、5、6、7 逐一覆盖;10 依据决策 | 集成 + 契约 |
| 幂等并发 | 同一订单重复消费、设备重试、队列重放、并发请求 | 集成 |
| 跨库补偿 | 账户提交后业务库失败;重试不重复扣款;对账识别并修复 | 集成 + 故障注入 |
| 退款 | 全额、部分、重复、超额;配置变更后退款;现金内部属性原路恢复 | 服务 + 集成 |
| 历史兼容 | 老用户、老充值单、老消费单、空快照、赠送字段为 0 | 回归 |
| 权限 | 无菜单权限、无功能权限、跨门店数据、操作日志 | 接口 + 手工 |
| 导出统计 | 页面合计与导出合计一致;支付/赠送/到账不混淆 | 接口 + 文件比对 |
| 小程序/PC | 金额预览、确认页、订单详情、流水、错误/加载/空状态 | 前端 + 手工 |
| 设备 | 新旧协议字段兼容、超时重试、处理中状态 | 联调 + 手工 |
建议新增专门的 GiftAccount 测试组,并回归既有:
tests/Security/SecurityFixesTest.phptests/apifox/http/run_dengbao_regression.sh- 卡务/消费、充值、补贴、退款相关 PHPUnit 与 Apifox 集。
15. 验收映射
| 需求点 | 技术验收证据 |
|---|---|
| 人员列表赠送余额 | API 返回值、页面截图、租户数据权限测试 |
| 赠送活动 | 规则 CRUD、命中边界测试、操作日志 |
| PC+小程序充值 | 三金额订单、双流水、重复回调测试 |
| 默认扣款顺序 | 配置值、6 种顺序参数化测试、订单快照 |
| 所有下单 | 来源矩阵测试及各入口统一服务调用证据 |
| 消费流水 | 现金/餐补/赠送流水和结算单一致性 |
| 原路退款 | 全额/部分退款账户前后快照 |
| 不可提现 | 活动本金与赠送提现拦截 |
| 导出 | 页面、接口、异步导出三方合计比对 |
| 跨库一致性 | 故障注入、重试与对账报告 |
16. 推荐实施切片
- 数据结构、迁移脚本、账户不变量与对账基线。
- 赠送规则和 PC 账户设置权限/审计。
- PC、小程序充值三金额与双流水。
- 统一账户结算服务和消费幂等结算单。
- 按来源逐个接入所有下单入口。
- 消费退款、提现资格和跨库补偿。
- PC/小程序页面、统计、异步导出与全链路验收。
每个切片都应先补回归测试,再合并到下一切片;资金链路不建议一次性大爆炸发布。
17. 评审结论
方案可以作为研发拆分基线,但不能在 D1~D7 未确认前直接编码。尤其是“默认扣款顺序”“活动本金不可提现模型”“部分退款规则”三项会直接决定数据库字段和历史兼容策略。
文档评审通过后,应为各实施切片创建独立研发任务,并分别补充 DDL、API 契约、测试用例和发布证据。