# LINE Pay Data Model ## Relationship Overview ```text pos_store 1 ── N pos_store_line_pay (immutable credential versions) │ └── current_store_id UNIQUE: one current version/store pos_order (`order_source=USER/MERCHANT`) 1 ── N pos_order_line_payment (append-only attempts) │ ├── credential_id -> exact credential version ├── payment_mode -> ONLINE/OFFLINE gateway dispatch ├── active_dd_id UNIQUE: one blocking attempt/order └── 1 ── 0..1 pos_order_line_refund payment_gateway_log ── optional payment_id/refund_id/credential_id references ``` 不声明物理外键,保持与现有业务表部署习惯一致;Service 在事务内保证逻辑引用存在。 ## 1. `pos_store_line_pay` 不可变门店凭证版本。Secret 按用户决定以明文保存。普通列表仅返回 `hasCredential`,平台详情接口可返回当前版本 Secret。 | Column | Type | Null | Notes | |---|---|---:|---| | `id` | BIGINT | no | auto increment | | `store_id` | BIGINT | no | `pos_store.id` | | `credential_version` | INT | no | store 内从 1 递增 | | `channel_id` | VARCHAR(50) ASCII | no | LINE Channel ID;业务层按字符串处理 | | `channel_secret` | VARCHAR(255) | no | 明文 | | `environment` | VARCHAR(16) | no | `SANDBOX/PRODUCTION` | | `credential_status` | VARCHAR(32) | no | `VERIFIED/VERIFY_FAILED`;仅 VERIFIED 可成为 current | | `is_enabled` | TINYINT | no | current 版本的新支付开关 | | `current_store_id` | BIGINT | yes | current 时等于 `store_id`,历史版本 NULL | | `verify_return_code` | VARCHAR(8) | yes | 最后探测结果码 | | `verify_return_message` | VARCHAR(255) | yes | 摘要 | | `verified_time` | DATETIME | yes | 验证通过时间 | | `create_time/update_time` | DATETIME | no | 审计时间 | | `create_by/update_by` | VARCHAR(64) | yes | 管理员 | Constraints and indexes: - `UNIQUE(store_id, credential_version)` - `UNIQUE(current_store_id)`;MySQL 允许多个 NULL。 - `INDEX(store_id, create_time)` - 当前版本切换使用事务:锁定当前行/版本,插入新行,再把旧 `current_store_id` 置 NULL,新行置 storeId。 - 相同 Channel ID、Secret 和 environment 重复保存时直接返回现有当前版本,不创建重复行。 ## 2. `pos_order_line_payment` 每次真正发起的 LINE Request 对应一行,历史永不覆盖。行内更新仅用于同一尝试的状态推进和补充网关标识。 | Column | Type | Null | Notes | |---|---|---:|---| | `id` | BIGINT | no | `paymentId` | | `dd_id` | VARCHAR(64) | no | `pos_order.dd_id` | | `line_order_id` | VARCHAR(100) ASCII BINARY | no | Request 前生成,永久唯一 | | `transaction_id` | VARCHAR(32) ASCII BINARY | yes | Request 成功后获得,禁止 Long | | `credential_id` | BIGINT | no | 发起时选中的不可变凭证版本 | | `store_id` | BIGINT | no | 门店快照 | | `amount` | INT | no | TWD 整数金额 | | `captured_amount` | INT | yes | LINE 实际扣款金额;正常支付等于 `amount`,金额异常时作为全额退款金额与恢复证据 | | `currency` | CHAR(3) ASCII | no | 首版 `TWD` | | `payment_mode` | VARCHAR(16) ASCII | no | `ONLINE/OFFLINE`;默认 `ONLINE`,新尝试显式写入 | | `payment_url_web` | VARCHAR(1000) | yes | Sandbox/浏览器跳转 URL | | `payment_url_app` | VARCHAR(1000) | yes | 生产 LINE App URL,可空 | | `payment_provider` | VARCHAR(16) | yes | 网关原值;进入 `PAID` 时必须非空,已知 `TSP/EPI` 但不封闭枚举 | | `status` | VARCHAR(40) | no | 下方状态机 | | `active_dd_id` | VARCHAR(64) | yes | 阻断新尝试时等于 ddId,明确终止后 NULL | | `version` | BIGINT | no | CAS,从 0 递增 | | `next_reconcile_at` | DATETIME | yes | 下次扫描 | | `reconcile_deadline` | DATETIME | yes | 当前恢复阶段截止 | | `reconcile_count` | INT | no | 默认 0 | | `lease_owner` | VARCHAR(64) | yes | 行租约节点/批次 ID | | `lease_until` | DATETIME | yes | 租约过期时间 | | `status_changed_at` | DATETIME | no | 状态变化时间 | | `auth_completed_time` | DATETIME | yes | Check 0110 时间 | | `pay_time` | DATETIME | yes | Capture 时间 | | `create_time/update_time` | DATETIME | no | 审计时间 | Constraints and indexes: - `UNIQUE(line_order_id)` - `UNIQUE(transaction_id)`;允许多个 NULL。 - `UNIQUE(active_dd_id)`;允许历史多 NULL。 - `INDEX(dd_id, create_time, id)` - `INDEX(status, next_reconcile_at, id)` - `INDEX(credential_id, create_time)` - `INDEX(payment_mode, status, next_reconcile_at, id)` - 所有状态更新条件至少包含 `id + expected status + version`,领取租约还需 `lease_until IS NULL OR lease_until < NOW()`。 - 既有支付行通过 DDL 默认值保持 `ONLINE`;新建 Online/Offline 尝试必须显式写模式,禁止根据 URL、交易号或状态猜测。 Payment states: ```text REQUESTING ├─ WAITING_AUTH ├─ REQUEST_UNKNOWN └─ FAILED (only explicit no-side-effect failure; releases active key) WAITING_AUTH / REQUEST_UNKNOWN ├─ READY_CONFIRM ├─ PAID ├─ CANCELLED_OR_EXPIRED (releases active key) ├─ FAILED (releases active key) └─ MANUAL_REVIEW READY_CONFIRM ├─ CONFIRMING └─ AUTH_DONE_ORDER_CANCELLED CONFIRMING ├─ PAID └─ CONFIRM_UNKNOWN CONFIRM_UNKNOWN ├─ PAID └─ MANUAL_REVIEW OFFLINE REQUESTING ├─ WAITING_AUTH (1145/1169;AUTH_READY) ├─ PAID (0000 且资金事实匹配) ├─ REQUEST_UNKNOWN (超时/响应丢失/重复待核实) ├─ AMOUNT_MISMATCH (真实付款但金额不一致) └─ FAILED (明确无副作用失败;释放 active key) OFFLINE WAITING_AUTH / REQUEST_UNKNOWN ├─ PAID (Check COMPLETE 且事实匹配) ├─ CANCELLED_OR_EXPIRED (Check CANCEL;释放 active key) ├─ FAILED (Check FAIL;释放 active key) ├─ AMOUNT_MISMATCH └─ MANUAL_REVIEW (截止仍未知或必要事实缺失) ``` `PAID`、UNKNOWN、`AMOUNT_MISMATCH`、`MANUAL_REVIEW` 不释放 `active_dd_id`。Offline 金额不一致退款成功后原订单仍关闭支付,商家必须创建新订单。旧尝试迟到证实 `PAID` 时照实记录,并阻断/退款,不覆盖当前尝试。 ## 3. `pos_order_line_refund` 首版只支持每笔已付款交易一次全额退款。 | Column | Type | Null | Notes | |---|---|---:|---| | `id` | BIGINT | no | refund id | | `payment_id` | BIGINT | no | payment,唯一 | | `dd_id` | VARCHAR(64) | no | 查询快照 | | `credential_id` | BIGINT | no | 默认 payment 的凭证版本 | | `transaction_id` | VARCHAR(32) ASCII BINARY | no | 原支付交易号 | | `refund_transaction_id` | VARCHAR(32) ASCII BINARY | yes | LINE refund transaction | | `amount` | INT | no | 本地全额审计;调用时不传 refundAmount | | `status` | VARCHAR(32) | no | `CREATED/PROCESSING/UNKNOWN/RETRY_WAIT/REFUNDED/FAILED/MANUAL_REVIEW` | | `source` | VARCHAR(32) | no | `USER_CANCEL/STORE_CANCEL/ADMIN/TASK` | | `version` | BIGINT | no | CAS | | `next_reconcile_at/reconcile_deadline` | DATETIME | yes | 恢复调度 | | `reconcile_count` | INT | no | 默认 0 | | `lease_owner/lease_until` | VARCHAR(64)/DATETIME | yes | 行租约 | | `refund_time` | DATETIME | yes | 确证全退时间 | | `create_time/update_time` | DATETIME | no | 审计时间 | Constraints and indexes: - `UNIQUE(payment_id)` - `UNIQUE(refund_transaction_id)`;允许多个 NULL。 - `INDEX(status, next_reconcile_at, id)` - `INDEX(dd_id, create_time, id)` - 退款端点不在退款行重复保存模式;通过不可变 `payment_id` 读取支付行 `payment_mode`,Online 使用 `transaction_id`,Offline 使用 `line_order_id`。 Refund transitions: ```text CREATED / RETRY_WAIT --CAS claim--> PROCESSING PROCESSING ├─ REFUNDED ├─ UNKNOWN ├─ RETRY_WAIT (only documented explicit retryable result) └─ FAILED (explicit terminal failure) UNKNOWN ├─ REFUNDED (Retrieve refundList proves it) └─ MANUAL_REVIEW (deadline) ``` ## 4. `payment_gateway_log` 仅记录 LINE Pay;不迁移 OMG。日志采用追加写,不用于决定当前支付状态。 | Column | Type | Null | Notes | |---|---|---:|---| | `id` | BIGINT | no | auto increment | | `correlation_id` | VARCHAR(64) ASCII | no | 同次 request/response 关联 | | `payment_id/refund_id/credential_id` | BIGINT | yes | 结构化关联 | | `store_id` | BIGINT | yes | 凭证验证也可关联门店 | | `dd_id` | VARCHAR(64) | yes | 业务订单 | | `gateway_order_id` | VARCHAR(100) ASCII BINARY | yes | line_order_id | | `transaction_id` | VARCHAR(32) ASCII BINARY | yes | 网关交易号 | | `action` | VARCHAR(32) | no | `CREDENTIAL_VERIFY/REQUEST/CHECK/CONFIRM/RETRIEVE/REFUND/REDIRECT` | | `direction` | VARCHAR(8) | no | `REQUEST/RESPONSE/EVENT` | | `source` | VARCHAR(32) | no | `APP/CALLBACK/TASK/ADMIN/CANCEL` | | `http_status` | INT | yes | HTTP status | | `return_code` | VARCHAR(8) | yes | LINE code | | `return_message` | VARCHAR(255) | yes | LINE message | | `success` | TINYINT | no | transport/business action summary | | `duration_ms` | BIGINT | yes | elapsed | | `payload` | LONGTEXT | yes | 脱敏 JSON/body/error summary;Offline REQUEST 不得包含原始 `oneTimeKey` | | `create_time` | DATETIME | no | append time | Indexes: - `(payment_id, create_time)` - `(refund_id, create_time)` - `(gateway_order_id, create_time)` - `(transaction_id, create_time)` - `(correlation_id)` - `(store_id, create_time)` ## 5. Order integration 新增字段: | Column | Type | Null | Notes | |---|---|---:|---| | `order_source` | VARCHAR(16) ASCII | no | `USER/MERCHANT`;默认 `USER`,仅商家创建入口显式写 `MERCHANT` | 兼容与写入规则: - 现有行和用户端新订单使用数据库默认 `USER`,不得根据 userId、创建时间、payType 或入口日志推断/回填。 - `/system/orderShOprate/createOrder` 创建的每个子订单显式写 `MERCHANT`;父订单不作为 Offline 支付归属事实。 - Offline 扫码必须同时满足 `order_source=MERCHANT`、当前 token 商家归属、单门店、未支付、未取消、未完成、金额大于零和 `pay_type="3"`。普通/夜市商家除校验 `sh_id=userId` 外,还必须校验订单 `md_id` 对应 `pos_store.user_id=userId`,禁止组合本人 `shId` 与其他商家的 `mdId` 使用他店凭证。 继续使用现有字段: - `pay_type="3"`: 新 LINE 订单选择;历史同值不自动视为 LINE,必须存在匹配 Line payment。 - `pay_status=0/1/2`: 未付/已付/已确认全额退款。 - `pay_url`: 当前活跃尝试的 web URL,可作为兼容展示;支付选择以 Line payment 为准。 - `state=4`: 已取消;仍允许记录迟到 `PAID`,随后创建退款 intent。 ## 6. Query selection invariants 1. App 以 `ddId` 查询时先读取订单 `pay_status`。 2. 未付款优先唯一 `active_dd_id=ddId`;没有则返回最新终态尝试。 3. 已付款选唯一尚未证实全退的 `PAID`;已退款选带 `REFUNDED` 的付款/退款组合。 4. 出现多条未全退 `PAID` 不自动任选,返回 `MANUAL_REVIEW`。 5. 回跳只按唯一 `line_order_id` 定位,transactionId 存在时必须匹配。 6. 定时/平台动作只按领取到的 `paymentId/refundId` 操作。 7. 商家 Offline 状态查询先验证 `order_source=MERCHANT` 和当前商家归属,再按唯一活跃尝试或最近终态选择 `payment_mode=OFFLINE` 的记录;不得返回其他商家或 Online 尝试的敏感上下文。 ## 7. Migration and compatibility - `updatesql/sql.md` 追加 `pos_order.order_source VARCHAR(16) NOT NULL DEFAULT 'USER'`。 - `updatesql/sql.md` 追加 `pos_order_line_payment.payment_mode VARCHAR(16) NOT NULL DEFAULT 'ONLINE'`、可空 `captured_amount` 及模式扫描索引。 - Entity、MyBatis `paymentColumns`、所有显式 select、insert、update 和 Service 构造参数同步新增字段;不直接执行 DDL。 - 默认值只提供历史兼容,业务代码创建订单/支付尝试时仍必须显式写入来源和模式,防止调用入口变化后静默误分类。