# 骑手闪送 LINE Pay Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans for native execution, or superpowers:subagent-driven-development only if the user explicitly chooses delegation. Steps use checkbox syntax. Do not implement before this written plan is reviewed. **Goal:** 用户在骑手接单后通过该骑手的 LINE Pay 付款,支付成功才可取件,取件前取消全额退款,任意站已取后禁止取消。 **Architecture:** 骑手凭证、闪送支付尝试与退款分别持久化,不复用外卖流水关联。复用 LINE Pay v4 客户端;网络副作用采用持久化意图、独立外部请求及条件更新,订单行锁仅保护短事务。闪送入口接入独立支付服务,保持普通外卖及现金/转账收款流程。 **Tech Stack:** JDK 21、Spring Boot、MyBatis/MyBatis-Plus、MySQL、Redisson;Vue 2、Element UI、四语 i18n。 **Spec:** [rider-flash-online-spec.md](rider-flash-online-spec.md),原 LINE Pay 规格第 17 节及闪送 FR-111 至 FR-115。 ## Global Constraints - 当前 `test-202609v2` 原地实施;不创建或切换分支/工作树。不自动提交或推送,等待用户指令。 - 不修改 App、云函数、普通外卖资金流、OMG、废弃支付代码、钱包或提现;App 只提供接入文档。 - `ruoyi-admin -> ruoyi-system`;外部 HTTP 编排在 admin,持久化与闸门在 system。Controller 明确 DTO、请求注解及权限,不用 Map 入参或 Bean Validation。 - 仅登记 SQL 至 `updatesql/sql.md`,包括表、索引、字段说明和菜单权限;不执行数据库迁移或回写历史资金事实。 - 密钥不写日志/审计、测试样例或文档;平台详情授权策略沿用门店配置,列表不返回 Secret。测试只用明确假的凭证,不使用用户提供的数据库密码或 token。 - 保留 CRLF/编码及原有脏文件。关键支付副作用、凭证版本、取消/取件竞态写原因型注释;平台文字四语,后端补齐现有语言资源。 - 未验证的测试、真实 MySQL 并发、Sandbox 和部署不宣称通过;进度使用未开始/实现中/已提交/已验证。 ## Review Focus 1. 同时支付、取消或取件:共享订单行锁和固定锁序,取件后绝不能取消;任务 4 测试部分取件与竞争失败路径。 2. 请求或 Confirm 已发送但进程崩溃:活跃尝试与租约保留,恢复只读核实,不重复扣款;任务 2/3 测试失联与租约过期。 3. 凭证轮换/禁用以及环境不一致:新支付拒绝,历史查询退款固定原版本;任务 1/3 测试原版本归属与历史退款。 4. 回跳伪造、越权和交易号精度:字符串保存 LINE 交易号,校验已保存的绑定;任务 2 测试错误绑定、他人订单和浏览器假成功。 5. 旧订单缺站点或计数不一致:PICKED_UP/DELIVERED 直接禁止取消,站点事实优先;任务 4 测试旧数据与平台资格失效。 ## 固定契约与文件职责 - 新表 `info_rider_line_pay`:不可变凭证版本;可空唯一 `current_rider_id` 保证一个当前版本。环境来自服务器,界面只读;轮换时只变更当前标记,不覆盖旧密钥。新支付要求用户类型 2、账号有效及当前凭证启用且环境匹配。 - 新表 `flash_delivery_line_payment`:订单 ID、riderId、credentialId、服务端金额/TWD、唯一 lineOrderId/transactionId、可空唯一 activeOrderId、状态、版本、租约及下次核实/截止时间。订单 1:N 尝试;未知结果阻断新尝试。 - 新表 `flash_delivery_line_refund`:paymentId 唯一、全额金额、状态、网关退款交易号、版本、租约及恢复时间;支付 PAID 事实不被退款覆盖。 - 支付状态 `CREATED/REQUESTING/WAITING_AUTH/CONFIRMING/REQUEST_UNKNOWN/CONFIRM_UNKNOWN/PAID/FAILED/CANCELLED/EXPIRED/MANUAL_REVIEW`;只有网关明确证明的终止尝试可释放活跃键,浏览器取消不是证据。 - 退款状态 `CREATED/PROCESSING/UNKNOWN/REFUNDED/FAILED/MANUAL_REVIEW`;UNKNOWN 与过期 PROCESSING 只读核实,不盲重发退款。不能证明结果时进入人工核对,不伪造成功。 - 闪送原 `paymentStatus`:现金/转账保持 0/1;LINE Pay 0=未核实到账、1=已支付、2=已全额退款。SQL 修改列说明,不根据旧收款标记伪造 LINE 支付流水;历史 payType=3 无可信流水时禁止取件并提示核对。 - `FlashLinePayPaymentView`:`Long orderId/paymentId`、`String status/refundStatus`、`String paymentUrl`;无尝试返回 status=NOT_STARTED,refundStatus 无退款时为空。paymentUrl 只返回寄件付款人,骑手查询仅返回状态;敏感流水/密钥不进 App 响应。 - 新 App API:`POST /system/flashDelivery/orders/{id}/linePay/create`(明确空业务 DTO,不接收金额);`GET /system/flashDelivery/orders/{id}/linePay/status`。仅寄件人发起付款;寄件人及本单骑手可读取状态,不返回其他人订单。 - 新匿名回跳:`GET /pay/line/flash/confirm` 与 `/cancel`,显式 orderId(LINE 订单号)、transactionId 字符串参数;仅定位已绑定尝试并触发核实,不信任浏览器支付结果。App 回链沿用既有 payment/result,加 `orderSource=FLASH`,orderId 为闪送 ID,文档要求前端重新查询。 - `LinePayClient.request(credential, request, confirmUrl, cancelUrl)` 重载允许服务端指定闪送回跳;原双参数签名继续使用原配置。新增 `line-pay.flash-confirm-url/flash-cancel-url` 部署配置,无有效 HTTPS 回跳时阻止新闪送支付,不改变外卖配置。 - 官方契约依据:[Online API v4](https://developers-pay.line.me/online-api-v4)、[付款请求](https://developers-pay.line.me/online-api-v4/request-payment)、[退款](https://developers-pay.line.me/online/handle-refund)。使用 Request/Check/Confirm/Retrieve/Refund,不新增 Capture/Void。 ## Task 1(T061):平台骑手凭证闭环 **Files:** 新增 system 下 `domain/InfoRiderLinePay.java`、`domain/dto/RiderLinePayCredentialDto.java`、`domain/dto/RiderLinePayToggleDto.java`、`domain/vo/InfoRiderLinePayVo.java`、`mapper/InfoRiderLinePayMapper.java`、`resources/mapper/infouser/InfoRiderLinePayMapper.xml`、`service/IInfoRiderLinePayService.java`、`service/impl/InfoRiderLinePayServiceImpl.java`;admin 下新增 `com/ruoyi/app/pay/RiderLinePayController.java`。平台新增 `src/api/infouser/riderLinePay.js`、`src/views/infouser/riderPayment/index.vue`、`tests/rider-line-pay.test.cjs`;迁移与菜单登记至 updatesql/sql.md,四个 src/api/language 文件新增同名 key。 **Interfaces:** `getCurrent(Long riderId)`、`getEnabledCurrent(Long riderId)`、`getById(Long credentialId)` 返回 InfoRiderLinePay;`saveVerifiedCredential(RiderLinePayCredentialDto dto, String environment, String returnCode, String returnMessage)` 返回版本;`setCurrentEnabled(Long riderId, boolean enabled)` 返回 int。列表/详情沿用门店分页 VO 模式。平台路径 `/system/riderLinePay`,list/detail/saveCredentials/toggleEnable 分别采用 GET/GET/PUT/PUT;权限 `infouser:riderLinePay:list/query/saveCredentials/toggleEnable`。 - [x] 编写 `InfoRiderLinePayServiceTest` 与 `RiderLinePayControllerTest`:`nonRiderRejectedBeforeProbe` 验证非骑手 0 网关调用;`invalidProbePreservesCurrent` 验证原版本不变;`rotationKeepsOldVersion` 验证旧凭证可查;`listDoesNotExposeSecret` 验证列表无 Secret;`environmentMismatchNotEnabled` 验证环境错配不可新收款。 - [x] 运行上述定向测试确认新增行为未实现导致失败;记录预期业务失败,不能把缺依赖/语法错误当红测。 - [x] 一批实现模型、XML、服务、控制器、手工 SQL、页面及四语。先校验骑手再只读随机订单 probe;probe 1150 或 0000 且空 info 数组通过,其余失败/未知不替换原配置。保存/启用共用骑手行锁,唯一键兜底。保存接口禁用请求/响应操作日志原文,避免 Secret 进入 @Log 审计。 - [x] 重跑定向测试及 `node --test tests/rider-line-pay.test.cjs`,核对配置列表、保存、禁用、权限按钮及环境只读;保留可独立演示的平台配置闭环,暂不开放真实闪送支付。 ## Task 2(T062):接单后支付、回跳与支付事实 **Files:** 新增 system `domain/flash/FlashDeliveryLinePayment.java`、`mapper/flash/FlashDeliveryLinePaymentMapper.java`、`resources/mapper/flash/FlashDeliveryLinePaymentMapper.xml`;admin `com/ruoyi/app/flashdelivery/payment/FlashLinePayService.java`、`FlashLinePayFactService.java`、`FlashLinePayController.java`、`FlashLinePayReturnController.java`、`FlashLinePayReturnPageRenderer.java`,DTO `FlashLinePayCreateRequest.java`、`FlashLinePayPaymentView.java`。修改 LinePayClient.java、LinePayProperties.java,应用非敏感配置、updatesql/sql.md 及 i18n。 **Interfaces:** `FlashLinePayService.create(Long userId, Long orderId)`、`query(Long callerId, Long orderId)` 返回 FlashLinePayPaymentView;`reconcile(Long paymentId, String source)` 返回 void;`findByLineOrderId(String lineOrderId, String transactionId)` 返回持久化尝试。`FlashLinePayFactService.applyPaidFact(Long paymentId, String transactionId, long paidAmount, Date paidAt)` 返回 boolean,事务内先锁订单再更新尝试与 paymentStatus。 - [x] 编写 `FlashLinePayServiceTest`、`FlashLinePayControllerTest`、`FlashLinePayFactServiceTest` 和 LinePayClientTest 增量:`unacceptedOrderCannotPay`、`crossUserCannotPay`、`serverAmountAndRiderAreUsed`、`serverAmountOverflowRejected`、`duplicateCreateUsesExistingAttempt`、`requestTimeoutDoesNotCreateSecondAttempt`、`confirmTimeoutOnlyRetrieves`、`forgedReturnCannotMarkPaid`、`transactionIdRetainsExactDigits`、`riderQueryDoesNotExposePaymentUrl`;断言未经核实 paymentStatus 不为 1,客户端金额/骑手不能覆盖后端,金额溢出时无网关调用,重复网关 Request/Confirm 至多一次。 - [x] 运行定向测试证明新增契约失败,再实现持久化意图→事务外网络→CAS 结果。金额用 long 精确转 SDK int,必须正整数 TWD,溢出拒绝;生成唯一闪送 lineOrderId 并绑定骑手/凭证版本。异常事实保留人工核对,不放行取件。 - [x] 实现服务端 Check 判断认证状态后 Confirm,超时只 Retrieve;校验订单号、交易号、payInfo 金额合计及网关明确的支付事实,币种以已保存请求为依据,响应提供币种时额外核对。不要求响应具有官方未保证字段。事实落库不能使取消订单恢复 ACCEPTED。 - [x] 实现专属回跳 URL 与安全返回页,App 状态查询触发/返回恢复结果;浏览器 cancel 不取消履约订单。重跑定向测试及原 LinePayClientTest/LinePayControllerTest,证明普通外卖原回跳不变。 ## Task 3(T063):取消全额退款与持久恢复 **Files:** 新增 system `domain/flash/FlashDeliveryLineRefund.java`、`mapper/flash/FlashDeliveryLineRefundMapper.java`、`resources/mapper/flash/FlashDeliveryLineRefundMapper.xml`;admin `com/ruoyi/app/flashdelivery/payment/FlashLinePayRefundService.java`、`com/ruoyi/app/task/FlashLinePayReconcileTask.java`。修改任务 2 的支付事实服务及 updatesql/sql.md。 **Interfaces:** `FlashLinePayFactService.ensureCancellationRefundIntent(Long orderId)` 返回 void,在订单锁内创建唯一退款意图或登记未决付款补偿;`FlashLinePayRefundService.reconcile(Long refundId, String source)` 返回 void。定时任务 `reconcile()` 分批处理支付与退款,使用独立 Redisson 任务锁和行租约,沿用既有 LINE 恢复批量/租约配置,不更改外卖任务。 - [x] 编写 `FlashLinePayRefundServiceTest`、`FlashLinePayReconcileTaskTest`:`paidCancellationCreatesOneFullRefund` 验证 paymentId 唯一;`latePaymentOnCancelledOrderCreatesRefund` 验证取消不恢复、真实扣款自动补偿;`refundTimeoutOnlyRetrieves` 验证 UNKNOWN 不二次 Refund;`rotatedCredentialRefundUsesOriginalVersion` 验证原密钥;`expiredLeaseRequiresReadOnlyRecovery` 验证崩溃后不重扣;`refundFactDoesNotErasePaidFact` 验证支付仍 PAID,订单收款状态仅退款证实后为 2。 - [x] 运行上述红测后,一批实现退款记录、事先只读证据核实、Full Refund 及成功事实。取消前未知支付保留活跃阻断,恢复查询若发现付款即补偿;本地退款意图与取消原子提交,不依赖仅内存的提交后回调。 - [x] 请求失联/响应不明确只查询退款证据;金额、交易号或原凭证不匹配进入人工核对。取消先完成不得再发起新 Confirm;在途 Confirm 只读恢复真实扣款。任务不得把截止时间达到当作未扣款证据,不能仅本地超时释放活跃键。 - [x] 重跑退款、事实及任务测试;核对任务异常单笔隔离、恢复有界、所有资金调用在数据库事务外,不对 TestTask 作修改。 ## Task 4(T064):闪送接单、取件与取消统一门禁 **Files:** 修改 system PaymentMethodGateService.java、domain/flash/FlashDeliveryOrder.java、mapper/flash/FlashDeliveryOrderMapper.java/XML;admin FlashDeliveryApplicationService.java、FlashDeliveryStateMachine.java、DTO FlashDeliveryAdminOrderDetailView.java/FlashDeliveryOrderView.java。平台修改 src/views/flashDelivery/shared.js、orders/index.vue、orders/OrderDetail.vue、multiPickup.js 及相关四语/测试。 **Interfaces:** 闸门 RIDER_FLASH 在 payeeUserId 非空时检查任务 1 凭证,未知骑手仅服从平台开关;保留现有支付选择。新增 `FlashLinePayFactService.assertPickupPaid(FlashDeliveryOrder order)` 返回 void,只验证可信 PAID 流水并无退款阻断,不仅检查 paymentStatus。平台列表订单返回只读非持久化 `Boolean canAdminCancel`,详情同名字段;资格计算复用短方法 `canCancelBeforePickup(FlashDeliveryOrder order, List stops)`。 - [x] 在 FlashDeliveryApplicationServiceTest、FlashDeliveryStateMachineTest、PaymentMethodGateServiceTest 编写 `partiallyPickedOrderCannotAdminCancel`、`legacyPickedStatusCannotCancel`、`noStationFactsCannotDefaultToCancellable`、`linePayWithoutVerifiedPaymentCannotPickupAnyStop`、`manualReceiptCannotMarkLinePayPaid`、`riderWithoutCredentialCannotListOrAccept`、`cancelAndPickupConditionFailureDoesNotApplySideEffects`;断言取消拒绝无写入、任一取件入口不绕过付款、现金/转账原收款流程不变。 - [x] 运行定向红测后收紧 canAdminCancel 仅 WAITING_ACCEPTANCE/ACCEPTED;行锁内检查 anyPicked 最新站点,历史状态和异常缺站点安全拒绝。已取物品处置旧数据仍只读展示,取消表单不再出现取件后处置入口,保留未取件取消原因/问题站/处理结果。取消成功事务内调用任务 3 意图创建;真实网络退款交恢复执行。 - [x] 平台列表批量读取站点事实生成 canAdminCancel,避免逐单查询;详情按相同规则生成。按钮只在字段严格为 true 且权限匹配时出现,缺字段安全隐藏;弹窗读取详情重新确认,后端最终重校验。页面显示支付与退款状态但不能手工伪造 LINE 收款。 - [x] 重跑上述测试及平台 flash-delivery/flash-multi-pickup 测试,新增 `acceptedButPartlyPickedHidesCancel`、`missingEligibilityHidesCancel`、`staleDialogCannotSubmitCancel`、`allUnpickedCanCancel` 断言;新增 API状态字段/注释同步 Entity、XML、DTO、Controller、i18n 与 SQL 列说明。 ## Task 5(T065):接入文档、统一验证及交付 **Files:** 新增 docs/flash-delivery-line-pay-app-integration.md;更新 specs/019-line-pay/spec.md、plan.md、tasks.md、quickstart.md、本增量规格/计划;更新 specs/024-flash-delivery 与 specs/031-pay-method-config 的 spec.md/plan.md/tasks.md 兼容说明。新增/更新测试保持任务 1 至 4 的实际类名,不另建重复测试框架。 - [x] 接入文档给出 payType=3 下单→接单→create→LINE认证→服务端核实→取件;列明精确路径、token、DTO、状态字段、回链 orderSource=FLASH 与查询流程。说明失败/未知不能重复付款、退款不等于取消、密钥仅服务端保存、历史线上单须核对;请求示例使用虚构信息,不写实际密钥。 - [x] 在 PowerShell 当前命令环境临时设置 JAVA_HOME 为 `C:/Users/99029/.jdks/graalvm-jdk-21.0.12+7.1` 并将 bin 加入 PATH;先验证 java -version。每任务定向命令统一采用 `mvn -pl ruoyi-admin -am '-Dtest=<该任务全部测试类名,逗号分隔>' -Dsurefire.failIfNoSpecifiedTests=false test`,system 类也随 reactor 执行,不改全局环境。 - [x] 最终执行 `mvn -pl ruoyi-admin -am test` 和 `mvn -pl ruoyi-admin -am -DskipTests package`,要求 BUILD SUCCESS、新鲜报告 0 失败/错误;记录实际项数,不沿用历史数字。平台目录执行 `node --test tests/rider-line-pay.test.cjs tests/flash-delivery.test.cjs tests/flash-multi-pickup.test.cjs`、改动 Vue/脚本及四语解析检查;有依赖时执行 npm run build:stage,依赖缺失则明确未执行,不擅自安装升级。 - [x] 检查 diff、范围、CRLF/编码、Controller签名、模块依赖、SQL仅登记、敏感值脱敏及原因型注释。真实 MySQL多连接竞争、手工迁移、LINE Sandbox商户、浏览器/App联调及生产开通列为独立未验证项,由用户提供环境后执行,不在本轮偷偷调用真实资金接口。 - [x] 按实际结果更新进度;仅用户要求 commit push 时暂存本任务文件并核对清单,报告 SHA/推送分支/验证结果。工作区原有修改或同时发生的其他功能不混入提交。 ## 依赖与审阅结论 T061 → T062 → T063 → T064 → T065,采用一次原地实施闭环,不将未完成的接单后付款路径视为已交付;计划未要求创建 Agent。主流程、凭证、App边界及注释覆盖 US-R1 至 US-R4;五项 Review Focus 均对应定向测试。 执行方式已确认:本会话原地执行,不创建分支,不自动提交。T061–T065 本地实现及验证已收口,仍未提交/推送。后端全量 1140 项、平台 16 项及两端构建通过,最终独立只读审查 5 项 Important 均用失败→通过回归修复;没有启动实现分工或重复审查。实际结果见 tasks.md;不执行数据库迁移或真实资金调用,真实 MySQL 竞争及 App 联调另行验收。