# Data Model: OMG AIO 创建支付尝试与付款结果

## `pos_order_omg_attempt`

采用追加式支付尝试表。创建时保存订单、商户和凭证快照；付款结果回调在同一行追加网关最终事实，不覆盖历史尝试。

| Column | Type | Null | Meaning |
|---|---|---:|---|
| `id` | `BIGINT` | No | 自增主键 |
| `dd_id` | `VARCHAR(64)` | No | `pos_order.dd_id` |
| `merchant_trade_no` | `VARCHAR(20)` ASCII binary | No | OMG MerchantTradeNo，全局唯一 |
| `store_id` | `BIGINT` | No | 创建时订单门店快照 |
| `merchant_id` | `VARCHAR(10)` ASCII binary | No | 创建时门店 MerchantID 快照 |
| `amount` | `INT` | No | 创建时整数 TWD 金额快照 |
| `hash_key_snapshot` | `VARCHAR(64)` | No | 创建时 HashKey 快照，仅用于该尝试验签 |
| `hash_iv_snapshot` | `VARCHAR(64)` | No | 创建时 HashIV 快照，仅用于该尝试验签 |
| `attempt_status` | `TINYINT` | No | `0=CREATED, 1=PAID, 2=FAILED, 3=SUPERSEDED` |
| `active_dd_id` | `VARCHAR(64)` generated | Yes | status=0 时等于 dd_id，否则 NULL |
| `trade_no` | `VARCHAR(20)` ASCII binary | Yes | OMG 金流交易编号，非空时全局唯一 |
| `rtn_code` | `INT` | Yes | OMG 原始交易状态码 |
| `rtn_msg` | `VARCHAR(200)` | Yes | OMG 原始交易说明 |
| `payment_type` | `VARCHAR(20)` ASCII | Yes | OMG 回覆付款方式 |
| `payment_date` | `DATETIME` | Yes | OMG 付款时间 |
| `trade_date` | `DATETIME` | Yes | OMG 订单成立时间 |
| `payment_type_charge_fee` | `INT` | Yes | OMG 回传手续费 |
| `simulate_paid` | `TINYINT` | Yes | `0=一般付款, 1=模拟付款` |
| `last_notify_time` | `DATETIME` | Yes | 最近一次合法通知处理时间 |
| `create_time` | `DATETIME` | No | 本地创建时间 |
| `update_time` | `DATETIME` | No | 本地更新时间 |

### DDL registered in `updatesql/sql.md`

## 2026-08-13 OMG AIO 创建支付订单从零重建（020-omg-payment-rebuild）

```sql
-- 保存创建事实、凭证快照与最终付款结果；不保存完整创建表单。
-- 本 SQL 只登记，由开发者手动执行；实现过程禁止直接执行 DDL。
CREATE TABLE pos_order_omg_attempt (
  id BIGINT NOT NULL AUTO_INCREMENT COMMENT '本地支付尝试主键',
  dd_id VARCHAR(64) NOT NULL COMMENT '关联 pos_order.dd_id',
  merchant_trade_no VARCHAR(20) CHARACTER SET ascii COLLATE ascii_bin NOT NULL COMMENT 'OMG MerchantTradeNo',
  store_id BIGINT NOT NULL COMMENT '创建时订单门店快照',
  merchant_id VARCHAR(10) CHARACTER SET ascii COLLATE ascii_bin NOT NULL COMMENT '创建时 OMG MerchantID 快照',
  amount INT NOT NULL COMMENT '创建时整数 TWD 金额快照',
  hash_key_snapshot VARCHAR(64) NOT NULL COMMENT '创建时 HashKey 快照',
  hash_iv_snapshot VARCHAR(64) NOT NULL COMMENT '创建时 HashIV 快照',
  attempt_status TINYINT NOT NULL DEFAULT 0 COMMENT '本地状态:0=CREATED,1=PAID,2=FAILED,3=SUPERSEDED',
  active_dd_id VARCHAR(64)
    GENERATED ALWAYS AS (IF(attempt_status = 0, dd_id, NULL)) VIRTUAL
    COMMENT '单订单单活跃尝试唯一键载体',
  trade_no VARCHAR(20) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL COMMENT 'OMG 金流交易编号',
  rtn_code INT DEFAULT NULL COMMENT 'OMG 原始结果码',
  rtn_msg VARCHAR(200) DEFAULT NULL COMMENT 'OMG 原始结果说明',
  payment_type VARCHAR(20) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL COMMENT 'OMG 回覆付款方式',
  payment_date DATETIME DEFAULT NULL COMMENT 'OMG 付款时间',
  trade_date DATETIME DEFAULT NULL COMMENT 'OMG 订单成立时间',
  payment_type_charge_fee INT DEFAULT NULL COMMENT 'OMG 回传手续费',
  simulate_paid TINYINT DEFAULT NULL COMMENT '0一般付款,1模拟付款',
  last_notify_time DATETIME DEFAULT NULL COMMENT '最近合法通知处理时间',
  create_time DATETIME NOT NULL COMMENT '创建时间',
  update_time DATETIME NOT NULL COMMENT '更新时间',
  PRIMARY KEY (id),
  UNIQUE KEY uk_omg_attempt_trade_no (merchant_trade_no),
  UNIQUE KEY uk_omg_attempt_gateway_trade_no (trade_no),
  UNIQUE KEY uk_omg_attempt_active_dd (active_dd_id),
  KEY idx_omg_attempt_dd_time (dd_id, create_time, id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='OMG AIO 新支付创建尝试';
```

### Invariants

1. `merchant_trade_no` 只含大写 ASCII 英数字且长度不超过 20。
2. `amount > 0`、`store_id` 和 `merchant_id` 均在插入前校验。
3. `attempt_status=0` 的行通过生成列唯一索引保证每个 `dd_id` 最多一条。
4. `PAID` 不可降级；`FAILED` 可被后续合法成功通知升级为 `PAID`。
5. 密钥快照永远不输出到 API、普通日志或 `ipn_log`；完整回调内容只来自 HTTP 请求本身，不拼入数据库密钥。
6. 不执行外键约束：沿用项目现有业务表风格，订单归属在事务锁与服务校验中保证。

## Java types

### `OmgPaymentAttempt`

```java
public class OmgPaymentAttempt {
    private Long id;
    private String ddId;
    private String merchantTradeNo;
    private Long storeId;
    private String merchantId;
    private Integer amount;
    private String hashKeySnapshot;
    private String hashIvSnapshot;
    private Integer attemptStatus;
    private String activeDdId;
    private String tradeNo;
    private Integer rtnCode;
    private String rtnMsg;
    private String paymentType;
    private Date paymentDate;
    private Date tradeDate;
    private BigDecimal paymentTypeChargeFee;
    private Integer simulatePaid;
    private Date lastNotifyTime;
    private Date createTime;
    private Date updateTime;
}
```

### `OmgPaymentOrderSnapshot`

只承载锁定订单时创建支付所需字段，不暴露到 Controller：

```java
public class OmgPaymentOrderSnapshot {
    private Long id;
    private String ddId;
    private String parentDdId;
    private Long storeId;
    private Long userId;
    private Integer amount;
    private Long state;
    private Long payStatus;
    private String payType;
}
```

## Mapper operations

```java
OmgPaymentOrderSnapshot selectOrderForUpdate(String ddId);
OmgPaymentAttempt selectActiveByDdId(String ddId);
OmgPaymentAttempt selectByMerchantTradeNoForUpdate(String merchantTradeNo);
int insertCreated(OmgPaymentAttempt attempt);
int markPaid(OmgPaymentAttempt attempt);
int markFailed(OmgPaymentAttempt attempt);
int supersedeOtherCreated(String ddId, Long paidAttemptId);
int markOrderPaid(String ddId);
```

`selectOrderForUpdate` 与 `selectByMerchantTradeNoForUpdate` 必须在事务中使用 `SELECT ... FOR UPDATE`。`insertCreated` 必须显式列出插入列并排除生成列 `active_dd_id`。状态更新必须带当前状态条件，保证 `PAID` 不可降级。

## Existing `ipn_log`

不新增字段。每个 HTTP 回调独立新增一行：

| Column | Value |
|---|---|
| `ip` | 请求来源 IP |
| `cretim` | 本地接收时间 |
| `ipn_log` | 完整原始 form-urlencoded 请求体 |
| `type` | 固定 `omg` |
