# LINE Pay HTTP Contracts 所有 JSON 业务响应沿用项目 `AjaxResult` 外壳。交易号字段始终是 JSON string。业务错误文本从 `MessageUtils.message(...)` 获取。 ## 1. App API ### `POST /pay/line/create` Annotations: `@Anonymous`, `@Auth`, `@RequestHeader String token`, `@RequestBody LinePayOrderRequest`. Request: ```json { "ddId": "202608120001" } ``` Success `data`: ```json { "ddId": "202608120001", "paymentId": 42, "lineOrderId": "LP20260812...", "transactionId": "2026081200000000001", "paymentUrl": "https://sandbox-web-pay.line.me/...", "status": "WAITING_AUTH", "reusedAttempt": false } ``` Rules: - token 用户必须拥有 ddId 对应订单。 - 只接受订单当前 `payType="3"`,单门店、未支付、合法非终态、门店当前 LINE 版本已验证并启用。 - 同 ddId 有阻断性活跃尝试时返回该行,`reusedAttempt=true`,不再 Request。 - 只有最新尝试已明确 `CANCELLED_OR_EXPIRED/FAILED` 才新增行。 - 父单跨多门店时返回业务错误,不拆分多次支付。 ### `POST /pay/line/query` Annotations 与 create 相同。 Request: ```json { "ddId": "202608120001" } ``` Success `data`: ```json { "ddId": "202608120001", "paymentId": 42, "orderPayStatus": 1, "paymentStatus": "PAID", "refundStatus": null, "transactionId": "2026081200000000001", "updatedAt": "2026-08-12T12:34:56+08:00" } ``` 该接口只读本地状态,不在 App 请求线程中调用 LINE。 ## 2. Merchant Offline API ### Existing `POST /system/orderShOprate/createOrder` Request 继续使用现有 `OrderCreateInput`,其中 `paymentMethod="3"` 写入子订单 `pay_type="3"`。本增量不接受新的来源字段: - token 必须对应有效商家用户。 - `items` 必须非空且恰好包含一个门店包,所有商品和金额属于同一门店。 - `userType=1/3` 同时校验 `PosOrder.shId=userId` 和目标 `PosStore.userId=userId`;其他摊位商家按 `PosOrder.mdId=InfoUser.storeId` 校验。 - 服务端为创建出的每个 `PosOrder` 显式写 `order_source="MERCHANT"`;客户端不能传入或覆盖该值。 ### `POST /system/orderShOprate/linePay/offline/pay` Annotations: `@Anonymous`, `@Auth`, `@RequestHeader String token`, `@RequestBody LinePayOfflinePayRequest`。 Request: ```json { "ddId": "202608180001", "oneTimeKey": "123456789012345678" } ``` Success `data`: ```json { "ddId": "202608180001", "paymentId": 84, "lineOrderId": "LPOFF20260818...", "transactionId": "2026081800000000001", "status": "AUTH_REQUIRED", "reusedAttempt": false, "updatedAt": "2026-08-18T12:34:56+08:00" } ``` Rules: - `oneTimeKey` 必须是 18 位数字,只在本次内存调用链使用;响应、异常、数据库和任何日志不得返回原值。 - 订单必须为当前商家拥有的单门店 `MERCHANT` 新订单,且 `payType="3"`、未支付、未取消、未完成、金额大于零、门店凭证已启用。 - 已有任一 Online/Offline 阻断性尝试时不得创建新尝试;已有 Offline 活跃尝试时忽略新 `oneTimeKey`,返回已有本地状态并令 `reusedAttempt=true`。 - 若阻断性活跃尝试属于 Online,付款接口返回业务错误 `line.pay.offline.attempt.blocked`,不得返回 Online 跳转地址、交易上下文或发送 My Code。 - 金额、TWD、商品摘要、storeId、credentialId 和 LINE orderId 全部取服务端事实。 - 首次进入认证等待使用 30 分钟固定截止;未知 Pay/Check 返回码不会按失败释放订单。CAS 冲突重读数据库状态,唯一键冲突复用已有尝试且不调用第二次 Pay。 ### `GET /system/orderShOprate/linePay/offline/status?ddId={ddId}` Annotations: `@Anonymous`, `@Auth`, `@RequestHeader String token`, `@RequestParam String ddId`。 该接口验证 `MERCHANT` 来源和当前商家归属后只读本地状态,不在请求线程调用 LINE。Success `data` 与付款接口相同,但 `reusedAttempt` 可省略。规范状态: 若订单尚无 Offline 尝试(包括只有 Online 历史/活跃尝试),返回业务错误 `line.pay.offline.attempt.not.found`,不得回传 Online 支付上下文。 ```text AUTH_REQUIRED 1145/1169/AUTH_READY/WAITING_AUTH,客户仍需在 LINE Pay 操作 PROCESSING Pay 结果未知或重复待核实,后端只读查单中 PAID 已严格核实订单号、交易号、金额和 paymentProvider CANCELLED 客户取消或支付过期,可使用新的 My Code 重试 FAILED 明确无副作用失败,可使用新的 My Code 重试 MANUAL_REVIEW 恢复截止仍未知或支付事实字段不完整 ``` `AUTH_REQUIRED/PROCESSING/PAID/MANUAL_REVIEW` 均阻止再次扫码。金额不一致内部状态 `AMOUNT_MISMATCH` 对外返回 `MANUAL_REVIEW`,持久化 LINE 实际扣款额,并以该金额自动创建唯一全额退款;原订单不再开放支付。 ## 3. LINE redirect endpoints ### `GET /pay/line/confirm?orderId={lineOrderId}&transactionId={transactionId}` - `orderId` 是 LINE Request 的 `line_order_id`,不是 ddId。 - 两个参数都用显式 `@RequestParam`;按 lineOrderId 定位并校验 transactionId。 - 只登记回跳/触发可恢复处理,不同步等待 Confirm。 - 返回 `text/html;charset=UTF-8` 和: - `Cache-Control: no-store, no-cache, must-revalidate` - `Pragma: no-cache` - `Referrer-Policy: no-referrer` - `Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'` - 页面尝试 `com.twanmsdyh.app://payment/result?orderId=`,并显示手动按钮和“正在确认支付,请回 App 查询”的安全提示。 ### `GET /pay/line/cancel` Optional explicit query parameters: `orderId`, `transactionId`. - 只记录浏览器事件并触发只读核对;不能把到达 cancelUrl 当成取消终态。 - 返回与 confirm 相同安全页面/响应头。 ## 4. Platform credential API Base path: `/system/storeLinePay` ### `GET /list` Permission: `chanting:storeLinePay:list`. Query uses explicit optional `@RequestParam`: `posName`, `isStall`, `credentialStatus`, `isEnabled`. Rows include store identity/current version/status/channelId/hasCredential/verifiedTime, never include `channelSecret`. ### `GET /{storeId}` Permission: `chanting:storeLinePay:query`. Returns current credential detail. Per approved product decision this privileged response may contain plaintext `channelSecret`. ### `PUT /saveCredentials` Permission: `chanting:storeLinePay:saveCredentials`. ```json { "storeId": 123, "channelId": "2000000000", "channelSecret": "plain-secret" } ``` Business validation is in Controller/Service without `@Valid`. Candidate is verified before an immutable version becomes current and enabled. Failed/unknown probe leaves existing current version untouched. ### `PUT /toggleEnable` Permission: `chanting:storeLinePay:toggleEnable`. ```json { "storeId": 123, "enabled": false } ``` Only changes whether the current verified version may serve new payments; old transactions remain queryable/refundable through their `credential_id`. ## 5. Platform order API ### `GET /system/order/{id}/status-context` Extend existing response with: ```json { "linePaymentId": 42, "linePaymentStatus": "PAID", "lineRefundStatus": null, "lineTransactionId": "2026081200000000001" } ``` Only populate LINE fields when a matching `pos_order_line_payment` exists. Never infer LINE solely from `payType="3"`. ### `POST /system/order/{id}/line-payment/reconcile` Permission: `system:order:linePaymentReconcile`. Uses the selected paymentId from stable server-side rules, takes a row lease, then performs Retrieve/Check recovery. ### `POST /system/order/{id}/line-refund` Permission: `system:order:lineRefund`. Creates/uses the unique full-refund row for the selected `PAID` payment. UNKNOWN only retrieves; it never directly repeats Refund. ## 6. LINE v4 outbound contracts ### Request `POST /v4/payments/request`, 10 second timeout. ```json { "amount": 100, "currency": "TWD", "orderId": "LP20260812...", "packages": [{ "id": "202608120001", "amount": 100, "products": [{"name": "Order 202608120001", "quantity": 1, "price": 100}] }], "redirectUrls": { "confirmUrl": "https://api.example/pay/line/confirm", "cancelUrl": "https://api.example/pay/line/cancel" }, "options": { "payment": {"capture": true} } } ``` The total amount must equal package amount and product `price * quantity` sum. ### Check `GET /v4/payments/requests/{transactionId}/check`, 20 second timeout. ### Confirm `POST /v4/payments/{transactionId}/confirm`, 40 second read timeout or greater. ```json {"amount": 100, "currency": "TWD"} ``` Confirm is sent only after local CAS claim and amount/currency/order/store/credential/active checks. ### Retrieve `GET /v4/payments?orderId={encodedLineOrderId}` or `?transactionId={encodedTransactionId}`, 20 second timeout. The exact ordered query string is included in the HMAC input. ### Refund `POST /v4/payments/{transactionId}/refund`, 20 second timeout, body `{}`. `refundAmount` is intentionally omitted for full refund; there is no currency request field. ### Offline Pay `POST /v4/payments/oneTimeKeys/pay`, read timeout 40 seconds or greater. ```json { "amount": 100, "currency": "TWD", "orderId": "LPOFF20260818...", "oneTimeKey": "<仅发送给 LINE,不记录>", "packages": [{ "id": "202608180001", "amount": 100, "products": [{"name": "Order 202608180001", "quantity": 1, "price": 100}] }] } ``` - 不发送 redirect URL、设备请求头、Capture 或 Void 字段。 - `0000` 必须核对响应 `orderId`、字符串 `transactionId`、`payInfo[].amount` 合计和非空 `paymentProvider` 后才能落账。 - `1145/1169` 进入 `WAITING_AUTH`;超时、不可解析响应和不明确结果进入 `REQUEST_UNKNOWN`,后续不得再次调用 Pay。 ### Offline Check `GET /v4/payments/orders/{encodedLineOrderId}/check`, 20 second timeout. Path 中使用永久 `lineOrderId`,签名 URI 使用编码后的最终路径。 ```text AUTH_READY -> WAITING_AUTH / AUTH_REQUIRED COMPLETE -> strict fact validation -> PAID or AMOUNT_MISMATCH/MANUAL_REVIEW CANCEL -> CANCELLED_OR_EXPIRED / CANCELLED FAIL -> FAILED ``` ### Offline Refund `POST /v4/payments/orders/{encodedLineOrderId}/refund`, 20 second timeout, body `{}`。`refundAmount` 省略;退款响应未知时沿用 Retrieve/refundList 只读恢复,禁止直接重发。 Online 与 Offline Refund 只能根据持久化的 `payment_mode` 分派,不能根据 URL、transactionId 是否存在或调用者参数判断。 ### Required headers ```text X-LINE-ChannelId: X-LINE-Authorization-Nonce: X-LINE-Authorization: Base64(HMAC-SHA256(channelSecret, channelSecret + URI + bodyOrQuery + nonce)) Content-Type: application/json ``` Offline 请求不得添加 `X-LINE-MerchantDeviceProfileId` 或 `X-LINE-MerchantDeviceType`。网关审计保存 Offline REQUEST 时必须删除 `oneTimeKey` 或固定替换为 ``;捕获异常时不得拼接请求 DTO/body。 ## 7. App deep-link contract The mobile application must register and handle: ```text com.twanmsdyh.app://payment/result?orderId= ``` On open, the App reads its normal login token and calls `/pay/line/query`. It must not trust Scheme parameters as payment success. Auto-open and fallback button require production-device acceptance testing; Sandbox web payment cannot prove LINE App handoff behavior.