# 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 1 ── N pos_order_line_payment (append-only attempts) │ ├── credential_id -> exact credential version ├── 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 整数金额 | | `currency` | CHAR(3) ASCII | no | 首版 `TWD` | | `payment_url_web` | VARCHAR(1000) | yes | Sandbox/浏览器跳转 URL | | `payment_url_app` | VARCHAR(1000) | yes | 生产 LINE App URL,可空 | | `payment_provider` | VARCHAR(16) | yes | 原始值可空;展示归一为 TSP | | `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)` - 所有状态更新条件至少包含 `id + expected status + version`,领取租约还需 `lease_until IS NULL OR lease_until < NOW()`。 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 ``` `PAID`、UNKNOWN、`MANUAL_REVIEW` 不释放 `active_dd_id`。旧尝试迟到证实 `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)` 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 | raw JSON/body/error summary | | `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 不新增 `pos_order` 字段。使用现有: - `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` 操作。