{
  "source_path": "work_store/VWCG-1121-gift-account-mvp/technical-design.html",
  "status": "historical-conflict-snapshot-not-an-active-contract",
  "original_sha256": "e3d806c3ee7b68c81aa5be40474e41d9defbf978f614054ae0201b3a959f289f",
  "original_text": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <title>VWCG-1121 充值赠送账户 MVP 技术方案</title>\n  <style>\n    :root {\n      --ink: #172033;\n      --muted: #64748b;\n      --line: #dce3ed;\n      --paper: #ffffff;\n      --canvas: #f4f7fb;\n      --primary: #2156a5;\n      --primary-soft: #eaf2ff;\n      --warning: #9a5b00;\n      --warning-soft: #fff4d8;\n      --code: #101827;\n    }\n    * { box-sizing: border-box; }\n    html { scroll-behavior: smooth; }\n    body {\n      margin: 0;\n      color: var(--ink);\n      background:\n        radial-gradient(circle at 10% 0%, #e6efff 0, transparent 28rem),\n        var(--canvas);\n      font: 15px/1.72 -apple-system, BlinkMacSystemFont, \"Segoe UI\",\n        \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n    }\n    main {\n      width: min(1120px, calc(100% - 32px));\n      margin: 34px auto 72px;\n      padding: 48px 56px 64px;\n      background: var(--paper);\n      border: 1px solid rgba(220, 227, 237, .9);\n      border-radius: 20px;\n      box-shadow: 0 24px 70px rgba(31, 53, 86, .10);\n    }\n    h1, h2, h3, h4 { line-height: 1.3; color: #10203d; }\n    h1 {\n      margin: 0 0 24px;\n      padding-bottom: 20px;\n      font-size: clamp(28px, 4vw, 42px);\n      border-bottom: 3px solid var(--primary);\n    }\n    h2 {\n      margin: 46px 0 18px;\n      padding-left: 13px;\n      font-size: 25px;\n      border-left: 5px solid var(--primary);\n    }\n    h3 { margin: 30px 0 12px; font-size: 19px; }\n    h4 { margin: 24px 0 10px; font-size: 16px; }\n    p, ul, ol { margin: 10px 0 14px; }\n    li + li { margin-top: 4px; }\n    blockquote {\n      margin: 20px 0;\n      padding: 15px 18px;\n      color: #32415d;\n      background: var(--primary-soft);\n      border-left: 4px solid var(--primary);\n      border-radius: 0 10px 10px 0;\n    }\n    table {\n      width: 100%;\n      margin: 16px 0 24px;\n      border-collapse: separate;\n      border-spacing: 0;\n      border: 1px solid var(--line);\n      border-radius: 10px;\n      overflow: hidden;\n      font-size: 14px;\n    }\n    th, td {\n      padding: 10px 12px;\n      vertical-align: top;\n      border-right: 1px solid var(--line);\n      border-bottom: 1px solid var(--line);\n      text-align: left;\n    }\n    th:last-child, td:last-child { border-right: 0; }\n    tr:last-child td { border-bottom: 0; }\n    th { color: #213656; background: #eef4fb; font-weight: 650; }\n    tbody tr:nth-child(even) { background: #fafcff; }\n    code {\n      padding: .12em .36em;\n      color: #17477d;\n      background: #eef3f8;\n      border-radius: 5px;\n      font-family: \"SFMono-Regular\", Consolas, monospace;\n      font-size: .9em;\n    }\n    pre {\n      margin: 16px 0 24px;\n      padding: 18px 20px;\n      overflow: auto;\n      color: #d9e6f7;\n      background: var(--code);\n      border-radius: 10px;\n      box-shadow: inset 0 1px 0 rgba(255,255,255,.05);\n    }\n    pre code { padding: 0; color: inherit; background: transparent; }\n    .mermaid {\n      margin: 20px 0 28px;\n      padding: 20px;\n      overflow: auto;\n      text-align: center;\n      background: #fbfdff;\n      border: 1px solid var(--line);\n      border-radius: 12px;\n    }\n    a { color: var(--primary); }\n    strong { color: #152b4f; }\n    @media (max-width: 760px) {\n      main { width: 100%; margin: 0; padding: 28px 18px 52px; border-radius: 0; }\n      table { display: block; overflow-x: auto; white-space: nowrap; }\n      h2 { margin-top: 36px; }\n    }\n    @media print {\n      body { background: #fff; }\n      main { width: 100%; margin: 0; padding: 0; border: 0; box-shadow: none; }\n      h2 { break-after: avoid; }\n      table, pre, .mermaid { break-inside: avoid; }\n    }\n  </style>\n</head>\n<body>\n  <main><h1>VWCG-1121 充值赠送账户 MVP 技术方案</h1>\n<blockquote>\n<p>版本：V0.9（评审稿）<br>日期：2026-07-27<br>适用仓库：<code>store</code>、<code>ai_api</code>、<code>ai_app</code><br>风险等级：高<br>结论状态：有条件可实施，7 项产品/范围决策确认后进入开发</p>\n</blockquote>\n<h2>1. 方案摘要</h2>\n<p>本需求不是单纯增加一个 <code>gift_balance</code> 字段。要完整满足“充值赠送、不可提现、全渠道消费、可配置扣款顺序、原路退款”，必须同时改造账户、充值、消费、退款和对账链路。</p>\n<p>推荐方案：</p>\n<ol>\n<li>在 <code>yoshop_user</code> 增加赠送余额，并把现有现金余额内部拆为“可提现现金 + 活动本金”。</li>\n<li>新建充值赠送规则表与赠送流水表，充值订单复用三金额字段并保存命中规则快照。</li>\n<li>新建账户结算单作为消费/退款幂等锚点，统一 PC、消费设备和小程序扣款算法。</li>\n<li>在 <code>ydy_meal_order</code> 增加赠送支付、赠送退款、赠送剩余金额及扣款顺序快照。</li>\n<li><code>ydy_config</code> 保存全局扣款顺序，暂定默认值为“餐补 → 现金 → 赠送”；只影响新订单。</li>\n<li>跨账户库与业务库采用“账户侧幂等提交 + 业务侧重试同步 + 对账补偿”，不依赖伪跨库事务。</li>\n<li>充值订单、消费订单及相关人员/部门/设备统计同步调整异步导出。</li>\n</ol>\n<h2>2. 关键冲突与开发前决策</h2>\n<table>\n<thead>\n<tr>\n<th>编号</th>\n<th>问题</th>\n<th>暂定方案</th>\n<th>是否阻断开发</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>D1</td>\n<td>云效 PRD 默认“餐补→赠送→现金”，本轮输入默认“餐补→现金→赠送”</td>\n<td>以本轮输入为暂定值：<code>subsidy,cash,gift</code></td>\n<td>是</td>\n</tr>\n<tr>\n<td>D2</td>\n<td>命中赠送活动的充值本金不可提现，但现有 <code>balance</code> 没有本金属性</td>\n<td>新增 <code>withdrawable_balance</code>、<code>activity_principal_balance</code>，且两者之和等于 <code>balance</code></td>\n<td>是</td>\n</tr>\n<tr>\n<td>D3</td>\n<td>部分退款如何恢复账户</td>\n<td>建议按该订单实际扣款逆序恢复，确保每次退款可复算、可追溯</td>\n<td>是</td>\n</tr>\n<tr>\n<td>D4</td>\n<td>旧充值套餐赠送与新赠送活动关系</td>\n<td>同一订单只能命中一种赠送机制；建议新活动表成为统一入口，旧套餐迁移后停用</td>\n<td>是</td>\n</tr>\n<tr>\n<td>D5</td>\n<td>PC/小程序之外的旧 H5 充值是否改造</td>\n<td>本期默认不含旧 H5，但上线前必须确认是否仍有真实流量</td>\n<td>是</td>\n</tr>\n<tr>\n<td>D6</td>\n<td><code>source=10</code> 是否直接产生扣款</td>\n<td>先确认主/聚合订单是否只是父单；父单不得重复扣款</td>\n<td>是</td>\n</tr>\n<tr>\n<td>D7</td>\n<td>PRD 的 <code>scanPay.vue</code> 在当前 <code>ai_app</code> 分支不存在</td>\n<td>确认目标分支或真实页面后再排期</td>\n<td>是</td>\n</tr>\n</tbody></table>\n<h2>3. 范围与非范围</h2>\n<h3>3.1 本期范围</h3>\n<ul>\n<li>PC 机构人员列表显示赠送余额。</li>\n<li>PC 账户设置新增赠送活动、扣款顺序设置及操作日志。</li>\n<li>PC 和小程序充值展示并记录支付金额、赠送金额、实际到账金额。</li>\n<li>充值成功后现金与赠送余额、流水分别入账。</li>\n<li>PC、小程序、绑盘机、消费机、虚拟订单、外部订单、闸机、点餐机共用账户结算规则。</li>\n<li>消费订单展示和保存现金、餐补、赠送拆分。</li>\n<li>全额/部分退款按原订单实际来源恢复。</li>\n<li>相关统计、流水、导出与人工验收。</li>\n</ul>\n<h3>3.2 本期非范围</h3>\n<ul>\n<li>赠送余额有效期、冻结、转赠、提现。</li>\n<li>按商品、餐次、人群、部门等复杂赠送活动条件。</li>\n<li>多活动叠加。</li>\n<li>对历史订单重算赠送金额。</li>\n<li>把两套数据库合并为一个数据库。</li>\n</ul>\n<h2>4. 系统边界与目标架构</h2>\n<div class=\"mermaid\">flowchart LR\n    subgraph Clients[\"客户端与设备\"]\n        PC[\"PC 管理后台\"]\n        WX[\"小程序 ai_app\"]\n        DEV[\"绑盘机/消费机/闸机/点餐机\"]\n        EXT[\"外部订单\"]\n    end\n\n    subgraph Services[\"服务层\"]\n        STORE[\"store\\nPC、设备、订单域\"]\n        AI[\"ai_api\\n小程序、账户域\"]\n        SETTLE[\"统一 AccountSettlementService\\n规划、扣款、退款、幂等\"]\n        RECON[\"对账与补偿任务\"]\n    end\n\n    subgraph Databases[\"数据层\"]\n        YDY[(\"业务库 ydy_*\\nmeal_order/config/refund\")]\n        YOSHOP[(\"账户库 yoshop_*\\nuser/recharge/log/settlement\")]\n    end\n\n    PC --> STORE\n    DEV --> STORE\n    EXT --> STORE\n    WX --> AI\n    STORE --> SETTLE\n    AI --> SETTLE\n    SETTLE --> YOSHOP\n    STORE --> YDY\n    AI --> YDY\n    RECON --> YOSHOP\n    RECON --> YDY</div><h3>4.1 设计原则</h3>\n<ul>\n<li>账户库是余额事实源，业务订单库保存业务状态与账户拆分快照。</li>\n<li>所有金额使用 <code>decimal(10,2)</code>，服务层使用字符串/定点运算，禁止浮点比较。</li>\n<li>客户端预览只用于展示，创建充值订单时必须由服务端重新匹配规则。</li>\n<li>消费与退款都以业务订单号作为幂等键。</li>\n<li>当前配置只用于新订单；历史订单退款只使用订单快照。</li>\n<li>新接口字段采用增量兼容，旧客户端可忽略。</li>\n</ul>\n<h2>5. 账户与金额模型</h2>\n<h3>5.1 账户定义</h3>\n<table>\n<thead>\n<tr>\n<th>账户</th>\n<th align=\"right\">用户可见</th>\n<th align=\"right\">可消费</th>\n<th align=\"right\">可提现</th>\n<th>有效期</th>\n<th>说明</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>餐补</td>\n<td align=\"right\">是</td>\n<td align=\"right\">是</td>\n<td align=\"right\">否</td>\n<td>沿用现状</td>\n<td>继续以有效补贴明细合计为事实值</td>\n</tr>\n<tr>\n<td>个人现金</td>\n<td align=\"right\">是</td>\n<td align=\"right\">是</td>\n<td align=\"right\">部分可提现</td>\n<td>永久</td>\n<td>展示值仍为 <code>balance</code></td>\n</tr>\n<tr>\n<td>可提现现金</td>\n<td align=\"right\">可在提现页展示</td>\n<td align=\"right\">是</td>\n<td align=\"right\">是</td>\n<td>永久</td>\n<td><code>withdrawable_balance</code></td>\n</tr>\n<tr>\n<td>活动本金</td>\n<td align=\"right\">可在明细中标识</td>\n<td align=\"right\">是</td>\n<td align=\"right\">否</td>\n<td>永久</td>\n<td>命中赠送活动时的 <code>pay_price</code></td>\n</tr>\n<tr>\n<td>赠送余额</td>\n<td align=\"right\">是</td>\n<td align=\"right\">是</td>\n<td align=\"right\">否</td>\n<td>永久</td>\n<td><code>gift_balance</code></td>\n</tr>\n</tbody></table>\n<h3>5.2 核心不变量</h3>\n<pre><code class=\"language-text\">actual_money = pay_price + gift_money\nbalance = withdrawable_balance + activity_principal_balance\nusable_total = valid_subsidy + balance + gift_balance\nmeal_order.pay_price = subsidy + cash + gift\ncash = withdrawable_cash_payment + activity_principal_payment\n累计退款金额 &lt;= 原订单各账户实际扣款金额</code></pre><p>建议现金账户消费内部顺序为“活动本金 → 可提现现金”，避免用户先消耗可提现资金而留下不可提现本金。这个内部顺序不改变前台显示的“现金”一级账户顺序，但须保存内部拆分以支持提现与退款。</p>\n<h2>6. 数据库设计</h2>\n<p>以下为逻辑设计，正式 DDL 应按项目数据库版本、索引长度和发布时间另行生成。</p>\n<h3>6.1 账户库 <code>yoshop_*</code></h3>\n<h4><code>yoshop_user</code></h4>\n<table>\n<thead>\n<tr>\n<th>字段</th>\n<th>类型</th>\n<th>默认值</th>\n<th>用途</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>gift_balance</code></td>\n<td><code>decimal(10,2)</code></td>\n<td><code>0.00</code></td>\n<td>赠送余额</td>\n</tr>\n<tr>\n<td><code>withdrawable_balance</code></td>\n<td><code>decimal(10,2)</code></td>\n<td>迁移确定</td>\n<td>可提现现金</td>\n</tr>\n<tr>\n<td><code>activity_principal_balance</code></td>\n<td><code>decimal(10,2)</code></td>\n<td><code>0.00</code></td>\n<td>活动本金</td>\n</tr>\n</tbody></table>\n<p>约束：</p>\n<ul>\n<li>所有余额字段非负。</li>\n<li>每次现金变动后检查 <code>balance = withdrawable_balance + activity_principal_balance</code>。</li>\n<li><code>subsidy_balance</code> 继续兼容展示，但有效补贴以补贴明细合计为准。</li>\n</ul>\n<h4><code>yoshop_recharge_gift_rule</code>（新增）</h4>\n<table>\n<thead>\n<tr>\n<th>字段组</th>\n<th>建议字段</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>主键与租户</td>\n<td><code>rule_id</code>、<code>store_id</code></td>\n</tr>\n<tr>\n<td>展示</td>\n<td><code>rule_name</code></td>\n</tr>\n<tr>\n<td>匹配条件</td>\n<<<<<<< HEAD\n<td><code>pay_method</code>、<code>min_money</code></td>\n=======\n<td><code>platform</code>、<code>pay_method</code>、<code>min_money</code></td>\n>>>>>>> 5d05833ef476571c794ef3016336bc136526872c\n</tr>\n<tr>\n<td>奖励</td>\n<td><code>gift_money</code></td>\n</tr>\n<tr>\n<td>状态</td>\n<td><code>status</code>、<code>is_delete</code></td>\n</tr>\n<tr>\n<td>审计</td>\n<td><code>create_user_id/name</code>、<code>update_user_id/name</code>、<code>create_time</code>、<code>update_time</code></td>\n</tr>\n</tbody></table>\n<p>匹配规则：</p>\n<ol>\n<li>只匹配当前门店、启用且未删除的规则。</li>\n<li><code>pay_price &gt;= min_money</code>。</li>\n<<<<<<< HEAD\n<li>支付方式仅允许 <code>cash</code>、<code>alipay_offline</code>、<code>wechat_offline</code>、<code>online_scan</code>，规则不设置适用端。</li>\n=======\n<li>平台、支付方式匹配；“全部”用明确枚举值表达。</li>\n>>>>>>> 5d05833ef476571c794ef3016336bc136526872c\n<li>多条命中时依次取：<code>min_money</code> 最大、<code>gift_money</code> 最大、<code>rule_id/create_time</code> 最新。</li>\n<li>同一充值订单最多命中一条规则。</li>\n</ol>\n<p>推荐索引：</p>\n<ul>\n<<<<<<< HEAD\n<li><code>(store_id, status, is_delete, pay_method, min_money)</code></li>\n=======\n<li><code>(store_id, status, is_delete, platform, pay_method, min_money)</code></li>\n>>>>>>> 5d05833ef476571c794ef3016336bc136526872c\n<li>列表页 <code>(store_id, is_delete, create_time)</code></li>\n</ul>\n<h4><code>yoshop_recharge_order</code>（复用并扩展）</h4>\n<p>继续复用：</p>\n<ul>\n<li><code>pay_price</code></li>\n<li><code>gift_money</code></li>\n<li><code>actual_money</code></li>\n</ul>\n<p>建议增加快照字段：</p>\n<table>\n<thead>\n<tr>\n<th>字段</th>\n<th>用途</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>gift_rule_id</code></td>\n<td>命中规则 ID，未命中为 0</td>\n</tr>\n<tr>\n<td><code>gift_rule_snapshot</code></td>\n<<<<<<< HEAD\n<td>规则名、门槛、赠送金额、支付方式 JSON 快照</td>\n=======\n<td>规则名、门槛、赠送金额、平台、支付方式 JSON 快照</td>\n>>>>>>> 5d05833ef476571c794ef3016336bc136526872c\n</tr>\n<tr>\n<td><code>withdrawable_amount</code></td>\n<td>本次进入可提现现金的金额</td>\n</tr>\n<tr>\n<td><code>activity_principal_amount</code></td>\n<td>本次进入活动本金的金额</td>\n</tr>\n</tbody></table>\n<p>规则：</p>\n<ul>\n<li>普通充值：<code>withdrawable_amount = pay_price</code>，<code>activity_principal_amount = 0</code>。</li>\n<li>命中赠送：<code>withdrawable_amount = 0</code>，<code>activity_principal_amount = pay_price</code>。</li>\n<li><code>gift_money</code> 只进入 <code>gift_balance</code>。</li>\n<li>旧订单四个新增字段默认 0/空，维持历史含义。</li>\n</ul>\n<h4><code>yoshop_user_gift_log</code>（新增）</h4>\n<table>\n<thead>\n<tr>\n<th>字段组</th>\n<th>建议字段</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>主键与租户</td>\n<td><code>log_id</code>、<code>store_id</code>、<code>user_id</code></td>\n</tr>\n<tr>\n<td>变动</td>\n<td><code>scene</code>、<code>money</code>、<code>balance_before</code>、<code>balance_after</code></td>\n</tr>\n<tr>\n<td>业务定位</td>\n<td><code>biz_type</code>、<code>biz_order_id</code>、<code>biz_order_no</code></td>\n</tr>\n<tr>\n<td>幂等</td>\n<td><code>idempotency_key</code></td>\n</tr>\n<tr>\n<td>审计</td>\n<td><code>describe</code>、<code>remark</code>、<code>operator_id/name</code>、<code>create_time</code></td>\n</tr>\n</tbody></table>\n<p>场景建议：</p>\n<ul>\n<li>10：充值赠送</li>\n<li>20：消费扣款</li>\n<li>30：后台调整（本期若无入口则保留枚举、不开放）</li>\n<li>40：消费退款</li>\n<li>50：充值退款冲正</li>\n</ul>\n<p>对 <code>idempotency_key</code> 建唯一索引，避免回调/消费/退款重复入账。</p>\n<h4><code>yoshop_account_settlement</code>（新增）</h4>\n<p>用于跨库消费和退款的幂等、重试与对账，不取代现有账户流水。</p>\n<table>\n<thead>\n<tr>\n<th>字段组</th>\n<th>建议字段</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>业务键</td>\n<td><code>settlement_id</code>、<code>biz_type</code>、<code>biz_order_no</code>、<code>refund_no</code></td>\n</tr>\n<tr>\n<td>主体</td>\n<td><code>user_id</code>、<code>store_id</code>、<code>order_source</code></td>\n</tr>\n<tr>\n<td>金额</td>\n<td><code>total_money</code>、<code>subsidy_money</code>、<code>cash_money</code>、<code>gift_money</code></td>\n</tr>\n<tr>\n<td>现金内部</td>\n<td><code>withdrawable_money</code>、<code>activity_principal_money</code></td>\n</tr>\n<tr>\n<td>快照</td>\n<td><code>deduction_order_snapshot</code>、<code>account_before_snapshot</code>、<code>account_after_snapshot</code></td>\n</tr>\n<tr>\n<td>状态</td>\n<td><code>status</code>：处理中/账户已提交/业务已同步/失败待处理</td>\n</tr>\n<tr>\n<td>重试</td>\n<td><code>retry_count</code>、<code>last_error</code>、<code>next_retry_time</code></td>\n</tr>\n<tr>\n<td>时间</td>\n<td><code>create_time</code>、<code>update_time</code>、<code>completed_time</code></td>\n</tr>\n</tbody></table>\n<p>唯一键：</p>\n<ul>\n<li>消费：<code>(biz_type, biz_order_no)</code></li>\n<li>退款：<code>(biz_type, biz_order_no, refund_no)</code></li>\n</ul>\n<h3>6.2 业务库 <code>ydy_*</code></h3>\n<h4><code>ydy_config</code></h4>\n<p>新增配置：</p>\n<pre><code class=\"language-text\">key: account_deduction_order\nvalue: [&quot;subsidy&quot;,&quot;cash&quot;,&quot;gift&quot;]\ndesc: 消费账户扣款顺序</code></pre><p>校验规则：</p>\n<ul>\n<li>必须恰好包含 <code>subsidy</code>、<code>cash</code>、<code>gift</code> 三项。</li>\n<li>不允许重复或未知账户。</li>\n<li>缺失、关闭或非法时回退到 <code>[&quot;subsidy&quot;,&quot;cash&quot;,&quot;gift&quot;]</code> 并告警。</li>\n<li>保存后使相关缓存失效。</li>\n</ul>\n<p>建议新增 <code>ydy_account_config_log</code>，记录配置键、变更前、变更后、操作人、时间、IP，满足账户设置审计要求。</p>\n<h4><code>ydy_meal_order</code></h4>\n<table>\n<thead>\n<tr>\n<th>字段</th>\n<th>用途</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>gift</code></td>\n<td>本单赠送余额支付金额</td>\n</tr>\n<tr>\n<td><code>gift_refund_amount</code></td>\n<td>已退赠送金额</td>\n</tr>\n<tr>\n<td><code>remain_gift</code></td>\n<td>可退赠送金额</td>\n</tr>\n<tr>\n<td><code>withdrawable_cash_payment</code></td>\n<td>本单消耗的可提现现金</td>\n</tr>\n<tr>\n<td><code>activity_principal_payment</code></td>\n<td>本单消耗的活动本金</td>\n</tr>\n<tr>\n<td><code>withdrawable_cash_refund</code></td>\n<td>已恢复的可提现现金</td>\n</tr>\n<tr>\n<td><code>activity_principal_refund</code></td>\n<td>已恢复的活动本金</td>\n</tr>\n<tr>\n<td><code>deduction_order_snapshot</code></td>\n<td>本单实际使用的一级账户顺序</td>\n</tr>\n<tr>\n<td><code>account_settlement_id</code></td>\n<td>账户侧结算单 ID</td>\n</tr>\n</tbody></table>\n<p>兼容规则：</p>\n<ul>\n<li>新增金额字段默认 <code>0.00</code>，历史订单视为 <code>gift=0</code>。</li>\n<li>历史订单保留已有现金/餐补值，不回填虚构的扣款顺序。</li>\n<li>历史订单退款按旧逻辑兼容；新订单以是否存在结算快照区分。</li>\n<li>不修改 <code>source</code> 既有枚举值。</li>\n</ul>\n<h4><code>ydy_meal_order_refund</code></h4>\n<p>增加：</p>\n<ul>\n<li><code>gift</code></li>\n<li><code>withdrawable_cash</code></li>\n<li><code>activity_principal</code></li>\n<li><code>account_settlement_id</code></li>\n</ul>\n<p>退款单保存本次实际恢复金额，不只保存申请金额。</p>\n<h2>7. 充值流程</h2>\n<div class=\"mermaid\">sequenceDiagram\n    participant C as PC/小程序\n    participant API as 充值服务\n    participant PAY as 支付渠道\n    participant DB as 账户库\n\n<<<<<<< HEAD\n    C->>API: 充值预览(pay_price, pay_method)\n=======\n    C->>API: 充值预览(pay_price, platform, pay_method)\n>>>>>>> 5d05833ef476571c794ef3016336bc136526872c\n    API->>DB: 匹配启用规则\n    DB-->>API: 最优规则\n    API-->>C: pay/gift/actual 预览\n    C->>API: 创建充值订单\n    API->>DB: 服务端重新匹配并保存规则快照\n    API->>PAY: 发起支付\n    PAY-->>API: 支付成功回调\n    API->>DB: 锁定订单并校验未处理\n    API->>DB: 现金本金入账、赠送入账、分别写流水\n    API->>DB: 条件更新订单为已支付\n    API-->>PAY: 成功</div><h3>7.1 服务端规则</h3>\n<ul>\n<li>预览结果不作为入账依据。</li>\n<li>创建订单时重算并保存规则快照，后续规则修改不影响该订单。</li>\n<li>支付回调按订单快照入账，不能再次匹配当前规则。</li>\n<li>使用订单号/支付回调号做幂等，重复回调返回已有成功结果。</li>\n<li>订单、用户余额、现金流水、赠送流水在同一账户库事务中提交。</li>\n<li>回调失败必须向上抛出并进入渠道重试或内部补偿，禁止吞异常后返回成功。</li>\n</ul>\n<h3>7.2 三金额与流水</h3>\n<p>示例：支付 100 元，赠送 20 元。</p>\n<table>\n<thead>\n<tr>\n<th>项目</th>\n<th align=\"right\">变动</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>pay_price</code></td>\n<td align=\"right\">100.00</td>\n</tr>\n<tr>\n<td><code>gift_money</code></td>\n<td align=\"right\">20.00</td>\n</tr>\n<tr>\n<td><code>actual_money</code></td>\n<td align=\"right\">120.00</td>\n</tr>\n<tr>\n<td><code>balance</code></td>\n<td align=\"right\">+100.00</td>\n</tr>\n<tr>\n<td><code>activity_principal_balance</code></td>\n<td align=\"right\">+100.00</td>\n</tr>\n<tr>\n<td><code>gift_balance</code></td>\n<td align=\"right\">+20.00</td>\n</tr>\n<tr>\n<td>现金流水</td>\n<td align=\"right\">+100.00，标记活动本金</td>\n</tr>\n<tr>\n<td>赠送流水</td>\n<td align=\"right\">+20.00，场景为充值赠送</td>\n</tr>\n</tbody></table>\n<h2>8. 消费结算流程</h2>\n<h3>8.1 统一算法</h3>\n<p>输入：</p>\n<ul>\n<li>用户、门店、业务订单号、订单来源、应付金额。</li>\n<li>有效补贴明细、现金内部余额、赠送余额。</li>\n<li>当前合法扣款顺序。</li>\n</ul>\n<p>输出：</p>\n<ul>\n<li>餐补、现金、赠送拆分。</li>\n<li>现金内部的活动本金、可提现现金拆分。</li>\n<li>扣款顺序、账户前后快照和结算单 ID。</li>\n</ul>\n<p>伪代码：</p>\n<pre><code class=\"language-text\">remaining = payable\nfor account in configured_order:\n    used[account] = min(available(account), remaining)\n    remaining -= used[account]\nif remaining &gt; 0:\n    fail(&quot;账户余额不足&quot;)\n\nif used.cash &gt; 0:\n    used.activity_principal = min(activity_principal_balance, used.cash)\n    used.withdrawable = used.cash - used.activity_principal</code></pre><h3>8.2 跨库一致性</h3>\n<div class=\"mermaid\">stateDiagram-v2\n    [*] --> Processing: 创建/锁定幂等结算单\n    Processing --> AccountCommitted: 账户库扣款与流水提交\n    AccountCommitted --> BusinessSynced: 业务订单写入拆分和结算ID\n    AccountCommitted --> PendingSync: 业务库写入失败\n    PendingSync --> BusinessSynced: 重试成功\n    PendingSync --> ManualReview: 超过重试阈值\n    Processing --> Failed: 余额不足或业务校验失败</div><p>规则：</p>\n<ul>\n<li>账户扣款前锁定用户汇总行及需要使用的补贴明细。</li>\n<li>同一业务订单只允许一个成功消费结算单。</li>\n<li>账户库提交后，业务库更新失败时不得重复扣款；重试只同步原结算结果。</li>\n<li>对账任务按“已提交但未同步”“业务已支付但无账户结算”“金额拆分不等”三类检测。</li>\n<li>异常订单在设备/客户端返回“处理中”或明确失败，不能返回虚假成功。</li>\n</ul>\n<h3>8.3 所有下单入口接入</h3>\n<table>\n<thead>\n<tr>\n<th align=\"right\">来源</th>\n<th>当前入口</th>\n<th>改造要求</th>\n</tr>\n</thead>\n<tbody><tr>\n<td align=\"right\">1/2/3/6/7</td>\n<td><code>store/application/api/service/Consume.php</code></td>\n<td>同步与队列路径调用同一结算服务，删除重复算法</td>\n</tr>\n<tr>\n<td align=\"right\">4</td>\n<td><code>store</code> 与 <code>ai_api</code> 的 <code>meal/PaySuccess.php</code></td>\n<td>统一调用结算服务；只保留渠道编排</td>\n</tr>\n<tr>\n<td align=\"right\">4 的直接支付</td>\n<td><code>ai_api/app/api/service/meal/DirectPay.php</code></td>\n<td>移除独立 <code>deductBalance()</code> 规则</td>\n</tr>\n<tr>\n<td align=\"right\">5</td>\n<td>外部订单入口</td>\n<td>纳入相同幂等键、余额检查与拆账</td>\n</tr>\n<tr>\n<td align=\"right\">10</td>\n<td>主/聚合订单</td>\n<td>确认是否父单；父单不得与子单重复扣款</td>\n</tr>\n</tbody></table>\n<p>设备协议新增 <code>gift_payment</code>、<code>remain_gift</code> 等字段应保持可选，旧设备继续读取既有字段。</p>\n<h2>9. 退款与提现</h2>\n<h3>9.1 消费退款</h3>\n<ul>\n<li>全额退款：按原订单各账户实际扣款金额全部恢复。</li>\n<li>部分退款：建议从实际扣款顺序的末端向前恢复。例如订单按“餐补→现金→赠送”扣款 10/20/30，退款 35 时先恢复赠送 30，再恢复现金 5。</li>\n<li>现金恢复时继续按原订单保存的 <code>activity_principal_payment</code>、<code>withdrawable_cash_payment</code> 逆序恢复，不把活动本金误恢复为可提现现金。</li>\n<li>重复退款以退款单号幂等；累计恢复不能超过原订单拆分。</li>\n<li>配置修改不影响历史订单退款。</li>\n</ul>\n<h3>9.2 充值退款/提现</h3>\n<ul>\n<li>用户可提现金额上限为 <code>withdrawable_balance</code>，不能再仅依据 <code>balance</code> 或充值订单原始 <code>refundable_money</code>。</li>\n<li>命中赠送活动的充值：支付本金进入活动本金，赠送进入赠送余额，二者均不可提现。</li>\n<li>普通充值：支付本金进入可提现现金。</li>\n<li>充值退款如仍需支持，应先明确“已消费后的退款”规则，并在账户余额足够时按原充值来源冲正。</li>\n</ul>\n<h3>9.3 历史现金迁移</h3>\n<p>建议初始迁移：</p>\n<pre><code class=\"language-text\">withdrawable_balance = balance\nactivity_principal_balance = 0\ngift_balance = 0</code></pre><p>该方案保持历史现金可提现能力，不倒推历史活动属性。若产品要求按 365 天或旧活动记录重建可提现金额，必须另立数据治理任务并做差异报表，本期不能凭现有余额字段可靠推导。</p>\n<h2>10. API 与页面改造</h2>\n<h3>10.1 PC 后台</h3>\n<table>\n<thead>\n<tr>\n<th>模块</th>\n<th>改造</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>人员管理-机构人员</td>\n<td>列表返回并展示 <code>gift_balance</code>；总余额口径明确为餐补+现金+赠送</td>\n</tr>\n<tr>\n<td>账户设置-赠送活动</td>\n<td>规则列表、新增、编辑、启停、删除、命中预览</td>\n</tr>\n<tr>\n<td>账户设置-扣款顺序</td>\n<td>三账户拖拽/排序、保存、默认值、操作日志</td>\n</tr>\n<tr>\n<td>充值订单</td>\n<td>展示/筛选/统计 <code>pay_price</code>、<code>gift_money</code>、<code>actual_money</code> 及命中规则</td>\n</tr>\n<tr>\n<td>消费订单</td>\n<td>展示现金、餐补、赠送及退款拆分</td>\n</tr>\n<tr>\n<td>流水</td>\n<td>增加赠送流水页或在统一流水中增加账户类型筛选</td>\n</tr>\n</tbody></table>\n<p>权限要求：</p>\n<ul>\n<li>新页面和接口必须同时配置菜单、功能权限与数据权限。</li>\n<li>控制器不能只因继承基类就假设权限完整；需核对 <code>beforeActionList</code>。</li>\n<li>规则新增/编辑/启停、扣款顺序变更必须记录操作人、前后值和时间。</li>\n</ul>\n<h3>10.2 小程序 <code>ai_app</code> / <code>ai_api</code></h3>\n<table>\n<thead>\n<tr>\n<th>页面/接口</th>\n<th>改造</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>用户资产接口</td>\n<td>返回现金、可提现现金、活动本金、餐补、赠送及总可用余额</td>\n</tr>\n<tr>\n<td>余额页</td>\n<td>展示赠送余额和不可提现说明</td>\n</tr>\n<tr>\n<td>充值页</td>\n<td>输入金额后调用预览；展示支付、赠送、到账三金额</td>\n</tr>\n<tr>\n<td>充值记录</td>\n<td>展示三金额和命中活动</td>\n</tr>\n<tr>\n<td>收银台</td>\n<td>展示预计/实际账户拆分</td>\n</tr>\n<tr>\n<td>订单详情</td>\n<td>展示现金、餐补、赠送支付与退款</td>\n</tr>\n<tr>\n<td>流水页</td>\n<td>支持赠送账户流水与账户类型筛选</td>\n</tr>\n</tbody></table>\n<p>创建订单和支付成功响应建议增加：</p>\n<pre><code class=\"language-json\">{\n  &quot;pay_price&quot;: &quot;100.00&quot;,\n  &quot;gift_money&quot;: &quot;20.00&quot;,\n  &quot;actual_money&quot;: &quot;120.00&quot;,\n  &quot;account_payment&quot;: {\n    &quot;subsidy&quot;: &quot;10.00&quot;,\n    &quot;cash&quot;: &quot;20.00&quot;,\n    &quot;gift&quot;: &quot;30.00&quot;\n  },\n  &quot;deduction_order&quot;: [&quot;subsidy&quot;, &quot;cash&quot;, &quot;gift&quot;]\n}</code></pre><p>金额字段统一使用两位小数字符串，避免前端浮点误差。</p>\n<h2>11. 统计与异步导出</h2>\n<p>该列表页关联异步导出功能，需同步确认/修改异步导出逻辑。</p>\n<table>\n<thead>\n<tr>\n<th>关联列表</th>\n<th>必须调整的口径</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>消费订单</td>\n<td>增加赠送支付、赠送退款、赠送净额</td>\n</tr>\n<tr>\n<td>人员消费明细</td>\n<td>个人现金、餐补、赠送分列并校验合计</td>\n</tr>\n<tr>\n<td>部门消费明细</td>\n<td>汇总增加赠送支付与退款</td>\n</tr>\n<tr>\n<td>设备消费明细</td>\n<td>按设备/来源汇总增加赠送</td>\n</tr>\n<tr>\n<td>充值统计</td>\n<td>支付金额、赠送金额、实际到账分别统计，不能继续只汇总 <code>actual_money</code></td>\n</tr>\n</tbody></table>\n<p>菜品销量、餐厅经营汇总、档口营收结算是否受影响，取决于其收入口径是否只看订单实付总额。若只看订单总额且不展示账户来源，可不改金额口径，但仍需回归证明。</p>\n<h2>12. 安全、并发与审计</h2>\n<ul>\n<li>所有查询、规则、流水和配置按 <code>store_id</code> 做租户隔离。</li>\n<li>账户行、补贴批次按确定顺序加锁，降低死锁概率。</li>\n<li>充值回调、消费、退款均有唯一业务幂等键。</li>\n<li>后端拒绝负数、超过金额精度、非法账户顺序、越权门店和超额退款。</li>\n<li>不信任客户端传入的赠送金额、扣款拆分或可提现金额。</li>\n<li>流水不允许物理修改；冲正使用相反方向的新流水。</li>\n<li>配置与规则变更写审计日志，记录操作前后值。</li>\n<li>错误日志不记录支付密钥、数据库口令和完整个人敏感信息。</li>\n</ul>\n<h2>13. 兼容、上线与回滚</h2>\n<h3>13.1 上线顺序</h3>\n<ol>\n<li>在影子环境执行 DDL 并验证表锁、耗时、索引和回滚脚本。</li>\n<li>先部署后端兼容代码，新增字段读取为空时按 0/默认顺序处理。</li>\n<li>执行用户现金属性迁移并产出迁移前后余额对账报告。</li>\n<li>部署 PC 与小程序，保持赠送规则全部停用。</li>\n<li>对内部测试门店启用一条小额规则，覆盖充值、消费、退款、导出。</li>\n<li>观察对账无差异后逐门店启用。</li>\n</ol>\n<h3>13.2 监控</h3>\n<ul>\n<li>充值三金额不相等计数。</li>\n<li>账户余额不变量异常数。</li>\n<li>重复回调拦截数。</li>\n<li>账户已提交但业务未同步数量与最长滞留时间。</li>\n<li>订单实付与账户拆分差额。</li>\n<li>退款超额/幂等拦截数。</li>\n<li>各账户余额负数告警。</li>\n</ul>\n<h3>13.3 回滚</h3>\n<ul>\n<li>业务回滚：先停用全部赠送活动，将扣款顺序恢复默认；旧客户端继续使用原字段。</li>\n<li>代码回滚：保留新增字段和表，不在有数据后直接删列；回滚版本应忽略赠送规则并阻止产生新赠送。</li>\n<li>数据回滚：对已产生的赠送与消费不得直接覆盖余额，必须用冲正流水和结算单逐笔恢复。</li>\n<li>跨库未同步：先停止新交易，完成结算单对账和补偿，再决定代码回滚。</li>\n<li>DDL 回滚：只在确认新增表/列无业务数据时执行；生产已入账后采用前向修复。</li>\n</ul>\n<h2>14. 测试覆盖矩阵</h2>\n<table>\n<thead>\n<tr>\n<th>测试域</th>\n<th>必测场景</th>\n<th>自动化层级</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>规则匹配</td>\n<td>门槛前/等于/超过；平台/方式；禁用；多规则排序；规则修改后订单快照不变</td>\n<td>单元 + 接口</td>\n</tr>\n<tr>\n<td>充值</td>\n<td>无赠送/有赠送；三金额；现金/赠送双流水；重复回调；并发回调；回调异常不返回假成功</td>\n<td>服务 + 集成</td>\n</tr>\n<tr>\n<td>现金属性</td>\n<td>普通本金可提现；活动本金不可提现；现金内部优先级；余额不变量</td>\n<td>单元 + 集成</td>\n</tr>\n<tr>\n<td>扣款顺序</td>\n<td>6 种排列；单账户/多账户；临界余额；余额不足；非法配置回退</td>\n<td>参数化单元</td>\n</tr>\n<tr>\n<td>订单来源</td>\n<td>1、2、3、4、5、6、7 逐一覆盖；10 依据决策</td>\n<td>集成 + 契约</td>\n</tr>\n<tr>\n<td>幂等并发</td>\n<td>同一订单重复消费、设备重试、队列重放、并发请求</td>\n<td>集成</td>\n</tr>\n<tr>\n<td>跨库补偿</td>\n<td>账户提交后业务库失败；重试不重复扣款；对账识别并修复</td>\n<td>集成 + 故障注入</td>\n</tr>\n<tr>\n<td>退款</td>\n<td>全额、部分、重复、超额；配置变更后退款；现金内部属性原路恢复</td>\n<td>服务 + 集成</td>\n</tr>\n<tr>\n<td>历史兼容</td>\n<td>老用户、老充值单、老消费单、空快照、赠送字段为 0</td>\n<td>回归</td>\n</tr>\n<tr>\n<td>权限</td>\n<td>无菜单权限、无功能权限、跨门店数据、操作日志</td>\n<td>接口 + 手工</td>\n</tr>\n<tr>\n<td>导出统计</td>\n<td>页面合计与导出合计一致；支付/赠送/到账不混淆</td>\n<td>接口 + 文件比对</td>\n</tr>\n<tr>\n<td>小程序/PC</td>\n<td>金额预览、确认页、订单详情、流水、错误/加载/空状态</td>\n<td>前端 + 手工</td>\n</tr>\n<tr>\n<td>设备</td>\n<td>新旧协议字段兼容、超时重试、处理中状态</td>\n<td>联调 + 手工</td>\n</tr>\n</tbody></table>\n<p>建议新增专门的 <code>GiftAccount</code> 测试组，并回归既有：</p>\n<ul>\n<li><code>tests/Security/SecurityFixesTest.php</code></li>\n<li><code>tests/apifox/http/run_dengbao_regression.sh</code></li>\n<li>卡务/消费、充值、补贴、退款相关 PHPUnit 与 Apifox 集。</li>\n</ul>\n<h2>15. 验收映射</h2>\n<table>\n<thead>\n<tr>\n<th>需求点</th>\n<th>技术验收证据</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>人员列表赠送余额</td>\n<td>API 返回值、页面截图、租户数据权限测试</td>\n</tr>\n<tr>\n<td>赠送活动</td>\n<td>规则 CRUD、命中边界测试、操作日志</td>\n</tr>\n<tr>\n<td>PC+小程序充值</td>\n<td>三金额订单、双流水、重复回调测试</td>\n</tr>\n<tr>\n<td>默认扣款顺序</td>\n<td>配置值、6 种顺序参数化测试、订单快照</td>\n</tr>\n<tr>\n<td>所有下单</td>\n<td>来源矩阵测试及各入口统一服务调用证据</td>\n</tr>\n<tr>\n<td>消费流水</td>\n<td>现金/餐补/赠送流水和结算单一致性</td>\n</tr>\n<tr>\n<td>原路退款</td>\n<td>全额/部分退款账户前后快照</td>\n</tr>\n<tr>\n<td>不可提现</td>\n<td>活动本金与赠送提现拦截</td>\n</tr>\n<tr>\n<td>导出</td>\n<td>页面、接口、异步导出三方合计比对</td>\n</tr>\n<tr>\n<td>跨库一致性</td>\n<td>故障注入、重试与对账报告</td>\n</tr>\n</tbody></table>\n<h2>16. 推荐实施切片</h2>\n<ol>\n<li>数据结构、迁移脚本、账户不变量与对账基线。</li>\n<li>赠送规则和 PC 账户设置权限/审计。</li>\n<li>PC、小程序充值三金额与双流水。</li>\n<li>统一账户结算服务和消费幂等结算单。</li>\n<li>按来源逐个接入所有下单入口。</li>\n<li>消费退款、提现资格和跨库补偿。</li>\n<li>PC/小程序页面、统计、异步导出与全链路验收。</li>\n</ol>\n<p>每个切片都应先补回归测试，再合并到下一切片；资金链路不建议一次性大爆炸发布。</p>\n<h2>17. 评审结论</h2>\n<p>方案可以作为研发拆分基线，但不能在 D1～D7 未确认前直接编码。尤其是“默认扣款顺序”“活动本金不可提现模型”“部分退款规则”三项会直接决定数据库字段和历史兼容策略。</p>\n<p>文档评审通过后，应为各实施切片创建独立研发任务，并分别补充 DDL、API 契约、测试用例和发布证据。</p>\n</main>\n  <script type=\"module\">\n    import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';\n    mermaid.initialize({ startOnLoad: true, theme: 'neutral', securityLevel: 'strict' });\n  </script>\n</body>\n<<<<<<< HEAD\n</html>\n=======\n</html>\n>>>>>>> 5d05833ef476571c794ef3016336bc136526872c\n",
  "blocks": [
    {
      "line": 375,
      "variant_a": "<td><code>pay_method</code>、<code>min_money</code></td>\n",
      "variant_b": "<td><code>platform</code>、<code>pay_method</code>、<code>min_money</code></td>\n"
    },
    {
      "line": 398,
      "variant_a": "<li>支付方式仅允许 <code>cash</code>、<code>alipay_offline</code>、<code>wechat_offline</code>、<code>online_scan</code>，规则不设置适用端。</li>\n",
      "variant_b": "<li>平台、支付方式匹配；“全部”用明确枚举值表达。</li>\n"
    },
    {
      "line": 408,
      "variant_a": "<li><code>(store_id, status, is_delete, pay_method, min_money)</code></li>\n",
      "variant_b": "<li><code>(store_id, status, is_delete, platform, pay_method, min_money)</code></li>\n"
    },
    {
      "line": 436,
      "variant_a": "<td>规则名、门槛、赠送金额、支付方式 JSON 快照</td>\n",
      "variant_b": "<td>规则名、门槛、赠送金额、平台、支付方式 JSON 快照</td>\n"
    },
    {
      "line": 624,
      "variant_a": "    C->>API: 充值预览(pay_price, pay_method)\n",
      "variant_b": "    C->>API: 充值预览(pay_price, platform, pay_method)\n"
    },
    {
      "line": 1096,
      "variant_a": "</html>\n",
      "variant_b": "</html>\n"
    }
  ]
}
