rider-flash-online-plan.md 18 KB

骑手闪送 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,原 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、付款请求、退款。使用 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。

  • 编写 InfoRiderLinePayServiceTest 与 RiderLinePayControllerTest:nonRiderRejectedBeforeProbe 验证非骑手 0 网关调用;invalidProbePreservesCurrent 验证原版本不变;rotationKeepsOldVersion 验证旧凭证可查;listDoesNotExposeSecret 验证列表无 Secret;environmentMismatchNotEnabled 验证环境错配不可新收款。
  • 运行上述定向测试确认新增行为未实现导致失败;记录预期业务失败,不能把缺依赖/语法错误当红测。
  • 一批实现模型、XML、服务、控制器、手工 SQL、页面及四语。先校验骑手再只读随机订单 probe;probe 1150 或 0000 且空 info 数组通过,其余失败/未知不替换原配置。保存/启用共用骑手行锁,唯一键兜底。保存接口禁用请求/响应操作日志原文,避免 Secret 进入 @Log 审计。
  • 重跑定向测试及 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。

  • 编写 FlashLinePayServiceTest、FlashLinePayControllerTest、FlashLinePayFactServiceTest 和 LinePayClientTest 增量:unacceptedOrderCannotPay、crossUserCannotPay、serverAmountAndRiderAreUsed、serverAmountOverflowRejected、duplicateCreateUsesExistingAttempt、requestTimeoutDoesNotCreateSecondAttempt、confirmTimeoutOnlyRetrieves、forgedReturnCannotMarkPaid、transactionIdRetainsExactDigits、riderQueryDoesNotExposePaymentUrl;断言未经核实 paymentStatus 不为 1,客户端金额/骑手不能覆盖后端,金额溢出时无网关调用,重复网关 Request/Confirm 至多一次。
  • 运行定向测试证明新增契约失败,再实现持久化意图→事务外网络→CAS 结果。金额用 long 精确转 SDK int,必须正整数 TWD,溢出拒绝;生成唯一闪送 lineOrderId 并绑定骑手/凭证版本。异常事实保留人工核对,不放行取件。
  • 实现服务端 Check 判断认证状态后 Confirm,超时只 Retrieve;校验订单号、交易号、payInfo 金额合计及网关明确的支付事实,币种以已保存请求为依据,响应提供币种时额外核对。不要求响应具有官方未保证字段。事实落库不能使取消订单恢复 ACCEPTED。
  • 实现专属回跳 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 恢复批量/租约配置,不更改外卖任务。

  • 编写 FlashLinePayRefundServiceTest、FlashLinePayReconcileTaskTest:paidCancellationCreatesOneFullRefund 验证 paymentId 唯一;latePaymentOnCancelledOrderCreatesRefund 验证取消不恢复、真实扣款自动补偿;refundTimeoutOnlyRetrieves 验证 UNKNOWN 不二次 Refund;rotatedCredentialRefundUsesOriginalVersion 验证原密钥;expiredLeaseRequiresReadOnlyRecovery 验证崩溃后不重扣;refundFactDoesNotErasePaidFact 验证支付仍 PAID,订单收款状态仅退款证实后为 2。
  • 运行上述红测后,一批实现退款记录、事先只读证据核实、Full Refund 及成功事实。取消前未知支付保留活跃阻断,恢复查询若发现付款即补偿;本地退款意图与取消原子提交,不依赖仅内存的提交后回调。
  • 请求失联/响应不明确只查询退款证据;金额、交易号或原凭证不匹配进入人工核对。取消先完成不得再发起新 Confirm;在途 Confirm 只读恢复真实扣款。任务不得把截止时间达到当作未扣款证据,不能仅本地超时释放活跃键。
  • 重跑退款、事实及任务测试;核对任务异常单笔隔离、恢复有界、所有资金调用在数据库事务外,不对 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<FlashDeliveryOrderStop> stops)。

  • 在 FlashDeliveryApplicationServiceTest、FlashDeliveryStateMachineTest、PaymentMethodGateServiceTest 编写 partiallyPickedOrderCannotAdminCancel、legacyPickedStatusCannotCancel、noStationFactsCannotDefaultToCancellable、linePayWithoutVerifiedPaymentCannotPickupAnyStop、manualReceiptCannotMarkLinePayPaid、riderWithoutCredentialCannotListOrAccept、cancelAndPickupConditionFailureDoesNotApplySideEffects;断言取消拒绝无写入、任一取件入口不绕过付款、现金/转账原收款流程不变。
  • 运行定向红测后收紧 canAdminCancel 仅 WAITING_ACCEPTANCE/ACCEPTED;行锁内检查 anyPicked 最新站点,历史状态和异常缺站点安全拒绝。已取物品处置旧数据仍只读展示,取消表单不再出现取件后处置入口,保留未取件取消原因/问题站/处理结果。取消成功事务内调用任务 3 意图创建;真实网络退款交恢复执行。
  • 平台列表批量读取站点事实生成 canAdminCancel,避免逐单查询;详情按相同规则生成。按钮只在字段严格为 true 且权限匹配时出现,缺字段安全隐藏;弹窗读取详情重新确认,后端最终重校验。页面显示支付与退款状态但不能手工伪造 LINE 收款。
  • 重跑上述测试及平台 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 的实际类名,不另建重复测试框架。

  • 接入文档给出 payType=3 下单→接单→create→LINE认证→服务端核实→取件;列明精确路径、token、DTO、状态字段、回链 orderSource=FLASH 与查询流程。说明失败/未知不能重复付款、退款不等于取消、密钥仅服务端保存、历史线上单须核对;请求示例使用虚构信息,不写实际密钥。
  • 在 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 执行,不改全局环境。
  • 最终执行 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,依赖缺失则明确未执行,不擅自安装升级。
  • 检查 diff、范围、CRLF/编码、Controller签名、模块依赖、SQL仅登记、敏感值脱敏及原因型注释。真实 MySQL多连接竞争、手工迁移、LINE Sandbox商户、浏览器/App联调及生产开通列为独立未验证项,由用户提供环境后执行,不在本轮偷偷调用真实资金接口。
  • 按实际结果更新进度;仅用户要求 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 联调另行验收。