{
  "source_path": "work_store/VWCG-1119-consume-limit-account-scope/technical-development-design.md",
  "status": "historical-conflict-snapshot-not-an-active-contract",
  "original_sha256": "2d8d1d76a27cd69c49599ff8099b60367d8b823be0c6777b9a9b11dcac576ea3",
  "original_text": "<<<<<<< HEAD\n# VWCG-1119 消费限制按受限账户生效技术开发文档\n\n> 版本：V1.0 评审稿  \n> 日期：2026-07-29  \n> 目标仓库：`store`  \n> 需求状态：待处理  \n> 风险等级：中高  \n> 结论：可进入联合评审；P0 决策确认后再拆开发任务\n\n## 1. 结论摘要\n\n本需求不能只在表单增加一个多选框。现有支付链路在账户扣款拆分之前执行消费限制，次数统计也只统计订单总数；如果只保存账户类型，后端仍无法保证“未勾选账户不受限制”。\n\n推荐方案：\n\n1. 新增规则与账户的关系表 `ydy_consume_limit_account`，账户枚举统一使用 `cash`、`subsidy`、`gift`。\n2. 新增/编辑/详情接口增加 `accountTypes` / `account_types`，前后端都校验至少选择一项。\n3. 支付前先按最终 `pay_price` 和当前扣款顺序生成只读扣款计划，再把实际使用账户传给消费限制评估器。\n4. 范围限制只检查本单实际使用且被规则选中的账户。\n5. 次数限制按账户分别统计 `ydy_meal_order.cash/subsidy/gift > 0` 的成功订单，不能继续统计全部订单。\n6. 页面展示并启用个人账户、餐补账户和赠送账户；赠送账户只作为“受限账户”选项，不增加独立金额或独立规则配置。\n7. 不新增指定人员、身份、金额限制、账户独立档口配置或复杂规则组合。\n\n## 2. 当前 PRD 范围\n\n### 2.1 本期必须实现\n\n| 需求点 | 技术落点 |\n| --- | --- |\n| 增加“受限账户” | `addConsumeLimit.vue` 增加必填 Checkbox Group |\n| 位于应用场景之后 | 保持原型顺序，不调整其他表单结构 |\n| 个人/餐补/赠送多选 | API 接收 `cash/subsidy/gift`，页面展示中文 |\n| 至少选择一项 | Vue 表单校验 + PHP Validate + Service 防御校验 |\n| 保存并回显 | 新关系表、create/update/detail 同步读写 |\n| 所选账户受范围/次数限制 | 支付计划与限制规则做交集后评估 |\n| 未选账户不受限制 | 未命中账户直接跳过该规则 |\n| 赠送账户复用现有规则 | 与个人、餐补使用相同范围/次数限制，不增加独立金额配置 |\n\n### 2.2 明确不做\n\n- 不新增适用对象、身份、指定人员。\n- 不新增金额限制。\n- 不为不同账户分别配置不同档口、餐次或次数阈值。\n- 不改变现有范围限制的“禁止范围”语义。\n- 不改变现有页面导航、菜单、路由和消费限制类型。\n- 不在本任务实现赠送余额、赠送流水、消费顺序、退款和导出。\n\n### 2.3 与历史方案的差异\n\n`account-type-consume-limit-20260713` 历史方案属于扩展版设计，包含三维规则、金额限制和账户独立配置。云效当前 PRD 已将范围收敛为“同一条现有规则增加受限账户集合”。开发必须以云效当前版本为准，禁止把历史扩展内容顺带实现。\n\n## 3. 系统现状与缺口\n\n### 3.1 当前调用链\n\n```mermaid\nflowchart LR\n    UI[\"addConsumeLimit.vue\"] --> API[\"/api/consumeLimit/create|update|detail\"]\n    API --> CLS[\"ConsumeLimit Service\"]\n    CLS --> DB[\"consume_limit + 关系表\"]\n    PAY[\"Consume.php 在线支付\"] --> RULE[\"checkConsumeLimitForOrder\"]\n    RULE --> DB\n    PAY --> PRICE[\"营销价格计算\"]\n    PRICE --> SETTLE[\"账户余额扣款\"]\n```\n\n问题在于 `RULE` 发生在 `PRICE` 和 `SETTLE` 之前，此时不知道本单实际会使用个人账户、餐补账户还是赠送账户。\n\n### 3.2 已确认缺口\n\n| 层级 | 当前行为 | 本需求缺口 |\n| --- | --- | --- |\n| 数据 | 无账户关系 | 无法保存多选账户 |\n| 表单 | 无“受限账户” | 无法配置和回显 |\n| 接口 | 无 `accountTypes` | 无法校验和持久化 |\n| 范围校验 | 命中人员后全局拦截 | 无法跳过未选账户 |\n| 次数校验 | 统计人员全部订单 | 无法按支付账户区分 |\n| 支付时序 | 限制校验早于扣款拆分 | 无法识别本单实际账户 |\n| 日志 | 不记录账户 | 无法定位哪个账户触发限制 |\n| 赠送账户 | 当前分支已具备余额、扣款、退款和订单字段 | 本需求只需纳入同一受限账户判断 |\n=======\n# VWCG-1119 消费限制按受限账户余额生效技术开发文档\n\n> 版本：V1.2 兼容修订稿\n>\n> 日期：2026-07-29\n>\n> 目标仓库：`store`、`ai_api`\n>\n> 需求状态：待处理\n>\n> 风险等级：高\n>\n> 结论：方案可开发，但上线前必须确认“任一账户有钱”及透支口径\n\n## 1. 本次口径修订\n\n用户补充了三个关键条件：\n\n1. 消费限制规则增加“受限账户”，判断所选账户有没有钱。\n2. `store` 消费机订单覆盖 MQTT 和 HTTP API。\n3. 小程序端点餐也执行相同判断。\n4. 旧数据此前没有受限账户字段，默认勾选个人账户和餐补账户。\n\n因此，V1.0 中“先推演本单最终扣款账户，再按实际扣款账户分别计次”的设计不再适用。本版采用：\n\n```text\n受限账户可用余额门禁\n    + 现有范围限制/次数限制\n    = 本条消费限制是否拦截\n```\n\n“消费策略”按原 PRD 语境解释为“消费限制规则”，不改营销策略、优惠定价或账户扣款顺序。\n\n## 2. 最终业务规则\n\n### 2.1 推荐判定规则\n\n一条规则可选择 `cash`、`subsidy`、`gift` 中的一项或多项。校验时：\n\n1. 查询人员在判断时点的受限账户可用余额。\n2. 所选账户中**任意一个**可用余额大于 0，则本规则进入原有范围/次数判断。\n3. 所选账户可用余额全部小于等于 0，则跳过本规则。\n4. 未选账户的余额不参与本规则是否生效的判断。\n5. 一旦规则生效，范围和次数仍是整单限制，不自动切换资金来源。\n6. 次数继续按现有成功订单数统计，不按订单最终扣款账户拆分。\n\n示例：\n\n| 受限账户 | 判断时余额 | 是否执行规则 | 后续行为 |\n| --- | --- | --- | --- |\n| 餐补 | 餐补 10 | 是 | 检查当前档口/餐次与次数 |\n| 餐补 | 餐补 0、个人 100 | 否 | 个人余额不影响本规则 |\n| 个人+餐补 | 个人 0、餐补 3 | 是 | 任一所选账户有钱即生效 |\n| 个人+餐补 | 均为 0 | 否 | 跳过本规则 |\n| 赠送 | 赠送 5 | 是 | 仅在赠送账户能力启用后 |\n\n### 2.2 “有钱”的技术定义\n\n所有金额统一保留两位小数，使用 `bccomp(balance, '0.00', 2) > 0`，禁止使用 PHP 浮点直接比较。\n\n| 账户 | 可用余额定义 | 不计入 |\n| --- | --- | --- |\n| 个人账户 `cash` | `max(activity_principal_balance, 0) + max(withdrawable_balance, 0)`；旧数据未拆分时兼容回退 `max(balance, 0)` | `consume_overdraft_limit` 透支额度 |\n| 餐补账户 `subsidy` | 判断时点处于有效期、`is_delete=0` 且 `money>0` 的补贴余额之和 | 未开始、已过期、已删除、负数补贴 |\n| 赠送账户 `gift` | `max(gift_balance, 0)` | 未上线或未完成迁移的赠送能力 |\n\n余额查询异常、用户映射缺失或跨库不可用时必须失败并提示“账户信息获取失败，请稍后重试”，不能把异常当成 0 后跳过规则，否则会形成限制绕过。\n\n### 2.3 不改变的规则\n\n- 现有范围限制仍是“配置的档口 + 餐次禁止消费”。\n- 现有月、日、餐次次数阈值不变。\n- 次数仍查询对应应用场景的成功订单。\n- 不增加金额限制、指定人员、身份或每账户独立阈值。\n- 不因某受限账户触发而自动改用未受限账户。\n- 退款是否回补次数仍保持现状，本期不新增。\n\n## 3. 真实调用链与项目边界\n\n### 3.1 消费机 HTTP API\n\n```text\napplication/api/controller/Consume.php::orderPay()\n  -> application/api/service/Consume.php::orderPayAsync()\n  -> Redis 快照支付 或 runAsyncFallback()\n  -> Consume::orderPay()\n```\n\n### 3.2 消费机 MQTT\n\n```text\napplication/cron/command/MqttServer.php\n  -> Consume::orderPayAsync()\n  -> Redis 快照支付 或 runAsyncFallback()\n  -> Consume::orderPay()\n```\n\nHTTP 与 MQTT 最终汇入同一个支付服务，但 `orderPayAsync()` 内仍有 Redis 快照和数据库回退两条路径。只改控制器、只改 MQTT 命令或只改其中一条支付路径都会产生隐形绕过。\n\n### 3.3 小程序订餐\n\n小程序订餐由 `ai_api` 承载，不在 `store` 内创建：\n\n```text\nMealCheckout::submit()\n  -> meal/Checkout::onCheckout()\n  -> meal/Checkout::createOrder()\n  -> MealCashier / cashier/MealPayment\n  -> meal/PaySuccess\n\nMealCheckout::directPay()\n  -> meal/DirectPay::pay()\n```\n\n`store` 只识别 `OrderSourceEnum::ORDERFOOD = 4`，没有小程序提交订单入口。因此本需求是 `store + ai_api` 联动需求，只改 `store` 不完整。\n>>>>>>> 5d05833ef476571c794ef3016336bc136526872c\n\n## 4. 目标架构\n\n```mermaid\n<<<<<<< HEAD\nsequenceDiagram\n    participant C as 消费端\n    participant P as Consume 支付编排\n    participant A as 账户扣款规划器\n    participant L as 消费限制评估器\n    participant D as 业务库\n    participant S as 账户结算服务\n\n    C->>P: 发起在线消费\n    P->>P: 计算最终 pay_price\n    P->>A: 读取有效余额与扣款顺序\n    A-->>P: paymentPlan(cash/subsidy/gift)\n    P->>L: 人员、场景、档口、餐次、paymentPlan\n    L->>D: 查询规则、账户关系、历史账户订单次数\n    alt 有实际使用的受限账户触发规则\n        L-->>P: 拒绝 + 账户类型 + 原因\n        P-->>C: 支付失败\n    else 通过\n        L-->>P: 允许\n        P->>S: 使用同一计划结算\n        S-->>P: 实际账户拆分\n        P->>D: 保存订单\n        P-->>C: 支付成功\n    end\n```\n\n关键不变量：\n\n```text\npaymentPlan 中金额 > 0 的账户 = 本单实际使用账户\n限制评估使用的账户集合 = 实际结算使用的账户集合\n未在规则 accountTypes 中的账户，不执行该规则\n混合支付时，每个实际使用账户独立判断、独立计次\n=======\nflowchart TD\n    API[\"消费机 HTTP API\"] --> STORE[\"store Consume::orderPayAsync\"]\n    MQTT[\"消费机 MQTT\"] --> STORE\n    STORE --> SR[\"store 账户余额解析器\"]\n    SR --> SE[\"store 消费限制评估器\"]\n\n    MP[\"小程序点餐\"] --> CHECKOUT[\"ai_api MealCheckout / DirectPay\"]\n    CHECKOUT --> AR[\"ai_api 账户余额解析器\"]\n    AR --> AE[\"ai_api 消费限制评估器\"]\n\n    SE --> RULE[\"mysql_zhct 消费限制规则\"]\n    AE --> RULE\n    SR --> ACCOUNT[\"AI 账户库\"]\n    AR --> ACCOUNT\n    SE --> ORDER[\"ydy_meal_order 次数\"]\n    AE --> ORDER\n```\n\n两个项目分别实现适配层，但必须使用相同输入输出契约和同一套契约测试样例，避免规则漂移。\n\n评估器统一输入：\n\n```text\nstoreId\nstaffUuid\nzhctUserId\nscenario\nrestaurantId\nmealTimes\nconsumeTime\nmsgid/orderNo\naccountBalances {cash, subsidy, gift}\n```\n\n统一输出：\n\n```json\n{\n  \"allowed\": false,\n  \"message\": \"消费次数超限制\",\n  \"limit_id\": 123,\n  \"matched_account_types\": [\"subsidy\"],\n  \"reason\": \"times\"\n}\n>>>>>>> 5d05833ef476571c794ef3016336bc136526872c\n```\n\n## 5. 数据库设计\n\n### 5.1 新增账户关系表\n\n<<<<<<< HEAD\n建议新建 `ydy_consume_limit_account`，不把多选值存成逗号字符串或主表 JSON。\n=======\n新增 `ydy_consume_limit_account`：\n>>>>>>> 5d05833ef476571c794ef3016336bc136526872c\n\n| 字段 | 类型 | 约束 | 说明 |\n| --- | --- | --- | --- |\n| `id` | `int unsigned` | PK、AUTO_INCREMENT | 主键 |\n| `limit_id` | `int unsigned` | NOT NULL | 消费限制 ID |\n| `account_type` | `varchar(16)` | NOT NULL | `cash/subsidy/gift` |\n<<<<<<< HEAD\n| `store_id` | `int unsigned` | NOT NULL DEFAULT 0 | 商城/租户 |\n=======\n| `store_id` | `int unsigned` | NOT NULL DEFAULT 0 | 租户 |\n>>>>>>> 5d05833ef476571c794ef3016336bc136526872c\n| `create_time` | `datetime` | DEFAULT CURRENT_TIMESTAMP | 创建时间 |\n\n索引：\n\n```sql\nUNIQUE KEY uniq_limit_account (limit_id, account_type)\nKEY idx_store_account (store_id, account_type)\n```\n\n<<<<<<< HEAD\n选择关系表的原因：\n\n- 与部门、场景、范围关系表风格一致。\n- 避免模糊字符串查询。\n- 后续需要按账户筛选时可直接建立索引。\n- 不把当前 PRD扩展成“每账户独立阈值”。\n\n### 5.2 拦截日志扩展\n\n建议 `ydy_consume_limit_block_log` 增加：\n\n```text\naccount_type varchar(16) NOT NULL DEFAULT ''\n```\n\n用于记录实际触发拦截的账户。`counts` 和 `limits` 继续保存该账户的计次快照与阈值，不新增复杂日志表。\n\n### 5.3 历史数据迁移\n\n正式基线目前只有个人账户与餐补账户，建议对所有未删除历史规则回填：\n\n```text\ncash\nsubsidy\n```\n\n不自动回填 `gift`。当前分支虽然已具备赠送账户资金能力，但历史规则创建时没有“限制赠送账户”的明确业务意图；自动回填会扩大旧规则影响范围。\n\n迁移后校验：\n\n- 每个未删除规则至少有 2 条账户关系。\n- 不存在未知账户枚举。\n- 不存在重复 `(limit_id, account_type)`。\n- 不同 `store_id` 数据数量与主表一致。\n\n### 5.4 回滚原则\n\n- 生产有新规则后不直接删除关系表或账户日志字段。\n- 功能回滚时先部署忽略账户维度的旧代码，关系表保留。\n- 只有非生产且确认无新数据时，才执行物理删除表/列的回滚脚本。\n- 回滚后旧代码恢复“所有账户共同受限”的历史行为。\n\n## 6. API 契约\n\n### 6.1 新增/编辑\n\n接口保持不变：\n\n- `POST /api/consumeLimit/create`\n- `POST /api/consumeLimit/update`\n\n新增请求字段：\n\n```json\n{\n  \"accountTypes\": [\"cash\", \"subsidy\", \"gift\"]\n}\n```\n\n规则：\n\n- 类型必须为数组。\n- 去重后至少一项。\n- 只允许 `cash`、`subsidy`、`gift`。\n- 服务层再次校验，不能只依赖前端。\n\n错误文案建议：\n\n| 场景 | 文案 |\n| --- | --- |\n| 缺失/空数组 | 请选择受限账户 |\n| 非数组 | 受限账户参数格式错误 |\n| 未知枚举 | 受限账户参数不正确 |\n\n### 6.2 详情\n\n`POST /api/consumeLimit/detail` 增加：\n=======\n不使用逗号字符串或主表 JSON，避免模糊查询、重复值和后续迁移困难。\n\n### 5.2 拦截日志\n\n`ydy_consume_limit_block_log` 建议增加：\n\n```text\nmatched_account_types varchar(64) NOT NULL DEFAULT ''\naccount_balance_snapshot text NULL\nchannel varchar(16) NOT NULL DEFAULT ''\n```\n\n- `matched_account_types`：如 `cash,subsidy`。\n- `account_balance_snapshot`：只记录账户类型和金额，不记录敏感身份数据。\n- `channel`：`consume_api`、`consume_mqtt`、`mini_program`。\n\n### 5.3 历史规则\n\n历史未删除规则（包括启用和停用规则）统一回填 `cash + subsidy`，不回填 `gift`。回填 SQL 必须可重复执行，推荐使用唯一索引配合 `INSERT IGNORE` 或等价的“不存在才插入”语义。\n\n示例：\n\n```sql\nINSERT IGNORE INTO ydy_consume_limit_account\n    (limit_id, account_type, store_id, create_time)\nSELECT id, 'cash', store_id, NOW()\nFROM ydy_consume_limit\nWHERE is_delete = 0;\n\nINSERT IGNORE INTO ydy_consume_limit_account\n    (limit_id, account_type, store_id, create_time)\nSELECT id, 'subsidy', store_id, NOW()\nFROM ydy_consume_limit\nWHERE is_delete = 0;\n```\n\n兼容必须覆盖四层，不能只依赖一次性 SQL：\n\n1. **数据库回填**：上线前为全部 `is_delete=0` 旧规则补齐 `cash`、`subsidy` 两条关系。\n2. **详情接口兜底**：旧规则查不到账户关系时，仍返回 `account_types: [\"cash\", \"subsidy\"]`，并记录数据修复告警。\n3. **前端回显兜底**：新增页面默认勾选个人+餐补；编辑时接口字段缺失或为空，也显示个人+餐补，避免新旧版本混部时出现空选。\n4. **运行期评估兜底**：启用规则没有账户关系时按个人+餐补判断，并记录高优先级告警，不能静默放行。\n\n旧规则首次编辑保存后，必须把默认值真实持久化到关系表。任何兼容层都不得默认选中 `gift`。\n\n为避免迁移遗漏造成静默放行：\n\n- 迁移完成时每条未删除旧规则有两条默认账户关系；用户后续编辑后至少保留一条。\n- 运行期发现启用规则没有账户关系时，临时按 `cash + subsidy` 兼容，并记录高优先级告警。\n- 修复数据后移除告警，不能长期依赖运行期默认值。\n\n行为变化需要业务知晓：历史规则原来不看余额；回填后，当个人和有效餐补均为 0 时规则会跳过。这是本次新增口径的预期变化。\n\n## 6. API 与前端\n\n### 6.1 CRUD 契约\n\n现有接口不变：\n\n- `POST /api/consumeLimit/create`\n- `POST /api/consumeLimit/update`\n- `POST /api/consumeLimit/detail`\n\n新增/编辑请求：\n\n```json\n{\n  \"accountTypes\": [\"cash\", \"subsidy\"]\n}\n```\n\n详情响应：\n>>>>>>> 5d05833ef476571c794ef3016336bc136526872c\n\n```json\n{\n  \"account_types\": [\"cash\", \"subsidy\"]\n}\n```\n\n<<<<<<< HEAD\n为兼容前端命名习惯，内部数据库使用 snake_case，页面表单继续使用 `accountTypes`。\n\n### 6.3 列表\n\n当前 PRD 未要求列表展示或筛选受限账户。本期不新增列表列和筛选项，避免扩大范围。服务层可以不返回账户名称；若联调需要，可返回 `account_types` 但前端不展示。\n\n### 6.4 路由与权限\n\n- 复用现有接口，不修改 `application/route.php`。\n- 不新增菜单或按钮权限。\n- 保持现有登录、部门数据权限、餐厅/档口数据权限逻辑。\n- 新关系表所有写入必须带 `store_id`，读取必须通过主规则的 `store_id` 约束。\n\n## 7. 后端开发设计\n\n### 7.1 账户枚举\n\n建议新增统一枚举/值对象，例如：\n\n```text\napplication/common/enum/account/Type.php\n```\n\n映射：\n\n| 内部值 | 页面文案 | 订单支付字段 |\n| --- | --- | --- |\n| `cash` | 个人账户 | `ydy_meal_order.cash` |\n| `subsidy` | 餐补账户 | `ydy_meal_order.subsidy` |\n| `gift` | 赠送账户 | `ydy_meal_order.gift` |\n\n后端、测试和 `VWCG-1121` 必须共用同一组枚举，避免出现 `personal/cash`、`meal_allowance/subsidy` 等重复口径。\n\n### 7.2 消费限制 CRUD\n\n`application/api/controller/ConsumeLimit.php`：\n\n- create/update 中调用 `formatArray` 规范化 `accountTypes`。\n- 不改变现有档口权限校验。\n\n`application/api/validate/ConsumeLimit.php`：\n\n- 增加 `accountTypes` 的 require、array、自定义白名单和非空校验。\n- create/update scene 同步加入该字段。\n\n`application/api/service/ConsumeLimit.php`：\n\n- `syncRelations()` 同步删除并写入账户关系。\n- `detail()` 返回账户数组。\n- `delete()` 同步删除账户关系。\n- create/update 继续放在同一业务库事务内。\n- 捕获异常时保留内部日志，API 只返回可理解的业务错误。\n\n### 7.3 支付计划\n\n推荐以 `VWCG-1121` 的 `GiftAccountPolicy::planDeduction()` 为统一纯计算核心，并给 `AccountSettlementService` 增加只读计划能力：\n\n```text\npreviewDeduction(accountDb, userId, storeId, amount, consumeTime, overdraftLimit)\n```\n\n返回：\n\n```json\n{\n  \"subsidy\": \"8.00\",\n  \"cash\": \"12.00\",\n  \"gift\": \"0.00\",\n  \"deduction_order\": [\"subsidy\", \"cash\", \"gift\"]\n}\n```\n\n为防止预览后余额变化导致实际扣款账户变化，实际 `deduct()` 应：\n\n1. 在锁定账户和补贴行后重新计算计划。\n2. 与传入的 `expected_plan` 比较账户集合和金额。\n3. 不一致时中止，不扣款，返回“账户余额发生变化，请重试”。\n\n当前分支已具备赠送账户余额、扣款、退款和订单实付字段。本需求直接复用该能力，只增加 `gift` 是否受本条消费限制影响的判断，不新增赠送金额、扣款顺序或退款算法。\n\n### 7.4 消费限制评估器\n\n建议从 `Consume.php` 中抽出可测试的领域服务：\n\n```text\napplication/api/service/ConsumeLimitEvaluator.php\n```\n\n输入：\n\n```text\nstaffUuid\nuserId\nscenario\nrestaurantId\nmealTimes\nconsumeTime\nmsgid\npayAmount\naccountBalances\n```\n\n处理顺序：\n\n1. 找到人员、场景、门店下启用的规则。\n2. 批量加载各规则的 `accountTypes`。\n3. 对每条规则的每个受限账户分别判断范围限制和次数限制。\n4. 次数限制只统计该账户订单实付字段大于 0 的成功订单。\n5. 将命中限制的账户从本单可用余额中排除。\n6. 使用未受限账户重新计算可支付总额和扣款计划。\n7. 未受限账户可以覆盖整单：允许支付，并把受限账户集合传给最终扣款。\n8. 原账户总额足够但排除后不足：拦截整单并记录触发账户。\n\n### 7.5 范围限制\n\n继续使用当前“禁止范围”语义：\n\n```text\n规则选择了该账户\n+ 当前档口/餐次出现在 consume_limit_scope\n=> 本单排除该账户；剩余账户不足时才拦截整单\n```\n\n示例：\n\n| 规则受限账户 | 本单计划 | 当前档口命中 | 结果 |\n| --- | --- | ---: | --- |\n| 餐补 | 餐补 10；个人余额 20 | 是 | 排除餐补，改由个人支付 |\n| 餐补 | 个人 10 | 是 | 个人账户不受影响，放行 |\n| 个人+餐补 | 餐补 5 + 个人 5；赠送余额 10 | 是 | 排除个人和餐补，改由赠送支付 |\n| 赠送 | 赠送 10；其他账户不足 | 是 | 排除赠送后余额不足，拦截整单 |\n\n受限账户命中规则后必须使用未受限账户重算扣款计划。支付预览、支付前复检和最终扣款必须使用同一受限账户集合，避免页面显示与实际扣款不一致。\n\n### 7.6 次数限制\n\n推荐按账户分别计次，而不是把所有受限账户合计：\n\n```text\ncash 次数 = 成功订单中 cash > 0 的订单数\nsubsidy 次数 = 成功订单中 subsidy > 0 的订单数\ngift 次数 = 成功订单中 gift > 0 的订单数\n```\n\n混合支付订单会分别计入实际使用的每个账户一次。\n\n基础过滤保持现状：\n\n- `user_id`\n- `is_delete = 0`\n- `pay_status = SUCCESS`\n- 应用场景对应的订单来源\n- 月/日/餐次时间范围\n\n退款是否回补次数当前 PRD 未定义。为兼容现状，本期仍按成功订单计数，不因退款回补；需要产品在评审时确认并写入验收口径。\n\n### 7.7 拦截提示与日志\n\n建议提示带账户名称：\n\n- `餐补账户当前时间档口无消费权限`\n- `个人账户消费次数超限制`\n- `赠送账户消费次数超限制`\n\n日志至少记录：\n\n- 规则 ID\n- 触发账户类型\n- 人员、用户、场景、档口、餐次\n- 当前账户的月/日/餐计数\n- 规则阈值\n- 消费时间和幂等消息 ID\n\n### 7.8 两条在线消费路径\n\n当前 `Consume.php` 有同步支付和异步快照支付两处校验调用，两处必须一起改造：\n\n- 异步快照路径：基于快照余额生成计划。\n- 数据库同步路径：基于账户库有效余额生成计划。\n\n禁止只修改其中一处，否则不同设备/链路会得到不同结果。\n\n## 8. 前端开发设计\n\n目标文件：\n\n```text\npublic/static/src/view/marketingCenter/addConsumeLimit.vue\n```\n\n### 8.1 表单\n\n在“应用场景”后插入：\n=======\n校验：\n\n- 必须为数组，去重后至少一项。\n- 只允许 `cash/subsidy/gift`。\n- 赠送账户能力未开放时，传 `gift` 返回“赠送账户暂未开放”。\n- 前端、Validate、Service 三层均校验。\n\n### 6.2 PC 页面\n\n目标文件：\n\n`public/static/src/view/marketingCenter/addConsumeLimit.vue`\n\n在“应用场景”后增加：\n>>>>>>> 5d05833ef476571c794ef3016336bc136526872c\n\n```text\n受限账户 ⓘ\n[✓] 个人账户  [✓] 餐补账户  [ ] 赠送账户\n```\n\n<<<<<<< HEAD\n交互规则：\n\n- 使用 `el-checkbox-group`。\n- label 宽度、间距、颜色沿用当前页面。\n- tooltip 精确文案：`所选账户受范围或次数的限制。`\n- 默认值：`['cash', 'subsidy']`。\n- 赠送账户可选，但默认不勾选；它与个人、餐补复用当前规则。\n- 至少选择一项；错误信息显示在当前表单项下方。\n\n### 8.2 编辑回显与提交\n\n`initialForm()` 增加：\n\n```js\naccountTypes: ['cash', 'subsidy']\n```\n\n详情加载：\n\n```text\nd.account_types -> form.accountTypes\n```\n\n提交：\n\n```text\nform.accountTypes -> payload.accountTypes\n```\n\n历史数据完成后端回填后，详情应始终返回个人+餐补；前端不自行猜测缺失关系。\n\n### 8.3 其他页面\n\n`consumeLimitMgr.vue` 本期不增加列、筛选和弹窗字段。新增/编辑统一使用独立页面，避免维护两套表单。\n\n## 9. 兼容与依赖\n\n### 9.1 `VWCG-1121` 依赖\n\n当前 `VWCG-1121`：\n\n- 云效状态仍为待处理。\n- 当前分支已经包含赠送余额、扣款、退款和订单记录能力。\n\n推荐合并顺序：\n\n1. 使用当前统一账户结算能力。\n2. `VWCG-1119` 基于该结算服务增加“计划后限制评估”。\n3. 页面和接口同时启用 `cash/subsidy/gift`。\n\n### 9.2 多商户\n\n- 所有规则和关系按 `store_id` 隔离。\n- 不基于 `BUSINESS` 写死某客户。\n- 三种账户使用同一套多商户和 `store_id` 隔离规则。\n- 关系表回填必须逐 `store_id` 验证数量。\n\n### 9.3 离线补传\n\n当前 `order_type=2` 会跳过消费限制。本方案默认保持兼容，不把离线历史订单在补传时拦截；但这些订单是否计入后续账户次数必须确认。建议计入，因为订单支付字段能够反映实际账户来源。\n\n## 10. 测试设计\n\n### 10.1 自动化测试\n=======\n- 使用 `el-checkbox-group`。\n- 新增默认 `['cash', 'subsidy']`。\n- 提示：`所选账户有可用余额时，该消费限制规则生效。`\n- 赠送账户未开放时置灰。\n- 编辑时以后端 `account_types` 为准；字段缺失或空数组时防御性回退 `['cash', 'subsidy']`。\n- 旧规则回显默认个人+餐补后，用户可以主动取消其中一项，但至少保留一个账户。\n\n列表页本期不新增列或筛选，不改路由、菜单或按钮权限。\n\n## 7. `store` 后端改造\n\n### 7.1 账户余额解析器\n>>>>>>> 5d05833ef476571c794ef3016336bc136526872c\n\n建议新增：\n\n```text\n<<<<<<< HEAD\ntests/ConsumeLimit/ConsumeLimitAccountPolicyTest.php\ntests/ConsumeLimit/ConsumeLimitServiceTest.php\ntests/ConsumeLimit/ConsumeLimitPaymentTest.php\n```\n\n| 测试域 | 场景 |\n| --- | --- |\n| 参数 | 缺失、空数组、非数组、重复、未知枚举、三个合法枚举 |\n| CRUD | 新增保存、编辑替换、详情回显、删除清理、事务回滚 |\n| 历史迁移 | 每条历史规则回填 cash+subsidy、无重复、租户一致 |\n| 范围限制 | 选中账户命中/未命中；未选账户；混合支付 |\n| 次数限制 | cash/subsidy/gift 分别计次；混合订单分别计次；月/日/餐边界 |\n| 支付计划 | 单账户、两账户、余额临界、营销价变化、计划漂移 |\n| 赠送账户 | 页面选择、保存、回显；命中后只排除 gift，未选账户仍可支付 |\n| 权限 | 部门数据权限、档口权限、跨 store_id 拒绝 |\n| 日志 | 账户类型、规则、计数、阈值完整 |\n| 双链路 | 同步支付、异步快照支付结果一致 |\n\n必须回归：\n\n- `tests/DataPermission/ChangedInterfacesPermissionCoverageTest.php`\n- `tests/DataPermission/UserDataPermissionStaticTest.php`\n- `tests/DataPermission/YunxiaoAcceptanceCriteriaTest.php`\n- `vendor/bin/phpunit`\n\n### 10.2 前端与手工验收\n\n- `npm run lint`\n- `npm run build`\n- 新增、编辑、必填错误、回显，以及页面不存在独立“赠送金额/赠送规则”配置的截图。\n- 个人账户未选时使用纯个人账户支付应放行。\n- 餐补账户被选且命中范围时应拦截。\n- 同一规则选择个人+餐补时，两个账户按同一阈值分别计次。\n- 混合支付的限制与订单拆分一致。\n- 无权限账号不能绕过部门/档口数据权限修改规则。\n\n如仓库既有 lint/build 环境失败，必须记录基线错误并补充定向 Vue 编译，不得口头宣称已通过。\n\n### 10.3 Apifox\n\n至少补充：\n\n- create/update/detail 的账户字段正反例。\n- 纯个人、纯餐补、混合支付的范围/次数场景。\n- 赠送账户新增、编辑、回显和支付限制。\n- 历史规则回显。\n\n## 11. 发布、灰度与回滚\n\n### 11.1 上线顺序\n\n1. 备份并统计消费限制主表数量。\n2. 执行关系表、日志字段和历史回填 SQL。\n3. 校验每条历史规则均有个人+餐补关系。\n4. 部署后端 CRUD 与账户感知校验。\n5. 部署 PC 表单，展示并启用个人、餐补、赠送三个选项。\n6. 选择内部门店验证个人、餐补、赠送及混合支付。\n7. 观察账户维度拦截日志和支付失败率。\n8. 扩大门店范围。\n\n### 11.2 监控\n\n- 按 `account_type` 聚合的范围/次数拦截量。\n- 账户计划变化导致的重试量。\n- 支付失败率与余额不足率。\n- 新规则缺失账户关系数量。\n- 未知账户枚举数量。\n- 订单拆分之和与 `pay_price` 的差额。\n\n### 11.3 回滚\n\n- 紧急回滚先停用本次新增/编辑的规则或关闭账户维度开关。\n- 部署旧代码时保留新关系表和日志列。\n- 禁止删除已产生业务数据的关系表。\n- 若账户计划与实际结算不一致，先停止新支付并对账，不允许直接补写订单金额。\n\n## 12. PRD 验收映射\n\n| PRD 验收点 | 开发证据 |\n| --- | --- |\n| 页面出现受限账户 | 页面截图 + Vue 定向编译/构建 |\n| 支持多选且至少一项 | 前端校验 + API 反例测试 |\n| 提示文案准确 | UI 截图/组件测试 |\n| 保存并回显 | create/update/detail 接口测试 + DB 记录 |\n| 仅所选账户执行限制 | 纯个人/纯餐补/混合支付集成测试 |\n| 未选账户不受限制 | 未选账户正向支付用例 |\n| 风格与当前模块一致 | 人工 UI 评审 |\n| 不增加复杂配置 | 代码 diff 与页面结构评审 |\n\n## 13. 异步导出联动判断\n\n本需求不修改消费订单、人员/部门/设备明细、充值/补贴统计、菜品销量、餐厅经营汇总或档口结算列表的字段、查询条件、统计口径和导出数据源，因此 `VWCG-1119` 本身无需修改九类异步导出。\n\n`VWCG-1121` 对订单新增赠送支付字段产生的导出影响，应继续在 `VWCG-1121` 范围内处理，不能混入本任务。\n\n## 14. 推荐开发切片与估时\n\n| 切片 | 内容 | 非 AI 估时 |\n| --- | --- | ---: |\n| 1 | DDL、回填、回滚、账户枚举 | 4～6h |\n| 2 | CRUD、校验、详情回显 | 6～8h |\n| 3 | 扣款计划与限制评估器 | 12～16h |\n| 4 | 账户维度计次、日志、双支付链路 | 8～12h |\n| 5 | PC 表单与交互 | 4～6h |\n| 6 | 自动化、Apifox、手工回归与发布证据 | 10～14h |\n| 合计 | 个人+餐补+赠送完整闭环 | 44～62h |\n\n当前分支已包含赠送账户资金链路，本需求中的赠送账户选项直接启用并纳入相同回归矩阵。\n\n## 15. 评审前必须确认\n\n### P0\n\n1. 次数限制按每个受限账户分别计次，不合并账户次数。\n2. 受限账户命中后排除该账户并改用未受限账户；只有剩余账户不能覆盖整单时才拦截。\n3. 赠送账户与个人、餐补同时可选，复用同一范围/次数规则。\n\n### P1\n\n4. 历史规则是否只回填个人+餐补？本文推荐是，不自动限制未来赠送账户。\n5. 退款订单是否回补消费次数？本文推荐本期保持现状，不回补。\n6. 离线补传订单是否计入后续次数？本文推荐计入，但补传过程本身继续不拦截。\n\n## 16. 开发门禁结论\n\n当前已经具备：\n\n- 明确的本期范围和非范围。\n- 可落地的数据、接口、前端和支付链路方案。\n- 真实文件落点。\n- 测试、上线和回滚策略。\n- 已确认的按账户分别计次、排除受限账户后重算和赠送账户同步启用口径。\n\n发布前仍需完成：\n\n- 目标环境 DDL 执行与历史数据核验。\n- 产品、后端、前端和测试联合自测。\n- 退款计次与离线补传边界的最终验收记录。\n\n因此结论为：**可按已确认口径完成开发并进入联合自测；上线前必须先执行关系表迁移。**\n=======\napplication/common/service/account/AccountAvailabilityResolver.php\n```\n\n职责：\n\n1. `staff_uuid -> ydy_user -> ai_user_id`。\n2. 读取个人拆分余额、有效餐补、赠送余额。\n3. 使用业务消费时间过滤补贴有效期。\n4. 返回标准化 `{cash, subsidy, gift}`。\n5. 对旧账户数据执行个人余额兼容回退。\n\n### 7.2 评估器\n\n将 `Consume.php::checkConsumeLimitForOrder()` 抽为：\n\n```text\napplication/api/service/ConsumeLimitEvaluator.php\n```\n\n处理顺序：\n\n1. 按人员部门、场景、租户查启用规则。\n2. 批量加载规则账户关系。\n3. 对每条规则取 `selectedAccounts ∩ positiveBalanceAccounts`。\n4. 交集为空：跳过规则。\n5. 交集非空：按原逻辑检查范围和次数。\n6. 任一规则失败：整单拒绝并写日志。\n\n次数查询继续使用 `ydy_meal_order` 的成功订单，不增加 `cash/subsidy/gift > 0` 条件。\n\n### 7.3 MQTT 与 API 接入\n\n不要分别在入口复制业务判断。统一在 `Consume::orderPayAsync()` 进入支付锁之后、创建订单和扣款之前调用。\n\n必须覆盖：\n\n- Redis 快照路径 `handleAsyncSnapshotPayment()`。\n- 数据库回退路径 `runAsyncFallback() -> orderPay()`。\n- HTTP API 调用。\n- MQTT 调用。\n\n入口只负责传入 `channel`，用于日志区分。\n\n### 7.4 Redis 快照正确性\n\n当前消费快照 TTL 为 300 秒。直接使用旧快照判断“有没有钱”会出现：\n\n- 刚充值但快照仍为 0，规则被错误跳过。\n- 刚消费完但快照仍有余额，规则被错误执行。\n- 补贴刚生效/过期时判断错误。\n\n推荐在支付锁内从账户库刷新余额，再同步更新 Redis 快照；至少必须保证限制判断与随后扣款使用同一份、同一 `consumeTime` 的余额快照。不能用一份旧快照判断、再用另一份新余额扣款。\n\n### 7.5 消费时间\n\n- MQTT/API 在线订单：校验设备 `create_time` 与服务器时间的偏差，超出允许窗口时使用服务器时间或拒绝。\n- 小程序：使用服务器时间。\n- 补贴有效期、次数统计边界和日志统一使用最终确认的 `consumeTime`。\n- `order_type=2` 离线历史补传继续跳过实时限制，本期不改变。\n\n## 8. `ai_api` 小程序改造\n\n### 8.1 主校验点\n\n新增与 `store` 同契约的消费限制评估器，例如：\n\n```text\napp/common/service/consumeLimit/ConsumeLimitEvaluator.php\napp/common/service/account/AccountAvailabilityResolver.php\n```\n\n规则从 `mysql_zhct` 读取，账户余额从 `ai_api` 默认账户库读取。\n\n主接入点：\n\n1. `MealCheckout::submit()`：完成用户、餐厅、餐段、购物车校验后，在创建订单前检查。\n2. `meal/DirectPay::pay()`：在生成订单和扣款前检查。\n3. `cashier/MealPayment` / `meal/PaySuccess`：支付前防御复检，覆盖历史待支付订单和绕过提交入口的调用。\n\n### 8.2 为什么需要提交与支付双检查\n\n- 只在提交检查：用户提交后充值，支付时规则状态可能已变化。\n- 只在支付检查：会生成注定不能支付的脏订单，0 元订单还可能直接完成。\n- 双检查：提交时尽早反馈，支付时以最新余额和规则做最终防绕过。\n\n直接扣款必须在同一业务事务中完成“最终复检 -> 创建订单 -> 扣款”。若事务中余额已变化，应重新判断或失败重试。\n\n### 8.3 用户映射\n\n小程序登录用户使用 AI `user_id`；消费限制部门和订单次数使用智慧餐厅用户：\n\n```text\nAI user_id\n  -> ZhctUserModel / ZhctStaffModel\n  -> zhct user_id + staff_uuid\n```\n\n映射缺失时失败，禁止因为查不到部门而默认无规则放行。\n\n## 9. 并发、幂等与隐形 Bug 防护\n\n### 9.1 已识别高风险\n\n| 风险 | 后果 | 必须措施 |\n| --- | --- | --- |\n| MQTT/API 只改一个入口 | 同一设备因上报方式不同结果不同 | 统一汇入 Service，双入口契约测试 |\n| Redis 300 秒旧余额 | 规则误生效或被绕过 | 锁内刷新或保证同快照判断与扣款 |\n| 小程序只改 store | 小程序下单完全不判断 | `ai_api` 提交、直付、支付兜底接入 |\n| 个人余额直接用 `balance` | 赠送/拆分后重复或错误计算 | 使用拆分可用余额，旧数据才回退 |\n| 把透支当“有钱” | 0 余额用户仍触发规则 | 默认不包含透支，产品确认 |\n| 餐补不按业务时间过滤 | 过期/未生效补贴误判 | 使用统一 `consumeTime` |\n| 余额查询异常按 0 | 系统故障时规则被绕过 | 查询失败必须失败关闭 |\n| 提交时检查、支付时不复检 | 余额/规则变化后绕过 | 支付前最终复检 |\n| 次数先查后写 | 并发两单同时通过上限 | 跨项目同一用户/场景锁 |\n| 两项目算法复制后漂移 | 消费机与小程序行为不同 | 共享契约样例和版本化规则 |\n| 历史规则无关系数据 | 全部静默放行 | 回填 + 运行期告警兼容 |\n| 只做前端默认、不做数据回填 | API/任务链路仍把旧规则当空配置 | SQL、详情、前端、运行期四层兼容 |\n| 0 元订单绕过 | 免费订单不受范围/次数约束 | 在 0 元自动支付前检查 |\n\n### 9.2 次数并发\n\n现有“查询成功订单数 -> 判断 -> 创建订单”不是原子操作，两笔并发订单可能同时看到未达上限。\n\n最低要求：\n\n- `store` 和 `ai_api` 使用同一锁命名：`consume_limit:{storeId}:{zhctUserId}:{scenario}`。\n- 锁必须覆盖最终复检到成功订单落库。\n- 两项目 Redis 不共享时，使用 `mysql_zhct` 中的数据库锁/守卫行，不得各锁各的。\n- `msgid`、`order_no` 继续承担幂等，重复请求不重复占次数。\n\n### 9.3 失败策略\n\n| 异常 | 策略 |\n| --- | --- |\n| 无匹配规则 | 放行 |\n| 所选账户均无可用余额 | 跳过该规则 |\n| 账户库超时/用户映射失败 | 拒绝并可重试 |\n| 规则关系缺失 | 按历史 `cash+subsidy` 兼容并告警 |\n| 未知账户枚举 | 拒绝保存；运行期忽略未知值并告警 |\n| 日志写入失败 | 不影响明确的拦截结果，但必须错误告警 |\n\n## 10. 测试设计\n\n### 10.1 规则与余额\n\n- 个人有钱/没钱/拆分字段未迁移。\n- 餐补有效、未生效、已过期、负数、跨日边界。\n- 赠送有钱/没钱/能力未开放。\n- 多选任一有钱、多选全部没钱、未选账户有钱。\n- 旧规则迁移前详情、迁移后详情、重复执行迁移、首次编辑持久化。\n- 新增页面默认个人+餐补；编辑旧规则默认个人+餐补；gift 永不默认选中。\n- 余额查询异常、用户映射缺失。\n- 0 元订单。\n- 透支额度大于 0 但实际账户余额为 0。\n\n### 10.2 三类入口\n\n| 入口 | 必测路径 |\n| --- | --- |\n| 消费机 HTTP API | 刷卡、付款码、人脸；Redis 命中和 DB 回退 |\n| 消费机 MQTT | 刷卡、付款码；重复 msgid；Redis 命中和 DB 回退 |\n| 小程序订餐 | submit、0 元自动支付、余额支付、directPay、历史待支付复检 |\n\nHTTP 与 MQTT 使用同一输入时，除 `channel/trace_id` 外结果必须一致。\n\n### 10.3 范围、次数与并发\n\n- 有余额且命中禁止档口/餐次：拦截。\n- 无余额但命中禁止档口/餐次：规则跳过。\n- 有余额且月/日/餐次达上限：拦截。\n- 多账户有余额时仍只按订单计一次。\n- MQTT 与小程序同一人员并发冲击最后一次额度，只允许一单成功。\n- 重复请求、支付回调重试不重复计次。\n\n### 10.4 回归命令\n\n`store`：\n\n```bash\nvendor/bin/phpunit\ncd public/static && npm run lint && npm run build\n```\n\n`ai_api`：\n\n```bash\nvendor/bin/phpunit\n```\n\n另补充 Apifox 的 create/update/detail、消费机 API、小程序下单正反例；MQTT 使用同请求载荷做集成回归。\n\n## 11. 上线、监控与回滚\n\n### 11.1 上线顺序\n\n1. 合并并迁移 `VWCG-1121` 所需账户字段；未完成时先禁用 `gift`。\n2. 创建账户关系表、日志字段并回填历史规则。\n3. 校验所有未删除旧规则均有 cash+subsidy，且重复执行回填不会产生重复数据。\n4. 部署 `store` 后端评估器，保持 PC 新字段暂不可编辑。\n5. 部署 `ai_api` 小程序校验。\n6. 部署 PC 表单并开放个人/餐补。\n7. 内部门店同时验证 HTTP、MQTT、小程序。\n8. 观察后再逐步开放；赠送账户最后开启。\n\n### 11.2 监控\n\n- 按 `channel/reason/account_type` 的拦截量。\n- 账户查询失败与跨库超时量。\n- HTTP 与 MQTT 同设备结果差异。\n- 小程序提交通过但支付复检失败量。\n- 无账户关系规则数量。\n- 历史规则默认关系回填数量、兜底命中数量。\n- 并发锁等待、超时和次数超限量。\n- 规则跳过量，特别是“所选账户均为 0”。\n\n### 11.3 回滚\n\n- 增加服务端能力开关，按渠道关闭新余额门禁。\n- 回滚代码时保留关系表和日志列。\n- 关闭功能后恢复旧的“所有匹配人员直接执行范围/次数”行为。\n- 不删除已产生业务数据的表或列。\n- 若两个项目只成功发布一个，立即关闭新能力，禁止半套规则运行。\n\n## 12. 异步导出联动\n\n本需求不改变订单列表字段、筛选条件、统计口径或导出数据源。次数统计仍按订单，不按账户支付字段拆分，因此九类异步导出无需同步修改。\n\n该列表页关联异步导出功能的检查结论：本次只增加支付前限制判断和拦截日志，不改变成功订单的导出结构。\n\n## 13. 开发切片与估时\n\n| 切片 | 内容 | 估时 |\n| --- | --- | ---: |\n| 1 | DDL、回填、回滚、账户枚举 | 4～6h |\n| 2 | CRUD、详情回显、PC 表单 | 8～12h |\n| 3 | `store` 余额解析与评估器 | 8～12h |\n| 4 | HTTP/MQTT 双路径、快照一致性 | 8～12h |\n| 5 | `ai_api` submit/directPay/支付复检 | 10～14h |\n| 6 | 跨项目并发锁、日志、监控 | 6～10h |\n| 7 | 自动化、Apifox、MQTT 与灰度证据 | 10～14h |\n| 合计 | 个人+餐补完整闭环 | 54～80h |\n\n赠送账户启用另预留 6～10h 做迁移、回归与灰度。\n\n## 14. 评审前必须确认\n\n### P0\n\n1. 多选受限账户是否采用“任一所选账户有可用余额即生效”？本文按“任一有钱”设计。\n2. 个人账户“有钱”是否不包含消费机透支额度？本文默认不包含。\n3. 小程序的 0 元订单是否也执行限制？本文默认执行。\n4. `store` 与 `ai_api` 是否能使用同一 Redis 锁；不能时必须选择 `mysql_zhct` 数据库锁方案。\n\n### P1\n\n5. 在线消费机允许设备时间与服务器时间偏差多少？\n6. 赠送账户具体开放版本和能力开关由谁负责？\n7. 离线补传继续不拦截、但成功订单计入后续次数，是否维持现状？\n\n## 15. 开发门禁结论\n\n本版已根据用户补充口径完成以下修正：\n\n- 从“实际扣款账户判断”改为“受限账户可用余额判断”。\n- 明确次数不按账户拆分，继续沿用当前订单次数。\n- 明确消费机 HTTP API、MQTT 和小程序订餐三类入口。\n- 明确小程序改造位于 `ai_api`，不是只改 `store`。\n- 增加 Redis 旧快照、跨项目并发、0 元订单、跨库映射等隐形 Bug 防护。\n- 明确旧规则在数据库、详情、前端和运行期四层默认个人+餐补，赠送账户不默认选中。\n\n结论：**技术上可实现，但属于双项目、高一致性改造。P0 口径确认、跨项目锁方案和赠送账户依赖未关闭前，不建议直接上线。**\n>>>>>>> 5d05833ef476571c794ef3016336bc136526872c\n",
  "blocks": [
    {
      "line": 1,
      "variant_a": "# VWCG-1119 消费限制按受限账户生效技术开发文档\n\n> 版本：V1.0 评审稿  \n> 日期：2026-07-29  \n> 目标仓库：`store`  \n> 需求状态：待处理  \n> 风险等级：中高  \n> 结论：可进入联合评审；P0 决策确认后再拆开发任务\n\n## 1. 结论摘要\n\n本需求不能只在表单增加一个多选框。现有支付链路在账户扣款拆分之前执行消费限制，次数统计也只统计订单总数；如果只保存账户类型，后端仍无法保证“未勾选账户不受限制”。\n\n推荐方案：\n\n1. 新增规则与账户的关系表 `ydy_consume_limit_account`，账户枚举统一使用 `cash`、`subsidy`、`gift`。\n2. 新增/编辑/详情接口增加 `accountTypes` / `account_types`，前后端都校验至少选择一项。\n3. 支付前先按最终 `pay_price` 和当前扣款顺序生成只读扣款计划，再把实际使用账户传给消费限制评估器。\n4. 范围限制只检查本单实际使用且被规则选中的账户。\n5. 次数限制按账户分别统计 `ydy_meal_order.cash/subsidy/gift > 0` 的成功订单，不能继续统计全部订单。\n6. 页面展示并启用个人账户、餐补账户和赠送账户；赠送账户只作为“受限账户”选项，不增加独立金额或独立规则配置。\n7. 不新增指定人员、身份、金额限制、账户独立档口配置或复杂规则组合。\n\n## 2. 当前 PRD 范围\n\n### 2.1 本期必须实现\n\n| 需求点 | 技术落点 |\n| --- | --- |\n| 增加“受限账户” | `addConsumeLimit.vue` 增加必填 Checkbox Group |\n| 位于应用场景之后 | 保持原型顺序，不调整其他表单结构 |\n| 个人/餐补/赠送多选 | API 接收 `cash/subsidy/gift`，页面展示中文 |\n| 至少选择一项 | Vue 表单校验 + PHP Validate + Service 防御校验 |\n| 保存并回显 | 新关系表、create/update/detail 同步读写 |\n| 所选账户受范围/次数限制 | 支付计划与限制规则做交集后评估 |\n| 未选账户不受限制 | 未命中账户直接跳过该规则 |\n| 赠送账户复用现有规则 | 与个人、餐补使用相同范围/次数限制，不增加独立金额配置 |\n\n### 2.2 明确不做\n\n- 不新增适用对象、身份、指定人员。\n- 不新增金额限制。\n- 不为不同账户分别配置不同档口、餐次或次数阈值。\n- 不改变现有范围限制的“禁止范围”语义。\n- 不改变现有页面导航、菜单、路由和消费限制类型。\n- 不在本任务实现赠送余额、赠送流水、消费顺序、退款和导出。\n\n### 2.3 与历史方案的差异\n\n`account-type-consume-limit-20260713` 历史方案属于扩展版设计，包含三维规则、金额限制和账户独立配置。云效当前 PRD 已将范围收敛为“同一条现有规则增加受限账户集合”。开发必须以云效当前版本为准，禁止把历史扩展内容顺带实现。\n\n## 3. 系统现状与缺口\n\n### 3.1 当前调用链\n\n```mermaid\nflowchart LR\n    UI[\"addConsumeLimit.vue\"] --> API[\"/api/consumeLimit/create|update|detail\"]\n    API --> CLS[\"ConsumeLimit Service\"]\n    CLS --> DB[\"consume_limit + 关系表\"]\n    PAY[\"Consume.php 在线支付\"] --> RULE[\"checkConsumeLimitForOrder\"]\n    RULE --> DB\n    PAY --> PRICE[\"营销价格计算\"]\n    PRICE --> SETTLE[\"账户余额扣款\"]\n```\n\n问题在于 `RULE` 发生在 `PRICE` 和 `SETTLE` 之前，此时不知道本单实际会使用个人账户、餐补账户还是赠送账户。\n\n### 3.2 已确认缺口\n\n| 层级 | 当前行为 | 本需求缺口 |\n| --- | --- | --- |\n| 数据 | 无账户关系 | 无法保存多选账户 |\n| 表单 | 无“受限账户” | 无法配置和回显 |\n| 接口 | 无 `accountTypes` | 无法校验和持久化 |\n| 范围校验 | 命中人员后全局拦截 | 无法跳过未选账户 |\n| 次数校验 | 统计人员全部订单 | 无法按支付账户区分 |\n| 支付时序 | 限制校验早于扣款拆分 | 无法识别本单实际账户 |\n| 日志 | 不记录账户 | 无法定位哪个账户触发限制 |\n| 赠送账户 | 当前分支已具备余额、扣款、退款和订单字段 | 本需求只需纳入同一受限账户判断 |\n",
      "variant_b": "# VWCG-1119 消费限制按受限账户余额生效技术开发文档\n\n> 版本：V1.2 兼容修订稿\n>\n> 日期：2026-07-29\n>\n> 目标仓库：`store`、`ai_api`\n>\n> 需求状态：待处理\n>\n> 风险等级：高\n>\n> 结论：方案可开发，但上线前必须确认“任一账户有钱”及透支口径\n\n## 1. 本次口径修订\n\n用户补充了三个关键条件：\n\n1. 消费限制规则增加“受限账户”，判断所选账户有没有钱。\n2. `store` 消费机订单覆盖 MQTT 和 HTTP API。\n3. 小程序端点餐也执行相同判断。\n4. 旧数据此前没有受限账户字段，默认勾选个人账户和餐补账户。\n\n因此，V1.0 中“先推演本单最终扣款账户，再按实际扣款账户分别计次”的设计不再适用。本版采用：\n\n```text\n受限账户可用余额门禁\n    + 现有范围限制/次数限制\n    = 本条消费限制是否拦截\n```\n\n“消费策略”按原 PRD 语境解释为“消费限制规则”，不改营销策略、优惠定价或账户扣款顺序。\n\n## 2. 最终业务规则\n\n### 2.1 推荐判定规则\n\n一条规则可选择 `cash`、`subsidy`、`gift` 中的一项或多项。校验时：\n\n1. 查询人员在判断时点的受限账户可用余额。\n2. 所选账户中**任意一个**可用余额大于 0，则本规则进入原有范围/次数判断。\n3. 所选账户可用余额全部小于等于 0，则跳过本规则。\n4. 未选账户的余额不参与本规则是否生效的判断。\n5. 一旦规则生效，范围和次数仍是整单限制，不自动切换资金来源。\n6. 次数继续按现有成功订单数统计，不按订单最终扣款账户拆分。\n\n示例：\n\n| 受限账户 | 判断时余额 | 是否执行规则 | 后续行为 |\n| --- | --- | --- | --- |\n| 餐补 | 餐补 10 | 是 | 检查当前档口/餐次与次数 |\n| 餐补 | 餐补 0、个人 100 | 否 | 个人余额不影响本规则 |\n| 个人+餐补 | 个人 0、餐补 3 | 是 | 任一所选账户有钱即生效 |\n| 个人+餐补 | 均为 0 | 否 | 跳过本规则 |\n| 赠送 | 赠送 5 | 是 | 仅在赠送账户能力启用后 |\n\n### 2.2 “有钱”的技术定义\n\n所有金额统一保留两位小数，使用 `bccomp(balance, '0.00', 2) > 0`，禁止使用 PHP 浮点直接比较。\n\n| 账户 | 可用余额定义 | 不计入 |\n| --- | --- | --- |\n| 个人账户 `cash` | `max(activity_principal_balance, 0) + max(withdrawable_balance, 0)`；旧数据未拆分时兼容回退 `max(balance, 0)` | `consume_overdraft_limit` 透支额度 |\n| 餐补账户 `subsidy` | 判断时点处于有效期、`is_delete=0` 且 `money>0` 的补贴余额之和 | 未开始、已过期、已删除、负数补贴 |\n| 赠送账户 `gift` | `max(gift_balance, 0)` | 未上线或未完成迁移的赠送能力 |\n\n余额查询异常、用户映射缺失或跨库不可用时必须失败并提示“账户信息获取失败，请稍后重试”，不能把异常当成 0 后跳过规则，否则会形成限制绕过。\n\n### 2.3 不改变的规则\n\n- 现有范围限制仍是“配置的档口 + 餐次禁止消费”。\n- 现有月、日、餐次次数阈值不变。\n- 次数仍查询对应应用场景的成功订单。\n- 不增加金额限制、指定人员、身份或每账户独立阈值。\n- 不因某受限账户触发而自动改用未受限账户。\n- 退款是否回补次数仍保持现状，本期不新增。\n\n## 3. 真实调用链与项目边界\n\n### 3.1 消费机 HTTP API\n\n```text\napplication/api/controller/Consume.php::orderPay()\n  -> application/api/service/Consume.php::orderPayAsync()\n  -> Redis 快照支付 或 runAsyncFallback()\n  -> Consume::orderPay()\n```\n\n### 3.2 消费机 MQTT\n\n```text\napplication/cron/command/MqttServer.php\n  -> Consume::orderPayAsync()\n  -> Redis 快照支付 或 runAsyncFallback()\n  -> Consume::orderPay()\n```\n\nHTTP 与 MQTT 最终汇入同一个支付服务，但 `orderPayAsync()` 内仍有 Redis 快照和数据库回退两条路径。只改控制器、只改 MQTT 命令或只改其中一条支付路径都会产生隐形绕过。\n\n### 3.3 小程序订餐\n\n小程序订餐由 `ai_api` 承载，不在 `store` 内创建：\n\n```text\nMealCheckout::submit()\n  -> meal/Checkout::onCheckout()\n  -> meal/Checkout::createOrder()\n  -> MealCashier / cashier/MealPayment\n  -> meal/PaySuccess\n\nMealCheckout::directPay()\n  -> meal/DirectPay::pay()\n```\n\n`store` 只识别 `OrderSourceEnum::ORDERFOOD = 4`，没有小程序提交订单入口。因此本需求是 `store + ai_api` 联动需求，只改 `store` 不完整。\n"
    },
    {
      "line": 203,
      "variant_a": "sequenceDiagram\n    participant C as 消费端\n    participant P as Consume 支付编排\n    participant A as 账户扣款规划器\n    participant L as 消费限制评估器\n    participant D as 业务库\n    participant S as 账户结算服务\n\n    C->>P: 发起在线消费\n    P->>P: 计算最终 pay_price\n    P->>A: 读取有效余额与扣款顺序\n    A-->>P: paymentPlan(cash/subsidy/gift)\n    P->>L: 人员、场景、档口、餐次、paymentPlan\n    L->>D: 查询规则、账户关系、历史账户订单次数\n    alt 有实际使用的受限账户触发规则\n        L-->>P: 拒绝 + 账户类型 + 原因\n        P-->>C: 支付失败\n    else 通过\n        L-->>P: 允许\n        P->>S: 使用同一计划结算\n        S-->>P: 实际账户拆分\n        P->>D: 保存订单\n        P-->>C: 支付成功\n    end\n```\n\n关键不变量：\n\n```text\npaymentPlan 中金额 > 0 的账户 = 本单实际使用账户\n限制评估使用的账户集合 = 实际结算使用的账户集合\n未在规则 accountTypes 中的账户，不执行该规则\n混合支付时，每个实际使用账户独立判断、独立计次\n",
      "variant_b": "flowchart TD\n    API[\"消费机 HTTP API\"] --> STORE[\"store Consume::orderPayAsync\"]\n    MQTT[\"消费机 MQTT\"] --> STORE\n    STORE --> SR[\"store 账户余额解析器\"]\n    SR --> SE[\"store 消费限制评估器\"]\n\n    MP[\"小程序点餐\"] --> CHECKOUT[\"ai_api MealCheckout / DirectPay\"]\n    CHECKOUT --> AR[\"ai_api 账户余额解析器\"]\n    AR --> AE[\"ai_api 消费限制评估器\"]\n\n    SE --> RULE[\"mysql_zhct 消费限制规则\"]\n    AE --> RULE\n    SR --> ACCOUNT[\"AI 账户库\"]\n    AR --> ACCOUNT\n    SE --> ORDER[\"ydy_meal_order 次数\"]\n    AE --> ORDER\n```\n\n两个项目分别实现适配层，但必须使用相同输入输出契约和同一套契约测试样例，避免规则漂移。\n\n评估器统一输入：\n\n```text\nstoreId\nstaffUuid\nzhctUserId\nscenario\nrestaurantId\nmealTimes\nconsumeTime\nmsgid/orderNo\naccountBalances {cash, subsidy, gift}\n```\n\n统一输出：\n\n```json\n{\n  \"allowed\": false,\n  \"message\": \"消费次数超限制\",\n  \"limit_id\": 123,\n  \"matched_account_types\": [\"subsidy\"],\n  \"reason\": \"times\"\n}\n"
    },
    {
      "line": 289,
      "variant_a": "建议新建 `ydy_consume_limit_account`，不把多选值存成逗号字符串或主表 JSON。\n",
      "variant_b": "新增 `ydy_consume_limit_account`：\n"
    },
    {
      "line": 300,
      "variant_a": "| `store_id` | `int unsigned` | NOT NULL DEFAULT 0 | 商城/租户 |\n",
      "variant_b": "| `store_id` | `int unsigned` | NOT NULL DEFAULT 0 | 租户 |\n"
    },
    {
      "line": 314,
      "variant_a": "选择关系表的原因：\n\n- 与部门、场景、范围关系表风格一致。\n- 避免模糊字符串查询。\n- 后续需要按账户筛选时可直接建立索引。\n- 不把当前 PRD扩展成“每账户独立阈值”。\n\n### 5.2 拦截日志扩展\n\n建议 `ydy_consume_limit_block_log` 增加：\n\n```text\naccount_type varchar(16) NOT NULL DEFAULT ''\n```\n\n用于记录实际触发拦截的账户。`counts` 和 `limits` 继续保存该账户的计次快照与阈值，不新增复杂日志表。\n\n### 5.3 历史数据迁移\n\n正式基线目前只有个人账户与餐补账户，建议对所有未删除历史规则回填：\n\n```text\ncash\nsubsidy\n```\n\n不自动回填 `gift`。当前分支虽然已具备赠送账户资金能力，但历史规则创建时没有“限制赠送账户”的明确业务意图；自动回填会扩大旧规则影响范围。\n\n迁移后校验：\n\n- 每个未删除规则至少有 2 条账户关系。\n- 不存在未知账户枚举。\n- 不存在重复 `(limit_id, account_type)`。\n- 不同 `store_id` 数据数量与主表一致。\n\n### 5.4 回滚原则\n\n- 生产有新规则后不直接删除关系表或账户日志字段。\n- 功能回滚时先部署忽略账户维度的旧代码，关系表保留。\n- 只有非生产且确认无新数据时，才执行物理删除表/列的回滚脚本。\n- 回滚后旧代码恢复“所有账户共同受限”的历史行为。\n\n## 6. API 契约\n\n### 6.1 新增/编辑\n\n接口保持不变：\n\n- `POST /api/consumeLimit/create`\n- `POST /api/consumeLimit/update`\n\n新增请求字段：\n\n```json\n{\n  \"accountTypes\": [\"cash\", \"subsidy\", \"gift\"]\n}\n```\n\n规则：\n\n- 类型必须为数组。\n- 去重后至少一项。\n- 只允许 `cash`、`subsidy`、`gift`。\n- 服务层再次校验，不能只依赖前端。\n\n错误文案建议：\n\n| 场景 | 文案 |\n| --- | --- |\n| 缺失/空数组 | 请选择受限账户 |\n| 非数组 | 受限账户参数格式错误 |\n| 未知枚举 | 受限账户参数不正确 |\n\n### 6.2 详情\n\n`POST /api/consumeLimit/detail` 增加：\n",
      "variant_b": "不使用逗号字符串或主表 JSON，避免模糊查询、重复值和后续迁移困难。\n\n### 5.2 拦截日志\n\n`ydy_consume_limit_block_log` 建议增加：\n\n```text\nmatched_account_types varchar(64) NOT NULL DEFAULT ''\naccount_balance_snapshot text NULL\nchannel varchar(16) NOT NULL DEFAULT ''\n```\n\n- `matched_account_types`：如 `cash,subsidy`。\n- `account_balance_snapshot`：只记录账户类型和金额，不记录敏感身份数据。\n- `channel`：`consume_api`、`consume_mqtt`、`mini_program`。\n\n### 5.3 历史规则\n\n历史未删除规则（包括启用和停用规则）统一回填 `cash + subsidy`，不回填 `gift`。回填 SQL 必须可重复执行，推荐使用唯一索引配合 `INSERT IGNORE` 或等价的“不存在才插入”语义。\n\n示例：\n\n```sql\nINSERT IGNORE INTO ydy_consume_limit_account\n    (limit_id, account_type, store_id, create_time)\nSELECT id, 'cash', store_id, NOW()\nFROM ydy_consume_limit\nWHERE is_delete = 0;\n\nINSERT IGNORE INTO ydy_consume_limit_account\n    (limit_id, account_type, store_id, create_time)\nSELECT id, 'subsidy', store_id, NOW()\nFROM ydy_consume_limit\nWHERE is_delete = 0;\n```\n\n兼容必须覆盖四层，不能只依赖一次性 SQL：\n\n1. **数据库回填**：上线前为全部 `is_delete=0` 旧规则补齐 `cash`、`subsidy` 两条关系。\n2. **详情接口兜底**：旧规则查不到账户关系时，仍返回 `account_types: [\"cash\", \"subsidy\"]`，并记录数据修复告警。\n3. **前端回显兜底**：新增页面默认勾选个人+餐补；编辑时接口字段缺失或为空，也显示个人+餐补，避免新旧版本混部时出现空选。\n4. **运行期评估兜底**：启用规则没有账户关系时按个人+餐补判断，并记录高优先级告警，不能静默放行。\n\n旧规则首次编辑保存后，必须把默认值真实持久化到关系表。任何兼容层都不得默认选中 `gift`。\n\n为避免迁移遗漏造成静默放行：\n\n- 迁移完成时每条未删除旧规则有两条默认账户关系；用户后续编辑后至少保留一条。\n- 运行期发现启用规则没有账户关系时，临时按 `cash + subsidy` 兼容，并记录高优先级告警。\n- 修复数据后移除告警，不能长期依赖运行期默认值。\n\n行为变化需要业务知晓：历史规则原来不看余额；回填后，当个人和有效餐补均为 0 时规则会跳过。这是本次新增口径的预期变化。\n\n## 6. API 与前端\n\n### 6.1 CRUD 契约\n\n现有接口不变：\n\n- `POST /api/consumeLimit/create`\n- `POST /api/consumeLimit/update`\n- `POST /api/consumeLimit/detail`\n\n新增/编辑请求：\n\n```json\n{\n  \"accountTypes\": [\"cash\", \"subsidy\"]\n}\n```\n\n详情响应：\n"
    },
    {
      "line": 473,
      "variant_a": "为兼容前端命名习惯，内部数据库使用 snake_case，页面表单继续使用 `accountTypes`。\n\n### 6.3 列表\n\n当前 PRD 未要求列表展示或筛选受限账户。本期不新增列表列和筛选项，避免扩大范围。服务层可以不返回账户名称；若联调需要，可返回 `account_types` 但前端不展示。\n\n### 6.4 路由与权限\n\n- 复用现有接口，不修改 `application/route.php`。\n- 不新增菜单或按钮权限。\n- 保持现有登录、部门数据权限、餐厅/档口数据权限逻辑。\n- 新关系表所有写入必须带 `store_id`，读取必须通过主规则的 `store_id` 约束。\n\n## 7. 后端开发设计\n\n### 7.1 账户枚举\n\n建议新增统一枚举/值对象，例如：\n\n```text\napplication/common/enum/account/Type.php\n```\n\n映射：\n\n| 内部值 | 页面文案 | 订单支付字段 |\n| --- | --- | --- |\n| `cash` | 个人账户 | `ydy_meal_order.cash` |\n| `subsidy` | 餐补账户 | `ydy_meal_order.subsidy` |\n| `gift` | 赠送账户 | `ydy_meal_order.gift` |\n\n后端、测试和 `VWCG-1121` 必须共用同一组枚举，避免出现 `personal/cash`、`meal_allowance/subsidy` 等重复口径。\n\n### 7.2 消费限制 CRUD\n\n`application/api/controller/ConsumeLimit.php`：\n\n- create/update 中调用 `formatArray` 规范化 `accountTypes`。\n- 不改变现有档口权限校验。\n\n`application/api/validate/ConsumeLimit.php`：\n\n- 增加 `accountTypes` 的 require、array、自定义白名单和非空校验。\n- create/update scene 同步加入该字段。\n\n`application/api/service/ConsumeLimit.php`：\n\n- `syncRelations()` 同步删除并写入账户关系。\n- `detail()` 返回账户数组。\n- `delete()` 同步删除账户关系。\n- create/update 继续放在同一业务库事务内。\n- 捕获异常时保留内部日志，API 只返回可理解的业务错误。\n\n### 7.3 支付计划\n\n推荐以 `VWCG-1121` 的 `GiftAccountPolicy::planDeduction()` 为统一纯计算核心，并给 `AccountSettlementService` 增加只读计划能力：\n\n```text\npreviewDeduction(accountDb, userId, storeId, amount, consumeTime, overdraftLimit)\n```\n\n返回：\n\n```json\n{\n  \"subsidy\": \"8.00\",\n  \"cash\": \"12.00\",\n  \"gift\": \"0.00\",\n  \"deduction_order\": [\"subsidy\", \"cash\", \"gift\"]\n}\n```\n\n为防止预览后余额变化导致实际扣款账户变化，实际 `deduct()` 应：\n\n1. 在锁定账户和补贴行后重新计算计划。\n2. 与传入的 `expected_plan` 比较账户集合和金额。\n3. 不一致时中止，不扣款，返回“账户余额发生变化，请重试”。\n\n当前分支已具备赠送账户余额、扣款、退款和订单实付字段。本需求直接复用该能力，只增加 `gift` 是否受本条消费限制影响的判断，不新增赠送金额、扣款顺序或退款算法。\n\n### 7.4 消费限制评估器\n\n建议从 `Consume.php` 中抽出可测试的领域服务：\n\n```text\napplication/api/service/ConsumeLimitEvaluator.php\n```\n\n输入：\n\n```text\nstaffUuid\nuserId\nscenario\nrestaurantId\nmealTimes\nconsumeTime\nmsgid\npayAmount\naccountBalances\n```\n\n处理顺序：\n\n1. 找到人员、场景、门店下启用的规则。\n2. 批量加载各规则的 `accountTypes`。\n3. 对每条规则的每个受限账户分别判断范围限制和次数限制。\n4. 次数限制只统计该账户订单实付字段大于 0 的成功订单。\n5. 将命中限制的账户从本单可用余额中排除。\n6. 使用未受限账户重新计算可支付总额和扣款计划。\n7. 未受限账户可以覆盖整单：允许支付，并把受限账户集合传给最终扣款。\n8. 原账户总额足够但排除后不足：拦截整单并记录触发账户。\n\n### 7.5 范围限制\n\n继续使用当前“禁止范围”语义：\n\n```text\n规则选择了该账户\n+ 当前档口/餐次出现在 consume_limit_scope\n=> 本单排除该账户；剩余账户不足时才拦截整单\n```\n\n示例：\n\n| 规则受限账户 | 本单计划 | 当前档口命中 | 结果 |\n| --- | --- | ---: | --- |\n| 餐补 | 餐补 10；个人余额 20 | 是 | 排除餐补，改由个人支付 |\n| 餐补 | 个人 10 | 是 | 个人账户不受影响，放行 |\n| 个人+餐补 | 餐补 5 + 个人 5；赠送余额 10 | 是 | 排除个人和餐补，改由赠送支付 |\n| 赠送 | 赠送 10；其他账户不足 | 是 | 排除赠送后余额不足，拦截整单 |\n\n受限账户命中规则后必须使用未受限账户重算扣款计划。支付预览、支付前复检和最终扣款必须使用同一受限账户集合，避免页面显示与实际扣款不一致。\n\n### 7.6 次数限制\n\n推荐按账户分别计次，而不是把所有受限账户合计：\n\n```text\ncash 次数 = 成功订单中 cash > 0 的订单数\nsubsidy 次数 = 成功订单中 subsidy > 0 的订单数\ngift 次数 = 成功订单中 gift > 0 的订单数\n```\n\n混合支付订单会分别计入实际使用的每个账户一次。\n\n基础过滤保持现状：\n\n- `user_id`\n- `is_delete = 0`\n- `pay_status = SUCCESS`\n- 应用场景对应的订单来源\n- 月/日/餐次时间范围\n\n退款是否回补次数当前 PRD 未定义。为兼容现状，本期仍按成功订单计数，不因退款回补；需要产品在评审时确认并写入验收口径。\n\n### 7.7 拦截提示与日志\n\n建议提示带账户名称：\n\n- `餐补账户当前时间档口无消费权限`\n- `个人账户消费次数超限制`\n- `赠送账户消费次数超限制`\n\n日志至少记录：\n\n- 规则 ID\n- 触发账户类型\n- 人员、用户、场景、档口、餐次\n- 当前账户的月/日/餐计数\n- 规则阈值\n- 消费时间和幂等消息 ID\n\n### 7.8 两条在线消费路径\n\n当前 `Consume.php` 有同步支付和异步快照支付两处校验调用，两处必须一起改造：\n\n- 异步快照路径：基于快照余额生成计划。\n- 数据库同步路径：基于账户库有效余额生成计划。\n\n禁止只修改其中一处，否则不同设备/链路会得到不同结果。\n\n## 8. 前端开发设计\n\n目标文件：\n\n```text\npublic/static/src/view/marketingCenter/addConsumeLimit.vue\n```\n\n### 8.1 表单\n\n在“应用场景”后插入：\n",
      "variant_b": "校验：\n\n- 必须为数组，去重后至少一项。\n- 只允许 `cash/subsidy/gift`。\n- 赠送账户能力未开放时，传 `gift` 返回“赠送账户暂未开放”。\n- 前端、Validate、Service 三层均校验。\n\n### 6.2 PC 页面\n\n目标文件：\n\n`public/static/src/view/marketingCenter/addConsumeLimit.vue`\n\n在“应用场景”后增加：\n"
    },
    {
      "line": 689,
      "variant_a": "交互规则：\n\n- 使用 `el-checkbox-group`。\n- label 宽度、间距、颜色沿用当前页面。\n- tooltip 精确文案：`所选账户受范围或次数的限制。`\n- 默认值：`['cash', 'subsidy']`。\n- 赠送账户可选，但默认不勾选；它与个人、餐补复用当前规则。\n- 至少选择一项；错误信息显示在当前表单项下方。\n\n### 8.2 编辑回显与提交\n\n`initialForm()` 增加：\n\n```js\naccountTypes: ['cash', 'subsidy']\n```\n\n详情加载：\n\n```text\nd.account_types -> form.accountTypes\n```\n\n提交：\n\n```text\nform.accountTypes -> payload.accountTypes\n```\n\n历史数据完成后端回填后，详情应始终返回个人+餐补；前端不自行猜测缺失关系。\n\n### 8.3 其他页面\n\n`consumeLimitMgr.vue` 本期不增加列、筛选和弹窗字段。新增/编辑统一使用独立页面，避免维护两套表单。\n\n## 9. 兼容与依赖\n\n### 9.1 `VWCG-1121` 依赖\n\n当前 `VWCG-1121`：\n\n- 云效状态仍为待处理。\n- 当前分支已经包含赠送余额、扣款、退款和订单记录能力。\n\n推荐合并顺序：\n\n1. 使用当前统一账户结算能力。\n2. `VWCG-1119` 基于该结算服务增加“计划后限制评估”。\n3. 页面和接口同时启用 `cash/subsidy/gift`。\n\n### 9.2 多商户\n\n- 所有规则和关系按 `store_id` 隔离。\n- 不基于 `BUSINESS` 写死某客户。\n- 三种账户使用同一套多商户和 `store_id` 隔离规则。\n- 关系表回填必须逐 `store_id` 验证数量。\n\n### 9.3 离线补传\n\n当前 `order_type=2` 会跳过消费限制。本方案默认保持兼容，不把离线历史订单在补传时拦截；但这些订单是否计入后续账户次数必须确认。建议计入，因为订单支付字段能够反映实际账户来源。\n\n## 10. 测试设计\n\n### 10.1 自动化测试\n",
      "variant_b": "- 使用 `el-checkbox-group`。\n- 新增默认 `['cash', 'subsidy']`。\n- 提示：`所选账户有可用余额时，该消费限制规则生效。`\n- 赠送账户未开放时置灰。\n- 编辑时以后端 `account_types` 为准；字段缺失或空数组时防御性回退 `['cash', 'subsidy']`。\n- 旧规则回显默认个人+餐补后，用户可以主动取消其中一项，但至少保留一个账户。\n\n列表页本期不新增列或筛选，不改路由、菜单或按钮权限。\n\n## 7. `store` 后端改造\n\n### 7.1 账户余额解析器\n"
    },
    {
      "line": 772,
      "variant_a": "tests/ConsumeLimit/ConsumeLimitAccountPolicyTest.php\ntests/ConsumeLimit/ConsumeLimitServiceTest.php\ntests/ConsumeLimit/ConsumeLimitPaymentTest.php\n```\n\n| 测试域 | 场景 |\n| --- | --- |\n| 参数 | 缺失、空数组、非数组、重复、未知枚举、三个合法枚举 |\n| CRUD | 新增保存、编辑替换、详情回显、删除清理、事务回滚 |\n| 历史迁移 | 每条历史规则回填 cash+subsidy、无重复、租户一致 |\n| 范围限制 | 选中账户命中/未命中；未选账户；混合支付 |\n| 次数限制 | cash/subsidy/gift 分别计次；混合订单分别计次；月/日/餐边界 |\n| 支付计划 | 单账户、两账户、余额临界、营销价变化、计划漂移 |\n| 赠送账户 | 页面选择、保存、回显；命中后只排除 gift，未选账户仍可支付 |\n| 权限 | 部门数据权限、档口权限、跨 store_id 拒绝 |\n| 日志 | 账户类型、规则、计数、阈值完整 |\n| 双链路 | 同步支付、异步快照支付结果一致 |\n\n必须回归：\n\n- `tests/DataPermission/ChangedInterfacesPermissionCoverageTest.php`\n- `tests/DataPermission/UserDataPermissionStaticTest.php`\n- `tests/DataPermission/YunxiaoAcceptanceCriteriaTest.php`\n- `vendor/bin/phpunit`\n\n### 10.2 前端与手工验收\n\n- `npm run lint`\n- `npm run build`\n- 新增、编辑、必填错误、回显，以及页面不存在独立“赠送金额/赠送规则”配置的截图。\n- 个人账户未选时使用纯个人账户支付应放行。\n- 餐补账户被选且命中范围时应拦截。\n- 同一规则选择个人+餐补时，两个账户按同一阈值分别计次。\n- 混合支付的限制与订单拆分一致。\n- 无权限账号不能绕过部门/档口数据权限修改规则。\n\n如仓库既有 lint/build 环境失败，必须记录基线错误并补充定向 Vue 编译，不得口头宣称已通过。\n\n### 10.3 Apifox\n\n至少补充：\n\n- create/update/detail 的账户字段正反例。\n- 纯个人、纯餐补、混合支付的范围/次数场景。\n- 赠送账户新增、编辑、回显和支付限制。\n- 历史规则回显。\n\n## 11. 发布、灰度与回滚\n\n### 11.1 上线顺序\n\n1. 备份并统计消费限制主表数量。\n2. 执行关系表、日志字段和历史回填 SQL。\n3. 校验每条历史规则均有个人+餐补关系。\n4. 部署后端 CRUD 与账户感知校验。\n5. 部署 PC 表单，展示并启用个人、餐补、赠送三个选项。\n6. 选择内部门店验证个人、餐补、赠送及混合支付。\n7. 观察账户维度拦截日志和支付失败率。\n8. 扩大门店范围。\n\n### 11.2 监控\n\n- 按 `account_type` 聚合的范围/次数拦截量。\n- 账户计划变化导致的重试量。\n- 支付失败率与余额不足率。\n- 新规则缺失账户关系数量。\n- 未知账户枚举数量。\n- 订单拆分之和与 `pay_price` 的差额。\n\n### 11.3 回滚\n\n- 紧急回滚先停用本次新增/编辑的规则或关闭账户维度开关。\n- 部署旧代码时保留新关系表和日志列。\n- 禁止删除已产生业务数据的关系表。\n- 若账户计划与实际结算不一致，先停止新支付并对账，不允许直接补写订单金额。\n\n## 12. PRD 验收映射\n\n| PRD 验收点 | 开发证据 |\n| --- | --- |\n| 页面出现受限账户 | 页面截图 + Vue 定向编译/构建 |\n| 支持多选且至少一项 | 前端校验 + API 反例测试 |\n| 提示文案准确 | UI 截图/组件测试 |\n| 保存并回显 | create/update/detail 接口测试 + DB 记录 |\n| 仅所选账户执行限制 | 纯个人/纯餐补/混合支付集成测试 |\n| 未选账户不受限制 | 未选账户正向支付用例 |\n| 风格与当前模块一致 | 人工 UI 评审 |\n| 不增加复杂配置 | 代码 diff 与页面结构评审 |\n\n## 13. 异步导出联动判断\n\n本需求不修改消费订单、人员/部门/设备明细、充值/补贴统计、菜品销量、餐厅经营汇总或档口结算列表的字段、查询条件、统计口径和导出数据源，因此 `VWCG-1119` 本身无需修改九类异步导出。\n\n`VWCG-1121` 对订单新增赠送支付字段产生的导出影响，应继续在 `VWCG-1121` 范围内处理，不能混入本任务。\n\n## 14. 推荐开发切片与估时\n\n| 切片 | 内容 | 非 AI 估时 |\n| --- | --- | ---: |\n| 1 | DDL、回填、回滚、账户枚举 | 4～6h |\n| 2 | CRUD、校验、详情回显 | 6～8h |\n| 3 | 扣款计划与限制评估器 | 12～16h |\n| 4 | 账户维度计次、日志、双支付链路 | 8～12h |\n| 5 | PC 表单与交互 | 4～6h |\n| 6 | 自动化、Apifox、手工回归与发布证据 | 10～14h |\n| 合计 | 个人+餐补+赠送完整闭环 | 44～62h |\n\n当前分支已包含赠送账户资金链路，本需求中的赠送账户选项直接启用并纳入相同回归矩阵。\n\n## 15. 评审前必须确认\n\n### P0\n\n1. 次数限制按每个受限账户分别计次，不合并账户次数。\n2. 受限账户命中后排除该账户并改用未受限账户；只有剩余账户不能覆盖整单时才拦截。\n3. 赠送账户与个人、餐补同时可选，复用同一范围/次数规则。\n\n### P1\n\n4. 历史规则是否只回填个人+餐补？本文推荐是，不自动限制未来赠送账户。\n5. 退款订单是否回补消费次数？本文推荐本期保持现状，不回补。\n6. 离线补传订单是否计入后续次数？本文推荐计入，但补传过程本身继续不拦截。\n\n## 16. 开发门禁结论\n\n当前已经具备：\n\n- 明确的本期范围和非范围。\n- 可落地的数据、接口、前端和支付链路方案。\n- 真实文件落点。\n- 测试、上线和回滚策略。\n- 已确认的按账户分别计次、排除受限账户后重算和赠送账户同步启用口径。\n\n发布前仍需完成：\n\n- 目标环境 DDL 执行与历史数据核验。\n- 产品、后端、前端和测试联合自测。\n- 退款计次与离线补传边界的最终验收记录。\n\n因此结论为：**可按已确认口径完成开发并进入联合自测；上线前必须先执行关系表迁移。**\n",
      "variant_b": "application/common/service/account/AccountAvailabilityResolver.php\n```\n\n职责：\n\n1. `staff_uuid -> ydy_user -> ai_user_id`。\n2. 读取个人拆分余额、有效餐补、赠送余额。\n3. 使用业务消费时间过滤补贴有效期。\n4. 返回标准化 `{cash, subsidy, gift}`。\n5. 对旧账户数据执行个人余额兼容回退。\n\n### 7.2 评估器\n\n将 `Consume.php::checkConsumeLimitForOrder()` 抽为：\n\n```text\napplication/api/service/ConsumeLimitEvaluator.php\n```\n\n处理顺序：\n\n1. 按人员部门、场景、租户查启用规则。\n2. 批量加载规则账户关系。\n3. 对每条规则取 `selectedAccounts ∩ positiveBalanceAccounts`。\n4. 交集为空：跳过规则。\n5. 交集非空：按原逻辑检查范围和次数。\n6. 任一规则失败：整单拒绝并写日志。\n\n次数查询继续使用 `ydy_meal_order` 的成功订单，不增加 `cash/subsidy/gift > 0` 条件。\n\n### 7.3 MQTT 与 API 接入\n\n不要分别在入口复制业务判断。统一在 `Consume::orderPayAsync()` 进入支付锁之后、创建订单和扣款之前调用。\n\n必须覆盖：\n\n- Redis 快照路径 `handleAsyncSnapshotPayment()`。\n- 数据库回退路径 `runAsyncFallback() -> orderPay()`。\n- HTTP API 调用。\n- MQTT 调用。\n\n入口只负责传入 `channel`，用于日志区分。\n\n### 7.4 Redis 快照正确性\n\n当前消费快照 TTL 为 300 秒。直接使用旧快照判断“有没有钱”会出现：\n\n- 刚充值但快照仍为 0，规则被错误跳过。\n- 刚消费完但快照仍有余额，规则被错误执行。\n- 补贴刚生效/过期时判断错误。\n\n推荐在支付锁内从账户库刷新余额，再同步更新 Redis 快照；至少必须保证限制判断与随后扣款使用同一份、同一 `consumeTime` 的余额快照。不能用一份旧快照判断、再用另一份新余额扣款。\n\n### 7.5 消费时间\n\n- MQTT/API 在线订单：校验设备 `create_time` 与服务器时间的偏差，超出允许窗口时使用服务器时间或拒绝。\n- 小程序：使用服务器时间。\n- 补贴有效期、次数统计边界和日志统一使用最终确认的 `consumeTime`。\n- `order_type=2` 离线历史补传继续跳过实时限制，本期不改变。\n\n## 8. `ai_api` 小程序改造\n\n### 8.1 主校验点\n\n新增与 `store` 同契约的消费限制评估器，例如：\n\n```text\napp/common/service/consumeLimit/ConsumeLimitEvaluator.php\napp/common/service/account/AccountAvailabilityResolver.php\n```\n\n规则从 `mysql_zhct` 读取，账户余额从 `ai_api` 默认账户库读取。\n\n主接入点：\n\n1. `MealCheckout::submit()`：完成用户、餐厅、餐段、购物车校验后，在创建订单前检查。\n2. `meal/DirectPay::pay()`：在生成订单和扣款前检查。\n3. `cashier/MealPayment` / `meal/PaySuccess`：支付前防御复检，覆盖历史待支付订单和绕过提交入口的调用。\n\n### 8.2 为什么需要提交与支付双检查\n\n- 只在提交检查：用户提交后充值，支付时规则状态可能已变化。\n- 只在支付检查：会生成注定不能支付的脏订单，0 元订单还可能直接完成。\n- 双检查：提交时尽早反馈，支付时以最新余额和规则做最终防绕过。\n\n直接扣款必须在同一业务事务中完成“最终复检 -> 创建订单 -> 扣款”。若事务中余额已变化，应重新判断或失败重试。\n\n### 8.3 用户映射\n\n小程序登录用户使用 AI `user_id`；消费限制部门和订单次数使用智慧餐厅用户：\n\n```text\nAI user_id\n  -> ZhctUserModel / ZhctStaffModel\n  -> zhct user_id + staff_uuid\n```\n\n映射缺失时失败，禁止因为查不到部门而默认无规则放行。\n\n## 9. 并发、幂等与隐形 Bug 防护\n\n### 9.1 已识别高风险\n\n| 风险 | 后果 | 必须措施 |\n| --- | --- | --- |\n| MQTT/API 只改一个入口 | 同一设备因上报方式不同结果不同 | 统一汇入 Service，双入口契约测试 |\n| Redis 300 秒旧余额 | 规则误生效或被绕过 | 锁内刷新或保证同快照判断与扣款 |\n| 小程序只改 store | 小程序下单完全不判断 | `ai_api` 提交、直付、支付兜底接入 |\n| 个人余额直接用 `balance` | 赠送/拆分后重复或错误计算 | 使用拆分可用余额，旧数据才回退 |\n| 把透支当“有钱” | 0 余额用户仍触发规则 | 默认不包含透支，产品确认 |\n| 餐补不按业务时间过滤 | 过期/未生效补贴误判 | 使用统一 `consumeTime` |\n| 余额查询异常按 0 | 系统故障时规则被绕过 | 查询失败必须失败关闭 |\n| 提交时检查、支付时不复检 | 余额/规则变化后绕过 | 支付前最终复检 |\n| 次数先查后写 | 并发两单同时通过上限 | 跨项目同一用户/场景锁 |\n| 两项目算法复制后漂移 | 消费机与小程序行为不同 | 共享契约样例和版本化规则 |\n| 历史规则无关系数据 | 全部静默放行 | 回填 + 运行期告警兼容 |\n| 只做前端默认、不做数据回填 | API/任务链路仍把旧规则当空配置 | SQL、详情、前端、运行期四层兼容 |\n| 0 元订单绕过 | 免费订单不受范围/次数约束 | 在 0 元自动支付前检查 |\n\n### 9.2 次数并发\n\n现有“查询成功订单数 -> 判断 -> 创建订单”不是原子操作，两笔并发订单可能同时看到未达上限。\n\n最低要求：\n\n- `store` 和 `ai_api` 使用同一锁命名：`consume_limit:{storeId}:{zhctUserId}:{scenario}`。\n- 锁必须覆盖最终复检到成功订单落库。\n- 两项目 Redis 不共享时，使用 `mysql_zhct` 中的数据库锁/守卫行，不得各锁各的。\n- `msgid`、`order_no` 继续承担幂等，重复请求不重复占次数。\n\n### 9.3 失败策略\n\n| 异常 | 策略 |\n| --- | --- |\n| 无匹配规则 | 放行 |\n| 所选账户均无可用余额 | 跳过该规则 |\n| 账户库超时/用户映射失败 | 拒绝并可重试 |\n| 规则关系缺失 | 按历史 `cash+subsidy` 兼容并告警 |\n| 未知账户枚举 | 拒绝保存；运行期忽略未知值并告警 |\n| 日志写入失败 | 不影响明确的拦截结果，但必须错误告警 |\n\n## 10. 测试设计\n\n### 10.1 规则与余额\n\n- 个人有钱/没钱/拆分字段未迁移。\n- 餐补有效、未生效、已过期、负数、跨日边界。\n- 赠送有钱/没钱/能力未开放。\n- 多选任一有钱、多选全部没钱、未选账户有钱。\n- 旧规则迁移前详情、迁移后详情、重复执行迁移、首次编辑持久化。\n- 新增页面默认个人+餐补；编辑旧规则默认个人+餐补；gift 永不默认选中。\n- 余额查询异常、用户映射缺失。\n- 0 元订单。\n- 透支额度大于 0 但实际账户余额为 0。\n\n### 10.2 三类入口\n\n| 入口 | 必测路径 |\n| --- | --- |\n| 消费机 HTTP API | 刷卡、付款码、人脸；Redis 命中和 DB 回退 |\n| 消费机 MQTT | 刷卡、付款码；重复 msgid；Redis 命中和 DB 回退 |\n| 小程序订餐 | submit、0 元自动支付、余额支付、directPay、历史待支付复检 |\n\nHTTP 与 MQTT 使用同一输入时，除 `channel/trace_id` 外结果必须一致。\n\n### 10.3 范围、次数与并发\n\n- 有余额且命中禁止档口/餐次：拦截。\n- 无余额但命中禁止档口/餐次：规则跳过。\n- 有余额且月/日/餐次达上限：拦截。\n- 多账户有余额时仍只按订单计一次。\n- MQTT 与小程序同一人员并发冲击最后一次额度，只允许一单成功。\n- 重复请求、支付回调重试不重复计次。\n\n### 10.4 回归命令\n\n`store`：\n\n```bash\nvendor/bin/phpunit\ncd public/static && npm run lint && npm run build\n```\n\n`ai_api`：\n\n```bash\nvendor/bin/phpunit\n```\n\n另补充 Apifox 的 create/update/detail、消费机 API、小程序下单正反例；MQTT 使用同请求载荷做集成回归。\n\n## 11. 上线、监控与回滚\n\n### 11.1 上线顺序\n\n1. 合并并迁移 `VWCG-1121` 所需账户字段；未完成时先禁用 `gift`。\n2. 创建账户关系表、日志字段并回填历史规则。\n3. 校验所有未删除旧规则均有 cash+subsidy，且重复执行回填不会产生重复数据。\n4. 部署 `store` 后端评估器，保持 PC 新字段暂不可编辑。\n5. 部署 `ai_api` 小程序校验。\n6. 部署 PC 表单并开放个人/餐补。\n7. 内部门店同时验证 HTTP、MQTT、小程序。\n8. 观察后再逐步开放；赠送账户最后开启。\n\n### 11.2 监控\n\n- 按 `channel/reason/account_type` 的拦截量。\n- 账户查询失败与跨库超时量。\n- HTTP 与 MQTT 同设备结果差异。\n- 小程序提交通过但支付复检失败量。\n- 无账户关系规则数量。\n- 历史规则默认关系回填数量、兜底命中数量。\n- 并发锁等待、超时和次数超限量。\n- 规则跳过量，特别是“所选账户均为 0”。\n\n### 11.3 回滚\n\n- 增加服务端能力开关，按渠道关闭新余额门禁。\n- 回滚代码时保留关系表和日志列。\n- 关闭功能后恢复旧的“所有匹配人员直接执行范围/次数”行为。\n- 不删除已产生业务数据的表或列。\n- 若两个项目只成功发布一个，立即关闭新能力，禁止半套规则运行。\n\n## 12. 异步导出联动\n\n本需求不改变订单列表字段、筛选条件、统计口径或导出数据源。次数统计仍按订单，不按账户支付字段拆分，因此九类异步导出无需同步修改。\n\n该列表页关联异步导出功能的检查结论：本次只增加支付前限制判断和拦截日志，不改变成功订单的导出结构。\n\n## 13. 开发切片与估时\n\n| 切片 | 内容 | 估时 |\n| --- | --- | ---: |\n| 1 | DDL、回填、回滚、账户枚举 | 4～6h |\n| 2 | CRUD、详情回显、PC 表单 | 8～12h |\n| 3 | `store` 余额解析与评估器 | 8～12h |\n| 4 | HTTP/MQTT 双路径、快照一致性 | 8～12h |\n| 5 | `ai_api` submit/directPay/支付复检 | 10～14h |\n| 6 | 跨项目并发锁、日志、监控 | 6～10h |\n| 7 | 自动化、Apifox、MQTT 与灰度证据 | 10～14h |\n| 合计 | 个人+餐补完整闭环 | 54～80h |\n\n赠送账户启用另预留 6～10h 做迁移、回归与灰度。\n\n## 14. 评审前必须确认\n\n### P0\n\n1. 多选受限账户是否采用“任一所选账户有可用余额即生效”？本文按“任一有钱”设计。\n2. 个人账户“有钱”是否不包含消费机透支额度？本文默认不包含。\n3. 小程序的 0 元订单是否也执行限制？本文默认执行。\n4. `store` 与 `ai_api` 是否能使用同一 Redis 锁；不能时必须选择 `mysql_zhct` 数据库锁方案。\n\n### P1\n\n5. 在线消费机允许设备时间与服务器时间偏差多少？\n6. 赠送账户具体开放版本和能力开关由谁负责？\n7. 离线补传继续不拦截、但成功订单计入后续次数，是否维持现状？\n\n## 15. 开发门禁结论\n\n本版已根据用户补充口径完成以下修正：\n\n- 从“实际扣款账户判断”改为“受限账户可用余额判断”。\n- 明确次数不按账户拆分，继续沿用当前订单次数。\n- 明确消费机 HTTP API、MQTT 和小程序订餐三类入口。\n- 明确小程序改造位于 `ai_api`，不是只改 `store`。\n- 增加 Redis 旧快照、跨项目并发、0 元订单、跨库映射等隐形 Bug 防护。\n- 明确旧规则在数据库、详情、前端和运行期四层默认个人+餐补，赠送账户不默认选中。\n\n结论：**技术上可实现，但属于双项目、高一致性改造。P0 口径确认、跨项目锁方案和赠送账户依赖未关闭前，不建议直接上线。**\n"
    }
  ]
}
