Răsfoiți Sursa

027线下转账支付规格文档(spec/plan/tasks/契约/冒烟)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
qmj 1 zi în urmă
părinte
comite
8e0f779320

Fișier diff suprimat deoarece este prea mare
+ 11 - 0
.claude/homunculus/observations.jsonl


+ 1 - 1
.specify/feature.json

@@ -1,3 +1,3 @@
 {
-  "feature_directory": "specs/026-store-menu-copy"
+  "feature_directory": "specs/027-offline-transfer-payment"
 }

+ 1 - 1
CLAUDE.md

@@ -204,5 +204,5 @@ Strong success criteria let you loop independently. Weak criteria ("make it work
 
 <!-- SPECKIT START -->
 For additional context about technologies to be used, project structure,
-shell commands, and other important information, read the current plan: `specs/019-line-pay/plan.md`
+shell commands, and other important information, read the current plan: `specs/027-offline-transfer-payment/plan.md`
 <!-- SPECKIT END -->

+ 35 - 0
specs/027-offline-transfer-payment/checklists/requirements.md

@@ -0,0 +1,35 @@
+# Specification Quality Checklist: 線下轉賬支付
+
+**Purpose**: Validate specification completeness and quality before proceeding to planning
+**Created**: 2026-09-03
+**Feature**: [spec.md](../spec.md)
+
+## Content Quality
+
+- [x] No implementation details (languages, frameworks, APIs)
+- [x] Focused on user value and business needs
+- [x] Written for non-technical stakeholders
+- [x] All mandatory sections completed
+
+## Requirement Completeness
+
+- [x] No [NEEDS CLARIFICATION] markers remain
+- [x] Requirements are testable and unambiguous
+- [x] Success criteria are measurable
+- [x] Success criteria are technology-agnostic (no implementation details)
+- [x] All acceptance scenarios are defined
+- [x] Edge cases are identified
+- [x] Scope is clearly bounded
+- [x] Dependencies and assumptions identified
+
+## Feature Readiness
+
+- [x] All functional requirements have clear acceptance criteria
+- [x] User scenarios cover primary flows
+- [x] Feature meets measurable outcomes defined in Success Criteria
+- [x] No implementation details leak into specification
+
+## Notes
+
+- 所有关键决策已在对话中确认:选项显示条件(银行三字段齐全)、商家确认收款解锁骑手、骑手门禁与列表过滤、运费纯线下、payType=6、App 端不在本期。
+- 支付方式值域(1-6)与订单状态机为既有业务事实,保留值域表便于与现有系统对照,不属于实现泄漏。

+ 75 - 0
specs/027-offline-transfer-payment/contracts/api.md

@@ -0,0 +1,75 @@
+# API Contracts: 線下轉賬支付
+
+**日期**:2026-09-03 | **风格**:沿用项目规范(`@RequestHeader String token`、`@RequestParam`/`@RequestBody` 显式注解、DTO 承载)
+
+## 新增接口(后端)
+
+### 1. 商家收款账户查询(收银台展示用)
+
+```
+GET /chanting/store/bankInfo?id={storeId}
+```
+
+- 鉴权:匿名(`@Anonymous`,同 getstore 风格)
+- 逻辑:`pos_store.id=storeId → user_id → info_user`,取 bankAccountName / bankName / bankAccountNo
+- 返回:
+
+```json
+// 三项齐全
+{ "code": 200, "data": { "accountName": "AMAZEWAY LIMITED", "bankName": "國泰世華銀行", "accountNo": "1234567890" } }
+// 任一为空 → 收银台隐藏「線下支付」选项
+{ "code": 200, "data": null }
+```
+
+- App 用法:选项显示条件 = `data != null`;展开卡片显示三字段(参考原型),建议账号行带复制按钮。
+
+### 2. 商家确认转账收款
+
+```
+GET /system/orderShOprate/confirmTransferPayment?id={orderId}
+Header: token(商家,须有该店权限 requireMerchantOrderAccess)
+```
+
+- 逻辑(镜像 confirmCashPayment):
+  1. 校验 `payType == 6`,否则 `no.order.paytype.not.transfer`
+  2. 校验 `state != 4`(已取消),否则 `no.order.transfer.cancelled`
+  3. 校验 `payStatus != 1`,否则 `no.order.transfer.already.paid`
+  4. `payStatus` 0→1;`orderLogHelper` 记「商家{名}确认转账收款」
+  5. 外送单(type=0)调 `deliveryOrderNotificationService.notifyOrderAvailable(order)` 推送附近骑手
+- 返回:`{ "code": 200 }` 或国际化错误
+
+### 3. 骑手接单门禁(修改现有接口)
+
+`GET /system/orderQsOprate/acceptOrder`(现有)增加校验:
+
+- `payType == 6 && payStatus != 1` → 拒绝,`no.order.transfer.not.paid`(「商家确认收款后骑手才能接单」)
+
+## 复用接口(App 端直接用,无改动)
+
+| 接口 | 用途 |
+|---|---|
+| `POST /infouser/user/../createOrder`(paymentMethod="6") | 转账单创建,透传 payType=6,无需后端改动 |
+| 骑手 `orderList`(tab=newTask) | 已过滤 payStatus=1,转账单确认前不可见(零改动) |
+| `GET /chanting/store/bankList` | 026 已交付,银行名称字典(商家资料侧用) |
+
+## 后端国际化 key(五份 properties)
+
+`ruoyi-admin/src/main/resources/i18n/messages{,_zh_CN,_zh_TW,_en_US,_vi}.properties`:
+
+| key | zh_CN 文案 |
+|---|---|
+| no.order.paytype.not.transfer | 该订单不是转账支付订单 |
+| no.order.transfer.cancelled | 订单已取消,不能确认收款 |
+| no.order.transfer.already.paid | 该订单已确认收款 |
+| no.order.transfer.not.paid | 商家确认收款后骑手才能接单 |
+
+## 商家 Web(foodie-store)
+
+- `index.vue` `paymentTypeLabel` 映射加 `6: 'OfflineTransfer'`;i18n `index` 命名空间四语言:
+  - zh:线下转账 / tw:轉賬支付 / en:Transfer Payment / vi:Chuyển khoản
+
+## App 端交互说明(交付给 App 团队,不在本期代码内)
+
+1. **收银台**:进入时调 `bankInfo`,`data != null` 才显示「線下支付」选项(副标题「轉賬後請聯繫客服確認」);选中展开红色提示(轉帳完成後,請透過即時通訊客服發送憑證…)+ 灰底卡片三行(帳戶名稱/銀行名稱/銀行帳戶);提交下单 `paymentMethod=6`。
+2. **商家 App**:转账单显示「轉賬支付」标签;待收款单提供「確認收款」按钮调 `confirmTransferPayment`;确认后订单进入正常接单/出餐流程。
+3. **骑手 App**:无需改动——未确认单本来不可见不可接;确认后单出现在抢单列表并收到推送。

+ 49 - 0
specs/027-offline-transfer-payment/data-model.md

@@ -0,0 +1,49 @@
+# Data Model: 線下轉賬支付
+
+**日期**:2026-09-03 | **结论**:零数据库变更,全部复用现有字段
+
+## 字段语义(现有,不变)
+
+### PosOrder.pay_type(支付方式)
+| 值 | 含义 | 本功能关系 |
+|---|---|---|
+| 1 | 外送到付 | 不变 |
+| 2 | 信用卡(OMG 渠道) | 不变 |
+| 3 | LINE Pay | 不变 |
+| 4 | 现金(到店/当面,商家确认收款已有 confirmCashPayment) | 不变,与新接口互不影响 |
+| 5 | Apple Pay | 不变 |
+| **6** | **線下轉賬(新增语义)** | 本功能 |
+
+### PosOrder.pay_status(支付状态)
+- 0=未支付,1=已支付。转账单:创建时 0,商家确认收款时 0→1(confirmTransferPayment)。
+- 自取/堂食转账单:商家完成订单时现有逻辑自动置 1(completeOrder state 2→3)。
+
+### PosOrder.collect_payment
+- 到付=1;转账单不设(createOrder 只在 paymentMethod=1 时设置,透传 6 不触发)。
+
+## 商家收款账户(数据源,已有表)
+
+`info_user`(商家主账号):
+- `bank_account_name` 帳戶名稱(2026-09-03 加)
+- `bank_name` 銀行名稱(下拉字典 taiwan_bank_list)
+- `bank_account_no` 銀行帳號
+
+查询路径:`pos_store.user_id → info_user`;商家级,连锁门店共享。任一为空 → 接口返回 null → 收银台隐藏选项。
+
+## 状态机(现有,不变)
+
+```
+转账单(外送 type=0):
+下单(state=0, payStatus=0, deliveryStatus=0)
+  → 商家确认收款(payStatus→1) + notifyOrderAvailable 推骑手
+  → 骑手接单(deliveryStatus=1)
+  → 商家接单(state→1)、出餐(state→2)        ← requireRiderAssigned 现状约束
+  → 骑手取餐(deliveryStatus=2, 到店向商家收运费[线下])
+  → 骑手送达(deliveryStatus=3, state→3)
+```
+
+骑手抢单池可见条件(现有查询已含):type=0 && deliveryStatus=0 && state=0 && **payStatus=1** && afterSaleStatus=0 && qsId IS NULL → 转账单确认前自动不可见。
+
+## 新增常量
+
+`OrderLifecycleService.PAY_TYPE_TRANSFER = "6"`(与 PAY_TYPE_CASH 同文件同风格)。

+ 58 - 0
specs/027-offline-transfer-payment/plan.md

@@ -0,0 +1,58 @@
+# Implementation Plan: 線下轉賬支付(payType=6)
+
+**Branch**: `test` | **Date**: 2026-09-03 | **Spec**: [spec.md](spec.md)
+
+**Input**: Feature specification from `/specs/027-offline-transfer-payment/spec.md`
+
+## Summary
+
+收银台新增「線下支付」:商家银行三字段齐全才显示,选中展开收款账户卡片;下单 payType=6;商家确认收款(payStatus 0→1)后推送附近骑手、解锁骑手接单;金额含运费全归商家,骑手到店线下收运费。**下单透传、骑手抢单池过滤(payStatus=1)均零改动**;核心改动 = 收款账户查询接口 + 确认收款接口(挂骑手通知钩子)+ 骑手接单防绕过门禁 + 商家 Web 支付标签。零 DDL。App 端不在工作区,交付接口与交互说明。
+
+## Technical Context
+
+**Language/Version**: Java 21(Spring Boot + MyBatis-Plus),前端 Vue2 + Element UI
+**Primary Dependencies**: ruoyi-admin(改动集中)/ ruoyi-system(仅常量)/ foodie-store(标签)
+**Storage**: MySQL,零 DDL(复用 pos_order.pay_type/pay_status、info_user 银行三字段)
+**Testing**: mvn compile(JDK21)+ curl 冒烟(quickstart.md)+ review 子代理
+**Target Platform**: 商家/骑手 App(复用接口)、商家 Web(foodie-store)
+**Performance Goals**: N/A(低频操作,与现有订单接口同级)
+**Constraints**: 不动现金确认/线上支付链路;后端文件 CRLF,Python 脚本编辑;注释禁 `*/`
+**Scale/Scope**: 后端 4 文件 + i18n 5 properties + 前端 5 文件
+
+## Constitution Check
+
+`​.specify/memory/constitution.md` 为未填写的模板占位(无项目宪法约束)→ 无门槛项,通过。
+
+## Project Structure
+
+### Documentation (this feature)
+
+```text
+specs/027-offline-transfer-payment/
+├── plan.md              # 本文件
+├── research.md          # 决策与依据(D1-D9)
+├── data-model.md        # 字段语义与状态机(零 DDL)
+├── quickstart.md        # 冒烟验证场景
+├── contracts/api.md     # 接口契约 + App 交互说明
+└── tasks.md             # /speckit-tasks 生成
+```
+
+### Source Code (repository root)
+
+```text
+ruoyi-admin/src/main/java/com/ruoyi/app/
+├── order/OrderLifecycleService.java          # +PAY_TYPE_TRANSFER 常量
+├── order/PosOrderShOprateController.java     # +confirmTransferPayment(注入 DeliveryOrderNotificationService)
+├── order/PosOrderQsOprateController.java     # acceptOrder +转账单门禁
+└── mendian/PosStoreController.java           # +bankInfo 收款账户查询
+ruoyi-admin/src/main/resources/i18n/           # 4 个新 key × 5 份 properties
+E:/QtwCode/foodie/foodie-store/src/
+├── views/index.vue                           # paymentTypeLabel +6
+└── lang/{zh,tw,en,vi}.js                     # index.OfflineTransfer ×4
+```
+
+**Structure Decision**: 全部落在现有控制器/服务的既有模式上(银行接口随门店控制器、确认收款随商家订单操作控制器、门禁随骑手控制器),不新建类。
+
+## Complexity Tracking
+
+无宪法违规,无需记录。

+ 47 - 0
specs/027-offline-transfer-payment/quickstart.md

@@ -0,0 +1,47 @@
+# Quickstart 验证: 線下轉賬支付
+
+**前置**:后端已启动(JDK21 编译);数据库已执行 026 的银行字段 ALTER(测试商家资料可填银行三字段);foodie-store 已构建。
+
+## 场景 1:收款账户查询
+
+```bash
+# 门店换成测试商家(银行三项齐全)的门店 id
+curl "http://localhost:8080/chanting/store/bankInfo?id=<门店id>"
+# 期望 data 含 accountName/bankName/accountNo
+# 清空任一字段后再查 → data=null
+```
+
+## 场景 2:转账单创建 + 骑手门禁
+
+```bash
+# 1) 用户 token 下单(客户App链路,也可用商家代下);paymentMethod 传 "6"
+#    期望:订单创建成功,payType=6、state=0、payStatus=0
+
+# 2) 骑手 token 直接强行接单(未确认收款)
+curl -H "token: <骑手token>" "http://localhost:8080/system/orderQsOprate/acceptOrder?id=<orderId>"
+# 期望:拒绝,提示「商家确认收款后骑手才能接单」
+
+# 3) 骑手抢单列表 tab=newTask 查询
+# 期望:该单不在列表中
+```
+
+## 场景 3:确认收款解锁
+
+```bash
+# 商家 token 确认收款
+curl -H "token: <商家token>" "http://localhost:8080/system/orderShOprate/confirmTransferPayment?id=<orderId>"
+# 期望:成功;重复调用 → 「该订单已确认收款」;订单操作日志出现「商家XX确认转账收款」
+
+# 再试骑手接单 → 成功;商家接单/出餐/取餐/送达全链路正常
+# (外送单确认后附近骑手收到新单推送)
+```
+
+## 场景 4:回归(现金/线上不受影响)
+
+- payType=4 现金单 `confirmCashPayment` 行为不变(非现金单调用仍报「不是现金支付订单」)
+- OMG/LINE Pay 单支付回调、骑手推送、商家接单链路不变
+- 商家 Web 订单列表:到付/线上/转账三类标签各不相同且随语言切换正确(zh/tw/en/vi)
+
+## 场景 5:商家 Web 标签
+
+foodie-store 订单管理页查看转账单,支付方式列显示「线下转账」;切四种界面语言均正确。

+ 55 - 0
specs/027-offline-transfer-payment/research.md

@@ -0,0 +1,55 @@
+# Research: 線下轉賬支付(payType=6)
+
+**日期**:2026-09-03 | **状态**:定稿(对话讨论 + 代码核实,无未决项)
+
+## 决策清单
+
+### D1 支付方式值 = 6
+- **决策**:`PosOrder.payType = "6"` 表示線下轉賬。
+- **依据**:现有值域 1=外送到付、2=信用卡(OMG)、3=LINE Pay、4=现金、5=Apple Pay(specs/023 已定);6 为空闲新值。常量加 `OrderLifecycleService.PAY_TYPE_TRANSFER`(现有 `PAY_TYPE_CASH="4"` 同文件)。
+- **备选**:复用 4(现金)加标记位——拒绝,语义混淆且破坏现金确认逻辑。
+
+### D2 收款账户查询接口放门店控制器,商家级数据
+- **决策**:`GET /chanting/store/bankInfo?id={storeId}`(匿名),路径:门店 → `pos_store.user_id`(商家主账号)→ `info_user` 三字段(bankAccountName/bankName/bankAccountNo,026 已加);任一为空返回 `data=null`。
+- **依据**:审核资料银行字段挂在商家主账号上,连锁店共用;App 拿 null 隐藏「線下支付」选项(spec FR-001)。
+- **备选**:挂到订单接口按 orderId 查——拒绝,选项在选择支付方式时(下单前)就需要,只有 storeId 可用。
+
+### D3 下单链路零改动
+- **决策**:`UserOrderController.createOrder` 的 `paymentMethod` 直接透传为 `payType`(第259行),无白名单校验;6 天然可下单(state=0、payStatus=0、collectPayment 不设)。
+- **依据**:代码核实;与线上支付单未付态完全同构。
+
+### D4 确认收款镜像现金版 + 挂骑手通知钩子
+- **决策**:新增 `GET /system/orderShOprate/confirmTransferPayment`,结构照抄 `confirmCashPayment`(PosOrderShOprateController:397):校验 payType=6、state≠4(已取消拒绝)、payStatus 0→1、`orderLogHelper` 记「商家XX确认转账收款」;**外送单(type=0)确认后调用 `deliveryOrderNotificationService.notifyOrderAvailable(order)`** 通知附近骑手。
+- **依据**:`notifyOrderAvailable` = "支付完成且仍待骑手接单时通知附近骑手",OMG 支付成功后正是这样调(OmgPaymentNotifyService:189);转账单确认收款 = 它的"支付完成"时刻,不挂钩子则订单进骑手池但无人被通知。
+- **备选**:复用 confirmCashPayment 放宽校验到 4 和 6——拒绝,错误提示与日志语义会混(现金/转账),App 端也不好区分埋点。
+
+### D5 骑手可见性:零改动(天然满足)
+- **决策**:骑手抢单列表(`orderList` tab=newTask)现有查询已含 `eq(PosOrder::getPayStatus, 1L)`(PosOrderQsOprateController:347),payStatus=0 的转账单自动不可见。
+- **依据**:代码核实;这也是线上未支付单不进骑手池的同一机制。
+- **推送排除**:同样天然满足——骑手新单推送只由"支付完成"钩子触发(OMG/LINE 回调),下单时不推。
+
+### D6 骑手接单门禁:防绕过的显式校验
+- **决策**:`PosOrderQsOprateController.acceptOrder` 增加校验:`payType=6 && payStatus≠1` → 拒绝,提示 key `no.order.transfer.not.paid`(「商家确认收款后骑手才能接单」,五份 properties)。
+- **依据**:列表过滤挡不住直接调接口(App 抢单页刷新时差、越权调用);spec FR-004 要求接口级拒绝。
+- **备选**:不加(信任列表过滤)——拒绝,spec 明确要求接口拒绝且成本一行。
+
+### D7 后端国际化资源:五份 properties
+- **决策**:新 key 加到 `ruoyi-admin/src/main/resources/i18n/` 下 messages.properties(默认,内容为 vi)、messages_zh_CN、messages_zh_TW、messages_en_US、messages_vi 全部五份。
+- **依据**:现金单的 key(no.order.paytype.not.cash 等)在五份文件中均有先例,直接对齐。
+- **新增 key**:`no.order.paytype.not.transfer`(非转账单)、`no.order.transfer.cancelled`(已取消不可确认)、`no.order.transfer.already.paid`(已收款)、`no.order.transfer.not.paid`(骑手门禁提示)。
+
+### D8 商家 Web 标签
+- **决策**:foodie-store `index.vue` 的 `paymentTypeLabel` 映射加 `6: 'OfflineTransfer'`;i18n key 加 `index` 命名空间四语言(zh 线下转账 / tw 轉賬支付 / en Transfer Payment / vi Chuyển khoản)。列表列与详情弹窗共用该函数,一处改两处生效。
+- **依据**:现有 1-4 映射同型(index.vue:388-396)。
+
+### D9 明确不做
+- App 端 UI(客户收银台、商家 App 确认按钮、骑手 App):不在工作区,交付接口+交互说明。
+- Web 商家端「确认收款」按钮:本期只做接口,按钮后续按需。
+- 运费代收/结算、凭证上传、转账单超时取消:纯线下,spec 已排除。
+- 零 DDL:复用 payType/payStatus 现有字段。
+
+## 风险与注意
+
+- **confirmTransferPayment 需注入 DeliveryOrderNotificationService**:PosOrderShOprateController 目前未注入(PosOrderController 有),照 PosOrderController:130 的方式补。
+- **notifyOrderAvailable 的时机**:其内部有事务同步(afterCommit);确认收款方法不加 @Transactional(对齐现金版),保存后直接调用即可。
+- **自取/堂食转账单**:完成时现有 completeOrder 自动置 payStatus=1(state 2→3 时),无需特殊处理;确认收款按钮同样可用。

+ 124 - 0
specs/027-offline-transfer-payment/spec.md

@@ -0,0 +1,124 @@
+# 功能规格:線下轉賬支付(payType=6)
+
+**功能标识**:`027-offline-transfer-payment`
+
+**创建日期**:2026-09-03
+
+**状态**:设计已与需求方确认(对话定稿),进入 plan
+
+**输入**:任务206「支付方式加上转账支付」。用户下单时可选「線下支付」,选中后展开商家的收款账户信息(帳戶名稱/銀行名稱/銀行帳戶);用户线下银行转账后联系商家,商家线下核对后确认收款,订单进入配送流程。资金流:订单金额含运费全部线下转给商家,骑手正常接单不垫付任何费用,骑手到店后向商家收取运费再配送(运费交接纯线下,系统不处理)。支付方式编号使用 **6**。
+
+## 用户场景与测试
+
+### 用户故事 1 - 用户选择線下支付并完成下单(Priority: P1)
+
+用户在收银台看到支付方式列表,其中「線下支付」选项**仅在该商家收款账户三项信息(帳戶名稱/銀行名稱/銀行帳戶)齐全时**出现;选中后展开提示语(转账后请联系客服确认)与商家收款账户卡片;用户提交订单,订单以转账支付方式创建,进入待商家接单,全程不触发任何线上支付。
+
+**为什么这个优先级**:这是功能的入口与前提,没有它后续环节都无从谈起;单独成立即可验证展示与下单链路。
+
+**独立测试**:商家 A 银行三项齐全、商家 B 缺银行账号;A 店收银台出现「線下支付」并显示三项账户信息,B 店不出现该选项;A 店提交转账单成功创建。
+
+**验收场景**:
+
+1. **假如** 商家银行三项齐全,**当** 用户打开收银台,**那么** 「線下支付」可选,选中后展开三项收款信息与转账提示。
+2. **假如** 商家任一银行字段为空,**当** 用户打开收银台,**那么** 不显示「線下支付」选项(不出现空账户卡片)。
+3. **假如** 用户以線下支付提交订单,**当** 订单创建,**那么** 订单支付方式=6、待商家接单、未支付,且不发起任何线上支付。
+4. **假如** 同一商家旗下多家门店(连锁),**当** 任一门店收银台查询收款账户,**那么** 返回同一份账户信息(收款账户为商家级)。
+
+---
+
+### 用户故事 2 - 商家确认收款解锁配送(Priority: P1)
+
+用户线下转账后联系商家;商家核对到账后,在商家端对该转账单点击「确认收款」,订单标记为已收款;**此后**该订单才对骑手可见、可接单。商家接单、出餐、骑手取餐送达走现有流程不变。
+
+**为什么这个优先级**:确认收款是转账单唯一的资金闸门——未确认的单可能永远收不到钱(用户放弃/谎称已转),必须挡在骑手环节之外;同时骑手到店要向商家收运费,商家没确认收款前运费无从谈起。
+
+**独立测试**:创建转账单后,骑手列表看不到该单、直接调接单被拒;商家确认收款后,骑手列表出现该单,接单成功,后续接单/出餐/取餐/送达全部正常。
+
+**验收场景**:
+
+1. **假如** 转账单未确认收款,**当** 骑手查询订单列表,**那么** 该单不出现;**当** 骑手以订单号强行接单,**那么** 系统拒绝并提示「商家确认收款后才能接单」。
+2. **假如** 商家已确认收款,**当** 骑手接单,**那么** 接单成功,订单进入正常配送流。
+3. **假如** 商家对已确认收款的转账单再次确认,**当** 调用确认收款,**那么** 系统拒绝并提示已收款。
+4. **假如** 订单已取消,**当** 商家尝试确认收款,**那么** 系统拒绝。
+5. **假如** 商家确认收款操作发生,**当** 查看订单操作日志,**那么** 记录「商家XX确认转账收款」。
+
+---
+
+### 用户故事 3 - 商家端正确识别转账单(Priority: P2)
+
+商家 Web 端订单列表与订单详情中,转账单的支付方式显示「线下转账」标签(简中/繁中/英文/越南语四语言),商家能一眼区分转账单、到付单与线上支付单。
+
+**为什么这个优先级**:商家需凭支付方式决定核账动作(转账单要查银行流水再确认收款),标签不准则整个线下核对流程都会混乱。
+
+**独立测试**:分别创建到付单、线上单、转账单,商家订单列表三者的支付方式标签各不相同且正确。
+
+**验收场景**:
+
+1. **假如** 订单支付方式=6,**当** 商家在 Web 订单列表或详情查看,**那么** 显示「线下转账」(随界面语言切换为对应译文)。
+2. **假如** 界面语言依次切换简中/繁中/英文/越南文,**当** 查看同一转账单,**那么** 标签四种语言均正确显示,无缺失文案。
+
+---
+
+### 用户故事 4 - 资金与运费规则(Priority: P2)
+
+转账单的订单金额(含运费)全部由用户线下转给商家;骑手接单、配送不垫付任何费用;骑手到店取餐时向商家线下收取运费。系统不对运费交接做任何记录或结算处理。
+
+**为什么这个优先级**:这是转账模式与到付模式的本质差异(到付骑手向顾客收全款),必须明确写成规则避免实现时套错模式。
+
+**独立测试**:转账单全流程走完后,系统中不产生任何骑手代收、运费结算相关记录,订单金额与运费数值与用户下单时一致。
+
+**验收场景**:
+
+1. **假如** 转账单完成配送,**当** 查看订单,**那么** 金额与运费保持下单原值,系统内无骑手收款/运费代扣数据。
+
+### 边界场景
+
+- 商家银行信息不全(含审核通过后才补全/被清空):收银台选项随查询结果实时显隐,已下单的转账单不受影响,仍可确认收款。
+- 未确认收款期间用户取消订单:走现有取消流程,无资金争议(用户未付款成功即取消,商家未确认)。
+- 未确认收款期间商家取消订单:走现有取消流程(待接单/已接单可取消)。
+- 转账单长期无人确认:不做超时自动取消(商家自行管理),商家可手动取消。
+- 自取/堂食订单选择線下支付:允许;商家确认收款同样可用,且订单完成时系统按现状自动置为已支付。
+- 骑手在商家确认收款前后刷新列表:列表口径与接单口径一致,不出现「看得见接不了」的单。
+- 已有线上支付方式(信用卡/LINE Pay/Apple Pay/到付/现金)不受任何影响:现金确认收款(payType=4)逻辑保持原样。
+
+## 需求
+
+### 功能需求
+
+- **FR-001**: 系统 MUST 提供按门店查询商家收款账户的能力(帳戶名稱、銀行名稱、銀行帳戶三项),账户信息为商家级(连锁门店共用同一份);任一项缺失时 MUST 返回空,客户端据此隐藏「線下支付」选项。
+- **FR-002**: 用户以線下支付下单时,系统 MUST 以支付方式值 6 创建订单,订单进入待商家接单、未支付状态,MUST NOT 发起或等待任何线上支付。
+- **FR-003**: 系统 MUST 提供商家确认转账收款能力:将转账单标记为已收款并记录操作日志;对非转账单、已收款单、已取消单 MUST 拒绝并给出国际化提示;该能力 MUST NOT 影响现金单(支付方式=4)的现有确认收款逻辑。
+- **FR-004**: 商家确认收款前,骑手 MUST NOT 能接转账单:接单请求 MUST 被拒绝并提示「商家确认收款后才能接单」(国际化)。
+- **FR-005**: 未确认收款的转账单 MUST NOT 出现在骑手订单列表;如存在向骑手推送新单的链路,同样 MUST 排除。
+- **FR-006**: 确认收款后的转账单 MUST 回归现有状态机:商家接单/出餐仍要求骑手已接单(现状约束不变),取餐/送达流程不变。
+- **FR-007**: 商家 Web 端订单列表与详情 MUST 正确显示转账支付标签,文案覆盖简中/繁中/英文/越南语四种界面语言。
+- **FR-008**: 本功能 MUST NOT 引入数据库结构变更(复用订单现有支付方式与支付状态字段)。
+- **FR-009**: 系统 MUST NOT 为转账单生成骑手运费代收、代扣或结算数据(运费交接纯线下)。
+- **FR-010**: 客户 App 端与骑手 App 端界面不在本期实现范围;本期交付可用接口与交互说明,App 后续直接复用。
+
+### 关键实体
+
+- **订单支付方式(值域)**:1=外送到付、2=信用卡、3=LINE Pay、4=现金(到店/线下当面)、5=Apple Pay、**6=線下轉賬(本功能新增)**。
+- **订单支付状态**:0=未支付、1=已支付;转账单在商家确认收款时由 0 置 1。
+- **商家收款账户**:帳戶名稱/銀行名稱/銀行帳戶三项,来自商家审核资料,商家主账号级,连锁门店共享。
+- **订单状态机**:state(商家轴:0待接单→1已接单→2已出餐→…)与 deliveryStatus(骑手轴:1已接单→2已取餐→3已送达);外送单商家接单/出餐要求骑手已接单(现状)。
+
+## 成功标准
+
+### 可度量结果
+
+- **SC-001**: 转账单从下单到商家确认收款,用户端除线下转账本身外零线上支付操作,商家确认后订单 100% 可被骑手正常接单并走完配送流。
+- **SC-002**: 未确认收款的转账单在骑手侧可见性与可接单性均为零(列表不可见 + 强行接单被拒,抽查 100% 成立)。
+- **SC-003**: 商家银行信息不全的门店,收银台「線下支付」选项出现率为 0,不出现空账户展示。
+- **SC-004**: 转账单、现金单、到付单在商家端的支付方式标签互不混淆,四语言显示全部正确。
+- **SC-005**: 零数据库结构变更;线上支付方式(信用卡/LINE Pay/Apple Pay)与现金确认收款行为与上线前完全一致。
+
+## 假设
+
+- 商家收款账户数据源为商家审核资料中的银行三字段(2026-09-03 已加),App 端展示文案与交互按参考原型(选项+副标题「轉賬後請聯繫客服確認」+展开卡片+红色提示语)实现,由 App 团队负责。
+- 转账凭证核对完全线下(即时通讯联系),系统不做凭证上传、自动对账。
+- 转账凭证无超时自动取消机制;长期未确认单由商家手动取消。
+- 骑手运费由骑手与商家线下交接,系统不记录、不结算。
+- 支付方式值 6 为全新值,无历史数据冲突(历史值域 1-5)。
+- 商家确认收款入口同时面向商家 App 与商家 Web 预留(本期实现接口,Web 端按钮可后续按需补)。

+ 61 - 0
specs/027-offline-transfer-payment/tasks.md

@@ -0,0 +1,61 @@
+# Tasks: 線下轉賬支付(payType=6)
+
+**Feature**: specs/027-offline-transfer-payment | **Generated**: 2026-09-03
+
+**参考**: plan.md(改动文件清单)、research.md(D1-D9 决策)、contracts/api.md(接口契约)、quickstart.md(冒烟场景)
+
+**通用约束(每个任务遵守)**:
+- 后端/前端文件均为 CRLF,用 Python 脚本编辑(`python << 'PYEOF'`),锚点唯一性校验后再写
+- Java 注释禁出现 `*/` 序列;Controller 规范(`@RequestHeader token` + 显式 `@RequestParam`,禁 Map 入参)
+- 每个后端任务完成后 `mvn -q -pl ruoyi-system,ruoyi-admin -am compile -DskipTests`(JAVA_HOME=C:\Users\qmj\.jdks\graalvm-jdk-21.0.7)验证
+- 不执行任何数据库变更(本功能零 DDL)
+
+## Phase 1: Setup
+
+- [ ] T001 [P] 在 ruoyi-admin/src/main/java/com/ruoyi/app/order/OrderLifecycleService.java 增加常量 `public static final String PAY_TYPE_TRANSFER = "6";`(紧邻 PAY_TYPE_CASH,带 Javadoc:線下轉賬支付,商家线下确认收款后解锁配送),编译验证
+
+## Phase 2: Foundational(无)
+
+## Phase 3: US1 收银台收款账户查询(P1)
+
+目标:App 依据接口决定「線下支付」选项显隐与账户展示。独立测试:齐全商家门店返回三字段;缺任一字段返回 data=null;连锁多店返回同一份。
+
+- [ ] T002 [P] [US1] 在 ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosStoreController.java 新增 `GET /chanting/store/bankInfo`(`@Anonymous`,`@RequestParam Integer id`):按门店查 `pos_store.user_id` → `info_user` 的 bankAccountName/bankName/bankAccountNo;三项均非空返回 `{accountName, bankName, accountNo}`,任一为空返回 `success(null)`;参考 getstore 风格,Javadoc 注明「商家收款账户(線下轉賬收银台展示用),商家级、连锁共享」,编译验证
+- [ ] T003 [US1] 冒烟验证 T002:curl 三种情形(三字段齐全 / 缺银行账号 / 不存在门店 id→data=null),对照 contracts/api.md 场景 1
+
+## Phase 4: US2 商家确认收款 + 骑手门禁(P1)
+
+目标:确认收款解锁配送。独立测试:未确认时骑手接单被拒(提示「商家确认收款后骑手才能接单」)、newTask 列表不可见;确认后接单成功、附近骑手收到推送、日志记录;重复确认/已取消被拒;现金单 confirmCashPayment 行为不变。
+
+- [ ] T004 [US2] 在 ruoyi-admin/src/main/resources/i18n/ 的五份 properties(messages.properties、messages_zh_CN、messages_zh_TW、messages_en_US、messages_vi)追加 4 个 key:no.order.paytype.not.transfer / no.order.transfer.cancelled / no.order.transfer.already.paid / no.order.transfer.not.paid(文案见 contracts/api.md,tw 繁体自拟同义),Python 脚本逐份插入(锚点用 no.order.cash.already.paid 所在行),编译验证
+- [ ] T005 [US2] 在 ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java 新增 `GET /confirmTransferPayment`(`@RequestHeader token` + `@RequestParam Long id`),镜像 confirmCashPayment(397行):requireMerchantOrderAccess → 校验 payType==PAY_TYPE_TRANSFER(否则 no.order.paytype.not.transfer)→ state==4 拒绝(no.order.transfer.cancelled)→ payStatus==1 拒绝(no.order.transfer.already.paid)→ payStatus 置 1 + orderLogHelper 记「商家{名}确认转账收款」;**外送单(type=0)保存后调用 deliveryOrderNotificationService.notifyOrderAvailable(order)**(注入方式照 PosOrderController:130),编译验证
+- [ ] T006 [US2] 在 ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderQsOprateController.java 的 acceptOrder(76行)校验区增加门禁:`PAY_TYPE_TRANSFER.equals(payType) && payStatus != 1` → 抛 ServiceException(no.order.transfer.not.paid),编译验证
+- [ ] T007 [US2] 冒烟验证 US2 全链路:下单 paymentMethod=6 → 强行骑手接单被拒 → newTask 列表无此单 → confirmTransferPayment 成功(重复调用报已收款)→ 骑手接单成功 → 商家接单/出餐/取餐/送达正常;同时回归 confirmCashPayment 对非现金单仍报「不是现金支付订单」(quickstart.md 场景 2/3/4)
+
+## Phase 5: US3 商家 Web 支付标签(P2)
+
+目标:转账单在商家 Web 正确显示标签。独立测试:到付/线上/转账三类标签互不混淆,四语言正确。
+
+- [ ] T008 [P] [US3] 在 foodie-store/src/views/index.vue 的 paymentTypeLabel 映射(388-396行)加 `6: 'OfflineTransfer'`
+- [ ] T009 [P] [US3] 在 foodie-store/src/lang/{zh,tw,en,vi}.js 的 `index` 命名空间(找 index:{ 的 CashOnDelivery/OMG/LinePay/cash 同级)加 `OfflineTransfer: '线下转账' / '轉賬支付' / 'Transfer Payment' / 'Chuyển khoản'`(Python 脚本,锚点取 index 命名空间内 cash 键行)
+- [ ] T010 [US3] 验证:商家订单列表与详情弹窗对 payType=6 显示「线下转账」,四语言切换正确(quickstart.md 场景 5)
+
+## Phase 6: Polish
+
+- [ ] T011 对照 specs/027-offline-transfer-payment/spec.md 的 FR-001~FR-010 与 quickstart.md 五场景做完整核验(机械检查:编译 PASS + grep 确认 4 个 i18n key 在五份 properties、payType=6 无散落硬编码;冒烟:curl 走完场景 1-4),派 1 个 review 子代理逐 FR 核对改动点
+- [ ] T012 交付 App 团队对接说明:接口清单(bankInfo / confirmTransferPayment / acceptOrder 门禁 / createOrder 透传 6)与收银台交互稿要点(contracts/api.md 的 App 端小节),整理为简短消息发给用户转交
+
+## Dependencies
+
+```text
+T001 ──> T005(常量)
+T002 ──> T003(US1 冒烟)          [US1 与 US2/US3 并行]
+T004 ──> T005/T006(提示 key)
+T005,T006 ──> T007(US2 冒烟)
+T008,T009 ──> T010                 [US3 与 US2 并行]
+T007,T010 ──> T011 ──> T012
+```
+
+## MVP
+
+US2(T001+T004+T005+T006):确认收款 + 骑手门禁是资金闸门核心;US1 银行信息接口紧随其后(App 展示依赖),US3 标签纯展示可最后。

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff