Browse Source

修复LINE Pay无扫码记录时状态查询报错

qmj 1 ngày trước cách đây
mục cha
commit
a3ef8ce3a9

+ 9 - 2
ruoyi-admin/src/main/java/com/ruoyi/app/pay/LinePayOfflineService.java

@@ -169,8 +169,15 @@ public class LinePayOfflineService {
                                 Comparator.nullsFirst(Date::compareTo))
                         .thenComparing(PosOrderLinePayment::getId,
                                 Comparator.nullsFirst(Long::compareTo)))
-                .orElseThrow(() -> new ServiceException(
-                        MessageUtils.message("line.pay.offline.attempt.not.found")));
+                .orElse(null);
+        if (selected == null) {
+            // 详情页可在首次扫码前查状态;无 Offline 流水是正常查询结果,不创建流水或调用网关。
+            // NOT_STARTED 只表示尚无本地扫码记录,不代表整单未付款,也不能据此自动重发扣款。
+            LinePayOfflineResult notStarted = new LinePayOfflineResult();
+            notStarted.setDdId(String.valueOf(order.getDdId()));
+            notStarted.setStatus("NOT_STARTED");
+            return notStarted;
+        }
         return result(selected, null);
     }
 

+ 1 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/pay/dto/LinePayOfflineResult.java

@@ -10,6 +10,7 @@ public class LinePayOfflineResult {
     private Long paymentId;
     private String lineOrderId;
     private String transactionId;
+    /** NOT_STARTED 表示尚无 Offline 流水,仅用于状态查询;其他值沿用付款结果。 */
     private String status;
     private Boolean reusedAttempt;
     private Date updatedAt;

+ 1 - 2
ruoyi-admin/src/test/java/com/ruoyi/app/pay/LinePayOfflineMerchantAccessTest.java

@@ -109,8 +109,7 @@ class LinePayOfflineMerchantAccessTest {
     @Test
     void authorizedOrderWithoutAttemptReportsNotStartedInsteadOfForbidden() {
         when(payments.getByDdId(order.getDdId())).thenReturn(List.of());
-        assertEquals("line.pay.offline.attempt.not.found",
-                assertThrows(ServiceException.class, () -> service.status(936L, order.getDdId())).getMessage());
+        assertEquals("NOT_STARTED", service.status(936L, order.getDdId()).getStatus());
         verifyNoInteractions(client);
     }
 

+ 41 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/pay/LinePayOfflineServiceTest.java

@@ -69,6 +69,47 @@ class LinePayOfflineServiceTest {
                 eq("TWD"), eq("OFFLINE"), any())).thenReturn(intent());
     }
 
+    @Test
+    void noOfflineAttemptIsNormalNotStartedWithoutGatewayOrWrites() {
+        when(payments.getByDdId("DD-100")).thenReturn(java.util.List.of());
+        try (var messages = mockStatic(MessageUtils.class, call -> call.getArgument(0))) {
+            LinePayOfflineResult result = service.status(101L, " DD-100 ");
+            assertEquals("DD-100", result.getDdId());
+            assertEquals("NOT_STARTED", result.getStatus());
+            org.junit.jupiter.api.Assertions.assertNull(result.getPaymentId());
+            org.junit.jupiter.api.Assertions.assertNull(result.getTransactionId());
+            org.mockito.Mockito.verifyNoInteractions(client, facts, credentials);
+            verify(payments).getByDdId("DD-100");
+            org.mockito.Mockito.verifyNoMoreInteractions(payments);
+        }
+    }
+
+    @Test
+    void onlineAttemptIsNotExposedAsOfflinePayment() {
+        PosOrderLinePayment online = intent();
+        online.setPaymentMode("ONLINE");
+        online.setStatus("PAID");
+        when(payments.getByDdId("DD-100")).thenReturn(java.util.List.of(online));
+        try (var messages = mockStatic(MessageUtils.class, call -> call.getArgument(0))) {
+            LinePayOfflineResult result = service.status(101L, "DD-100");
+            assertEquals("NOT_STARTED", result.getStatus());
+            org.junit.jupiter.api.Assertions.assertNull(result.getLineOrderId());
+            org.mockito.Mockito.verifyNoInteractions(client, facts, credentials);
+        }
+    }
+
+    @Test
+    void existingOfflineAttemptStillReturnsItsActualStatus() {
+        PosOrderLinePayment offline = intent();
+        offline.setPaymentMode("OFFLINE");
+        offline.setStatus("REQUEST_UNKNOWN");
+        when(payments.getByDdId("DD-100")).thenReturn(java.util.List.of(offline));
+        LinePayOfflineResult result = service.status(101L, "DD-100");
+        assertEquals("PROCESSING", result.getStatus());
+        assertEquals(offline.getId(), result.getPaymentId());
+        org.mockito.Mockito.verifyNoInteractions(client, facts, credentials);
+    }
+
     @Test
     void successfulPayRequiresStrictFactsAndPersistsProvider() throws Exception {
         JSONObject info = new JSONObject();

+ 2 - 1
specs/019-line-pay/contracts/api.md

@@ -119,9 +119,10 @@ Annotations: `@Anonymous`, `@Auth`, `@RequestHeader String token`, `@RequestPara
 
 该接口验证 `MERCHANT` 来源和当前商家归属后只读本地状态,不在请求线程调用 LINE。Success `data` 与付款接口相同,但 `reusedAttempt` 可省略。规范状态:
 
-若订单尚无 Offline 尝试(包括只有 Online 历史/活跃尝试),返回业务错误 `line.pay.offline.attempt.not.found`,不得回传 Online 支付上下文。
+若订单尚无 Offline 尝试(包括只有 Online 历史/活跃尝试),返回 `code=200`、`data.status=NOT_STARTED` 和 `ddId`,支付流水字段为空;不得回传 Online 支付上下文,也不表示整单未支付或可自动重扫。
 
 ```text
+NOT_STARTED    尚无本地 Offline 尝试;刷新订单后由商家明确操作,禁止自动重发付款
 AUTH_REQUIRED  1145/1169/AUTH_READY/WAITING_AUTH,客户仍需在 LINE Pay 操作
 PROCESSING     Pay 结果未知或重复待核实,后端只读查单中
 PAID           已严格核实订单号、交易号、金额和 paymentProvider

+ 11 - 0
specs/019-line-pay/merchant-offline-app-integration.md

@@ -130,6 +130,7 @@ Header: token
 
 | `status` | 含义 | 前端操作 |
 |---|---|---|
+| `NOT_STARTED` | 查询时尚无本地扫码付款记录 | 不显示服务器错误;刷新订单及付款方式,禁止自动重发付款或自动重扫 |
 | `AUTH_REQUIRED` | 客户需要在 LINE Pay 完成选择付款方式或认证 | 提示客户在 LINE Pay 完成操作,继续查询状态,不允许重新扫码 |
 | `PROCESSING` | 后端正在确认付款结果 | 显示“支付结果确认中”,继续查询状态,不允许重新扫码 |
 | `PAID` | 付款成功 | 停止查询,提示商家收款成功,刷新订单 |
@@ -224,3 +225,13 @@ Sandbox 环境不能使用客户真实 LINE App 生成的 My Code。
 | 商家创建订单 | POST | `/system/orderShOprate/createOrder` |
 | 提交扫码付款 | POST | `/system/orderShOprate/linePay/offline/pay` |
 | 查询付款状态 | GET | `/system/orderShOprate/linePay/offline/status?ddId={ddId}` |
+
+## 2026-11-04 未扫码状态兼容
+
+`GET /offline/status` 在授权通过但没有 Offline 流水时,改为返回:
+
+```json
+{"code":200,"msg":"操作成功","data":{"ddId":"991793699529652","status":"NOT_STARTED"}}
+```
+
+其他支付流水字段为空或省略。NOT_STARTED只说明查询时尚无本地扫码记录,即使存在Online付款也不会在此接口返回Online流水。不能因此把订单标记未付款/失败,更不能自动重发`/offline/pay`。初次打开页面可显示尚未扫码;如果之前付款请求超时,应先刷新订单和状态,避免原请求仍在处理时直接重新扫码。实际付款仍由服务端支付锁、订单支付状态和活跃流水再次校验。

+ 8 - 0
specs/019-line-pay/plan.md

@@ -248,3 +248,11 @@ foodie-admin-vue/
 1. 使用真实 MerchantStoreAccessService 和模拟 Mapper 构建回归,覆盖查询、付款复用、无流水、撤权/停用/门店转移及用户单拒绝;先证明旧实现拒绝有效子账号。
 2. 在 LinePayOfflineService 共用归属门禁中注入现有授权服务,增加子账号分支,以实时授权集合、主账号及订单门店归属校验;保持其他用户类型原逻辑。
 3. 运行 JDK21 定向权限、Offline 和商家订单回归及模块构建,检查增量差异。只读线上复现与数据库取证不执行付款、数据库写入或部署;发布后需复测现场接口。
+
+## 2026-11-04 无扫码记录的状态查询
+
+只读核对订单991793699529652:MERCHANT、堂食、payType=3、payStatus=0,查询时无LINE Pay流水。当前报错源于status的orElseThrow,与已修复的分管账号权限不同。
+
+1. 在现有服务/权限测试增加无流水、仅Online流水和已有Offline流水场景,先复现报错。
+2. 仅修改status无记录分支返回NOT_STARTED;保留鉴权、对账异常与支付入口,添加注释和DTO说明。
+3. 同步规格、API及App接入说明,执行JDK21 Offline服务/权限/持久化/控制器测试及模块构建。不执行支付、数据库变更或部署。

+ 8 - 1
specs/019-line-pay/spec.md

@@ -670,5 +670,12 @@ Header: token
 - 商家分管账号(userType=5)的 Offline 付款及状态查询,必须通过022现有门店授权服务解析实时权限;不能使用可为空的 InfoUser.storeId 判断归属。
 - 有效分管账号仅可访问已授权且仍属于有效主账号的门店订单;订单 shId、门店 userId 须与解析出的主账号一致。撤权、分管账号/主账号停用、门店删除或转移后拒绝付款及状态读取。
 - 保留普通商家、夜市商家和摊主原归属规则,以及 MERCHANT 来源、单门店、付款资格和重复支付保护。
-- 授权校验通过但不存在 OFFLINE 支付流水时,沿用 line.pay.offline.attempt.not.found;不得推断为已支付或为了查询创建支付流水。
+- 授权校验通过但不存在 OFFLINE 支付流水时,按 2026-11-04 修正规则返回正常 NOT_STARTED 状态;不得推断为已支付或为了查询创建支付流水。
 - 验收:有效分管账号 storeId 为空时可查已存在的 OFFLINE 状态、可复用处理中流水;无流水返回明确的未发起提示;权限失效时在访问支付流水或网关前拒绝。
+
+## 2026-11-04 Offline 无流水状态查询修复
+
+- 授权商家可在扫码前查询状态。尚无 OFFLINE 流水(包括仅有 ONLINE 流水)时返回成功包装、`ddId`、`status=NOT_STARTED`,`paymentId/lineOrderId/transactionId/updatedAt/reusedAttempt` 为空或省略。
+- NOT_STARTED 不是支付失败、已支付或允许重扫的凭证;只表达查询时刻未查到本地扫码记录,不覆盖订单本身及 Online 的支付事实。
+- 原有来源、门店归属、分管账号实时授权与单店校验保持;无权限仍拒绝。已有 Offline 流水沿用原状态映射。对账任务按 paymentId 找不到记录仍保留异常。
+- 查询只读,不调用支付网关、不新增支付流水、不修改订单。App 不得因 NOT_STARTED 自动重发付款;需刷新订单并由商家明确操作,付款入口继续执行锁及活跃流水防重。

+ 8 - 0
specs/019-line-pay/tasks.md

@@ -196,3 +196,11 @@
 - 日志:`ruoyi-admin/target/linepay-subaccount-red.log`、`ruoyi-admin/target/linepay-subaccount-green.log`。差异检查通过,生产文件保留CRLF;未改动此前010发票和039小票文档的工作区修改。
 - 现场仅GET查询与只读事务取证;未执行付款、退款、数据库写入或迁移。未运行全项目回归、真实网关付款和发布后App联调;上述验证阶段未提交或部署;后续提交信息以 Git 记录为准,发布后仍需复测。
 - 现场订单查询时尚无LINE Pay流水,发布后权限校验通过仍应提示尚未发起扫码付款,不能把此状态当成PAID。
+
+## 2026-11-04 无扫码流水查询
+
+- [x] T069 只读核对现场订单;回归37项中3项因无记录报错,复现问题。
+- [x] T070 status无Offline流水返回NOT_STARTED,保留权限和原支付状态语义;同步API和App文档。
+- [x] T071 执行定向测试、模块构建及差异检查,记录未执行的网关/App联调。
+
+验证记录:JDK21执行 `mvn -q -pl ruoyi-admin -am -Dtest=LinePayOffline*Test,LinePayOrderGuardTest -Dsurefire.failIfNoSpecifiedTests=false package` 成功,42项测试,0失败/错误/跳过,模块已打包;`git diff --check`通过。日志保存在忽略目录target/line-offline-empty-red.log及line-offline-empty-green.log。未执行真实网关扣款、App联调或部署;无需SQL。提交和推送状态以Git记录为准。