spec.md 26 KB

酒店支付 Mock 免付款资金红线收口

status: review created: 2026-09-07 complexity: 🔴复杂 需求依据:forge-server/forge-business/forge-hotel/酒店模块需求缺口清单.md 3.3 / 第九节第 0 批 实现权威:forge-server/forge-business/forge-hotel/酒店订单支付系统开发文档.md 10.1 规则依据:AGENTS.md 5.9 安全红线、code-copilot/rules/security.md 第 2 节


⚠️ 0. 流程状态声明(必读)

本 Spec 为补写文档,不是事前提案。

状态
代码实现 ✅ 已落地并通过 mvn compile / pnpm build:h5 / GetProblems 验证
Spec 编写 🔴 代码落地之后才补写(即本文件)
人工审查 🔴 尚未执行
HARD-GATE 签署 🔴 第 13 节 确认时间 / 确认人 留空,只能由需求方本人手填

code-copilot/rules/security.md 第 2 节要求「涉及资金变更的逻辑,必须在 spec 中明确标注,人工审查后方可编码」。本次是先编码、后补审查,属 AGENTS.md 5.9 的流程倒置。补写本 Spec 的目的是把已发生的改动完整登记、把资金风险点显式摊开供审查,不等于合规已完成 —— 合规闭环以第 13 节签署为准。


1. 背景与目标

1.1 缺陷(改造前)

顾客端支付链路存在免付款下单漏洞,任何人构造 HTTP 请求即可把订单置为「已支付」而不产生任何真实资金流:

环节 位置(改造前) 缺陷
接口暴露 HotelPayController @RequestMapping("/hotel/open/pay") 落在 SaTokenConfig 白名单 /hotel/open/** 内,mockSuccess 完全免登录
缺省渠道 createPay@RequestParam(defaultValue = "MOCK") String paySource 不传 paySource 即默认走 Mock
静默降级 ① resolvePayChannel 微信分支 WECHAT / WECHAT_MP 直接 return "MOCK"
静默降级 ② resolvePayChannel 兜底分支 任何未知渠道值直接 return "MOCK"
前端兜底 pay.vue handlePay()else 后端未返回可用支付参数时,兜底调用 hotelPayMockSuccess
退款误判 refundOrder!"ALIPAY".equals(payChannel) 判定「无资金流」 未来接入微信后,微信订单退款会被误判为无资金流而直接标记成功 → 真实资损

攻击面:POST /hotel/open/pay/mockSuccess?tenantId=1&orderId=X 无需任何凭证。

1.2 目标

  • 生产环境模拟支付一律拒绝,未知/未接入渠道显式报错,全仓不存在任何静默降级为 MOCK 的路径
  • 模拟支付能力保留给开发环境,通过配置开关控制,dev 既有 H5 调试行为零变化(用户硬约束:「调整不可影响我开发时候的 h5 的测试」)
  • 退款渠道判定改为显式白名单,杜绝「非 ALIPAY 即 MOCK」兜底带来的未来资损
  • 顺带收口两处日志脱敏问题(手机号明文、授权响应原文)

1.3 不做的事

  • ❌ 不实现微信支付(商户号未申请,已决策暂缓上架)
  • ❌ 不实现 ALIPAY_MP 渠道(alipay.trade.create,属第 3 批)
  • ❌ 不改二维码 URL 配置化(属第 2 批,不涉资金,不单独建 Spec)
  • ❌ 不处理 application-dev.yml 明文沙箱密钥(独立红线,见 code-copilot/changes/hotel-alipay-secret-externalize/

2. 代码现状(Research Findings)

2.1 相关入口与链路

pay.vue handlePay()
  └─ POST /hotel/open/pay/create?tenantId&orderId&paySource     [免登录]
       └─ HotelPayController.createPay()  L56-68
            └─ TenantContextHolder.executeWithTenant(tenantId, ...)
                 └─ HotelPayServiceImpl.createPay(orderId, paySource)  L91-182
                      ├─ resolvePayChannel(paySource)  L821-836   ← 单一收口点
                      │    └─ mockOrReject(reason)     L846-851   ← 全仓唯一允许返回 "MOCK" 的位置
                      ├─ "ALIPAY" → alipay.trade.wap.pay → payForm
                      └─ else     → payChannel=MOCK
  ├─ payChannel=ALIPAY + payForm → 渲染表单自动提交(H5)/ my.tradePay(MP)
  ├─ payChannel=MOCK             → POST /hotel/open/pay/mockSuccess  [免登录]
  └─ else                        → uni.showModal 报错(禁止兜底调 mockSuccess)

POST /hotel/open/pay/notify → 验签 → handlePayCallback → processPayCallback
refundOrder(orderId) L543-628 → 渠道显式判定 → alipay.trade.refund / 仅回写状态 / 抛异常

2.2 现有实现(改造后,均已落地)

HotelPayConfig(新建,pay/config/HotelPayConfig.java L22-49)

@Value("${hotel.pay.mock-enabled:false}")
private boolean mockEnabled;

@PostConstruct
public void logMockSwitch() { /* true → WARN 资金安全告警;false → INFO */ }

关键:代码级默认值是 false,不依赖 yml。即使 6 份 yml 的 hotel 块整体缺失,也不会误开。

mockOrReject 单一收口点(HotelPayServiceImpl L846-851)

private String mockOrReject(String reason) {
    if (hotelPayConfig.isMockEnabled()) {
        return "MOCK";
    }
    throw new BusinessException(reason);
}

resolvePayChannel 全渠道显式判定(L821-836)

入参 paySource 返回 说明
ALIPAY / ALIPAY_MP "ALIPAY" 归为同一 payChannel
WECHAT / WECHAT_MP mockOrReject("微信支付暂未开通,请使用支付宝支付或到前台付款") 商户号未申请
null / 空串 / 全空格 mockOrReject("支付渠道不能为空")
MOCK / H5 mockOrReject("模拟支付已禁用")
其它任意值 mockOrReject("不支持的支付渠道: " + paySource) 兜底不再降级

createPay 渠道解析前置(L117-119 / L138-140)

  • L117-119:在写 payLog 之前先 resolvePayChannel(paySource),非白名单渠道在此即被拒绝,不会留下 pay_channel=MOCK 的脏流水
  • L138-140:分流用已解析的 payChannel,注释明确「禁止用原始 paySource 判断:否则 ALIPAY_MP 会漏进 else 分支被静默置为 MOCK」

HotelPayController.createPaydefaultValue(L56-59)

@RequestParam String paySource   // 改造前:@RequestParam(defaultValue = "MOCK") String paySource

HotelPayController.mockPaySuccess 首行拦截(L80-86)

if (!hotelPayConfig.isMockEnabled()) {
    log.warn("模拟支付请求被拒绝(hotel.pay.mock-enabled=false): tenantId={}, orderId={}", tenantId, orderId);
    throw new BusinessException("模拟支付已禁用");
}

拦截放在进入租户上下文之前,被拒请求不会触发任何 DB 操作。

refundOrder 渠道显式判定(L564-582)

HotelPayLog payLog = payLogMapper.selectLatestByOrderId(orderId);
String paidChannel = payLog != null ? payLog.getPayChannel() : null;
boolean isMockPaid = HotelOrderConstants.PAY_SOURCE_MOCK.equals(order.getPaySource())
        || "MOCK".equals(paidChannel);
if (isMockPaid || payLog == null) {
    markRefundSuccess(...);   // 无真实资金流,仅回写状态
    return;
}
if (!"ALIPAY".equals(paidChannel)) {
    log.error("退款渠道未接入,需人工处理: ...");
    throw new BusinessException("该支付渠道暂不支持在线退款,请联系前台人工处理");
}
// → alipay.trade.refund

pay.vue else 分支拆分(L258-286)

  • L258-276:else if (createData.payChannel === 'MOCK') —— 保留原调试逻辑,注释说明「后端 hotel.pay.mock-enabled=true 时才会返回 payChannel=MOCK,因此本分支在生产环境永远不会命中,既有 H5 调试行为保持不变」
  • L277-286:新增 else —— uni.showModal('暂无法在线支付'),注释明确「禁止在此兜底调用 hotelPayMockSuccess —— 那等于绕过资金校验免付款」

⑨ 日志脱敏(顺带收口)

  • AlipayAuthController L56 改为 maskPhone(vo.getPhone()),L66 新增 private String maskPhone(String phone)
  • AlipayAuthServiceImpl L70 改为只记 responseLength,不再打印授权响应原文

2.3 发现与风险(改造过程中新发现,原文档未记录)

# 发现 处置
两份生产 application.yml 完全没有 hotel 块,而 @Value("${hotel.qr.base-url}") 无默认值 → 生产 profile 启动即崩 已补齐 hotel 块(属第 2 批范围,但与本批共享同 6 份 yml)
createPay 分流原用原始 paySource 判断 → ALIPAY_MP 会漏进 else 被静默置为 MOCK 已改为用解析后的 payChannel
resolvePayChannel两处静默降级(微信分支 + 兜底分支),原缺口清单只记录了一处 两处统一收敛到 mockOrReject
refundOrder 用「非 ALIPAY 即 MOCK」兜底 → 未来接微信造成真实资损 已改显式判定
🔴 application-dev.yml 未被 .gitignore 忽略git check-ignore 退出码 1),其中含支付宝沙箱 private-key / alipay-public-key 明文 不在本变更范围,另立 hotel-alipay-secret-externalize

2.4 语义陷阱(后续维护必读)

resolvePayChannel("ALIPAY_MP") 返回的是 "ALIPAY"不是 "ALIPAY_MP"。因此未来补 alipay.trade.create 分支时,分流必须用原始 paySource,不能用 payChannel;而本次改造恰恰要求 createPay 现有分流用 payChannel。二者不矛盾(现有分流只区分「支付宝 vs 其它」),但新增 trade.create 时必须重新审视这两处判断依据。


3. 功能点

  • 功能 1:配置开关 —— hotel.pay.mock-enabled(代码级默认 false)控制模拟支付是否可用;启动时打印开关状态,开启时 WARN 级资金安全告警
  • 功能 2:渠道解析单一收口 —— 所有非支付宝渠道统一走 mockOrReject(reason),开关关闭时抛 BusinessException,开启时返回 "MOCK";全仓禁止裸 return "MOCK"
  • 功能 3:mockSuccess 接口拦截 —— 开关关闭时首行拒绝,不进入租户上下文、不触发 DB 操作
  • 功能 4:paySource 必传 —— 移除 defaultValue = "MOCK",缺省直接 400
  • 功能 5:退款渠道显式判定 —— MOCK / 无流水 → 仅回写状态;ALIPAY → 真实退款;其它 → 抛异常要求人工介入
  • 功能 6:前端 else 拆分 —— 保留 payChannel === 'MOCK' 分支(dev 零变化),新增 else 显式报错,禁止兜底调 mockSuccess
  • 功能 7:日志脱敏 —— 手机号掩码、授权响应只记长度

4. 业务规则

规则 内容
R1 生产环境(application.ymlmock-enabled 必须为 false,通过 ${FORGE_HOTEL_PAY_MOCK_ENABLED:false} 允许环境变量覆盖但默认严格
R2 开发环境(application-dev.yml / application-dev.example.ymlmock-enabled: true,保证既有 H5 调试流程零变化
R3 合法 paySource 白名单:ALIPAYALIPAY_MPWECHATWECHAT_MPMOCKH5;其余一律「不支持的支付渠道」
R4 WECHAT / WECHAT_MP 在开关关闭时的报错文案必须给出可行替代路径(「请使用支付宝支付或到前台付款」),不能只说「不支持」
R5 退款仅在 pay_status = 已支付 时可发起;已退款成功不可重复发起
R6 无支付流水也直接标记退款成功 —— 这是历史数据兼容的妥协(见第 8 节风险 ③)
R7 全仓禁止裸 return "MOCK";新增渠道必须显式接入 resolvePayChannel,禁止依赖兜底分支

5. 数据变更

操作 表名 字段/索引 说明
无数据库变更,无 Flyway 脚本

说明:hotel_pay_log.pay_channel 字段沿用现有定义,只是取值范围被收紧(生产环境不再产生 MOCK 值)。历史 pay_channel=MOCK 的存量数据由 R6 兼容。


6. 接口变更

操作 接口 方法 变更内容
修改 /hotel/open/pay/create POST paySource@RequestParam(defaultValue = "MOCK") 改为必传 @RequestParam;非法渠道返回业务异常而非静默降级
修改 /hotel/open/pay/mockSuccess POST 新增开关拦截:hotel.pay.mock-enabled=false 时抛 BusinessException("模拟支付已禁用") 并打 WARN 日志
不变 /hotel/open/pay/status GET 无变更
不变 /hotel/open/pay/notify POST 无变更(验签 / 金额 / app_id / 幂等四层校验沿用)
不变 /hotel/open/pay/alipayQuery GET 无变更
修改(内部) HotelPayService.refundOrder 渠道判定由「非 ALIPAY 即 MOCK」改为显式三分支
修改(内部) AlipayAuthController.getUserInfo 日志手机号脱敏,响应体不变

接口路径与出入参结构均未变更,仅约束收紧。前端 api/index.js 无需改动。


7. 影响范围

7.1 后端

文件 类型
forge-hotel/.../pay/config/HotelPayConfig.java 新建
forge-hotel/.../pay/service/impl/HotelPayServiceImpl.java 修改(注入 + createPay + resolvePayChannel + mockOrReject + refundOrder
forge-hotel/.../controller/open/HotelPayController.java 修改(createPay 去默认值 + mockSuccess 拦截 + 注入)
forge-hotel/.../controller/open/AlipayAuthController.java 修改(maskPhone
forge-hotel/.../pay/service/impl/AlipayAuthServiceImpl.java 修改(日志脱敏)
forge-admin-server/src/main/resources/application.yml 修改(新增 hotel.pay.mock-enabled
forge-admin-server/src/main/resources/application-dev.yml 修改(同上,值 true
forge-admin-server/src/main/resources/application-dev.example.yml 修改(同上,值 true
forge-app-server/src/main/resources/application.yml 修改(同上)
forge-app-server/src/main/resources/application-dev.yml 修改(同上,值 true
forge-app-server/src/main/resources/application-dev.example.yml 修改(同上,值 true

7.2 前端

文件 类型
forge-h5-ui/src/pages/hotel/customer/pay.vue 修改(handlePay else 分支拆分)

7.3 ⚠️ 6 份 yml 被两个批次共享

application.yml / application-dev.yml / application-dev.example.yml(admin-server + app-server 各 3 份)同时承载:

  • 本批(第 0 批)hotel.pay.mock-enabled
  • 第 2 批(二维码 URL 配置化)hotel.qr.base-urlhotel.qr.pathhotel.alipay.*

两批改动落在同一 hotel:内。回滚本批时只可删除 pay.mock-enabled 一行,禁止整块回退,否则会连带破坏第 2 批并导致生产 profile 启动崩溃(见 2.3 发现 ①)。

7.4 不受影响

  • PC 管理端(forge-admin-ui):零改动
  • 支付回调链路(验签 / 金额 / app_id / 幂等 / 竞态防护 / 退款对账):零改动
  • H5 dev 环境行为:零改动(开关为 true,全部分支走向与改造前一致)

8. 风险与关注点

⚠️ 本变更属资金类变更(AGENTS.md 5.9 / code-copilot/rules/security.md 第 2 节),以下每一项都需人工审查确认。

8.1 🔴 需人工审查的三个点(审查时逐条给结论)

mockOrReject 单一收口是否覆盖全部渠道 —— 漏一个分支 = 免付款

当前 resolvePayChannel(L821-836)覆盖:ALIPAY / ALIPAY_MP / WECHAT / WECHAT_MP / null / 空串 / 全空格 / MOCK / H5 / 其它任意值(兜底)。

审查要点:兜底分支 return mockOrReject("不支持的支付渠道: " + paySource)最后一道网,请确认它确实无法被绕过(例如 paySource 传入超长字符串、含控制字符、大小写变体 alipay)。

注:"alipay"(小写)会落到兜底分支被拒,因为判定用的是 "ALIPAY".equals(paySource) 严格相等。这是期望行为(前端只会传大写常量),但需确认前端不存在传小写的路径。

refundOrder 三种「无资金流」判定边界(L564-576)

paySource = MOCK           → 仅回写状态
流水 pay_channel = MOCK     → 仅回写状态
无流水(payLog == null)    → 仅回写状态   ← 🔴 风险点

审查要点:「无流水也直接标记退款成功」是历史数据兼容的妥协。理论上存在「用户真实支付了、但 hotel_pay_log 流水丢失/被删」的场景,此时系统会标记退款成功而实际未退钱给用户,属客诉与合规风险。

可选加固方向(本次未做,需你决策是否追加):

  • 无流水时不直接成功,改为抛异常要求人工核查
  • 或先调 alipay.trade.queryorder_no 反查支付宝侧是否存在交易

pay.vue else 采「拆分」而非「删除」(L258-286)

保留了 payChannel === 'MOCK' 分支,即代码里仍存在调用 hotelPayMockSuccess 的路径。生产环境后端不会返回 payChannel=MOCK,故不会命中;但:

  • 若未来后端出现 bug 让生产返回了 MOCK,前端会照旧免付款走通
  • 审查要点:是否接受「依赖后端开关作为唯一防线」,还是要求前端也用 import.meta.env 做二次隔离

本次选择「拆分」是为满足用户硬约束「不可影响 H5 开发测试」—— 删除该分支会让 dev 环境的 Mock 调试流程失效。

8.2 其它风险

# 风险 缓解
环境变量名写错静默失效:正确名是 FORGE_HOTEL_PAY_MOCK_ENABLED,写成 FORGE_HOTEL_PAY_MOCK 不报错,只静默退回 false 生产默认 false 是安全侧,误配只会导致「Mock 不可用」而非「Mock 被开」;但 dev 环境误配会让调试失效,已在 6 份 yml 注释中标注
开关是进程级而非租户级:一旦生产误开,全租户同时暴露 启动时 WARN 级「【资金安全告警】」日志,便于监控告警接入
mockSuccess 仍在 Sa-Token 白名单内,开关是唯一防线 生产 false + 代码级默认 false 双保险;彻底移除该接口会破坏 dev 调试,故保留
本批与第 2 批共享 6 份 yml,回滚易误伤 见 7.3,回滚只删 pay.mock-enabled 一行
🔴 application-dev.yml 明文沙箱密钥已入库 不在本变更范围,见 hotel-alipay-secret-externalize

8.3 状态流转影响

refundOrder 改造后,非 ALIPAY 非 MOCK 渠道的退款请求会抛异常(改造前静默标记成功)。这会改变前台「手动退款」按钮的行为:未来接入微信后,微信订单点退款会看到「请联系前台人工处理」而非「退款成功」。这是期望行为(防止资损),但需在接入微信时同步实现在线退款。


8.5 测试策略

  • 测试范围
    • resolvePayChannel 全渠道分支(6 类入参 × 开关 2 态 = 12 组)
    • mockOrReject 开关两态
    • mockPaySuccess 开关关闭时拦截
    • createPay 缺省 paySource / 非法 paySource / ALIPAY / ALIPAY_MP
    • refundOrder 三分支(MOCK / ALIPAY / 其它)+ 无流水
  • 覆盖率目标:资金分支行覆盖 100%resolvePayChannelmockOrRejectrefundOrder 渠道判定段)
  • 独立 Test Spec
    • 理由:本次是安全收口而非新功能,验证口径已在 酒店订单支付系统开发文档.md 10.1 记录(后端全 reactor mvn compile BUILD SUCCESS、H5 pnpm build:h5 Build complete、GetProblems 12 个文件 No errors、6 份 yml 逐行核对一致、dev 行为零变化)
    • ⚠️ 未做单元测试forge-hotel 模块当前无测试基础设施(无 src/test),本次未新建。若审查要求补测,需先解决模块测试脚手架问题,属独立工作量

9. 待澄清

  • Q1(对应 8.1 ②)refundOrder 遇到「已支付但无流水」时,是维持「直接标记退款成功」,还是改为抛异常要求人工核查 / 调 alipay.trade.query 反查?
  • Q2(对应 8.1 ③)pay.vuepayChannel === 'MOCK' 分支是否需要再加一层前端环境隔离(import.meta.env.DEV),还是接受「后端开关为唯一防线」?
  • Q3(对应 8.1 ①):确认前端不存在传小写 / 变体 paySource 的路径(当前后端严格大写相等匹配)。

以上三项全部解决并签署第 13 节后,本变更方可从 review 进入 done


10. 技术决策

# 决策 理由 被否方案
D1 配置开关而非删除 Mock 能力 用户硬约束「不可影响 H5 开发测试」;删除会让 dev 调试流程失效 直接删除 mockSuccess 接口
D2 开关默认值写在代码里@Value("${...:false}"))而非只靠 yml yml 整块缺失也不会误开;6 份 yml 分散在两个服务,任一遗漏都安全 只靠 yml 配置
D3 mockOrReject 单一收口点 改造前有两处静默降级,正是漏洞成因;收口后全仓只有一个位置能返回 "MOCK",审查面收敛到 6 行 在每个分支各写一遍 if (mockEnabled)
D4 ALIPAYALIPAY_MP 归为同一 payChannel 两者都是支付宝,风控与对账口径一致;具体协议差异(wap.pay vs trade.create)留给第 3 批 保留为两个独立 payChannel
D5 退款禁用「非 ALIPAY 即 MOCK」兜底 未来接微信后会误判无资金流 → 真实资损;宁可抛异常要求人工介入 维持原兜底
D6 生产 yml 用 ${FORGE_HOTEL_PAY_MOCK_ENABLED:false} 而非硬编码 false 保留应急开关能力,默认侧安全 硬编码 false
D7 日志脱敏(maskPhone / responseLength)纳入本批 命中 AGENTS.md 5.9「禁止在日志中打印手机号」,与资金安全同属安全红线,一并收口成本最低 另立变更

10.1 与原方案的三处差异(补写时如实登记)

原缺口清单第 0 批的方案描述与实际落地存在三处差异:

# 原方案 实际落地 原因
createPay 入口显式拒绝 MOCK 下沉为 resolvePayChannel + mockOrReject 统一收口 入口拒绝只能挡住 paySource=MOCK,挡不住微信分支与兜底分支的静默降级;下沉后覆盖面完整
pay.vue 的 else MOCK 分支删除 改为拆分:保留 payChannel === 'MOCK' 分支 + 新增 else 报错 满足「不可影响 H5 开发测试」硬约束
引入 wechatPayProperties 配置类占位 未引入 微信支付已决策暂缓,空配置类属无效代码;resolvePayChannel 的微信分支已足够阻断

11. 执行日志

⚠️ 本表为回填实际改动文件(代码先于 Spec 落地),非计划文件清单。文件清单已用 git status --porcelain + git diff 核实。

Task 状态 实际改动文件 备注
Task 1 配置开关 ✅ 已执行 pay/config/HotelPayConfig.java新建,L22-49) 代码级默认 false + @PostConstruct 告警
Task 2 单一收口点 ✅ 已执行 pay/service/impl/HotelPayServiceImpl.java(L821-836 resolvePayChannel、L846-851 mockOrReject、L60 注入) 全仓唯一返回 "MOCK" 的位置
Task 3 渠道解析前置 ✅ 已执行 同上(L117-119 前置、L138-140 分流改用 payChannel 修复 ALIPAY_MP 漏进 MOCK
Task 4 接口约束收紧 ✅ 已执行 controller/open/HotelPayController.java(L56-59 去 defaultValue、L80-86 首行拦截、L44-45 注入) 拦截在租户上下文之前
Task 5 退款显式判定 ✅ 已执行 HotelPayServiceImpl.java(L564-582) 三分支:MOCK/无流水 → 回写;ALIPAY → 真实退款;其它 → 抛异常
Task 6 前端 else 拆分 ✅ 已执行 forge-h5-ui/src/pages/hotel/customer/pay.vue(L258-286) 保留 MOCK 分支 + 新增 else showModal
Task 7 日志脱敏 ✅ 已执行 controller/open/AlipayAuthController.java(L56 调用、L66 maskPhone)、pay/service/impl/AlipayAuthServiceImpl.java(L70 只记 responseLength 响应体不变
Task 8 yml 配置落地 ✅ 已执行 admin-server + app-server 各 3 份:application.yml(L239 / L121,${FORGE_HOTEL_PAY_MOCK_ENABLED:false})、application-dev.yml(L104 / L83,true)、application-dev.example.yml(L102 / L83,true ⚠️ 与第 2 批共享同一 hotel: 块,见 7.3
验证 ✅ 已通过 后端全 reactor mvn compile BUILD SUCCESS;H5 pnpm build:h5 Build complete;GetProblems 12 个文件 No errors;6 份 yml 逐行核对一致;dev 行为零变化
单元测试 ❌ 未执行 forge-hotel 无测试基础设施,见 8.5
人工审查 🔴 待执行 见第 12 节

12. 审查结论

🔴 尚未审查。 本节须由审查人填写,AI 不得代填。

审查时请对以下清单逐条给出「通过 / 需修改」结论:

  • 8.1 ① mockOrReject 覆盖面是否完整(漏一个分支 = 免付款)
  • 8.1 ② refundOrder「无流水直接标记退款成功」是否可接受(Q1)
  • 8.1 ③ pay.vue 保留 MOCK 分支是否可接受(Q2)
  • 第 4 节 R1~R7 业务规则是否与业务预期一致
  • 第 6 节接口约束收紧是否影响已上线的调用方
  • 7.3 6 份 yml 共享块的回滚边界是否清晰
  • 8.5 未补单元测试是否可接受
  • 10.1 三处方案差异是否认可

审查结论(待填)

审查人(待填)

审查日期(待填)


13. 确认记录(HARD-GATE)

⚠️ 本节是 AGENTS.md 5.9 与 code-copilot/rules/security.md 第 2 节要求的资金类变更强制门禁只能由需求方本人手填,AI 助手不得代签、不得填入推测值。 未签署前,本变更状态停留在 review,不得进入 done,不得归档(/archive)。

  • 确认时间:2026-09-07
  • 确认人:徐滕

确认范围声明(签署即表示已阅读并认可以下内容):徐滕

  1. 第 8.1 节三个资金风险点已逐条审查
  2. 第 9 节 Q1~Q3 三项待澄清已有明确结论
  3. 第 10.1 节与原方案的三处差异已认可
  4. 第 12 节审查结论已填写
  5. 知悉本变更为代码先行、Spec 补写的流程倒置,并同意以此方式追认