api.md 11 KB

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:

{
  "ddId": "202608120001"
}

Success data:

{
  "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:

{
  "ddId": "202608120001"
}

Success data:

{
  "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:

{
  "ddId": "202608180001",
  "oneTimeKey": "123456789012345678"
}

Success data:

{
  "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 支付上下文。

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=<urlEncodedDdId>,并显示手动按钮和“正在确认支付,请回 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.

{
  "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.

{
  "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:

{
  "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.

{
  "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.

{"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.

{
  "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、字符串 transactionIdpayInfo[].amount 合计和非空 paymentProvider 后才能落账。
  • 1145/1169 进入 WAITING_AUTH;超时、不可解析响应和不明确结果进入 REQUEST_UNKNOWN,后续不得再次调用 Pay。

Offline Check

GET /v4/payments/orders/{encodedLineOrderId}/check, 20 second timeout. Path 中使用永久 lineOrderId,签名 URI 使用编码后的最终路径。

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

X-LINE-ChannelId: <channel id>
X-LINE-Authorization-Nonce: <unique nonce>
X-LINE-Authorization: Base64(HMAC-SHA256(channelSecret, channelSecret + URI + bodyOrQuery + nonce))
Content-Type: application/json

Offline 请求不得添加 X-LINE-MerchantDeviceProfileIdX-LINE-MerchantDeviceType。网关审计保存 Offline REQUEST 时必须删除 oneTimeKey 或固定替换为 <redacted>;捕获异常时不得拼接请求 DTO/body。

7. App deep-link contract

The mobile application must register and handle:

com.twanmsdyh.app://payment/result?orderId=<ddId>

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.