# Data Model: 商家 ezPay 发票开通管理

**Phase**: Phase 1 — 表结构、字段、状态机
**Date**: 2026-06-15

## 变更一：pos_store 加列（已有表）

`pos_store` 新增一列，按全栈清单走（实体 + resultMap + select/insert/update + 前端 + i18n + SQL）。

| 字段 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `invoice_exempt` | `tinyint(1)` | `0` | 是否免用发票（不需开票）：0需开票 / 1免用发票 |

> 不在本表加任何 ezPay 凭证字段（凭证进独立表，见决策 5）。

## 变更二：新增表 pos_store_ezpay（与 pos_store 1:1）

```sql
-- 2026-06-15 商家 ezPay 发票开通管理
CREATE TABLE pos_store_ezpay (
  id                 BIGINT       NOT NULL AUTO_INCREMENT COMMENT '主键',
  store_id           BIGINT       NOT NULL COMMENT '关联 pos_store.id（门店）',
  ezpay_status       TINYINT      NOT NULL DEFAULT 0 COMMENT '申请状态:0未申请/1申请中/2已开通',
  is_enabled         TINYINT      NOT NULL DEFAULT 1 COMMENT '启用开关:0停用/1启用(仅status=2有意义)',
  merchant_id        VARCHAR(32)  DEFAULT NULL COMMENT 'ezPay 商店代号 MerchantID_',
  hash_key           VARCHAR(64)  DEFAULT NULL COMMENT 'ezPay HashKey(32字节)',
  hash_iv            VARCHAR(64)  DEFAULT NULL COMMENT 'ezPay HashIV(16字节)',
  company_id         VARCHAR(32)  DEFAULT NULL COMMENT 'ezPay 会员编号 CompanyID_(字轨用,可空)',
  ubn                VARCHAR(16)  DEFAULT NULL COMMENT '统一编号(统编,商家上传)',
  apply_time         DATETIME     DEFAULT NULL COMMENT '提交申请时间(0->1)',
  approved_time      DATETIME     DEFAULT NULL COMMENT '开通时间(->2)',
  last_verify_result VARCHAR(255) DEFAULT NULL COMMENT '最近一次凭证验证结果',
  remark             VARCHAR(500) DEFAULT NULL COMMENT '备注',
  create_time        DATETIME     DEFAULT NULL COMMENT '创建时间',
  update_time        DATETIME     DEFAULT NULL COMMENT '更新时间',
  create_by          VARCHAR(64)  DEFAULT NULL COMMENT '创建人',
  update_by          VARCHAR(64)  DEFAULT NULL COMMENT '更新人',
  PRIMARY KEY (id),
  UNIQUE KEY uk_store_id (store_id),
  KEY idx_ezpay_status (ezpay_status)
) ENGINE=InnoDB DEFAULT CHARSET=utf88mb4 COMMENT='门店 ezPay 发票开通配置';
```

### 字段语义

- `store_id`：唯一键，一门店一行。**门店无此行 = 未申请**（列表 LEFT JOIN，无行按 status=0 处理）。
- `ezpay_status`：核心状态机字段（见下）。
- `is_enabled`：运行时开关，仅 `ezpay_status=2` 时有意义；停用不影响 status。
- `merchant_id`/`hash_key`/`hash_iv`：运营录入的 ezPay 凭证；`company_id` 字轨用可空。
- `ubn`：商家在商家端上传的统一编号。
- `last_verify_result`：记录最近验证的 ezPay 回应摘要，便于排查。

### 实体（PosStoreEzpay.java）

`ruoyi-system/.../domain/PosStoreEzpay.java`，MyBatis-Plus `@TableName("pos_store_ezpay")`、`@TableId(type=IdType.AUTO)`，字段与上表一一对应（驼峰）。

## 状态机

```
                ┌─────────────────────────────┐
                ▼                             │ 重置/重新申请
  未申请(0) ──发起申请──▶ 申请中(1) ──录入凭证+验证通过──▶ 已开通(2)
                              │                              │
                              │ 验证失败/网络异常             │ 停用
                              ▼ (保持 1)                     ▼
                          （原地，回传错误）          已开通+停用(enabled=0)
                                                             │ 恢复
                                                             ▼
                                                       已开通+启用(enabled=1)
```

**不变量**:
- `ezpay_status=2` 的前置：必须经过一次成功的凭证验证（`merchant_id`/`hash_key`/`hash_iv` 非空且 `last_verify_result` 为通过）。
- `is_enabled` 仅在 `ezpay_status=2` 时可切换为有效语义；其它状态下恒视为"不可开票"。
- **免用发票（pos_store.invoice_exempt=1）优先于 ezPay 状态**：免用门店即使有 `pos_store_ezpay` 行也视为不开票，且不进入待办过滤。

## 列表查询口径

平台后台列表 = `pos_store` LEFT JOIN `pos_store_ezpay`，过滤"实际卖货门店"：

- 纳入：普通店铺（`is_stall=0`）+ 摊位（`is_stall=1`，含夜市下摊位）
- 排除：夜市（userType=3）本身不经 pos_store 开票；纯夜市容器门店按实现时数据口径确认

筛选维度：
- `invoice_exempt=1` → 免用发票
- `invoice_exempt=0 AND ezpay_status=0` → 未申请（"还要去开通"）
- `invoice_exempt=0 AND ezpay_status=1` → 申请中
- `invoice_exempt=0 AND ezpay_status=2` → 已开通
- "还没开通" = `invoice_exempt=0 AND ezpay_status IN (0,1)`
- "还要去开通" = `invoice_exempt=0 AND ezpay_status=0`

## 验证规则

- 录入凭证：`merchant_id` 必填、`hash_key` 必填且 32 字节、`hash_iv` 必填且 16 字节（长度校验，ezPay 固定规格）。
- 验证调用：超时 10s；网络异常按"未通过"处理，不抛 500，回传可重试提示。
- 统编 `ubn`：商家端非免用门店建议必填（8 位数字），免用门店不强制。**发起申请（apply）时强制校验：非空且为 8 位数字，不满足则拒绝推进为"申请中"。**
