重构:完善 IoT 模型架构规范和数据库设计
- 完善 GORM 模型规范:货币字段使用 int64(分为单位)、JSONB 字段规范、模型结构规范 - 修复所有 IoT 模型的架构违规问题 - 更新 CLAUDE.md 开发指南,补充完整的数据库设计规范和模型示例 - 添加数据库迁移脚本(000006)用于架构重构 - 归档 OpenSpec 变更文档(2026-01-12-fix-iot-models-violations) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
# 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`
|
||||
- 简化模型结构减少存储开销和查询复杂度
|
||||
Reference in New Issue
Block a user