# 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. 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 相同安全页面/响应头。 ## 3. 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`. ## 4. 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. ## 5. 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. ### 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 ``` ## 6. 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.