# Capability: model-organization

## MODIFIED Requirements

### Requirement: Data models MUST follow unified structure conventions

All IoT data models MUST follow unified structure conventions. 所有 IoT 相关数据模型必须与现有用户体系模型（Account、PersonalCustomer）保持一致的架构风格和字段定义规范。

#### Scenario: IoT 卡模型结构规范化

**Given** 系统存在 IoT 卡数据模型
**When** 开发者定义或修改 IoT 卡模型
**Then** 模型必须：
- 嵌入 `gorm.Model`（提供 ID、CreatedAt、UpdatedAt、DeletedAt 字段）
- 嵌入 `BaseModel`（提供 Creator、Updater 审计字段）
- 所有字段显式指定 `gorm:"column:字段名"` 标签
- 所有字符串字段显式指定类型和长度（如 `type:varchar(100)`）
- 所有金额字段使用 `int64` 类型和 `type:bigint`，单位为"分"
- 所有必填字段添加 `not null` 约束
- 所有字段添加中文 `comment` 注释
- 所有唯一字段添加 `uniqueIndex:索引名,where:deleted_at IS NULL`
- 所有关联 ID 字段添加 `index` 索引
- 表名使用 `tb_iot_card`（`tb_` 前缀 + 单数）

**Example:**
```go
package model

import "gorm.io/gorm"

type IotCard struct {
    gorm.Model                    // ID, CreatedAt, UpdatedAt, DeletedAt
    BaseModel    `gorm:"embedded"` // Creator, Updater

    ICCID           string     `gorm:"column:iccid;type:varchar(50);uniqueIndex:idx_iccid,where:deleted_at IS NULL;not null;comment:ICCID(唯一标识)" json:"iccid"`
    CardType        string     `gorm:"column:card_type;type:varchar(50);not null;comment:卡类型" json:"card_type"`
    CardCategory    string     `gorm:"column:card_category;type:varchar(20);default:'normal';not null;comment:卡业务类型 normal-普通卡 industry-行业卡" json:"card_category"`
    CarrierID       uint       `gorm:"column:carrier_id;type:bigint;not null;index;comment:运营商ID" json:"carrier_id"`
    CostPrice       int64      `gorm:"column:cost_price;type:bigint;default:0;not null;comment:成本价（分）" json:"cost_price"`
    DistributePrice int64      `gorm:"column:distribute_price;type:bigint;default:0;not null;comment:分销价（分）" json:"distribute_price"`
    Status          int        `gorm:"column:status;type:int;default:1;not null;index;comment:状态 1-在库 2-已分销 3-已激活 4-已停用" json:"status"`
    OwnerType       string     `gorm:"column:owner_type;type:varchar(20);default:'platform';not null;comment:所有者类型 platform-平台 agent-代理 user-用户 device-设备" json:"owner_type"`
    OwnerID         uint       `gorm:"column:owner_id;type:bigint;default:0;not null;index;comment:所有者ID" json:"owner_id"`
    ActivatedAt     *time.Time `gorm:"column:activated_at;comment:激活时间" json:"activated_at,omitempty"`
}

func (IotCard) TableName() string {
    return "tb_iot_card"
}
```

#### Scenario: 设备模型结构规范化

**Given** 系统存在设备数据模型
**When** 开发者定义或修改设备模型
**Then** 模型必须遵循与 IoT 卡模型相同的规范（gorm.Model + BaseModel + 字段标签）
**And** 表名使用 `tb_device`

#### Scenario: 号卡模型结构规范化

**Given** 系统存在号卡数据模型
**When** 开发者定义或修改号卡模型
**Then** 模型必须遵循与 IoT 卡模型相同的规范
**And** 表名使用 `tb_number_card`
**And** 价格字段使用 `int64` 类型（分为单位）

#### Scenario: 套餐相关模型结构规范化

**Given** 系统存在套餐系列、套餐、代理套餐分配、套餐使用情况等模型
**When** 开发者定义或修改套餐相关模型
**Then** 所有套餐相关模型必须遵循统一规范：
- 套餐系列：`tb_package_series`
- 套餐：`tb_package`
- 代理套餐分配：`tb_agent_package_allocation`
- 套餐使用情况：`tb_package_usage`
**And** 所有价格字段使用 `int64` 类型（分为单位）

#### Scenario: 订单模型结构规范化

**Given** 系统存在订单数据模型
**When** 开发者定义或修改订单模型
**Then** 模型必须遵循统一规范
**And** 表名使用 `tb_order`
**And** 金额字段使用 `int64` 类型（分为单位）
**And** JSONB 字段使用 `gorm.io/datatypes.JSON` 类型（不是 `pq.StringArray`）

**Example:**
```go
import (
    "gorm.io/datatypes"
    "gorm.io/gorm"
)

type Order struct {
    gorm.Model
    BaseModel    `gorm:"embedded"`

    OrderNo          string         `gorm:"column:order_no;type:varchar(100);uniqueIndex:idx_order_no,where:deleted_at IS NULL;not null;comment:订单号(唯一标识)" json:"order_no"`
    OrderType        int            `gorm:"column:order_type;type:int;not null;index;comment:订单类型 1-套餐订单 2-号卡订单" json:"order_type"`
    Amount           int64          `gorm:"column:amount;type:bigint;not null;comment:订单金额（分）" json:"amount"`
    CarrierOrderData datatypes.JSON `gorm:"column:carrier_order_data;type:jsonb;comment:运营商订单原始数据" json:"carrier_order_data,omitempty"`
    Status           int            `gorm:"column:status;type:int;default:1;not null;index;comment:状态 1-待支付 2-已支付 3-已完成 4-已取消 5-已退款" json:"status"`
}

func (Order) TableName() string {
    return "tb_order"
}
```

#### Scenario: 分佣相关模型结构规范化

**Given** 系统存在代理层级、分佣规则、分佣记录等模型
**When** 开发者定义或修改分佣相关模型
**Then** 所有分佣相关模型必须遵循统一规范：
- 代理层级：`tb_agent_hierarchy`
- 分佣规则：`tb_commission_rule`
- 阶梯分佣配置：`tb_commission_ladder`
- 组合分佣条件：`tb_commission_combined_condition`
- 分佣记录：`tb_commission_record`
- 分佣审批：`tb_commission_approval`
- 分佣模板：`tb_commission_template`
- 运营商结算：`tb_carrier_settlement`
**And** 所有金额字段使用 `int64` 类型（分为单位）

#### Scenario: 财务相关模型结构规范化

**Given** 系统存在佣金提现申请、提现设置、收款商户设置等模型
**When** 开发者定义或修改财务相关模型
**Then** 所有财务相关模型必须遵循统一规范：
- 佣金提现申请：`tb_commission_withdrawal_request`
- 佣金提现设置：`tb_commission_withdrawal_setting`
- 收款商户设置：`tb_payment_merchant_setting`
**And** 所有金额字段使用 `int64` 类型（分为单位）
**And** JSONB 字段使用 `gorm.io/datatypes.JSON` 类型

#### Scenario: 系统配置和日志模型规范化

**Given** 系统存在运营商、轮询配置、流量记录、开发能力配置等模型
**When** 开发者定义或修改系统配置和日志模型
**Then** 模型必须遵循统一规范：
- 运营商：`tb_carrier`（gorm.Model + BaseModel）
- 轮询配置：`tb_polling_config`（gorm.Model + BaseModel）
- 流量记录：`tb_data_usage_record`（仅 ID + CreatedAt，不需要 UpdatedAt 和 DeletedAt）
- 开发能力配置：`tb_dev_capability_config`（gorm.Model + BaseModel）
- 换卡申请：`tb_card_replacement_request`（gorm.Model + BaseModel）

**Example (流量记录 - 简化模型):**
```go
type DataUsageRecord struct {
    ID          uint      `gorm:"column:id;primaryKey;comment:流量使用记录ID" json:"id"`
    IotCardID   uint      `gorm:"column:iot_card_id;type:bigint;not null;index;comment:IoT卡ID" json:"iot_card_id"`
    DataUsageMB int64     `gorm:"column:data_usage_mb;type:bigint;not null;comment:流量使用量(MB)" json:"data_usage_mb"`
    CheckTime   time.Time `gorm:"column:check_time;not null;comment:检查时间" json:"check_time"`
    CreatedAt   time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
}

func (DataUsageRecord) TableName() string {
    return "tb_data_usage_record"
}
```

#### Scenario: 设备-SIM 卡绑定关系模型规范化

**Given** 系统存在设备-IoT 卡绑定关系模型
**When** 开发者定义或修改绑定关系模型
**Then** 模型必须遵循统一规范
**And** 表名使用 `tb_device_sim_binding`
**And** 支持复合索引（`device_id` + `slot_position`）

**Example:**
```go
type DeviceSimBinding struct {
    gorm.Model
    BaseModel    `gorm:"embedded"`

    DeviceID     uint       `gorm:"column:device_id;type:bigint;not null;index:idx_device_slot;comment:设备ID" json:"device_id"`
    IotCardID    uint       `gorm:"column:iot_card_id;type:bigint;not null;index;comment:IoT卡ID" json:"iot_card_id"`
    SlotPosition int        `gorm:"column:slot_position;type:int;index:idx_device_slot;comment:插槽位置(1, 2, 3, 4)" json:"slot_position"`
    BindStatus   int        `gorm:"column:bind_status;type:int;default:1;not null;comment:绑定状态 1-已绑定 2-已解绑" json:"bind_status"`
    BindTime     *time.Time `gorm:"column:bind_time;comment:绑定时间" json:"bind_time,omitempty"`
    UnbindTime   *time.Time `gorm:"column:unbind_time;comment:解绑时间" json:"unbind_time,omitempty"`
}

func (DeviceSimBinding) TableName() string {
    return "tb_device_sim_binding"
}
```

### Requirement: Currency amount fields MUST use integer type (unit: cents)

All currency amount fields MUST use integer type (unit: cents). 所有货币金额字段必须使用 `int64` 类型存储，单位为"分"（1 元 = 100 分），避免浮点精度问题。

#### Scenario: 金额字段定义规范

**Given** 模型包含货币金额字段（如价格、成本、佣金、提现金额等）
**When** 开发者定义金额字段
**Then** 字段必须：
- 使用 `int64` Go 类型（不是 `float64`）
- 数据库类型为 `bigint`（不是 `decimal` 或 `numeric`）
- 默认值为 `0`
- 添加 `not null` 约束
- 注释中明确标注"（分）"单位

**Example:**
```go
CostPrice int64 `gorm:"column:cost_price;type:bigint;default:0;not null;comment:成本价（分）" json:"cost_price"`
Amount    int64 `gorm:"column:amount;type:bigint;not null;comment:订单金额（分）" json:"amount"`
```

#### Scenario: API 层金额单位转换

**Given** API 接收或返回金额数据
**When** Handler 层处理请求或响应
**Then** 必须进行单位转换：
- 输入：API 接收 `float64`（元） → 业务层使用 `int64`（分）
- 输出：业务层返回 `int64`（分） → API 返回 `float64`（元）

**Example:**
```go
// 输入转换（Handler 层）
type CreateOrderRequest struct {
    Amount float64 `json:"amount"` // 元
}

func (h *OrderHandler) CreateOrder(c *fiber.Ctx) error {
    var req CreateOrderRequest
    // ... 解析请求 ...

    // 转换：元 → 分
    amountInCents := int64(req.Amount * 100)

    // 调用 Service 层
    order, err := h.orderService.CreateOrder(ctx, amountInCents, ...)
    // ...
}

// 输出转换（Handler 层）
type OrderResponse struct {
    Amount float64 `json:"amount"` // 元
}

func (h *OrderHandler) GetOrder(c *fiber.Ctx) error {
    order, err := h.orderService.GetOrder(ctx, orderID)
    // ...

    // 转换：分 → 元
    resp := OrderResponse{
        Amount: float64(order.Amount) / 100.0,
    }

    return response.Success(c, resp)
}
```

### Requirement: Table names MUST follow unified naming conventions

All database table names MUST follow unified naming conventions. 所有数据库表名必须遵循项目约定的 `tb_` 前缀 + 单数形式。

#### Scenario: 表名命名规范

**Given** 开发者定义数据模型
**When** 实现 `TableName()` 方法
**Then** 表名必须：
- 使用 `tb_` 前缀
- 使用单数形式（不是复数）
- 使用下划线命名法（snake_case）

**Example:**
```go
// ✅ 正确
func (IotCard) TableName() string {
    return "tb_iot_card"
}

func (Device) TableName() string {
    return "tb_device"
}

func (Order) TableName() string {
    return "tb_order"
}

// ❌ 错误
func (IotCard) TableName() string {
    return "iot_cards" // 缺少 tb_ 前缀，使用复数
}

func (Device) TableName() string {
    return "devices" // 缺少 tb_ 前缀，使用复数
}
```

#### Scenario: 关联表和中间表命名

**Given** 模型表示多对多关系或绑定关系
**When** 定义关联表或中间表
**Then** 表名必须使用 `tb_` 前缀 + 完整描述性名称（单数）

**Example:**
```go
// 设备-SIM 卡绑定
func (DeviceSimBinding) TableName() string {
    return "tb_device_sim_binding" // 不是 tb_device_sim_bindings
}

// 代理套餐分配
func (AgentPackageAllocation) TableName() string {
    return "tb_agent_package_allocation" // 不是 tb_agent_package_allocations
}
```

### Requirement: All fields MUST explicitly specify database column names and types

All model fields MUST explicitly specify database column names and types. 模型字段定义必须清晰明确，不依赖 GORM 的自动转换和推断。

#### Scenario: 字段 GORM 标签完整性检查

**Given** 模型包含业务字段
**When** 开发者定义字段
**Then** 每个字段必须包含：
- `column:字段名`（显式指定数据库列名）
- `type:数据类型`（显式指定数据库类型）
- `comment:中文注释`（说明业务含义）
- 可选：`not null`、`default:值`、`index`、`uniqueIndex` 等约束

**Example:**
```go
// ✅ 完整的字段定义
Username string `gorm:"column:username;type:varchar(50);uniqueIndex:idx_username,where:deleted_at IS NULL;not null;comment:用户名" json:"username"`
Status   int    `gorm:"column:status;type:int;default:1;not null;index;comment:状态 1-启用 2-禁用" json:"status"`
Phone    string `gorm:"column:phone;type:varchar(20);comment:手机号码" json:"phone,omitempty"`

// ❌ 不完整的字段定义
Username string `gorm:"comment:用户名" json:"username"` // 缺少 column 和 type
Status   int    `gorm:"default:1" json:"status"`        // 缺少 column、type 和 comment
```

#### Scenario: 唯一索引软删除兼容

**Given** 字段需要全局唯一（如 ICCID、订单号、虚拟商品编码）
**When** 模型支持软删除（嵌入 `gorm.Model`）
**Then** 唯一索引必须包含 `where:deleted_at IS NULL` 过滤条件

**Example:**
```go
ICCID string `gorm:"column:iccid;type:varchar(50);uniqueIndex:idx_iccid,where:deleted_at IS NULL;not null;comment:ICCID(唯一标识)" json:"iccid"`
OrderNo string `gorm:"column:order_no;type:varchar(100);uniqueIndex:idx_order_no,where:deleted_at IS NULL;not null;comment:订单号" json:"order_no"`
```

**Explanation:**
- 软删除后，`deleted_at` 不为 NULL
- 索引只对 `deleted_at IS NULL` 的记录生效
- 允许软删除后重新使用相同的唯一值

### Requirement: All models MUST support audit tracking (Creator and Updater)

All business data models MUST support audit tracking (Creator and Updater). 所有业务数据模型必须记录创建人和更新人，便于审计和追溯。

#### Scenario: 嵌入 BaseModel 提供审计字段

**Given** 模型表示业务数据实体
**When** 开发者定义模型
**Then** 模型必须嵌入 `BaseModel`
**And** `BaseModel` 提供 `Creator` 和 `Updater` 字段

**Example:**
```go
type IotCard struct {
    gorm.Model
    BaseModel `gorm:"embedded"` // 提供 Creator 和 Updater

    // 业务字段...
}

// BaseModel 定义在 internal/model/base.go
type BaseModel struct {
    Creator uint `gorm:"column:creator;not null;comment:创建人ID" json:"creator"`
    Updater uint `gorm:"column:updater;not null;comment:更新人ID" json:"updater"`
}
```

#### Scenario: 业务逻辑层自动填充审计字段

**Given** Service 层或 Store 层创建或更新数据
**When** 执行数据库插入或更新操作
**Then** 必须自动填充 `Creator` 和 `Updater` 字段（从上下文获取当前用户 ID）

**Example:**
```go
// Service 层或 Store 层
func (s *IotCardService) CreateIotCard(ctx context.Context, req CreateIotCardRequest) (*IotCard, error) {
    // 从上下文获取当前用户 ID
    currentUserID := middleware.GetUserIDFromContext(ctx)

    card := &IotCard{
        BaseModel: BaseModel{
            Creator: currentUserID,
            Updater: currentUserID,
        },
        ICCID:    req.ICCID,
        CardType: req.CardType,
        // ...
    }

    if err := s.db.Create(card).Error; err != nil {
        return nil, err
    }

    return card, nil
}

func (s *IotCardService) UpdateIotCard(ctx context.Context, id uint, req UpdateIotCardRequest) error {
    currentUserID := middleware.GetUserIDFromContext(ctx)

    updates := map[string]interface{}{
        "updater":   currentUserID,
        "card_type": req.CardType,
        // ...
    }

    return s.db.Model(&IotCard{}).Where("id = ?", id).Updates(updates).Error
}
```

### Requirement: Log and record tables MUST use appropriate model structure

Append-only log and record tables MUST use simplified model structure. 对于只追加、不更新的日志表（如流量记录），必须使用简化的模型结构，不需要 `UpdatedAt` 和 `DeletedAt`。

#### Scenario: 流量记录简化模型

**Given** 模型表示只追加的日志数据（不会被修改或删除）
**When** 开发者定义日志模型
**Then** 模型可以：
- 手动定义 `ID`（不嵌入 `gorm.Model`）
- 只包含 `CreatedAt`（不需要 `UpdatedAt` 和 `DeletedAt`）
- 不嵌入 `BaseModel`（如果不需要审计）

**Example:**
```go
type DataUsageRecord struct {
    ID             uint      `gorm:"column:id;primaryKey;comment:流量使用记录ID" json:"id"`
    IotCardID      uint      `gorm:"column:iot_card_id;type:bigint;not null;index;comment:IoT卡ID" json:"iot_card_id"`
    DataUsageMB    int64     `gorm:"column:data_usage_mb;type:bigint;not null;comment:流量使用量(MB)" json:"data_usage_mb"`
    DataIncreaseMB int64     `gorm:"column:data_increase_mb;type:bigint;default:0;comment:相比上次的增量(MB)" json:"data_increase_mb"`
    CheckTime      time.Time `gorm:"column:check_time;not null;comment:检查时间" json:"check_time"`
    Source         string    `gorm:"column:source;type:varchar(50);default:'polling';comment:数据来源 polling-轮询 manual-手动 gateway-回调" json:"source"`
    CreatedAt      time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
}

func (DataUsageRecord) TableName() string {
    return "tb_data_usage_record"
}
```

**Explanation:**
- 流量记录只追加，不修改，不需要 `UpdatedAt`
- 流量记录不删除（或物理删除），不需要 `DeletedAt`
- 简化模型结构减少存储开销和查询复杂度
