diff --git a/CLAUDE.md b/CLAUDE.md index 58e2f53..0667b8f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -310,28 +310,126 @@ internal/ - 短文本(名称、标题等):`VARCHAR(255)` 或 `VARCHAR(100)` - 中等文本(描述、备注等):`VARCHAR(500)` 或 `VARCHAR(1000)` - 长文本(内容、详情等):`TEXT` 类型 +- **货币金额字段必须使用整数类型存储(分为单位)**: + - Go 类型:`int64`(不是 `float64`) + - 数据库类型:`BIGINT`(不是 `DECIMAL` 或 `NUMERIC`) + - 示例:`CostPrice int64 gorm:"column:cost_price;type:bigint;default:0;comment:成本价(分为单位)" json:"cost_price"` + - 理由:避免浮点数精度问题,确保货币计算的准确性 + - 显示时转换:金额除以 100 转换为元(如 10000 分 = 100.00 元) - 数值字段精度必须明确定义: - - 货币金额:`DECIMAL(10, 2)` 或 `DECIMAL(18, 2)`(根据业务需求) - - 百分比:`DECIMAL(5, 2)` 或 `DECIMAL(5, 4)` + - 百分比:使用 `int64` 存储千分比或万分比(如 2000 表示 20%,避免浮点精度问题) - 计数器:`INTEGER` 或 `BIGINT` + - 流量数据:`BIGINT`(如 MB、KB 为单位的流量使用量) - 所有字段必须添加中文注释,说明字段用途和业务含义 - 必填字段必须在 GORM 标签中指定 `not null` - 唯一字段必须在 GORM 标签中指定 `unique` 或通过数据库索引保证唯一性 - 枚举字段应该使用 `VARCHAR` 或 `INTEGER` 类型,并在代码中定义常量映射 +- JSONB 字段必须使用 `datatypes.JSON` 类型(从 `gorm.io/datatypes` 包导入) + - 示例:`AccountInfo datatypes.JSON gorm:"column:account_info;type:jsonb;comment:收款账户信息" json:"account_info"` + - 不使用 `pq.StringArray` 或其他 PostgreSQL 特定类型 -**字段命名示例:** +**GORM 模型结构规范:** +- **所有业务实体模型必须嵌入 `gorm.Model` 和 `BaseModel`**: + - `gorm.Model` 提供:`ID`(主键)、`CreatedAt`、`UpdatedAt`、`DeletedAt`(软删除支持) + - `BaseModel` 提供:`Creator`、`Updater`(审计字段) + - 禁止手动定义 `ID`、`CreatedAt`、`UpdatedAt`、`DeletedAt` 字段 + - 示例: + ```go + type IotCard struct { + gorm.Model + BaseModel `gorm:"embedded"` + // 业务字段... + } + ``` +- **日志表和只追加(append-only)表不需要软删除和审计字段**: + - 这类表只定义 `ID` 和 `CreatedAt`,不嵌入 `gorm.Model` 或 `BaseModel` + - 示例:`DataUsageRecord`(流量使用记录) + - 示例: + ```go + type DataUsageRecord struct { + ID uint `gorm:"column:id;primaryKey;comment:流量使用记录ID" json:"id"` + IotCardID uint `gorm:"column:iot_card_id;index;not null;comment:IoT卡ID" json:"iot_card_id"` + CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` + } + ``` + +**表名命名规范:** + +- **所有表名必须使用 `tb_` 前缀 + 单数形式**: + - 示例:`tb_iot_card`(不是 `iot_cards`) + - 示例:`tb_package`(不是 `packages`) + - 示例:`tb_order`(不是 `orders`) +- **必须实现 `TableName()` 方法显式指定表名**: + ```go + func (IotCard) TableName() string { + return "tb_iot_card" + } + ``` +- 禁止依赖 GORM 的自动表名推断(避免复数形式导致的不一致) + +**索引和约束规范:** + +- **外键字段必须添加 `index` 标签**: + - 示例:`CarrierID uint gorm:"column:carrier_id;index;not null;comment:运营商ID" json:"carrier_id"` + - 提高关联查询性能 +- **唯一索引必须支持软删除兼容性**: + - 添加 `where:deleted_at IS NULL` 条件,确保软删除后的记录不影响唯一性约束 + - 示例:`gorm:"column:iccid;type:varchar(50);uniqueIndex:idx_iot_card_iccid,where:deleted_at IS NULL;not null;comment:ICCID" json:"iccid"` + - 这样同一个 ICCID 的卡可以多次创建/删除而不违反唯一性约束 +- **复合索引命名规范**: + - 使用 `idx_{table}_{field1}_{field2}` 格式 + - 示例:`uniqueIndex:idx_device_sim_binding_device_slot,where:deleted_at IS NULL` +- 禁止定义数据库级别的外键约束(Foreign Key Constraints) + +**完整模型示例:** + +标准业务实体模型(带软删除和审计字段): ```go -type User struct { - ID uint `gorm:"column:id;primaryKey;comment:用户 ID" json:"id"` - UserID string `gorm:"column:user_id;type:varchar(100);uniqueIndex;not null;comment:用户唯一标识" json:"user_id"` - Email string `gorm:"column:email;type:varchar(255);uniqueIndex;not null;comment:用户邮箱" json:"email"` - Phone string `gorm:"column:phone;type:varchar(20);comment:手机号码" json:"phone"` - Nickname string `gorm:"column:nickname;type:varchar(100);comment:用户昵称" json:"nickname"` - Balance int64 `gorm:"column:balance;type:bigint;default:0;comment:账户余额(分为单位)" json:"balance"` - Status int `gorm:"column:status;type:int;default:1;comment:用户状态 1-正常 2-禁用" json:"status"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` +type IotCard struct { + gorm.Model // 提供 ID, CreatedAt, UpdatedAt, DeletedAt + BaseModel `gorm:"embedded"` // 提供 Creator, Updater + ICCID string `gorm:"column:iccid;type:varchar(50);uniqueIndex:idx_iot_card_iccid,where:deleted_at IS NULL;not null;comment:ICCID(唯一标识)" json:"iccid"` + CarrierID uint `gorm:"column:carrier_id;index;not null;comment:运营商ID" json:"carrier_id"` + CostPrice int64 `gorm:"column:cost_price;type:bigint;default:0;comment:成本价(分为单位)" json:"cost_price"` + DistributePrice int64 `gorm:"column:distribute_price;type:bigint;default:0;comment:分销价(分为单位)" json:"distribute_price"` + Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-未激活 2-已激活 3-已停用" json:"status"` +} + +func (IotCard) TableName() string { + return "tb_iot_card" +} +``` + +日志表模型(不需要软删除和审计): +```go +type DataUsageRecord struct { + ID uint `gorm:"column:id;primaryKey;comment:流量使用记录ID" json:"id"` + IotCardID uint `gorm:"column:iot_card_id;index;not null;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"` + CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` +} + +func (DataUsageRecord) TableName() string { + return "tb_data_usage_record" +} +``` + +包含 JSONB 字段的模型: +```go +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"` + 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"` + Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-待支付 2-已支付 3-已完成" json:"status"` +} + +func (Order) TableName() string { + return "tb_order" } ``` diff --git a/go.mod b/go.mod index 7a8d933..fb881a4 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( require ( dario.cat/mergo v1.0.2 // indirect + filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/andybalholm/brotli v1.2.0 // indirect @@ -59,6 +60,7 @@ require ( github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -129,4 +131,6 @@ require ( google.golang.org/grpc v1.75.1 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gorm.io/datatypes v1.2.7 // indirect + gorm.io/driver/mysql v1.5.6 // indirect ) diff --git a/go.sum b/go.sum index 89819a6..f8bf34c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= @@ -80,6 +82,9 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw= @@ -324,10 +329,15 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk= +gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY= +gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= +gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= diff --git a/internal/model/carrier.go b/internal/model/carrier.go index e909287..eb37d85 100644 --- a/internal/model/carrier.go +++ b/internal/model/carrier.go @@ -1,20 +1,21 @@ package model -import "time" +import ( + "gorm.io/gorm" +) // Carrier 运营商模型 // 存储运营商基础信息(中国移动、中国联通、中国电信) type Carrier struct { - ID uint `gorm:"column:id;primaryKey;comment:运营商ID" json:"id"` - CarrierCode string `gorm:"column:carrier_code;type:varchar(50);uniqueIndex;not null;comment:运营商编码(CMCC/CUCC/CTCC)" json:"carrier_code"` - CarrierName string `gorm:"column:carrier_name;type:varchar(100);not null;comment:运营商名称(中国移动/中国联通/中国电信)" json:"carrier_name"` - Description string `gorm:"column:description;type:varchar(500);comment:运营商描述" json:"description"` - Status int `gorm:"column:status;type:int;default:1;comment:状态 1-启用 2-禁用" json:"status"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` + gorm.Model + BaseModel `gorm:"embedded"` + CarrierCode string `gorm:"column:carrier_code;type:varchar(50);uniqueIndex:idx_carrier_code,where:deleted_at IS NULL;not null;comment:运营商编码(CMCC/CUCC/CTCC)" json:"carrier_code"` + CarrierName string `gorm:"column:carrier_name;type:varchar(100);not null;comment:运营商名称(中国移动/中国联通/中国电信)" json:"carrier_name"` + Description string `gorm:"column:description;type:varchar(500);comment:运营商描述" json:"description"` + Status int `gorm:"column:status;type:int;default:1;comment:状态 1-启用 2-禁用" json:"status"` } // TableName 指定表名 func (Carrier) TableName() string { - return "carriers" + return "tb_carrier" } diff --git a/internal/model/commission.go b/internal/model/commission.go index 68f5e38..d328e2d 100644 --- a/internal/model/commission.go +++ b/internal/model/commission.go @@ -1,164 +1,160 @@ package model -import "time" +import ( + "time" + + "gorm.io/gorm" +) // AgentHierarchy 代理层级关系模型 // 树形代理关系(每个代理只有一个上级) type AgentHierarchy struct { - ID uint `gorm:"column:id;primaryKey;comment:代理层级ID" json:"id"` - AgentID uint `gorm:"column:agent_id;type:bigint;uniqueIndex;not null;comment:代理用户ID" json:"agent_id"` - ParentAgentID uint `gorm:"column:parent_agent_id;type:bigint;comment:上级代理用户ID(NULL表示顶级代理)" json:"parent_agent_id"` - Level int `gorm:"column:level;type:int;not null;comment:代理层级(1, 2, 3...)" json:"level"` - Path string `gorm:"column:path;type:varchar(500);comment:代理路径(如: 1/5/12)" json:"path"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` + gorm.Model + BaseModel `gorm:"embedded"` + AgentID uint `gorm:"column:agent_id;uniqueIndex:idx_agent_hierarchy_agent,where:deleted_at IS NULL;not null;comment:代理用户ID" json:"agent_id"` + ParentAgentID uint `gorm:"column:parent_agent_id;index;comment:上级代理用户ID(NULL表示顶级代理)" json:"parent_agent_id"` + Level int `gorm:"column:level;type:int;not null;comment:代理层级(1, 2, 3...)" json:"level"` + Path string `gorm:"column:path;type:varchar(500);comment:代理路径(如: 1/5/12)" json:"path"` } // TableName 指定表名 func (AgentHierarchy) TableName() string { - return "agent_hierarchies" + return "tb_agent_hierarchy" } // CommissionRule 分佣规则模型 // 三种分佣类型:一次性/长期/组合 type CommissionRule struct { - ID uint `gorm:"column:id;primaryKey;comment:分佣规则ID" json:"id"` - AgentID uint `gorm:"column:agent_id;type:bigint;not null;comment:代理用户ID" json:"agent_id"` - BusinessType string `gorm:"column:business_type;type:varchar(50);not null;comment:业务类型" json:"business_type"` - CardType string `gorm:"column:card_type;type:varchar(50);not null;comment:卡类型 number_card-号卡 iot_card-IoT卡" json:"card_type"` - SeriesID uint `gorm:"column:series_id;type:bigint;comment:套餐系列ID(一次性分佣时用)" json:"series_id"` - PackageID uint `gorm:"column:package_id;type:bigint;comment:套餐ID(长期分佣时用)" json:"package_id"` - CommissionType string `gorm:"column:commission_type;type:varchar(50);not null;comment:分佣类型 one_time-一次性 long_term-长期 combined-组合" json:"commission_type"` - CommissionMode string `gorm:"column:commission_mode;type:varchar(20);not null;comment:分佣模式 fixed-固定金额 percent-百分比" json:"commission_mode"` - CommissionValue float64 `gorm:"column:commission_value;type:decimal(10,2);not null;comment:分佣值" json:"commission_value"` - UnfreezeDays int `gorm:"column:unfreeze_days;type:int;default:0;comment:解冻天数" json:"unfreeze_days"` - MinActivationForUnfreeze int `gorm:"column:min_activation_for_unfreeze;type:int;default:0;comment:解冻最小激活量" json:"min_activation_for_unfreeze"` - ApprovalType string `gorm:"column:approval_type;type:varchar(20);default:'auto';comment:审批类型 auto-自动 manual-人工" json:"approval_type"` - Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` + gorm.Model + BaseModel `gorm:"embedded"` + AgentID uint `gorm:"column:agent_id;index;not null;comment:代理用户ID" json:"agent_id"` + BusinessType string `gorm:"column:business_type;type:varchar(50);not null;comment:业务类型" json:"business_type"` + CardType string `gorm:"column:card_type;type:varchar(50);not null;comment:卡类型 number_card-号卡 iot_card-IoT卡" json:"card_type"` + SeriesID uint `gorm:"column:series_id;index;comment:套餐系列ID(一次性分佣时用)" json:"series_id"` + PackageID uint `gorm:"column:package_id;index;comment:套餐ID(长期分佣时用)" json:"package_id"` + CommissionType string `gorm:"column:commission_type;type:varchar(50);not null;comment:分佣类型 one_time-一次性 long_term-长期 combined-组合" json:"commission_type"` + CommissionMode string `gorm:"column:commission_mode;type:varchar(20);not null;comment:分佣模式 fixed-固定金额 percent-百分比" json:"commission_mode"` + CommissionValue int64 `gorm:"column:commission_value;type:bigint;not null;comment:分佣值(分为单位,百分比时为千分比如2000表示20%)" json:"commission_value"` + UnfreezeDays int `gorm:"column:unfreeze_days;type:int;default:0;comment:解冻天数" json:"unfreeze_days"` + MinActivationForUnfreeze int `gorm:"column:min_activation_for_unfreeze;type:int;default:0;comment:解冻最小激活量" json:"min_activation_for_unfreeze"` + ApprovalType string `gorm:"column:approval_type;type:varchar(20);default:'auto';comment:审批类型 auto-自动 manual-人工" json:"approval_type"` + Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"` } // TableName 指定表名 func (CommissionRule) TableName() string { - return "commission_rules" + return "tb_commission_rule" } // CommissionLadder 阶梯分佣配置模型 // 支持按激活量、提货量、充值量设置阶梯佣金 type CommissionLadder struct { - ID uint `gorm:"column:id;primaryKey;comment:阶梯分佣配置ID" json:"id"` - RuleID uint `gorm:"column:rule_id;type:bigint;not null;comment:分佣规则ID" json:"rule_id"` - LadderType string `gorm:"column:ladder_type;type:varchar(50);not null;comment:阶梯类型 activation-激活量 pickup-提货量 deposit-充值量" json:"ladder_type"` - ThresholdValue int `gorm:"column:threshold_value;type:int;not null;comment:阈值" json:"threshold_value"` - CommissionMode string `gorm:"column:commission_mode;type:varchar(20);not null;comment:分佣模式 fixed-固定金额 percent-百分比" json:"commission_mode"` - CommissionValue float64 `gorm:"column:commission_value;type:decimal(10,2);not null;comment:分佣值" json:"commission_value"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` + gorm.Model + BaseModel `gorm:"embedded"` + RuleID uint `gorm:"column:rule_id;index;not null;comment:分佣规则ID" json:"rule_id"` + LadderType string `gorm:"column:ladder_type;type:varchar(50);not null;comment:阶梯类型 activation-激活量 pickup-提货量 deposit-充值量" json:"ladder_type"` + ThresholdValue int `gorm:"column:threshold_value;type:int;not null;comment:阈值" json:"threshold_value"` + CommissionMode string `gorm:"column:commission_mode;type:varchar(20);not null;comment:分佣模式 fixed-固定金额 percent-百分比" json:"commission_mode"` + CommissionValue int64 `gorm:"column:commission_value;type:bigint;not null;comment:分佣值(分为单位,百分比时为千分比如2000表示20%)" json:"commission_value"` } // TableName 指定表名 func (CommissionLadder) TableName() string { - return "commission_ladder" + return "tb_commission_ladder" } // CommissionCombinedCondition 组合分佣条件模型 // 支持时间点 OR 套餐周期阈值的 OR 条件解冻 type CommissionCombinedCondition struct { - ID uint `gorm:"column:id;primaryKey;comment:组合分佣条件ID" json:"id"` - RuleID uint `gorm:"column:rule_id;type:bigint;uniqueIndex;not null;comment:分佣规则ID" json:"rule_id"` + gorm.Model + BaseModel `gorm:"embedded"` + RuleID uint `gorm:"column:rule_id;uniqueIndex:idx_commission_combined_rule,where:deleted_at IS NULL;not null;comment:分佣规则ID" json:"rule_id"` OneTimeCommissionMode string `gorm:"column:one_time_commission_mode;type:varchar(20);comment:一次性分佣模式 fixed-固定金额 percent-百分比" json:"one_time_commission_mode"` - OneTimeCommissionValue float64 `gorm:"column:one_time_commission_value;type:decimal(10,2);comment:一次性分佣值" json:"one_time_commission_value"` + OneTimeCommissionValue int64 `gorm:"column:one_time_commission_value;type:bigint;comment:一次性分佣值(分为单位,百分比时为千分比如2000表示20%)" json:"one_time_commission_value"` LongTermCommissionMode string `gorm:"column:long_term_commission_mode;type:varchar(20);comment:长期分佣模式 fixed-固定金额 percent-百分比" json:"long_term_commission_mode"` - LongTermCommissionValue float64 `gorm:"column:long_term_commission_value;type:decimal(10,2);comment:长期分佣值" json:"long_term_commission_value"` + LongTermCommissionValue int64 `gorm:"column:long_term_commission_value;type:bigint;comment:长期分佣值(分为单位,百分比时为千分比如2000表示20%)" json:"long_term_commission_value"` LongTermTriggerTimePoint *time.Time `gorm:"column:long_term_trigger_time_point;comment:长期分佣触发时间点(如实名后3个月)" json:"long_term_trigger_time_point"` LongTermTriggerPackageCycles int `gorm:"column:long_term_trigger_package_cycles;type:int;comment:长期分佣触发套餐周期数(如10个套餐周期)" json:"long_term_trigger_package_cycles"` LongTermTriggerNetworkMonths int `gorm:"column:long_term_trigger_network_months;type:int;comment:长期分佣触发在网月数(号卡专用)" json:"long_term_trigger_network_months"` LongTermUnfreezeDays int `gorm:"column:long_term_unfreeze_days;type:int;default:0;comment:长期分佣解冻天数" json:"long_term_unfreeze_days"` LongTermMinActivation int `gorm:"column:long_term_min_activation;type:int;default:0;comment:长期分佣解冻最小激活量" json:"long_term_min_activation"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` } // TableName 指定表名 func (CommissionCombinedCondition) TableName() string { - return "commission_combined_conditions" + return "tb_commission_combined_condition" } // CommissionRecord 分佣记录模型 // 记录分佣的冻结、解冻、发放状态 type CommissionRecord struct { - ID uint `gorm:"column:id;primaryKey;comment:分佣记录ID" json:"id"` - AgentID uint `gorm:"column:agent_id;type:bigint;not null;comment:代理用户ID" json:"agent_id"` - OrderID uint `gorm:"column:order_id;type:bigint;not null;comment:订单ID" json:"order_id"` - RuleID uint `gorm:"column:rule_id;type:bigint;not null;comment:分佣规则ID" json:"rule_id"` + gorm.Model + BaseModel `gorm:"embedded"` + AgentID uint `gorm:"column:agent_id;index;not null;comment:代理用户ID" json:"agent_id"` + OrderID uint `gorm:"column:order_id;index;not null;comment:订单ID" json:"order_id"` + RuleID uint `gorm:"column:rule_id;index;not null;comment:分佣规则ID" json:"rule_id"` CommissionType string `gorm:"column:commission_type;type:varchar(50);not null;comment:分佣类型 one_time-一次性 long_term-长期" json:"commission_type"` - Amount float64 `gorm:"column:amount;type:decimal(10,2);not null;comment:分佣金额(元)" json:"amount"` + Amount int64 `gorm:"column:amount;type:bigint;not null;comment:分佣金额(分为单位)" json:"amount"` Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-已冻结 2-解冻中 3-已发放 4-已失效" json:"status"` UnfrozenAt *time.Time `gorm:"column:unfrozen_at;comment:解冻时间" json:"unfrozen_at"` ReleasedAt *time.Time `gorm:"column:released_at;comment:发放时间" json:"released_at"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` } // TableName 指定表名 func (CommissionRecord) TableName() string { - return "commission_records" + return "tb_commission_record" } // CommissionApproval 分佣审批模型 // 分佣解冻审批流程 type CommissionApproval struct { - ID uint `gorm:"column:id;primaryKey;comment:分佣审批ID" json:"id"` - CommissionRecordID uint `gorm:"column:commission_record_id;type:bigint;not null;comment:分佣记录ID" json:"commission_record_id"` - ApproverID uint `gorm:"column:approver_id;type:bigint;comment:审批人用户ID" json:"approver_id"` - Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-待审批 2-已通过 3-已拒绝" json:"status"` - Reason string `gorm:"column:reason;type:text;comment:原因" json:"reason"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` + gorm.Model + BaseModel `gorm:"embedded"` + CommissionRecordID uint `gorm:"column:commission_record_id;index;not null;comment:分佣记录ID" json:"commission_record_id"` + ApproverID uint `gorm:"column:approver_id;index;comment:审批人用户ID" json:"approver_id"` + Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-待审批 2-已通过 3-已拒绝" json:"status"` + Reason string `gorm:"column:reason;type:text;comment:原因" json:"reason"` } // TableName 指定表名 func (CommissionApproval) TableName() string { - return "commission_approvals" + return "tb_commission_approval" } // CommissionTemplate 分佣模板模型 // 创建和管理分佣模板,快速为代理分配产品时设置佣金规则 type CommissionTemplate struct { - ID uint `gorm:"column:id;primaryKey;comment:分佣模板ID" json:"id"` - TemplateName string `gorm:"column:template_name;type:varchar(255);uniqueIndex;not null;comment:模板名称" json:"template_name"` - BusinessType string `gorm:"column:business_type;type:varchar(50);not null;comment:业务类型" json:"business_type"` - CardType string `gorm:"column:card_type;type:varchar(50);not null;comment:卡类型 number_card-号卡 iot_card-IoT卡" json:"card_type"` - CommissionType string `gorm:"column:commission_type;type:varchar(50);not null;comment:分佣类型 one_time-一次性 long_term-长期 combined-组合" json:"commission_type"` - CommissionMode string `gorm:"column:commission_mode;type:varchar(20);not null;comment:分佣模式 fixed-固定金额 percent-百分比" json:"commission_mode"` - CommissionValue float64 `gorm:"column:commission_value;type:decimal(10,2);not null;comment:分佣值" json:"commission_value"` - UnfreezeDays int `gorm:"column:unfreeze_days;type:int;default:0;comment:解冻天数" json:"unfreeze_days"` - MinActivationForUnfreeze int `gorm:"column:min_activation_for_unfreeze;type:int;default:0;comment:解冻最小激活量" json:"min_activation_for_unfreeze"` - ApprovalType string `gorm:"column:approval_type;type:varchar(20);default:'auto';comment:审批类型 auto-自动 manual-人工" json:"approval_type"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` + gorm.Model + BaseModel `gorm:"embedded"` + TemplateName string `gorm:"column:template_name;type:varchar(255);uniqueIndex:idx_commission_template_name,where:deleted_at IS NULL;not null;comment:模板名称" json:"template_name"` + BusinessType string `gorm:"column:business_type;type:varchar(50);not null;comment:业务类型" json:"business_type"` + CardType string `gorm:"column:card_type;type:varchar(50);not null;comment:卡类型 number_card-号卡 iot_card-IoT卡" json:"card_type"` + CommissionType string `gorm:"column:commission_type;type:varchar(50);not null;comment:分佣类型 one_time-一次性 long_term-长期 combined-组合" json:"commission_type"` + CommissionMode string `gorm:"column:commission_mode;type:varchar(20);not null;comment:分佣模式 fixed-固定金额 percent-百分比" json:"commission_mode"` + CommissionValue int64 `gorm:"column:commission_value;type:bigint;not null;comment:分佣值(分为单位,百分比时为千分比如2000表示20%)" json:"commission_value"` + UnfreezeDays int `gorm:"column:unfreeze_days;type:int;default:0;comment:解冻天数" json:"unfreeze_days"` + MinActivationForUnfreeze int `gorm:"column:min_activation_for_unfreeze;type:int;default:0;comment:解冻最小激活量" json:"min_activation_for_unfreeze"` + ApprovalType string `gorm:"column:approval_type;type:varchar(20);default:'auto';comment:审批类型 auto-自动 manual-人工" json:"approval_type"` } // TableName 指定表名 func (CommissionTemplate) TableName() string { - return "commission_templates" + return "tb_commission_template" } // CarrierSettlement 号卡运营商结算模型 // 运营商周期性结算的佣金总额,再分配给代理 type CarrierSettlement struct { - ID uint `gorm:"column:id;primaryKey;comment:运营商结算ID" json:"id"` - CommissionRecordID uint `gorm:"column:commission_record_id;type:bigint;uniqueIndex;not null;comment:分佣记录ID" json:"commission_record_id"` - AgentID uint `gorm:"column:agent_id;type:bigint;not null;comment:代理用户ID" json:"agent_id"` - SettlementMonth string `gorm:"column:settlement_month;type:varchar(20);not null;comment:结算月份(如 2026-01)" json:"settlement_month"` - SettlementAmount float64 `gorm:"column:settlement_amount;type:decimal(18,2);not null;comment:结算金额(元)" json:"settlement_amount"` - Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-待结算 2-已结算" json:"status"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` + gorm.Model + BaseModel `gorm:"embedded"` + CommissionRecordID uint `gorm:"column:commission_record_id;uniqueIndex:idx_carrier_settlement_record,where:deleted_at IS NULL;not null;comment:分佣记录ID" json:"commission_record_id"` + AgentID uint `gorm:"column:agent_id;index;not null;comment:代理用户ID" json:"agent_id"` + SettlementMonth string `gorm:"column:settlement_month;type:varchar(20);not null;comment:结算月份(如 2026-01)" json:"settlement_month"` + SettlementAmount int64 `gorm:"column:settlement_amount;type:bigint;not null;comment:结算金额(分为单位)" json:"settlement_amount"` + Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-待结算 2-已结算" json:"status"` } // TableName 指定表名 func (CarrierSettlement) TableName() string { - return "carrier_settlements" + return "tb_carrier_settlement" } diff --git a/internal/model/data_usage.go b/internal/model/data_usage.go index 5f9d372..e23a9a2 100644 --- a/internal/model/data_usage.go +++ b/internal/model/data_usage.go @@ -1,12 +1,15 @@ package model -import "time" +import ( + "time" +) // DataUsageRecord 流量使用记录模型 // 记录卡的流量历史,支持流量查询和分析 +// 注意:此模型是日志表,不需要软删除和审计字段 type DataUsageRecord struct { ID uint `gorm:"column:id;primaryKey;comment:流量使用记录ID" json:"id"` - IotCardID uint `gorm:"column:iot_card_id;type:bigint;not null;comment:IoT卡ID" json:"iot_card_id"` + IotCardID uint `gorm:"column:iot_card_id;index;not null;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"` @@ -16,5 +19,5 @@ type DataUsageRecord struct { // TableName 指定表名 func (DataUsageRecord) TableName() string { - return "data_usage_records" + return "tb_data_usage_record" } diff --git a/internal/model/device.go b/internal/model/device.go index 7a58b5b..f0e2c10 100644 --- a/internal/model/device.go +++ b/internal/model/device.go @@ -1,13 +1,18 @@ package model -import "time" +import ( + "time" + + "gorm.io/gorm" +) // Device 设备模型 // 用户的物联网设备(如 GPS 追踪器、智能传感器) // 可绑定 1-4 张 IoT 卡,主要用于批量管理和设备操作 type Device struct { - ID uint `gorm:"column:id;primaryKey;comment:设备ID" json:"id"` - DeviceNo string `gorm:"column:device_no;type:varchar(100);uniqueIndex;not null;comment:设备编号(唯一标识)" json:"device_no"` + gorm.Model + BaseModel `gorm:"embedded"` + DeviceNo string `gorm:"column:device_no;type:varchar(100);uniqueIndex:idx_device_no,where:deleted_at IS NULL;not null;comment:设备编号(唯一标识)" json:"device_no"` DeviceName string `gorm:"column:device_name;type:varchar(255);comment:设备名称" json:"device_name"` DeviceModel string `gorm:"column:device_model;type:varchar(100);comment:设备型号" json:"device_model"` DeviceType string `gorm:"column:device_type;type:varchar(50);comment:设备类型" json:"device_type"` @@ -15,17 +20,15 @@ type Device struct { Manufacturer string `gorm:"column:manufacturer;type:varchar(255);comment:制造商" json:"manufacturer"` BatchNo string `gorm:"column:batch_no;type:varchar(100);comment:批次号" json:"batch_no"` OwnerType string `gorm:"column:owner_type;type:varchar(20);default:'platform';not null;comment:所有者类型 platform-平台 agent-代理 user-用户" json:"owner_type"` - OwnerID uint `gorm:"column:owner_id;type:bigint;default:0;not null;comment:所有者ID" json:"owner_id"` + OwnerID uint `gorm:"column:owner_id;index;default:0;not null;comment:所有者ID" json:"owner_id"` Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-在库 2-已分销 3-已激活 4-已停用" json:"status"` ActivatedAt *time.Time `gorm:"column:activated_at;comment:激活时间" json:"activated_at"` DeviceUsername string `gorm:"column:device_username;type:varchar(100);comment:设备登录用户名" json:"device_username"` DevicePasswordEncrypted string `gorm:"column:device_password_encrypted;type:varchar(255);comment:设备登录密码(加密)" json:"device_password_encrypted"` DeviceAPIEndpoint string `gorm:"column:device_api_endpoint;type:varchar(500);comment:设备API端点" json:"device_api_endpoint"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` } // TableName 指定表名 func (Device) TableName() string { - return "devices" + return "tb_device" } diff --git a/internal/model/financial.go b/internal/model/financial.go index 156ee25..76a54ac 100644 --- a/internal/model/financial.go +++ b/internal/model/financial.go @@ -3,68 +3,66 @@ package model import ( "time" - "github.com/lib/pq" + "gorm.io/datatypes" + "gorm.io/gorm" ) // CommissionWithdrawalRequest 佣金提现申请模型 // 代理佣金提现申请、审批流程、提现记录查询 type CommissionWithdrawalRequest struct { - ID uint `gorm:"column:id;primaryKey;comment:提现申请ID" json:"id"` - AgentID uint `gorm:"column:agent_id;type:bigint;not null;comment:代理用户ID" json:"agent_id"` - Amount float64 `gorm:"column:amount;type:decimal(18,2);not null;comment:提现金额(元)" json:"amount"` - Fee float64 `gorm:"column:fee;type:decimal(18,2);default:0;comment:手续费(元)" json:"fee"` - ActualAmount float64 `gorm:"column:actual_amount;type:decimal(18,2);comment:实际到账金额(元)" json:"actual_amount"` + gorm.Model + BaseModel `gorm:"embedded"` + AgentID uint `gorm:"column:agent_id;index;not null;comment:代理用户ID" json:"agent_id"` + Amount int64 `gorm:"column:amount;type:bigint;not null;comment:提现金额(分为单位)" json:"amount"` + Fee int64 `gorm:"column:fee;type:bigint;default:0;comment:手续费(分为单位)" json:"fee"` + ActualAmount int64 `gorm:"column:actual_amount;type:bigint;comment:实际到账金额(分为单位)" json:"actual_amount"` WithdrawalMethod string `gorm:"column:withdrawal_method;type:varchar(20);comment:提现方式 alipay-支付宝 wechat-微信 bank-银行卡" json:"withdrawal_method"` - AccountInfo pq.StringArray `gorm:"column:account_info;type:jsonb;comment:收款账户信息(姓名、账号等)" json:"account_info"` + AccountInfo datatypes.JSON `gorm:"column:account_info;type:jsonb;comment:收款账户信息(姓名、账号等)" json:"account_info"` Status int `gorm:"column:status;type:int;default:1;comment:状态 1-待审核 2-已通过 3-已拒绝 4-已到账" json:"status"` - ApprovedBy uint `gorm:"column:approved_by;type:bigint;comment:审批人用户ID" json:"approved_by"` + ApprovedBy uint `gorm:"column:approved_by;index;comment:审批人用户ID" json:"approved_by"` ApprovedAt *time.Time `gorm:"column:approved_at;comment:审批时间" json:"approved_at"` PaidAt *time.Time `gorm:"column:paid_at;comment:到账时间" json:"paid_at"` RejectReason string `gorm:"column:reject_reason;type:text;comment:拒绝原因" json:"reject_reason"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` } // TableName 指定表名 func (CommissionWithdrawalRequest) TableName() string { - return "commission_withdrawal_requests" + return "tb_commission_withdrawal_request" } // CommissionWithdrawalSetting 佣金提现设置模型 // 提现参数配置(最低金额、手续费率、到账时间等) type CommissionWithdrawalSetting struct { - ID uint `gorm:"column:id;primaryKey;comment:提现设置ID" json:"id"` - MinWithdrawalAmount float64 `gorm:"column:min_withdrawal_amount;type:decimal(10,2);comment:最低提现金额(元)" json:"min_withdrawal_amount"` - FeeRate float64 `gorm:"column:fee_rate;type:decimal(5,4);comment:手续费率(如 0.01 表示 1%)" json:"fee_rate"` - ArrivalDays int `gorm:"column:arrival_days;type:int;comment:到账天数" json:"arrival_days"` - IsActive bool `gorm:"column:is_active;type:boolean;default:true;comment:是否生效(最新一条)" json:"is_active"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` + gorm.Model + BaseModel `gorm:"embedded"` + MinWithdrawalAmount int64 `gorm:"column:min_withdrawal_amount;type:bigint;comment:最低提现金额(分为单位)" json:"min_withdrawal_amount"` + FeeRate int64 `gorm:"column:fee_rate;type:bigint;comment:手续费率(万分比,如100表示1%)" json:"fee_rate"` + ArrivalDays int `gorm:"column:arrival_days;type:int;comment:到账天数" json:"arrival_days"` + IsActive bool `gorm:"column:is_active;type:boolean;default:true;comment:是否生效(最新一条)" json:"is_active"` } // TableName 指定表名 func (CommissionWithdrawalSetting) TableName() string { - return "commission_withdrawal_settings" + return "tb_commission_withdrawal_setting" } // PaymentMerchantSetting 收款商户设置模型 // 配置支付参数(支付宝、微信等收款账户) type PaymentMerchantSetting struct { - ID uint `gorm:"column:id;primaryKey;comment:收款商户ID" json:"id"` - UserID uint `gorm:"column:user_id;type:bigint;not null;comment:用户ID" json:"user_id"` - MerchantType string `gorm:"column:merchant_type;type:varchar(20);comment:商户类型 alipay-支付宝 wechat-微信 bank-银行卡" json:"merchant_type"` - AccountName string `gorm:"column:account_name;type:varchar(255);comment:账户名称" json:"account_name"` - AccountNumber string `gorm:"column:account_number;type:varchar(255);comment:账号" json:"account_number"` - BankName string `gorm:"column:bank_name;type:varchar(255);comment:银行名称(仅银行卡)" json:"bank_name"` - BankBranch string `gorm:"column:bank_branch;type:varchar(255);comment:开户行(仅银行卡)" json:"bank_branch"` - IsVerified bool `gorm:"column:is_verified;type:boolean;default:false;comment:是否已验证" json:"is_verified"` - IsDefault bool `gorm:"column:is_default;type:boolean;default:false;comment:是否默认账户" json:"is_default"` - Status int `gorm:"column:status;type:int;default:1;comment:状态 1-启用 2-禁用" json:"status"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` + gorm.Model + BaseModel `gorm:"embedded"` + UserID uint `gorm:"column:user_id;index;not null;comment:用户ID" json:"user_id"` + MerchantType string `gorm:"column:merchant_type;type:varchar(20);comment:商户类型 alipay-支付宝 wechat-微信 bank-银行卡" json:"merchant_type"` + AccountName string `gorm:"column:account_name;type:varchar(255);comment:账户名称" json:"account_name"` + AccountNumber string `gorm:"column:account_number;type:varchar(255);comment:账号" json:"account_number"` + BankName string `gorm:"column:bank_name;type:varchar(255);comment:银行名称(仅银行卡)" json:"bank_name"` + BankBranch string `gorm:"column:bank_branch;type:varchar(255);comment:开户行(仅银行卡)" json:"bank_branch"` + IsVerified bool `gorm:"column:is_verified;type:boolean;default:false;comment:是否已验证" json:"is_verified"` + IsDefault bool `gorm:"column:is_default;type:boolean;default:false;comment:是否默认账户" json:"is_default"` + Status int `gorm:"column:status;type:int;default:1;comment:状态 1-启用 2-禁用" json:"status"` } // TableName 指定表名 func (PaymentMerchantSetting) TableName() string { - return "payment_merchant_settings" + return "tb_payment_merchant_setting" } diff --git a/internal/model/iot_card.go b/internal/model/iot_card.go index f1146a2..9424fb2 100644 --- a/internal/model/iot_card.go +++ b/internal/model/iot_card.go @@ -1,25 +1,30 @@ package model -import "time" +import ( + "time" + + "gorm.io/gorm" +) // IotCard IoT 卡模型 // 物联网卡/流量卡的统一管理实体 // 支持平台自营、代理分销、用户购买等所有权模式 type IotCard struct { - ID uint `gorm:"column:id;primaryKey;comment:IoT 卡 ID" json:"id"` - ICCID string `gorm:"column:iccid;type:varchar(50);uniqueIndex;not null;comment:ICCID(唯一标识)" json:"iccid"` + gorm.Model + BaseModel `gorm:"embedded"` + ICCID string `gorm:"column:iccid;type:varchar(50);uniqueIndex:idx_iot_card_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;comment:运营商ID" json:"carrier_id"` + CarrierID uint `gorm:"column:carrier_id;index;not null;comment:运营商ID" json:"carrier_id"` IMSI string `gorm:"column:imsi;type:varchar(50);comment:IMSI" json:"imsi"` MSISDN string `gorm:"column:msisdn;type:varchar(20);comment:MSISDN(手机号码)" json:"msisdn"` BatchNo string `gorm:"column:batch_no;type:varchar(100);comment:批次号" json:"batch_no"` Supplier string `gorm:"column:supplier;type:varchar(255);comment:供应商" json:"supplier"` - CostPrice float64 `gorm:"column:cost_price;type:decimal(10,2);default:0;comment:成本价(元)" json:"cost_price"` - DistributePrice float64 `gorm:"column:distribute_price;type:decimal(10,2);default:0;comment:分销价(元)" json:"distribute_price"` + CostPrice int64 `gorm:"column:cost_price;type:bigint;default:0;comment:成本价(分为单位)" json:"cost_price"` + DistributePrice int64 `gorm:"column:distribute_price;type:bigint;default:0;comment:分销价(分为单位)" json:"distribute_price"` Status int `gorm:"column:status;type:int;default:1;not null;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;comment:所有者ID" json:"owner_id"` + OwnerID uint `gorm:"column:owner_id;index;default:0;not null;comment:所有者ID" json:"owner_id"` ActivatedAt *time.Time `gorm:"column:activated_at;comment:激活时间" json:"activated_at"` ActivationStatus int `gorm:"column:activation_status;type:int;default:0;not null;comment:激活状态 0-未激活 1-已激活" json:"activation_status"` RealNameStatus int `gorm:"column:real_name_status;type:int;default:0;not null;comment:实名状态 0-未实名 1-已实名(行业卡可以保持0)" json:"real_name_status"` @@ -29,11 +34,9 @@ type IotCard struct { LastDataCheckAt *time.Time `gorm:"column:last_data_check_at;comment:最后一次流量检查时间" json:"last_data_check_at"` LastRealNameCheckAt *time.Time `gorm:"column:last_real_name_check_at;comment:最后一次实名检查时间" json:"last_real_name_check_at"` LastSyncTime *time.Time `gorm:"column:last_sync_time;comment:最后一次与Gateway同步时间" json:"last_sync_time"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` } // TableName 指定表名 func (IotCard) TableName() string { - return "iot_cards" + return "tb_iot_card" } diff --git a/internal/model/number_card.go b/internal/model/number_card.go index 08157b2..632b3f6 100644 --- a/internal/model/number_card.go +++ b/internal/model/number_card.go @@ -1,25 +1,26 @@ package model -import "time" +import ( + "gorm.io/gorm" +) // NumberCard 号卡模型 // 完全独立的业务线,从上游平台下单 // 使用虚拟商品编码映射运营商订单 type NumberCard struct { - ID uint `gorm:"column:id;primaryKey;comment:号卡ID" json:"id"` - VirtualProductCode string `gorm:"column:virtual_product_code;type:varchar(100);uniqueIndex;not null;comment:虚拟商品编码(用于对应运营商订单)" json:"virtual_product_code"` - CardName string `gorm:"column:card_name;type:varchar(255);not null;comment:号卡名称" json:"card_name"` - CardType string `gorm:"column:card_type;type:varchar(50);comment:号卡类型" json:"card_type"` - Carrier string `gorm:"column:carrier;type:varchar(50);comment:运营商" json:"carrier"` - DataAmountMB int64 `gorm:"column:data_amount_mb;type:bigint;comment:流量额度(MB)" json:"data_amount_mb"` - Price float64 `gorm:"column:price;type:decimal(10,2);comment:价格(元)" json:"price"` - AgentID uint `gorm:"column:agent_id;type:bigint;comment:代理用户ID" json:"agent_id"` - Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-在售 2-下架" json:"status"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` + gorm.Model + BaseModel `gorm:"embedded"` + VirtualProductCode string `gorm:"column:virtual_product_code;type:varchar(100);uniqueIndex:idx_number_card_code,where:deleted_at IS NULL;not null;comment:虚拟商品编码(用于对应运营商订单)" json:"virtual_product_code"` + CardName string `gorm:"column:card_name;type:varchar(255);not null;comment:号卡名称" json:"card_name"` + CardType string `gorm:"column:card_type;type:varchar(50);comment:号卡类型" json:"card_type"` + Carrier string `gorm:"column:carrier;type:varchar(50);comment:运营商" json:"carrier"` + DataAmountMB int64 `gorm:"column:data_amount_mb;type:bigint;comment:流量额度(MB)" json:"data_amount_mb"` + Price int64 `gorm:"column:price;type:bigint;comment:价格(分为单位)" json:"price"` + AgentID uint `gorm:"column:agent_id;index;comment:代理用户ID" json:"agent_id"` + Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-在售 2-下架" json:"status"` } // TableName 指定表名 func (NumberCard) TableName() string { - return "number_cards" + return "tb_number_card" } diff --git a/internal/model/order.go b/internal/model/order.go index f253ee5..de12c7e 100644 --- a/internal/model/order.go +++ b/internal/model/order.go @@ -3,33 +3,33 @@ package model import ( "time" - "github.com/lib/pq" + "gorm.io/datatypes" + "gorm.io/gorm" ) // Order 订单模型 // 支持两种订单类型:套餐订单(单卡/设备级)、号卡订单 type Order struct { - ID uint `gorm:"column:id;primaryKey;comment:订单ID" json:"id"` - OrderNo string `gorm:"column:order_no;type:varchar(100);uniqueIndex;not null;comment:订单号(唯一标识)" json:"order_no"` + 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;comment:订单类型 1-套餐订单 2-号卡订单" json:"order_type"` - IotCardID uint `gorm:"column:iot_card_id;type:bigint;comment:IoT卡ID(单卡套餐订单时有值)" json:"iot_card_id"` - DeviceID uint `gorm:"column:device_id;type:bigint;comment:设备ID(设备级套餐订单时有值)" json:"device_id"` - NumberCardID uint `gorm:"column:number_card_id;type:bigint;comment:号卡ID(号卡订单时有值)" json:"number_card_id"` - PackageID uint `gorm:"column:package_id;type:bigint;comment:套餐ID(套餐订单时有值)" json:"package_id"` - UserID uint `gorm:"column:user_id;type:bigint;not null;comment:用户ID" json:"user_id"` - AgentID uint `gorm:"column:agent_id;type:bigint;comment:代理用户ID" json:"agent_id"` - Amount float64 `gorm:"column:amount;type:decimal(10,2);not null;comment:订单金额(元)" json:"amount"` + IotCardID uint `gorm:"column:iot_card_id;index;comment:IoT卡ID(单卡套餐订单时有值)" json:"iot_card_id"` + DeviceID uint `gorm:"column:device_id;index;comment:设备ID(设备级套餐订单时有值)" json:"device_id"` + NumberCardID uint `gorm:"column:number_card_id;index;comment:号卡ID(号卡订单时有值)" json:"number_card_id"` + PackageID uint `gorm:"column:package_id;index;comment:套餐ID(套餐订单时有值)" json:"package_id"` + UserID uint `gorm:"column:user_id;index;not null;comment:用户ID" json:"user_id"` + AgentID uint `gorm:"column:agent_id;index;comment:代理用户ID" json:"agent_id"` + Amount int64 `gorm:"column:amount;type:bigint;not null;comment:订单金额(分为单位)" json:"amount"` PaymentMethod string `gorm:"column:payment_method;type:varchar(20);comment:支付方式 wallet-钱包 online-在线支付 carrier-运营商支付" json:"payment_method"` Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-待支付 2-已支付 3-已完成 4-已取消 5-已退款" json:"status"` CarrierOrderID string `gorm:"column:carrier_order_id;type:varchar(255);comment:运营商订单ID" json:"carrier_order_id"` - CarrierOrderData pq.StringArray `gorm:"column:carrier_order_data;type:jsonb;comment:运营商订单原始数据(JSON)" json:"carrier_order_data"` + CarrierOrderData datatypes.JSON `gorm:"column:carrier_order_data;type:jsonb;comment:运营商订单原始数据(JSON)" json:"carrier_order_data"` PaidAt *time.Time `gorm:"column:paid_at;comment:支付时间" json:"paid_at"` CompletedAt *time.Time `gorm:"column:completed_at;comment:完成时间" json:"completed_at"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` } // TableName 指定表名 func (Order) TableName() string { - return "orders" + return "tb_order" } diff --git a/internal/model/package.go b/internal/model/package.go index 63bbd31..bd20afa 100644 --- a/internal/model/package.go +++ b/internal/model/package.go @@ -1,94 +1,95 @@ package model -import "time" +import ( + "time" + + "gorm.io/gorm" +) // PackageSeries 套餐系列模型 // 套餐的分组,用于一次性分佣规则配置 type PackageSeries struct { - ID uint `gorm:"column:id;primaryKey;comment:套餐系列ID" json:"id"` - SeriesCode string `gorm:"column:series_code;type:varchar(100);uniqueIndex;not null;comment:系列编码" json:"series_code"` - SeriesName string `gorm:"column:series_name;type:varchar(255);not null;comment:系列名称" json:"series_name"` - Description string `gorm:"column:description;type:text;comment:描述" json:"description"` - Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` + gorm.Model + BaseModel `gorm:"embedded"` + SeriesCode string `gorm:"column:series_code;type:varchar(100);uniqueIndex:idx_package_series_code,where:deleted_at IS NULL;not null;comment:系列编码" json:"series_code"` + SeriesName string `gorm:"column:series_name;type:varchar(255);not null;comment:系列名称" json:"series_name"` + Description string `gorm:"column:description;type:text;comment:描述" json:"description"` + Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"` } // TableName 指定表名 func (PackageSeries) TableName() string { - return "package_series" + return "tb_package_series" } // Package 套餐模型 // 只适用于 IoT 卡,支持真流量/虚流量共存机制 type Package struct { - ID uint `gorm:"column:id;primaryKey;comment:套餐ID" json:"id"` - PackageCode string `gorm:"column:package_code;type:varchar(100);uniqueIndex;not null;comment:套餐编码" json:"package_code"` - PackageName string `gorm:"column:package_name;type:varchar(255);not null;comment:套餐名称" json:"package_name"` - SeriesID uint `gorm:"column:series_id;type:bigint;comment:套餐系列ID" json:"series_id"` - PackageType string `gorm:"column:package_type;type:varchar(50);not null;comment:套餐类型 formal-正式套餐 addon-附加套餐" json:"package_type"` - DurationMonths int `gorm:"column:duration_months;type:int;not null;comment:套餐时长(月数) 1-月套餐 12-年套餐" json:"duration_months"` - DataType string `gorm:"column:data_type;type:varchar(20);comment:流量类型 real-真流量 virtual-虚流量" json:"data_type"` - RealDataMB int64 `gorm:"column:real_data_mb;type:bigint;default:0;comment:真流量额度(MB)" json:"real_data_mb"` - VirtualDataMB int64 `gorm:"column:virtual_data_mb;type:bigint;default:0;comment:虚流量额度(MB,用于停机判断)" json:"virtual_data_mb"` - DataAmountMB int64 `gorm:"column:data_amount_mb;type:bigint;default:0;comment:总流量额度(MB)" json:"data_amount_mb"` - Price float64 `gorm:"column:price;type:decimal(10,2);not null;comment:套餐价格(元)" json:"price"` - Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` + gorm.Model + BaseModel `gorm:"embedded"` + PackageCode string `gorm:"column:package_code;type:varchar(100);uniqueIndex:idx_package_code,where:deleted_at IS NULL;not null;comment:套餐编码" json:"package_code"` + PackageName string `gorm:"column:package_name;type:varchar(255);not null;comment:套餐名称" json:"package_name"` + SeriesID uint `gorm:"column:series_id;index;comment:套餐系列ID" json:"series_id"` + PackageType string `gorm:"column:package_type;type:varchar(50);not null;comment:套餐类型 formal-正式套餐 addon-附加套餐" json:"package_type"` + DurationMonths int `gorm:"column:duration_months;type:int;not null;comment:套餐时长(月数) 1-月套餐 12-年套餐" json:"duration_months"` + DataType string `gorm:"column:data_type;type:varchar(20);comment:流量类型 real-真流量 virtual-虚流量" json:"data_type"` + RealDataMB int64 `gorm:"column:real_data_mb;type:bigint;default:0;comment:真流量额度(MB)" json:"real_data_mb"` + VirtualDataMB int64 `gorm:"column:virtual_data_mb;type:bigint;default:0;comment:虚流量额度(MB,用于停机判断)" json:"virtual_data_mb"` + DataAmountMB int64 `gorm:"column:data_amount_mb;type:bigint;default:0;comment:总流量额度(MB)" json:"data_amount_mb"` + Price int64 `gorm:"column:price;type:bigint;not null;comment:套餐价格(分为单位)" json:"price"` + Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"` } // TableName 指定表名 func (Package) TableName() string { - return "packages" + return "tb_package" } // AgentPackageAllocation 代理套餐分配模型 // 为直属下级代理分配套餐,设置佣金模式 type AgentPackageAllocation struct { - ID uint `gorm:"column:id;primaryKey;comment:分配ID" json:"id"` - AgentID uint `gorm:"column:agent_id;type:bigint;not null;comment:代理用户ID" json:"agent_id"` - PackageID uint `gorm:"column:package_id;type:bigint;not null;comment:套餐ID" json:"package_id"` - CostPrice float64 `gorm:"column:cost_price;type:decimal(10,2);not null;comment:成本价(元)" json:"cost_price"` - RetailPrice float64 `gorm:"column:retail_price;type:decimal(10,2);not null;comment:零售价(元)" json:"retail_price"` - Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` + gorm.Model + BaseModel `gorm:"embedded"` + AgentID uint `gorm:"column:agent_id;index;not null;comment:代理用户ID" json:"agent_id"` + PackageID uint `gorm:"column:package_id;index;not null;comment:套餐ID" json:"package_id"` + CostPrice int64 `gorm:"column:cost_price;type:bigint;not null;comment:成本价(分为单位)" json:"cost_price"` + RetailPrice int64 `gorm:"column:retail_price;type:bigint;not null;comment:零售价(分为单位)" json:"retail_price"` + Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"` } // TableName 指定表名 func (AgentPackageAllocation) TableName() string { - return "agent_package_allocations" + return "tb_agent_package_allocation" } // DeviceSimBinding 设备-IoT卡绑定关系模型 // 管理设备与 IoT 卡的多对多绑定关系(1 设备绑定 1-4 张 IoT 卡) type DeviceSimBinding struct { - ID uint `gorm:"column:id;primaryKey;comment:绑定ID" json:"id"` - DeviceID uint `gorm:"column:device_id;type:bigint;not null;comment:设备ID" json:"device_id"` - IotCardID uint `gorm:"column:iot_card_id;type:bigint;not null;comment:IoT卡ID" json:"iot_card_id"` - SlotPosition int `gorm:"column:slot_position;type:int;comment:插槽位置(1, 2, 3, 4)" json:"slot_position"` + gorm.Model + BaseModel `gorm:"embedded"` + DeviceID uint `gorm:"column:device_id;index:idx_device_slot;not null;comment:设备ID" json:"device_id"` + IotCardID uint `gorm:"column:iot_card_id;index;not null;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;comment:绑定状态 1-已绑定 2-已解绑" json:"bind_status"` BindTime *time.Time `gorm:"column:bind_time;comment:绑定时间" json:"bind_time"` UnbindTime *time.Time `gorm:"column:unbind_time;comment:解绑时间" json:"unbind_time"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` } // TableName 指定表名 func (DeviceSimBinding) TableName() string { - return "device_sim_bindings" + return "tb_device_sim_binding" } // PackageUsage 套餐使用情况模型 // 跟踪单卡套餐和设备级套餐的流量使用 type PackageUsage struct { - ID uint `gorm:"column:id;primaryKey;comment:套餐使用ID" json:"id"` - OrderID uint `gorm:"column:order_id;type:bigint;not null;comment:订单ID" json:"order_id"` - PackageID uint `gorm:"column:package_id;type:bigint;not null;comment:套餐ID" json:"package_id"` + gorm.Model + BaseModel `gorm:"embedded"` + OrderID uint `gorm:"column:order_id;index;not null;comment:订单ID" json:"order_id"` + PackageID uint `gorm:"column:package_id;index;not null;comment:套餐ID" json:"package_id"` UsageType string `gorm:"column:usage_type;type:varchar(20);not null;comment:使用类型 single_card-单卡套餐 device-设备级套餐" json:"usage_type"` - IotCardID uint `gorm:"column:iot_card_id;type:bigint;comment:IoT卡ID(单卡套餐时有值)" json:"iot_card_id"` - DeviceID uint `gorm:"column:device_id;type:bigint;comment:设备ID(设备级套餐时有值)" json:"device_id"` + IotCardID uint `gorm:"column:iot_card_id;index;comment:IoT卡ID(单卡套餐时有值)" json:"iot_card_id"` + DeviceID uint `gorm:"column:device_id;index;comment:设备ID(设备级套餐时有值)" json:"device_id"` DataLimitMB int64 `gorm:"column:data_limit_mb;type:bigint;not null;comment:流量限额(MB)" json:"data_limit_mb"` DataUsageMB int64 `gorm:"column:data_usage_mb;type:bigint;default:0;comment:已使用流量(MB)" json:"data_usage_mb"` RealDataUsageMB int64 `gorm:"column:real_data_usage_mb;type:bigint;default:0;comment:真流量使用(MB)" json:"real_data_usage_mb"` @@ -97,11 +98,9 @@ type PackageUsage struct { ExpiresAt time.Time `gorm:"column:expires_at;not null;comment:套餐过期时间" json:"expires_at"` Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-生效中 2-已用完 3-已过期" json:"status"` LastPackageCheckAt *time.Time `gorm:"column:last_package_check_at;comment:最后一次套餐流量检查时间" json:"last_package_check_at"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` } // TableName 指定表名 func (PackageUsage) TableName() string { - return "package_usages" + return "tb_package_usage" } diff --git a/internal/model/polling.go b/internal/model/polling.go index ac3854c..7545f0c 100644 --- a/internal/model/polling.go +++ b/internal/model/polling.go @@ -1,28 +1,29 @@ package model -import "time" +import ( + "gorm.io/gorm" +) // PollingConfig 轮询配置模型 // 支持梯度轮询策略(实名检查、卡流量检查、套餐流量检查) type PollingConfig struct { - ID uint `gorm:"column:id;primaryKey;comment:轮询配置ID" json:"id"` - ConfigName string `gorm:"column:config_name;type:varchar(100);uniqueIndex;not null;comment:配置名称(如 未实名卡、实名卡)" json:"config_name"` - Description string `gorm:"column:description;type:varchar(500);comment:配置描述" json:"description"` - CardCondition string `gorm:"column:card_condition;type:varchar(50);comment:卡状态条件 not_real_name-未实名 real_name-已实名 activated-已激活 suspended-已停用" json:"card_condition"` - CarrierID uint `gorm:"column:carrier_id;type:bigint;comment:运营商ID(NULL表示所有运营商)" json:"carrier_id"` - RealNameCheckEnabled bool `gorm:"column:real_name_check_enabled;type:boolean;default:false;comment:是否启用实名检查" json:"real_name_check_enabled"` - RealNameCheckInterval int `gorm:"column:real_name_check_interval;type:int;default:60;comment:实名检查间隔(秒)" json:"real_name_check_interval"` - CardDataCheckEnabled bool `gorm:"column:card_data_check_enabled;type:boolean;default:false;comment:是否启用卡流量检查" json:"card_data_check_enabled"` - CardDataCheckInterval int `gorm:"column:card_data_check_interval;type:int;default:60;comment:卡流量检查间隔(秒)" json:"card_data_check_interval"` - PackageCheckEnabled bool `gorm:"column:package_check_enabled;type:boolean;default:false;comment:是否启用套餐流量检查" json:"package_check_enabled"` - PackageCheckInterval int `gorm:"column:package_check_interval;type:int;default:60;comment:套餐流量检查间隔(秒)" json:"package_check_interval"` - Priority int `gorm:"column:priority;type:int;default:100;not null;comment:优先级(数字越小优先级越高)" json:"priority"` - Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` + gorm.Model + BaseModel `gorm:"embedded"` + ConfigName string `gorm:"column:config_name;type:varchar(100);uniqueIndex:idx_polling_config_name,where:deleted_at IS NULL;not null;comment:配置名称(如 未实名卡、实名卡)" json:"config_name"` + Description string `gorm:"column:description;type:varchar(500);comment:配置描述" json:"description"` + CardCondition string `gorm:"column:card_condition;type:varchar(50);comment:卡状态条件 not_real_name-未实名 real_name-已实名 activated-已激活 suspended-已停用" json:"card_condition"` + CarrierID uint `gorm:"column:carrier_id;index;comment:运营商ID(NULL表示所有运营商)" json:"carrier_id"` + RealNameCheckEnabled bool `gorm:"column:real_name_check_enabled;type:boolean;default:false;comment:是否启用实名检查" json:"real_name_check_enabled"` + RealNameCheckInterval int `gorm:"column:real_name_check_interval;type:int;default:60;comment:实名检查间隔(秒)" json:"real_name_check_interval"` + CardDataCheckEnabled bool `gorm:"column:card_data_check_enabled;type:boolean;default:false;comment:是否启用卡流量检查" json:"card_data_check_enabled"` + CardDataCheckInterval int `gorm:"column:card_data_check_interval;type:int;default:60;comment:卡流量检查间隔(秒)" json:"card_data_check_interval"` + PackageCheckEnabled bool `gorm:"column:package_check_enabled;type:boolean;default:false;comment:是否启用套餐流量检查" json:"package_check_enabled"` + PackageCheckInterval int `gorm:"column:package_check_interval;type:int;default:60;comment:套餐流量检查间隔(秒)" json:"package_check_interval"` + Priority int `gorm:"column:priority;type:int;default:100;not null;comment:优先级(数字越小优先级越高)" json:"priority"` + Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"` } // TableName 指定表名 func (PollingConfig) TableName() string { - return "polling_configs" + return "tb_polling_config" } diff --git a/internal/model/system.go b/internal/model/system.go index 930b2f1..b8f5bce 100644 --- a/internal/model/system.go +++ b/internal/model/system.go @@ -1,45 +1,47 @@ package model -import "time" +import ( + "time" + + "gorm.io/gorm" +) // DevCapabilityConfig 开发能力配置模型 // 管理 API 对接参数(AppID、AppSecret、回调地址等) type DevCapabilityConfig struct { - ID uint `gorm:"column:id;primaryKey;comment:开发能力配置ID" json:"id"` - UserID uint `gorm:"column:user_id;type:bigint;not null;comment:用户ID(平台或代理)" json:"user_id"` - AppName string `gorm:"column:app_name;type:varchar(255);comment:应用名称" json:"app_name"` - AppID string `gorm:"column:app_id;type:varchar(100);uniqueIndex;comment:应用ID" json:"app_id"` - AppSecret string `gorm:"column:app_secret;type:varchar(255);comment:应用密钥" json:"app_secret"` - CallbackURL string `gorm:"column:callback_url;type:varchar(500);comment:回调地址" json:"callback_url"` - IPWhitelist string `gorm:"column:ip_whitelist;type:text;comment:IP白名单(多个IP用逗号分隔)" json:"ip_whitelist"` - Status int `gorm:"column:status;type:int;default:1;comment:状态 1-启用 2-禁用" json:"status"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` + gorm.Model + BaseModel `gorm:"embedded"` + UserID uint `gorm:"column:user_id;index;not null;comment:用户ID(平台或代理)" json:"user_id"` + AppName string `gorm:"column:app_name;type:varchar(255);comment:应用名称" json:"app_name"` + AppID string `gorm:"column:app_id;type:varchar(100);uniqueIndex:idx_dev_capability_app,where:deleted_at IS NULL;comment:应用ID" json:"app_id"` + AppSecret string `gorm:"column:app_secret;type:varchar(255);comment:应用密钥" json:"app_secret"` + CallbackURL string `gorm:"column:callback_url;type:varchar(500);comment:回调地址" json:"callback_url"` + IPWhitelist string `gorm:"column:ip_whitelist;type:text;comment:IP白名单(多个IP用逗号分隔)" json:"ip_whitelist"` + Status int `gorm:"column:status;type:int;default:1;comment:状态 1-启用 2-禁用" json:"status"` } // TableName 指定表名 func (DevCapabilityConfig) TableName() string { - return "dev_capability_configs" + return "tb_dev_capability_config" } // CardReplacementRequest 换卡申请模型 // 客户提交的换卡申请管理,处理换卡申请 type CardReplacementRequest struct { - ID uint `gorm:"column:id;primaryKey;comment:换卡申请ID" json:"id"` - UserID uint `gorm:"column:user_id;type:bigint;not null;comment:申请用户ID" json:"user_id"` + gorm.Model + BaseModel `gorm:"embedded"` + UserID uint `gorm:"column:user_id;index;not null;comment:申请用户ID" json:"user_id"` OldICCID string `gorm:"column:old_iccid;type:varchar(50);not null;comment:旧卡ICCID" json:"old_iccid"` NewICCID string `gorm:"column:new_iccid;type:varchar(50);comment:新卡ICCID(审批时填充)" json:"new_iccid"` Reason string `gorm:"column:reason;type:text;comment:换卡原因" json:"reason"` Status int `gorm:"column:status;type:int;default:1;comment:状态 1-待处理 2-已通过 3-已拒绝 4-已完成" json:"status"` - ApprovedBy uint `gorm:"column:approved_by;type:bigint;comment:处理人用户ID" json:"approved_by"` + ApprovedBy uint `gorm:"column:approved_by;index;comment:处理人用户ID" json:"approved_by"` ApprovedAt *time.Time `gorm:"column:approved_at;comment:处理时间" json:"approved_at"` CompletedAt *time.Time `gorm:"column:completed_at;comment:完成时间(新卡激活时间)" json:"completed_at"` RejectReason string `gorm:"column:reject_reason;type:text;comment:拒绝原因" json:"reject_reason"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` } // TableName 指定表名 func (CardReplacementRequest) TableName() string { - return "card_replacement_requests" + return "tb_card_replacement_request" } diff --git a/migrations/000006_refactor_iot_models_architecture.down.sql b/migrations/000006_refactor_iot_models_architecture.down.sql new file mode 100644 index 0000000..06c46f9 --- /dev/null +++ b/migrations/000006_refactor_iot_models_architecture.down.sql @@ -0,0 +1,251 @@ +-- IoT 模型架构重构回滚脚本 +-- 创建时间: 2026-01-12 +-- 说明: 回滚所有架构重构变更 +-- 1. 恢复唯一索引(移除软删除支持) +-- 2. 恢复金额字段为 DECIMAL 类型 +-- 3. 删除软删除字段和审计字段 +-- 4. 恢复表名为复数形式 + +-- ======================================== +-- 阶段 1: 恢复唯一约束(移除软删除支持) +-- ======================================== + +-- 1.1 运营商表 +DROP INDEX IF EXISTS idx_carrier_code; +ALTER TABLE tb_carrier ADD CONSTRAINT carriers_carrier_code_key UNIQUE (carrier_code); + +-- 1.2 IoT 卡表 +DROP INDEX IF EXISTS idx_iot_card_iccid; +ALTER TABLE tb_iot_card ADD CONSTRAINT iot_cards_iccid_key UNIQUE (iccid); + +-- 1.3 设备表 +DROP INDEX IF EXISTS idx_device_no; +ALTER TABLE tb_device ADD CONSTRAINT devices_device_no_key UNIQUE (device_no); + +-- 1.4 号卡表 +DROP INDEX IF EXISTS idx_number_card_code; +ALTER TABLE tb_number_card ADD CONSTRAINT number_cards_virtual_product_code_key UNIQUE (virtual_product_code); + +-- 1.5 套餐系列表 +DROP INDEX IF EXISTS idx_package_series_code; +ALTER TABLE tb_package_series ADD CONSTRAINT package_series_series_code_key UNIQUE (series_code); + +-- 1.6 套餐表 +DROP INDEX IF EXISTS idx_package_code; +ALTER TABLE tb_package ADD CONSTRAINT packages_package_code_key UNIQUE (package_code); + +-- 1.7 代理套餐分配表 +DROP INDEX IF EXISTS idx_agent_package_allocation_agent_package; +ALTER TABLE tb_agent_package_allocation ADD CONSTRAINT uk_agent_package UNIQUE (agent_id, package_id); + +-- 1.8 设备-SIM绑定表:跳过,保持原样 + +-- 1.9 订单表 +DROP INDEX IF EXISTS idx_order_no; +ALTER TABLE tb_order ADD CONSTRAINT orders_order_no_key UNIQUE (order_no); + +-- 1.10 轮询配置表 +DROP INDEX IF EXISTS idx_polling_config_name; +ALTER TABLE tb_polling_config ADD CONSTRAINT polling_configs_config_name_key UNIQUE (config_name); + +-- 1.11 代理层级表 +DROP INDEX IF EXISTS idx_agent_hierarchy_agent; +ALTER TABLE tb_agent_hierarchy ADD CONSTRAINT agent_hierarchies_agent_id_key UNIQUE (agent_id); + +-- 1.12 组合分佣条件表 +DROP INDEX IF EXISTS idx_commission_combined_rule; +ALTER TABLE tb_commission_combined_condition ADD CONSTRAINT commission_combined_conditions_rule_id_key UNIQUE (rule_id); + +-- 1.13 分佣模板表 +DROP INDEX IF EXISTS idx_commission_template_name; +ALTER TABLE tb_commission_template ADD CONSTRAINT commission_templates_template_name_key UNIQUE (template_name); + +-- 1.14 运营商结算表 +DROP INDEX IF EXISTS idx_carrier_settlement_record; +ALTER TABLE tb_carrier_settlement ADD CONSTRAINT carrier_settlements_commission_record_id_key UNIQUE (commission_record_id); + +-- 1.15 开发能力配置表 +DROP INDEX IF EXISTS idx_dev_capability_app; +ALTER TABLE tb_dev_capability_config ADD CONSTRAINT dev_capability_configs_app_id_key UNIQUE (app_id); + +-- ======================================== +-- 阶段 2: 恢复金额字段为 DECIMAL 类型(分 / 100 = 元) +-- ======================================== + +-- 2.1 IoT 卡表 +ALTER TABLE tb_iot_card + ALTER COLUMN cost_price TYPE DECIMAL(10,2) USING (cost_price / 100.0)::DECIMAL(10,2), + ALTER COLUMN distribute_price TYPE DECIMAL(10,2) USING (distribute_price / 100.0)::DECIMAL(10,2); + +COMMENT ON COLUMN tb_iot_card.cost_price IS '成本价(元)'; +COMMENT ON COLUMN tb_iot_card.distribute_price IS '分销价(元)'; + +-- 2.2 号卡表 +ALTER TABLE tb_number_card + ALTER COLUMN price TYPE DECIMAL(10,2) USING (price / 100.0)::DECIMAL(10,2); + +COMMENT ON COLUMN tb_number_card.price IS '价格(元)'; + +-- 2.3 套餐表 +ALTER TABLE tb_package + ALTER COLUMN price TYPE DECIMAL(10,2) USING (price / 100.0)::DECIMAL(10,2); + +COMMENT ON COLUMN tb_package.price IS '套餐价格(元)'; + +-- 2.4 代理套餐分配表 +ALTER TABLE tb_agent_package_allocation + ALTER COLUMN cost_price TYPE DECIMAL(10,2) USING (cost_price / 100.0)::DECIMAL(10,2), + ALTER COLUMN retail_price TYPE DECIMAL(10,2) USING (retail_price / 100.0)::DECIMAL(10,2); + +COMMENT ON COLUMN tb_agent_package_allocation.cost_price IS '成本价(元)'; +COMMENT ON COLUMN tb_agent_package_allocation.retail_price IS '零售价(元)'; + +-- 2.5 订单表 +ALTER TABLE tb_order + ALTER COLUMN amount TYPE DECIMAL(10,2) USING (amount / 100.0)::DECIMAL(10,2); + +COMMENT ON COLUMN tb_order.amount IS '订单金额(元)'; + +-- 2.6 分佣规则表 +ALTER TABLE tb_commission_rule + ALTER COLUMN commission_value TYPE DECIMAL(10,2) USING (commission_value / 100.0)::DECIMAL(10,2); + +COMMENT ON COLUMN tb_commission_rule.commission_value IS '分佣值(元或百分比)'; + +-- 2.7 阶梯分佣配置表 +ALTER TABLE tb_commission_ladder + ALTER COLUMN commission_value TYPE DECIMAL(10,2) USING (commission_value / 100.0)::DECIMAL(10,2); + +COMMENT ON COLUMN tb_commission_ladder.commission_value IS '分佣值(元或百分比)'; + +-- 2.8 组合分佣条件表 +ALTER TABLE tb_commission_combined_condition + ALTER COLUMN one_time_commission_value TYPE DECIMAL(10,2) USING (one_time_commission_value / 100.0)::DECIMAL(10,2), + ALTER COLUMN long_term_commission_value TYPE DECIMAL(10,2) USING (long_term_commission_value / 100.0)::DECIMAL(10,2); + +COMMENT ON COLUMN tb_commission_combined_condition.one_time_commission_value IS '一次性分佣值(元或百分比)'; +COMMENT ON COLUMN tb_commission_combined_condition.long_term_commission_value IS '长期分佣值(元或百分比)'; + +-- 2.9 分佣记录表 +ALTER TABLE tb_commission_record + ALTER COLUMN amount TYPE DECIMAL(10,2) USING (amount / 100.0)::DECIMAL(10,2); + +COMMENT ON COLUMN tb_commission_record.amount IS '分佣金额(元)'; + +-- 2.10 分佣模板表 +ALTER TABLE tb_commission_template + ALTER COLUMN commission_value TYPE DECIMAL(10,2) USING (commission_value / 100.0)::DECIMAL(10,2); + +COMMENT ON COLUMN tb_commission_template.commission_value IS '分佣值(元或百分比)'; + +-- 2.11 运营商结算表 +ALTER TABLE tb_carrier_settlement + ALTER COLUMN settlement_amount TYPE DECIMAL(10,2) USING (settlement_amount / 100.0)::DECIMAL(10,2); + +COMMENT ON COLUMN tb_carrier_settlement.settlement_amount IS '结算金额(元)'; + +-- 2.12 佣金提现申请表 +ALTER TABLE tb_commission_withdrawal_request + ALTER COLUMN amount TYPE DECIMAL(10,2) USING (amount / 100.0)::DECIMAL(10,2), + ALTER COLUMN fee TYPE DECIMAL(10,2) USING (fee / 100.0)::DECIMAL(10,2), + ALTER COLUMN actual_amount TYPE DECIMAL(10,2) USING (actual_amount / 100.0)::DECIMAL(10,2); + +COMMENT ON COLUMN tb_commission_withdrawal_request.amount IS '提现金额(元)'; +COMMENT ON COLUMN tb_commission_withdrawal_request.fee IS '手续费(元)'; +COMMENT ON COLUMN tb_commission_withdrawal_request.actual_amount IS '实际到账金额(元)'; + +-- 2.13 佣金提现设置表 +ALTER TABLE tb_commission_withdrawal_setting + ALTER COLUMN min_withdrawal_amount TYPE DECIMAL(10,2) USING (min_withdrawal_amount / 100.0)::DECIMAL(10,2); + +COMMENT ON COLUMN tb_commission_withdrawal_setting.min_withdrawal_amount IS '最低提现金额(元)'; + +-- ======================================== +-- 阶段 3: 删除软删除字段和审计字段 +-- ======================================== + +ALTER TABLE tb_carrier DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_iot_card DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_device DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_number_card DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_package_series DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_package DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_agent_package_allocation DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_device_sim_binding DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_order DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_package_usage DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_polling_config DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +-- 注意: tb_data_usage_record 没有添加这些字段,所以不需要删除 +ALTER TABLE tb_agent_hierarchy DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_commission_rule DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_commission_ladder DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_commission_combined_condition DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_commission_record DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_commission_approval DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_commission_template DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_carrier_settlement DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_commission_withdrawal_request DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_commission_withdrawal_setting DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_payment_merchant_setting DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_dev_capability_config DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; +ALTER TABLE tb_card_replacement_request DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater; + +-- ======================================== +-- 阶段 4: 恢复表名为复数形式 +-- ======================================== + +ALTER TABLE tb_carrier RENAME TO carriers; +ALTER TABLE tb_iot_card RENAME TO iot_cards; +ALTER TABLE tb_device RENAME TO devices; +ALTER TABLE tb_number_card RENAME TO number_cards; +ALTER TABLE tb_package_series RENAME TO package_series; +ALTER TABLE tb_package RENAME TO packages; +ALTER TABLE tb_agent_package_allocation RENAME TO agent_package_allocations; +ALTER TABLE tb_device_sim_binding RENAME TO device_sim_bindings; +ALTER TABLE tb_order RENAME TO orders; +ALTER TABLE tb_package_usage RENAME TO package_usages; +ALTER TABLE tb_polling_config RENAME TO polling_configs; +ALTER TABLE tb_data_usage_record RENAME TO data_usage_records; +ALTER TABLE tb_agent_hierarchy RENAME TO agent_hierarchies; +ALTER TABLE tb_commission_rule RENAME TO commission_rules; +ALTER TABLE tb_commission_ladder RENAME TO commission_ladder; +ALTER TABLE tb_commission_combined_condition RENAME TO commission_combined_conditions; +ALTER TABLE tb_commission_record RENAME TO commission_records; +ALTER TABLE tb_commission_approval RENAME TO commission_approvals; +ALTER TABLE tb_commission_template RENAME TO commission_templates; +ALTER TABLE tb_carrier_settlement RENAME TO carrier_settlements; +ALTER TABLE tb_commission_withdrawal_request RENAME TO commission_withdrawal_requests; +ALTER TABLE tb_commission_withdrawal_setting RENAME TO commission_withdrawal_settings; +ALTER TABLE tb_payment_merchant_setting RENAME TO payment_merchant_settings; +ALTER TABLE tb_dev_capability_config RENAME TO dev_capability_configs; +ALTER TABLE tb_card_replacement_request RENAME TO card_replacement_requests; + +-- ======================================== +-- 阶段 5: 恢复表注释 +-- ======================================== + +COMMENT ON TABLE carriers IS '运营商表'; +COMMENT ON TABLE iot_cards IS 'IoT 卡表(物联网卡/流量卡)'; +COMMENT ON TABLE devices IS '设备表(可容纳1-4张SIM卡)'; +COMMENT ON TABLE number_cards IS '号卡表(虚拟商品)'; +COMMENT ON TABLE package_series IS '套餐系列表'; +COMMENT ON TABLE packages IS '套餐表'; +COMMENT ON TABLE agent_package_allocations IS '代理套餐分配表'; +COMMENT ON TABLE device_sim_bindings IS '设备-SIM卡绑定表'; +COMMENT ON TABLE orders IS '订单表'; +COMMENT ON TABLE package_usages IS '套餐使用表'; +COMMENT ON TABLE polling_configs IS '轮询配置表'; +COMMENT ON TABLE data_usage_records IS '流量使用记录表'; +COMMENT ON TABLE agent_hierarchies IS '代理层级关系表'; +COMMENT ON TABLE commission_rules IS '分佣规则表'; +COMMENT ON TABLE commission_ladder IS '阶梯分佣配置表'; +COMMENT ON TABLE commission_combined_conditions IS '组合分佣条件表'; +COMMENT ON TABLE commission_records IS '分佣记录表'; +COMMENT ON TABLE commission_approvals IS '分佣审批表'; +COMMENT ON TABLE commission_templates IS '分佣模板表'; +COMMENT ON TABLE carrier_settlements IS '号卡运营商结算表'; +COMMENT ON TABLE commission_withdrawal_requests IS '佣金提现申请表'; +COMMENT ON TABLE commission_withdrawal_settings IS '佣金提现设置表'; +COMMENT ON TABLE payment_merchant_settings IS '收款商户设置表'; +COMMENT ON TABLE dev_capability_configs IS '开发能力配置表'; +COMMENT ON TABLE card_replacement_requests IS '换卡申请表'; diff --git a/migrations/000006_refactor_iot_models_architecture.up.sql b/migrations/000006_refactor_iot_models_architecture.up.sql new file mode 100644 index 0000000..36b692a --- /dev/null +++ b/migrations/000006_refactor_iot_models_architecture.up.sql @@ -0,0 +1,349 @@ +-- IoT 模型架构重构迁移脚本 +-- 创建时间: 2026-01-12 +-- 说明: 修改所有 IoT 相关表以符合项目架构规范 +-- 1. 表名改为 tb_ 前缀 + 单数形式 +-- 2. 添加软删除字段 deleted_at +-- 3. 添加审计字段 creator, updater +-- 4. 金额字段从 DECIMAL 改为 BIGINT(分为单位) +-- 5. 更新唯一索引以支持软删除 + +-- ======================================== +-- 阶段 1: 重命名表(复数 -> tb_ + 单数) +-- ======================================== + +ALTER TABLE carriers RENAME TO tb_carrier; +ALTER TABLE iot_cards RENAME TO tb_iot_card; +ALTER TABLE devices RENAME TO tb_device; +ALTER TABLE number_cards RENAME TO tb_number_card; +ALTER TABLE package_series RENAME TO tb_package_series; +ALTER TABLE packages RENAME TO tb_package; +ALTER TABLE agent_package_allocations RENAME TO tb_agent_package_allocation; +ALTER TABLE device_sim_bindings RENAME TO tb_device_sim_binding; +ALTER TABLE orders RENAME TO tb_order; +ALTER TABLE package_usages RENAME TO tb_package_usage; +ALTER TABLE polling_configs RENAME TO tb_polling_config; +ALTER TABLE data_usage_records RENAME TO tb_data_usage_record; +ALTER TABLE agent_hierarchies RENAME TO tb_agent_hierarchy; +ALTER TABLE commission_rules RENAME TO tb_commission_rule; +ALTER TABLE commission_ladder RENAME TO tb_commission_ladder; +ALTER TABLE commission_combined_conditions RENAME TO tb_commission_combined_condition; +ALTER TABLE commission_records RENAME TO tb_commission_record; +ALTER TABLE commission_approvals RENAME TO tb_commission_approval; +ALTER TABLE commission_templates RENAME TO tb_commission_template; +ALTER TABLE carrier_settlements RENAME TO tb_carrier_settlement; +ALTER TABLE commission_withdrawal_requests RENAME TO tb_commission_withdrawal_request; +ALTER TABLE commission_withdrawal_settings RENAME TO tb_commission_withdrawal_setting; +ALTER TABLE payment_merchant_settings RENAME TO tb_payment_merchant_setting; +ALTER TABLE dev_capability_configs RENAME TO tb_dev_capability_config; +ALTER TABLE card_replacement_requests RENAME TO tb_card_replacement_request; + +-- ======================================== +-- 阶段 2: 添加软删除字段和审计字段 +-- ======================================== + +-- 2.1 运营商表 +ALTER TABLE tb_carrier ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_carrier ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_carrier ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.2 IoT 卡表 +ALTER TABLE tb_iot_card ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_iot_card ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_iot_card ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.3 设备表 +ALTER TABLE tb_device ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_device ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_device ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.4 号卡表 +ALTER TABLE tb_number_card ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_number_card ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_number_card ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.5 套餐系列表 +ALTER TABLE tb_package_series ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_package_series ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_package_series ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.6 套餐表 +ALTER TABLE tb_package ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_package ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_package ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.7 代理套餐分配表 +ALTER TABLE tb_agent_package_allocation ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_agent_package_allocation ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_agent_package_allocation ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.8 设备-SIM绑定表 +ALTER TABLE tb_device_sim_binding ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_device_sim_binding ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_device_sim_binding ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.9 订单表 +ALTER TABLE tb_order ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_order ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_order ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.10 套餐使用表 +ALTER TABLE tb_package_usage ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_package_usage ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_package_usage ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.11 轮询配置表 +ALTER TABLE tb_polling_config ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_polling_config ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_polling_config ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 注意: tb_data_usage_record 是日志表,不需要软删除和审计字段 + +-- 2.12 代理层级表 +ALTER TABLE tb_agent_hierarchy ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_agent_hierarchy ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_agent_hierarchy ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.13 分佣规则表 +ALTER TABLE tb_commission_rule ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_commission_rule ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_commission_rule ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.14 阶梯分佣配置表 +ALTER TABLE tb_commission_ladder ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_commission_ladder ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_commission_ladder ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.15 组合分佣条件表 +ALTER TABLE tb_commission_combined_condition ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_commission_combined_condition ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_commission_combined_condition ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.16 分佣记录表 +ALTER TABLE tb_commission_record ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_commission_record ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_commission_record ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.17 分佣审批表 +ALTER TABLE tb_commission_approval ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_commission_approval ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_commission_approval ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.18 分佣模板表 +ALTER TABLE tb_commission_template ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_commission_template ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_commission_template ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.19 运营商结算表 +ALTER TABLE tb_carrier_settlement ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_carrier_settlement ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_carrier_settlement ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.20 佣金提现申请表 +ALTER TABLE tb_commission_withdrawal_request ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_commission_withdrawal_request ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_commission_withdrawal_request ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.21 佣金提现设置表 +ALTER TABLE tb_commission_withdrawal_setting ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_commission_withdrawal_setting ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_commission_withdrawal_setting ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.22 收款商户设置表 +ALTER TABLE tb_payment_merchant_setting ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_payment_merchant_setting ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_payment_merchant_setting ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.23 开发能力配置表 +ALTER TABLE tb_dev_capability_config ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_dev_capability_config ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_dev_capability_config ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- 2.24 换卡申请表 +ALTER TABLE tb_card_replacement_request ADD COLUMN deleted_at TIMESTAMP; +ALTER TABLE tb_card_replacement_request ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_card_replacement_request ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; + +-- ======================================== +-- 阶段 3: 修改金额字段从 DECIMAL 改为 BIGINT(分为单位) +-- ======================================== + +-- 3.1 IoT 卡表:成本价、分销价(元 * 100 = 分) +ALTER TABLE tb_iot_card + ALTER COLUMN cost_price TYPE BIGINT USING (cost_price * 100)::BIGINT, + ALTER COLUMN distribute_price TYPE BIGINT USING (distribute_price * 100)::BIGINT; + +COMMENT ON COLUMN tb_iot_card.cost_price IS '成本价(分为单位)'; +COMMENT ON COLUMN tb_iot_card.distribute_price IS '分销价(分为单位)'; + +-- 3.2 号卡表:价格 +ALTER TABLE tb_number_card + ALTER COLUMN price TYPE BIGINT USING (price * 100)::BIGINT; + +COMMENT ON COLUMN tb_number_card.price IS '价格(分为单位)'; + +-- 3.3 套餐表:价格 +ALTER TABLE tb_package + ALTER COLUMN price TYPE BIGINT USING (price * 100)::BIGINT; + +COMMENT ON COLUMN tb_package.price IS '套餐价格(分为单位)'; + +-- 3.4 代理套餐分配表:成本价和零售价 +ALTER TABLE tb_agent_package_allocation + ALTER COLUMN cost_price TYPE BIGINT USING (cost_price * 100)::BIGINT, + ALTER COLUMN retail_price TYPE BIGINT USING (retail_price * 100)::BIGINT; + +COMMENT ON COLUMN tb_agent_package_allocation.cost_price IS '成本价(分为单位)'; +COMMENT ON COLUMN tb_agent_package_allocation.retail_price IS '零售价(分为单位)'; + +-- 3.5 订单表:订单金额 +ALTER TABLE tb_order + ALTER COLUMN amount TYPE BIGINT USING (amount * 100)::BIGINT; + +COMMENT ON COLUMN tb_order.amount IS '订单金额(分为单位)'; + +-- 3.6 分佣规则表:分佣值 +ALTER TABLE tb_commission_rule + ALTER COLUMN commission_value TYPE BIGINT USING (commission_value * 100)::BIGINT; + +COMMENT ON COLUMN tb_commission_rule.commission_value IS '分佣值(分为单位,百分比时为千分比如2000表示20%)'; + +-- 3.7 阶梯分佣配置表:分佣值 +ALTER TABLE tb_commission_ladder + ALTER COLUMN commission_value TYPE BIGINT USING (commission_value * 100)::BIGINT; + +COMMENT ON COLUMN tb_commission_ladder.commission_value IS '分佣值(分为单位,百分比时为千分比如2000表示20%)'; + +-- 3.8 组合分佣条件表:一次性分佣值、长期分佣值 +ALTER TABLE tb_commission_combined_condition + ALTER COLUMN one_time_commission_value TYPE BIGINT USING (one_time_commission_value * 100)::BIGINT, + ALTER COLUMN long_term_commission_value TYPE BIGINT USING (long_term_commission_value * 100)::BIGINT; + +COMMENT ON COLUMN tb_commission_combined_condition.one_time_commission_value IS '一次性分佣值(分为单位,百分比时为千分比如2000表示20%)'; +COMMENT ON COLUMN tb_commission_combined_condition.long_term_commission_value IS '长期分佣值(分为单位,百分比时为千分比如2000表示20%)'; + +-- 3.9 分佣记录表:分佣金额 +ALTER TABLE tb_commission_record + ALTER COLUMN amount TYPE BIGINT USING (amount * 100)::BIGINT; + +COMMENT ON COLUMN tb_commission_record.amount IS '分佣金额(分为单位)'; + +-- 3.10 分佣模板表:分佣值 +ALTER TABLE tb_commission_template + ALTER COLUMN commission_value TYPE BIGINT USING (commission_value * 100)::BIGINT; + +COMMENT ON COLUMN tb_commission_template.commission_value IS '分佣值(分为单位,百分比时为千分比如2000表示20%)'; + +-- 3.11 运营商结算表:结算金额 +ALTER TABLE tb_carrier_settlement + ALTER COLUMN settlement_amount TYPE BIGINT USING (settlement_amount * 100)::BIGINT; + +COMMENT ON COLUMN tb_carrier_settlement.settlement_amount IS '结算金额(分为单位)'; + +-- 3.12 佣金提现申请表:提现金额、手续费、实际到账金额 +ALTER TABLE tb_commission_withdrawal_request + ALTER COLUMN amount TYPE BIGINT USING (amount * 100)::BIGINT, + ALTER COLUMN fee TYPE BIGINT USING (fee * 100)::BIGINT, + ALTER COLUMN actual_amount TYPE BIGINT USING (actual_amount * 100)::BIGINT; + +COMMENT ON COLUMN tb_commission_withdrawal_request.amount IS '提现金额(分为单位)'; +COMMENT ON COLUMN tb_commission_withdrawal_request.fee IS '手续费(分为单位)'; +COMMENT ON COLUMN tb_commission_withdrawal_request.actual_amount IS '实际到账金额(分为单位)'; + +-- 3.13 佣金提现设置表:最低提现金额 +ALTER TABLE tb_commission_withdrawal_setting + ALTER COLUMN min_withdrawal_amount TYPE BIGINT USING (min_withdrawal_amount * 100)::BIGINT; + +COMMENT ON COLUMN tb_commission_withdrawal_setting.min_withdrawal_amount IS '最低提现金额(分为单位)'; + +-- ======================================== +-- 阶段 4: 更新唯一索引以支持软删除 +-- ======================================== + +-- 4.1 运营商表 +ALTER TABLE tb_carrier DROP CONSTRAINT IF EXISTS carriers_carrier_code_key; +CREATE UNIQUE INDEX idx_carrier_code ON tb_carrier(carrier_code) WHERE deleted_at IS NULL; + +-- 4.2 IoT 卡表 +ALTER TABLE tb_iot_card DROP CONSTRAINT IF EXISTS iot_cards_iccid_key; +CREATE UNIQUE INDEX idx_iot_card_iccid ON tb_iot_card(iccid) WHERE deleted_at IS NULL; + +-- 4.3 设备表 +ALTER TABLE tb_device DROP CONSTRAINT IF EXISTS devices_device_no_key; +CREATE UNIQUE INDEX idx_device_no ON tb_device(device_no) WHERE deleted_at IS NULL; + +-- 4.4 号卡表 +ALTER TABLE tb_number_card DROP CONSTRAINT IF EXISTS number_cards_virtual_product_code_key; +CREATE UNIQUE INDEX idx_number_card_code ON tb_number_card(virtual_product_code) WHERE deleted_at IS NULL; + +-- 4.5 套餐系列表 +ALTER TABLE tb_package_series DROP CONSTRAINT IF EXISTS package_series_series_code_key; +CREATE UNIQUE INDEX idx_package_series_code ON tb_package_series(series_code) WHERE deleted_at IS NULL; + +-- 4.6 套餐表 +ALTER TABLE tb_package DROP CONSTRAINT IF EXISTS packages_package_code_key; +CREATE UNIQUE INDEX idx_package_code ON tb_package(package_code) WHERE deleted_at IS NULL; + +-- 4.7 代理套餐分配表(复合唯一索引) +ALTER TABLE tb_agent_package_allocation DROP CONSTRAINT IF EXISTS uk_agent_package; +CREATE UNIQUE INDEX idx_agent_package_allocation_agent_package ON tb_agent_package_allocation(agent_id, package_id) WHERE deleted_at IS NULL; + +-- 4.8 设备-SIM绑定表:暂时跳过,因为没有约束需要修改 +-- 原始表使用条件唯一索引 idx_device_sim_bindings_active_card,不需要修改 + +-- 4.9 订单表 +ALTER TABLE tb_order DROP CONSTRAINT IF EXISTS orders_order_no_key; +CREATE UNIQUE INDEX idx_order_no ON tb_order(order_no) WHERE deleted_at IS NULL; + +-- 4.10 轮询配置表 +ALTER TABLE tb_polling_config DROP CONSTRAINT IF EXISTS polling_configs_config_name_key; +CREATE UNIQUE INDEX idx_polling_config_name ON tb_polling_config(config_name) WHERE deleted_at IS NULL; + +-- 4.11 代理层级表 +ALTER TABLE tb_agent_hierarchy DROP CONSTRAINT IF EXISTS agent_hierarchies_agent_id_key; +CREATE UNIQUE INDEX idx_agent_hierarchy_agent ON tb_agent_hierarchy(agent_id) WHERE deleted_at IS NULL; + +-- 4.12 组合分佣条件表 +ALTER TABLE tb_commission_combined_condition DROP CONSTRAINT IF EXISTS commission_combined_conditions_rule_id_key; +CREATE UNIQUE INDEX idx_commission_combined_rule ON tb_commission_combined_condition(rule_id) WHERE deleted_at IS NULL; + +-- 4.13 分佣模板表 +ALTER TABLE tb_commission_template DROP CONSTRAINT IF EXISTS commission_templates_template_name_key; +CREATE UNIQUE INDEX idx_commission_template_name ON tb_commission_template(template_name) WHERE deleted_at IS NULL; + +-- 4.14 运营商结算表 +ALTER TABLE tb_carrier_settlement DROP CONSTRAINT IF EXISTS carrier_settlements_commission_record_id_key; +CREATE UNIQUE INDEX idx_carrier_settlement_record ON tb_carrier_settlement(commission_record_id) WHERE deleted_at IS NULL; + +-- 4.15 开发能力配置表 +ALTER TABLE tb_dev_capability_config DROP CONSTRAINT IF EXISTS dev_capability_configs_app_id_key; +CREATE UNIQUE INDEX idx_dev_capability_app ON tb_dev_capability_config(app_id) WHERE deleted_at IS NULL; + +-- ======================================== +-- 阶段 5: 更新表注释 +-- ======================================== + +COMMENT ON TABLE tb_carrier IS '运营商表'; +COMMENT ON TABLE tb_iot_card IS 'IoT卡表(物联网卡/流量卡)'; +COMMENT ON TABLE tb_device IS '设备表(可容纳1-4张SIM卡)'; +COMMENT ON TABLE tb_number_card IS '号卡表(虚拟商品)'; +COMMENT ON TABLE tb_package_series IS '套餐系列表'; +COMMENT ON TABLE tb_package IS '套餐表'; +COMMENT ON TABLE tb_agent_package_allocation IS '代理套餐分配表'; +COMMENT ON TABLE tb_device_sim_binding IS '设备-SIM卡绑定表'; +COMMENT ON TABLE tb_order IS '订单表'; +COMMENT ON TABLE tb_package_usage IS '套餐使用表'; +COMMENT ON TABLE tb_polling_config IS '轮询配置表'; +COMMENT ON TABLE tb_data_usage_record IS '流量使用记录表'; +COMMENT ON TABLE tb_agent_hierarchy IS '代理层级关系表'; +COMMENT ON TABLE tb_commission_rule IS '分佣规则表'; +COMMENT ON TABLE tb_commission_ladder IS '阶梯分佣配置表'; +COMMENT ON TABLE tb_commission_combined_condition IS '组合分佣条件表'; +COMMENT ON TABLE tb_commission_record IS '分佣记录表'; +COMMENT ON TABLE tb_commission_approval IS '分佣审批表'; +COMMENT ON TABLE tb_commission_template IS '分佣模板表'; +COMMENT ON TABLE tb_carrier_settlement IS '号卡运营商结算表'; +COMMENT ON TABLE tb_commission_withdrawal_request IS '佣金提现申请表'; +COMMENT ON TABLE tb_commission_withdrawal_setting IS '佣金提现设置表'; +COMMENT ON TABLE tb_payment_merchant_setting IS '收款商户设置表'; +COMMENT ON TABLE tb_dev_capability_config IS '开发能力配置表'; +COMMENT ON TABLE tb_card_replacement_request IS '换卡申请表'; diff --git a/openspec/changes/archive/2026-01-12-fix-iot-models-violations/.openspec.yaml b/openspec/changes/archive/2026-01-12-fix-iot-models-violations/.openspec.yaml new file mode 100644 index 0000000..e7e51fb --- /dev/null +++ b/openspec/changes/archive/2026-01-12-fix-iot-models-violations/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-01-12 diff --git a/openspec/changes/archive/2026-01-12-fix-iot-models-violations/design.md b/openspec/changes/archive/2026-01-12-fix-iot-models-violations/design.md new file mode 100644 index 0000000..5a3ae01 --- /dev/null +++ b/openspec/changes/archive/2026-01-12-fix-iot-models-violations/design.md @@ -0,0 +1,537 @@ +# 设计文档:修复 IoT 模型架构违规 + +## 1. 设计目标 + +将所有 IoT 相关数据模型重构为符合项目开发规范的标准模型,确保代码一致性、可维护性和长期可扩展性。 + +## 2. 核心设计原则 + +### 2.1 统一模型结构 + +所有数据模型必须遵循以下标准结构: + +```go +type ModelName struct { + gorm.Model // 标准字段:ID, CreatedAt, UpdatedAt, DeletedAt + BaseModel `gorm:"embedded"` // 基础字段:Creator, Updater + + // 业务字段(按字母顺序排列) + Field1 Type `gorm:"column:field1;..." json:"field1"` + Field2 Type `gorm:"column:field2;..." json:"field2"` +} + +func (ModelName) TableName() string { + return "tb_model_name" // tb_ 前缀 + 单数 +} +``` + +**设计理由:** +- `gorm.Model`:提供标准的主键、时间戳、软删除支持 +- `BaseModel`:提供审计字段,记录创建人和更新人 +- 显式 `column` 标签:明确 Go 字段和数据库列的映射关系,避免依赖 GORM 自动转换 +- `tb_` 前缀单数表名:项目统一规范,便于识别业务表 + +### 2.2 字段定义规范 + +**字符串字段:** +```go +Name string `gorm:"column:name;type:varchar(100);not null;comment:名称" json:"name"` +``` +- 必须显式指定 `column` 标签 +- 必须指定 `type:varchar(N)` 和长度 +- 必须指定 `not null`(如果必填) +- 必须添加中文 `comment` + +**货币金额字段:** +```go +Amount int64 `gorm:"column:amount;type:bigint;default:0;not null;comment:金额(分)" json:"amount"` +``` +- 使用 `int64` 类型(不是 `float64`) +- 单位为"分"(1元 = 100分) +- 必须指定 `type:bigint` +- 必须指定 `default:0` 和 `not null` +- 注释中明确标注"(分)" + +**设计理由:** +- 整数存储避免浮点精度问题(金融领域最佳实践) +- 分为单位便于精确计算和货币转换 + +**枚举字段:** +```go +Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"` +``` +- 使用 `int` 类型(不是 `string`) +- 必须在注释中列举所有枚举值 +- 必须指定 `default` 和 `not null` + +**关联 ID 字段:** +```go +UserID uint `gorm:"column:user_id;type:bigint;not null;index;comment:用户ID" json:"user_id"` +``` +- 使用 `uint` 类型(与 `gorm.Model` 的 ID 类型一致) +- 数据库类型使用 `bigint`(PostgreSQL) +- 必须添加 `index` 索引 +- 禁止使用 GORM 关联标签(`foreignKey`、`references`) + +**可选关联 ID 字段:** +```go +ShopID *uint `gorm:"column:shop_id;type:bigint;index;comment:店铺ID(可选)" json:"shop_id,omitempty"` +``` +- 使用指针类型 `*uint`(可为 NULL) +- 不指定 `not null` +- 仍需添加 `index` 索引 +- JSON 标签使用 `omitempty` + +**唯一索引字段:** +```go +ICCID string `gorm:"column:iccid;type:varchar(50);uniqueIndex:idx_iccid,where:deleted_at IS NULL;not null;comment:ICCID" json:"iccid"` +``` +- 使用 `uniqueIndex` 标签 +- 对于支持软删除的表,必须添加 `where:deleted_at IS NULL` 过滤条件 +- 索引名命名规范:`idx_{table}_{field}` 或 `idx_{field}` + +**时间字段:** +```go +ActivatedAt *time.Time `gorm:"column:activated_at;comment:激活时间" json:"activated_at,omitempty"` +``` +- 可选时间字段使用指针类型 `*time.Time` +- 不使用 `autoCreateTime` 或 `autoUpdateTime`(这些由 gorm.Model 提供) +- JSON 标签使用 `omitempty` + +**JSONB 字段(PostgreSQL):** +```go +Metadata datatypes.JSON `gorm:"column:metadata;type:jsonb;comment:元数据" json:"metadata,omitempty"` +``` +- 使用 `gorm.io/datatypes.JSON` 类型 +- 数据库类型使用 `jsonb`(PostgreSQL 优化存储) +- 使用 `omitempty` + +### 2.3 表名和索引命名规范 + +**表名:** +- 格式:`tb_{model_name}`(单数) +- 示例:`tb_iot_card`、`tb_device`、`tb_order` + +**索引名:** +- 普通索引:`idx_{table}_{field}` +- 唯一索引:`idx_{table}_{field}` 或 `uniq_{table}_{field}` +- 复合索引:`idx_{table}_{field1}_{field2}` + +**设计理由:** +- 统一前缀便于识别业务表(与系统表区分) +- 单数形式符合 Go 惯用命名(类型名为单数) +- 索引名清晰表达用途和字段 + +### 2.4 软删除支持 + +所有业务数据表都应支持软删除: + +```go +type BusinessModel struct { + gorm.Model // 包含 DeletedAt 字段 + // ... +} +``` + +**不需要软删除的表:** +- 纯配置表(如 `PollingConfig`、`CommissionWithdrawalSetting`) +- 日志表(如 `DataUsageRecord`) +- 中间表(如 `DeviceSimBinding` 可选支持) + +对于不需要软删除的表,可以手动定义字段: + +```go +type ConfigModel struct { + ID uint `gorm:"column:id;primaryKey;comment:ID" json:"id"` + BaseModel `gorm:"embedded"` + // ... + CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` +} +``` + +## 3. 模型分类和修复策略 + +### 3.1 核心业务实体(必须支持软删除) + +**完整模型结构(gorm.Model + BaseModel):** +- `IotCard`(IoT 卡) +- `Device`(设备) +- `NumberCard`(号卡) +- `PackageSeries`(套餐系列) +- `Package`(套餐) +- `AgentPackageAllocation`(代理套餐分配) +- `Order`(订单) +- `AgentHierarchy`(代理层级) +- `CommissionRule`(分佣规则) +- `CommissionTemplate`(分佣模板) +- `Carrier`(运营商) + +### 3.2 关联和绑定表(可选软删除) + +**完整模型结构(gorm.Model + BaseModel):** +- `DeviceSimBinding`(设备-SIM 卡绑定) + +### 3.3 使用记录和日志表(仅时间戳,不需要软删除) + +**简化模型结构(手动定义 ID + BaseModel + CreatedAt/UpdatedAt):** +- `PackageUsage`(套餐使用)- 保留 gorm.Model(需要软删除和更新) +- `DataUsageRecord`(流量记录)- 仅需 ID + CreatedAt(不需要 UpdatedAt 和 DeletedAt) + +### 3.4 财务和审批表(必须支持软删除) + +**完整模型结构(gorm.Model + BaseModel):** +- `CommissionRecord`(分佣记录) +- `CommissionApproval`(分佣审批) +- `CommissionWithdrawalRequest`(佣金提现申请) +- `PaymentMerchantSetting`(收款商户设置) +- `CarrierSettlement`(运营商结算) +- `CardReplacementRequest`(换卡申请) + +### 3.5 阶梯和条件配置表(可选软删除) + +**完整模型结构(gorm.Model + BaseModel):** +- `CommissionLadder`(阶梯分佣配置) +- `CommissionCombinedCondition`(组合分佣条件) + +### 3.6 系统配置表(可选软删除) + +**完整模型结构(gorm.Model + BaseModel):** +- `CommissionWithdrawalSetting`(提现设置) +- `PollingConfig`(轮询配置) +- `DevCapabilityConfig`(开发能力配置) + +## 4. 货币金额处理策略 + +### 4.1 金额字段映射 + +所有货币金额从 `float64`(元)改为 `int64`(分): + +| 原字段类型 | 新字段类型 | 原数据库类型 | 新数据库类型 | 说明 | +|-----------|-----------|------------|------------|-----| +| `float64` | `int64` | `DECIMAL(10,2)` | `BIGINT` | 金额单位从元改为分 | + +**影响的字段:** +- `IotCard.CostPrice`、`IotCard.DistributePrice` +- `NumberCard.Price` +- `Package.Price` +- `AgentPackageAllocation.CostPrice`、`AgentPackageAllocation.RetailPrice` +- `Order.Amount` +- `CommissionRule.CommissionValue` +- `CommissionLadder.CommissionValue` +- `CommissionCombinedCondition.OneTimeCommissionValue`、`CommissionCombinedCondition.LongTermCommissionValue` +- `CommissionRecord.Amount` +- `CommissionTemplate.CommissionValue` +- `CarrierSettlement.SettlementAmount` +- `CommissionWithdrawalRequest.Amount`、`CommissionWithdrawalRequest.Fee`、`CommissionWithdrawalRequest.ActualAmount` +- `CommissionWithdrawalSetting.MinWithdrawalAmount` + +### 4.2 业务逻辑调整 + +**API 输入输出:** +- API 接收的金额仍为 `float64`(元) +- Handler 层负责单位转换:元 → 分(乘以 100) +- 响应时转换回:分 → 元(除以 100) + +**示例:** +```go +// 输入:10.50 元 +inputAmount := 10.50 // float64 (元) +dbAmount := int64(inputAmount * 100) // 1050 分 + +// 输出:10.50 元 +dbAmount := int64(1050) // 分 +outputAmount := float64(dbAmount) / 100.0 // 10.50 元 +``` + +### 4.3 数据库迁移 + +对于已有测试数据: +```sql +-- 金额从 DECIMAL(元) 转为 BIGINT(分) +ALTER TABLE iot_cards RENAME COLUMN cost_price TO cost_price_old; +ALTER TABLE iot_cards ADD COLUMN cost_price BIGINT NOT NULL DEFAULT 0; +UPDATE iot_cards SET cost_price = CAST(cost_price_old * 100 AS BIGINT); +ALTER TABLE iot_cards DROP COLUMN cost_price_old; +``` + +## 5. JSONB 字段处理 + +### 5.1 问题 + +原模型使用 `pq.StringArray` 类型存储 JSONB: +```go +CarrierOrderData pq.StringArray `gorm:"column:carrier_order_data;type:jsonb;..."` +``` + +这是类型不匹配的:`pq.StringArray` 是 PostgreSQL 数组类型,不是 JSONB。 + +### 5.2 解决方案 + +使用 GORM 的 `datatypes.JSON` 类型: + +```go +import "gorm.io/datatypes" + +type Order struct { + // ... + CarrierOrderData datatypes.JSON `gorm:"column:carrier_order_data;type:jsonb;comment:运营商订单原始数据" json:"carrier_order_data,omitempty"` + // ... +} +``` + +**业务层使用:** +```go +// 写入 +data := map[string]interface{}{ + "order_id": "123", + "status": "paid", +} +order.CarrierOrderData, _ = json.Marshal(data) + +// 读取 +var data map[string]interface{} +json.Unmarshal(order.CarrierOrderData, &data) +``` + +## 6. 索引策略 + +### 6.1 唯一索引(Unique Index) + +对于需要全局唯一的字段(如 ICCID、订单号、虚拟商品编码): + +```go +ICCID string `gorm:"column:iccid;type:varchar(50);uniqueIndex:idx_iccid,where:deleted_at IS NULL;not null;comment:ICCID" json:"iccid"` +``` + +**关键点:** +- 必须添加 `where:deleted_at IS NULL` 过滤已软删除的记录 +- 否则软删除后无法重新使用相同的唯一值 + +### 6.2 普通索引(Index) + +对于频繁查询和过滤的字段(如状态、类型、关联 ID): + +```go +Status int `gorm:"column:status;type:int;default:1;not null;index;comment:状态" json:"status"` +UserID uint `gorm:"column:user_id;type:bigint;not null;index;comment:用户ID" json:"user_id"` +``` + +### 6.3 复合索引(Composite Index) + +对于联合查询的字段组合: + +```go +type DeviceSimBinding struct { + // ... + DeviceID uint `gorm:"column:device_id;type:bigint;not null;index:idx_device_slot;comment:设备ID" json:"device_id"` + SlotPosition int `gorm:"column:slot_position;type:int;index:idx_device_slot;comment:插槽位置" json:"slot_position"` + // ... +} +``` + +**复合索引命名:** +- `idx_device_slot`:表示 `device_id` 和 `slot_position` 的联合索引 + +## 7. 迁移路径 + +### 7.1 代码修改顺序 + +1. 修改所有模型文件(`internal/model/*.go`) +2. 更新模型的单元测试(如有) +3. 生成新的数据库迁移脚本 +4. 在开发环境测试迁移脚本 +5. 验证所有模型定义正确 + +### 7.2 数据库迁移策略 + +**场景 1:IoT 模块尚未部署(推荐)** +- 删除旧的迁移脚本(如果已创建) +- 生成新的初始迁移脚本 +- 重新运行迁移 + +**场景 2:IoT 模块已有测试数据** +- 保留旧的迁移脚本 +- 生成新的迁移脚本(包含表重命名、字段修改) +- 编写数据转换脚本(金额单位转换等) + +### 7.3 迁移脚本示例 + +```sql +-- 1. 重命名表(复数 → tb_ 前缀单数) +ALTER TABLE iot_cards RENAME TO tb_iot_card; +ALTER TABLE devices RENAME TO tb_device; +-- ... + +-- 2. 添加新字段 +ALTER TABLE tb_iot_card ADD COLUMN creator BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_iot_card ADD COLUMN updater BIGINT NOT NULL DEFAULT 0; +ALTER TABLE tb_iot_card ADD COLUMN deleted_at TIMESTAMP; + +-- 3. 修改金额字段(DECIMAL → BIGINT) +ALTER TABLE tb_iot_card RENAME COLUMN cost_price TO cost_price_old; +ALTER TABLE tb_iot_card ADD COLUMN cost_price BIGINT NOT NULL DEFAULT 0; +UPDATE tb_iot_card SET cost_price = CAST(cost_price_old * 100 AS BIGINT); +ALTER TABLE tb_iot_card DROP COLUMN cost_price_old; + +-- 4. 添加索引 +CREATE UNIQUE INDEX idx_iccid ON tb_iot_card(iccid) WHERE deleted_at IS NULL; +CREATE INDEX idx_status ON tb_iot_card(status); +CREATE INDEX idx_carrier_id ON tb_iot_card(carrier_id); +``` + +## 8. 验证清单 + +修复完成后需验证: + +- [ ] 所有模型嵌入 `gorm.Model` 或手动定义 `ID`、`CreatedAt`、`UpdatedAt` +- [ ] 所有业务模型嵌入 `BaseModel`(`Creator`、`Updater`) +- [ ] 所有字段显式指定 `column` 标签 +- [ ] 所有字符串字段指定类型和长度(`type:varchar(N)`) +- [ ] 所有金额字段使用 `int64` 类型和 `type:bigint` +- [ ] 所有必填字段指定 `not null` +- [ ] 所有字段添加中文 `comment` +- [ ] 所有唯一字段添加 `uniqueIndex` 并包含 `where:deleted_at IS NULL` +- [ ] 所有关联字段添加 `index` +- [ ] 所有表名使用 `tb_` 前缀 + 单数 +- [ ] 所有 JSONB 字段使用 `datatypes.JSON` 类型 +- [ ] 所有模型与现有 `Account`、`PersonalCustomer` 模型风格一致 + +## 9. 风险和注意事项 + +### 9.1 破坏性变更 + +- 表名变更会导致旧代码无法运行 +- 金额单位变更需要业务逻辑适配 +- 新增字段需要在业务逻辑中赋值 + +### 9.2 迁移风险 + +- 表重命名可能导致迁移失败(需谨慎测试) +- 金额转换可能出现精度问题(需验证) +- 索引重建可能耗时(大表需评估) + +### 9.3 开发流程影响 + +- 修复期间 IoT 模块功能开发需暂停 +- 所有依赖 IoT 模型的代码需同步修改 +- 需要重新生成数据库迁移脚本 + +## 10. 全局规范文档更新 + +### 10.1 更新目标 + +确保项目规范文档(CLAUDE.md)与实际实现的模型完全一致,为未来开发提供清晰、准确的指导。 + +### 10.2 CLAUDE.md 更新内容 + +**1. 补充 GORM 模型字段规范** + +在"数据库设计原则"部分添加详细的字段定义规范: + +```markdown +**GORM 模型字段规范:** + +**字段命名:** +- 数据库字段名必须使用下划线命名法(snake_case):`user_id`、`email_address`、`created_at` +- Go 结构体字段名必须使用驼峰命名法(PascalCase):`UserID`、`EmailAddress`、`CreatedAt` + +**字段标签要求:** +- **所有字段必须显式指定数据库列名**:使用 `gorm:"column:字段名"` 标签 + - 示例:`UserID uint gorm:"column:user_id;not null" json:"user_id"` + - 禁止省略 `column:` 标签,即使 GORM 能自动推断字段名 + - 这确保了 Go 字段名和数据库字段名的映射关系清晰可见,避免命名歧义 +- **所有字符串字段必须显式指定类型和长度**: + - 短文本:`type:varchar(100)` 或 `type:varchar(255)` + - 中等文本:`type:varchar(500)` 或 `type:varchar(1000)` + - 长文本:`type:text` +- **所有字段必须添加中文注释**:`comment:字段用途说明` + +**货币金额字段规范:** +- **必须使用整数类型**:Go 类型 `int64`,数据库类型 `bigint` +- **单位必须为"分"**(1 元 = 100 分) +- **注释中必须明确标注单位**:`comment:金额(分)` +- **理由**:避免浮点精度问题,符合金融系统最佳实践 + +示例: +```go +Amount int64 `gorm:"column:amount;type:bigint;not null;comment:订单金额(分)" json:"amount"` +``` + +**唯一索引软删除兼容性:** +- 对于支持软删除的表(嵌入 `gorm.Model`),唯一索引必须包含 `where:deleted_at IS NULL` 过滤条件 +- 示例: + ```go + ICCID string `gorm:"column:iccid;type:varchar(50);uniqueIndex:idx_iccid,where:deleted_at IS NULL;not null;comment:ICCID" json:"iccid"` + ``` +- 理由:允许软删除后重新使用相同的唯一值 + +**JSONB 字段规范(PostgreSQL):** +- 必须使用 `gorm.io/datatypes.JSON` 类型 +- 数据库类型为 `jsonb` +- 示例: + ```go + import "gorm.io/datatypes" + + Metadata datatypes.JSON `gorm:"column:metadata;type:jsonb;comment:元数据" json:"metadata,omitempty"` + ``` +``` + +**2. 更新模型示例代码** + +将现有的模型示例(如 Account)更新为包含完整字段标签的版本,确保所有示例都遵循规范。 + +**3. 添加金额单位转换说明** + +在"API 设计规范"或"错误处理规范"附近添加: + +```markdown +**API 层金额单位转换:** + +- API 接收和返回的金额使用 `float64` 类型(元) +- 业务层和数据库使用 `int64` 类型(分) +- Handler 层负责单位转换 + +**输入转换(API → 业务层):** +```go +// API 接收 10.50 元 +inputAmount := 10.50 // float64 (元) +dbAmount := int64(inputAmount * 100) // 1050 分 +``` + +**输出转换(业务层 → API):** +```go +// 数据库存储 1050 分 +dbAmount := int64(1050) // 分 +outputAmount := float64(dbAmount) / 100.0 // 10.50 元 +``` + +**注意事项:** +- 转换时注意四舍五入和边界情况 +- 建议封装转换函数,避免重复代码 +- 在金额字段的 DTO 注释中明确单位(元) +``` + +### 10.3 验证清单 + +更新完成后需验证: + +- [ ] CLAUDE.md 中的所有模型示例包含完整的字段标签 +- [ ] 所有字段定义规范清晰、完整、无歧义 +- [ ] 金额字段整数存储的说明详细且易懂 +- [ ] 唯一索引软删除兼容性规范已添加 +- [ ] JSONB 字段使用规范已添加 +- [ ] API 层金额单位转换说明已添加 +- [ ] 规范文档与实际实现的模型完全一致 + +## 11. 后续任务 + +模型修复和规范文档更新完成后,需要: + +1. 更新 DTO 模型(请求/响应结构体) +2. 调整 Store 层(数据访问层) +3. 调整 Service 层(业务逻辑层)- 金额单位转换 +4. 调整 Handler 层(API 层)- 金额单位转换 +5. 生成数据库迁移脚本 +6. 编写单元测试验证模型定义 +7. 更新 API 文档 diff --git a/openspec/changes/archive/2026-01-12-fix-iot-models-violations/proposal.md b/openspec/changes/archive/2026-01-12-fix-iot-models-violations/proposal.md new file mode 100644 index 0000000..a54f587 --- /dev/null +++ b/openspec/changes/archive/2026-01-12-fix-iot-models-violations/proposal.md @@ -0,0 +1,152 @@ +## Why + +在之前的 IoT SIM 管理系统提案(2026-01-12-iot-sim-management)中创建的所有数据模型存在严重的架构违规问题,完全没有遵循项目的核心开发规范。这些违规导致代码不一致、可维护性差、违背项目设计原则。 + +**核心问题:** + +1. **未使用基础模型**:所有 IoT 模型都没有嵌入 `BaseModel`,缺少统一的 `creator` 和 `updater` 字段 +2. **未使用 gorm.Model**:部分模型没有嵌入 `gorm.Model`,缺少标准的 `ID`、`CreatedAt`、`UpdatedAt`、`DeletedAt` 字段 +3. **字段命名不规范**:未显式指定 `column` 标签,依赖 GORM 自动转换(违反规范) +4. **字段定义不完整**:缺少必要的数据库约束标签(`not null`、`uniqueIndex`、索引等) +5. **数据类型不一致**: + - 货币字段使用 `float64` 而不是整数(分为单位) + - ID 字段类型不一致(`uint` vs `bigint`) + - 时间字段缺少 `autoCreateTime`/`autoUpdateTime` 标签 +6. **表名不符合规范**:使用复数形式(`iot_cards`)而不是项目约定的 `tb_` 前缀单数形式 +7. **缺少中文注释**:部分字段缺少清晰的中文注释说明业务含义 +8. **软删除支持不一致**:某些应该支持软删除的模型缺少 `gorm.Model` 嵌入 + +**对比现有规范模型(Account、PersonalCustomer):** + +✅ **正确示例(Account 模型):** +```go +type Account struct { + gorm.Model // ✅ 嵌入标准模型(ID、CreatedAt、UpdatedAt、DeletedAt) + BaseModel `gorm:"embedded"` // ✅ 嵌入基础模型(Creator、Updater) + Username string `gorm:"column:username;type:varchar(50);uniqueIndex:idx_account_username,where:deleted_at IS NULL;not null;comment:用户名" json:"username"` + // ✅ 显式 column 标签 + // ✅ 明确类型和长度 + // ✅ 唯一索引 + 软删除过滤 + // ✅ not null 约束 + // ✅ 中文注释 +} + +func (Account) TableName() string { + return "tb_account" // ✅ tb_ 前缀 + 单数 +} +``` + +❌ **错误示例(IotCard 模型):** +```go +type IotCard struct { + ID uint `gorm:"column:id;primaryKey;comment:IoT 卡 ID" json:"id"` + // ❌ 没有 gorm.Model + // ❌ 没有 BaseModel + // ❌ 手动定义 ID(应该由 gorm.Model 提供) + // ❌ 没有 DeletedAt(无法软删除) + + CostPrice float64 `gorm:"column:cost_price;type:decimal(10,2);default:0;comment:成本价(元)" json:"cost_price"` + // ❌ 使用 float64 而不是整数(分为单位) + + CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"` + // ❌ 手动定义(应该由 gorm.Model 提供) +} + +func (IotCard) TableName() string { + return "iot_cards" // ❌ 复数形式,没有 tb_ 前缀 +} +``` + +**影响范围:** + +需要修复以下所有 IoT 相关模型(约 25 个模型文件): +- `internal/model/iot_card.go`(IotCard) +- `internal/model/device.go`(Device、DeviceSimBinding) +- `internal/model/number_card.go`(NumberCard) +- `internal/model/package.go`(PackageSeries、Package、AgentPackageAllocation、PackageUsage) +- `internal/model/order.go`(Order) +- `internal/model/commission.go`(AgentHierarchy、CommissionRule、CommissionLadder、CommissionCombinedCondition、CommissionRecord、CommissionApproval、CommissionTemplate、CarrierSettlement) +- `internal/model/financial.go`(CommissionWithdrawalRequest、CommissionWithdrawalSetting、PaymentMerchantSetting) +- `internal/model/system.go`(DevCapabilityConfig、CardReplacementRequest) +- `internal/model/carrier.go`(Carrier) +- `internal/model/data_usage.go`(DataUsageRecord) +- `internal/model/polling.go`(PollingConfig) + +## What Changes + +- 重构所有 IoT 相关数据模型,使其完全符合项目开发规范 +- 统一所有模型的字段定义、类型、约束、注释格式 +- 确保所有模型与现有用户体系模型(Account、PersonalCustomer)保持一致的架构风格 +- 更新数据库迁移脚本以反映模型变更 + +## Capabilities + +### Modified Capabilities + +#### 核心数据模型规范化 + +- `iot-card`: 修改 IoT 卡业务模型 - 统一字段定义,嵌入 BaseModel 和 gorm.Model,修正表名为 `tb_iot_card`,使用整数存储金额,完善索引和约束 +- `iot-device`: 修改设备业务模型 - 统一字段定义,嵌入 BaseModel 和 gorm.Model,修正表名为 `tb_device`,规范化所有关联字段 +- `iot-number-card`: 修改号卡业务模型 - 统一字段定义,嵌入 BaseModel 和 gorm.Model,修正表名为 `tb_number_card`,使用整数存储金额 +- `iot-package`: 修改套餐管理模型 - 统一字段定义,嵌入 BaseModel 和 gorm.Model,修正表名(`tb_package_series`、`tb_package`、`tb_agent_package_allocation`、`tb_package_usage`),使用整数存储金额 +- `iot-order`: 修改订单管理模型 - 统一字段定义,嵌入 BaseModel 和 gorm.Model,修正表名为 `tb_order`,使用整数存储金额,规范化 JSONB 字段 +- `iot-agent-commission`: 修改代理分佣模型 - 统一所有分佣相关模型字段定义,嵌入 BaseModel 和 gorm.Model,修正表名(添加 `tb_` 前缀),使用整数存储金额 + +#### 财务和系统模型规范化 + +- 修改财务相关模型(CommissionWithdrawalRequest、CommissionWithdrawalSetting、PaymentMerchantSetting)- 统一字段定义,使用整数存储金额,完善索引和约束 +- 修改系统配置模型(DevCapabilityConfig、CardReplacementRequest)- 统一字段定义,嵌入 BaseModel 和 gorm.Model +- 修改运营商模型(Carrier)- 统一字段定义,嵌入 BaseModel 和 gorm.Model,修正表名为 `tb_carrier` +- 修改流量记录模型(DataUsageRecord)- 统一字段定义,嵌入 gorm.Model,修正表名为 `tb_data_usage_record` +- 修改轮询配置模型(PollingConfig)- 统一字段定义,嵌入 BaseModel 和 gorm.Model,修正表名为 `tb_polling_config` + +## Impact + +**代码变更:** +- 重构约 25 个 GORM 模型文件(`internal/model/`) +- 所有模型的字段定义将发生变化(字段名、类型、标签) +- 所有表名将从复数变为 `tb_` 前缀单数形式 + +**数据库变更:** +- 需要生成新的数据库迁移脚本以反映模型变更 +- 表名变更(如 `iot_cards` → `tb_iot_card`) +- 字段变更(如 `cost_price DECIMAL` → `cost_price BIGINT`,金额从元改为分) +- 新增字段(`creator`、`updater`、`deleted_at`) +- 新增索引和约束 + +**向后兼容性:** +- ❌ **不兼容变更**:此次修复涉及破坏性变更(表名、字段类型) +- 由于 IoT 模块尚未实际部署到生产环境,可以直接修改而无需数据迁移 +- 如果已有测试数据,需要编写数据迁移脚本 + +**业务影响:** +- 不影响现有用户体系(Account、Role、Permission 等) +- 不影响个人客户模块(PersonalCustomer) +- IoT 模块的 Service 层和 Handler 层代码需要相应调整(字段类型变化) + +**依赖关系:** +- 必须在实现 IoT 业务逻辑(Handlers、Services、Stores)之前修复 +- 修复后才能生成正确的数据库迁移脚本 +- 修复后才能生成准确的 API 文档 + +**文档变更:** +- 更新 `CLAUDE.md` 中的数据库设计原则和 GORM 模型字段规范 +- 补充完整的字段定义规范(显式 column 标签、类型定义、注释要求) +- 添加金额字段整数存储的详细说明和示例 +- 完善表名命名规范和 BaseModel 使用说明 +- 确保全局规范文档与实际实现保持一致 + +**明确排除的范围**(本次不涉及): +- Handler 层代码修改(将在后续任务中处理) +- Service 层代码修改(将在后续任务中处理) +- Store 层代码修改(将在后续任务中处理) +- DTO 模型调整(请求/响应结构体) +- 单元测试和集成测试 +- API 文档更新 + +**风险和注意事项:** +- 所有金额字段从 `float64` 改为 `int64`(分为单位),需要在业务逻辑中进行单位转换 +- 表名变更需要确保迁移脚本正确执行 +- 新增的 `creator` 和 `updater` 字段需要在业务逻辑中正确赋值 +- 软删除(`DeletedAt`)的引入可能需要调整查询逻辑(GORM 会自动处理) diff --git a/openspec/changes/archive/2026-01-12-fix-iot-models-violations/specs/model-organization/spec.md b/openspec/changes/archive/2026-01-12-fix-iot-models-violations/specs/model-organization/spec.md new file mode 100644 index 0000000..432d5bf --- /dev/null +++ b/openspec/changes/archive/2026-01-12-fix-iot-models-violations/specs/model-organization/spec.md @@ -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` +- 简化模型结构减少存储开销和查询复杂度 diff --git a/openspec/changes/archive/2026-01-12-fix-iot-models-violations/tasks.md b/openspec/changes/archive/2026-01-12-fix-iot-models-violations/tasks.md new file mode 100644 index 0000000..4a8d58d --- /dev/null +++ b/openspec/changes/archive/2026-01-12-fix-iot-models-violations/tasks.md @@ -0,0 +1,643 @@ +# Tasks + +本文档列出修复 IoT 模型架构违规所需的所有任务,按优先级和依赖关系排序。 + +## 阶段 1: 核心业务实体模型修复(必须优先完成) + +### Task 1.1: 修复 IoT 卡模型 (IotCard) + +**文件**: `internal/model/iot_card.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 金额字段(`CostPrice`、`DistributePrice`)从 `float64` 改为 `int64`,数据库类型从 `decimal(10,2)` 改为 `bigint` +- 表名从 `iot_cards` 改为 `tb_iot_card` +- `ICCID` 唯一索引添加 `where:deleted_at IS NULL` +- 所有关联 ID 字段(`CarrierID`、`OwnerID`)添加 `index` 标签 +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 +- 使用 `gofmt` 格式化代码 +- 与 `Account` 模型对比,确保风格一致 + +--- + +### Task 1.2: 修复设备模型 (Device) + +**文件**: `internal/model/device.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 表名从 `devices` 改为 `tb_device` +- `DeviceNo` 唯一索引添加 `where:deleted_at IS NULL` +- 所有关联 ID 字段(`OwnerID`)添加 `index` 标签 +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 +- 使用 `gofmt` 格式化代码 + +--- + +### Task 1.3: 修复号卡模型 (NumberCard) + +**文件**: `internal/model/number_card.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 金额字段(`Price`)从 `float64` 改为 `int64`,数据库类型从 `decimal(10,2)` 改为 `bigint` +- 表名从 `number_cards` 改为 `tb_number_card` +- `VirtualProductCode` 唯一索引添加 `where:deleted_at IS NULL` +- 关联 ID 字段(`AgentID`)添加 `index` 标签 +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 +- 使用 `gofmt` 格式化代码 + +--- + +### Task 1.4: 修复运营商模型 (Carrier) + +**文件**: `internal/model/carrier.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 表名从 `carriers` 改为 `tb_carrier` +- `CarrierCode` 唯一索引添加 `where:deleted_at IS NULL` +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 +- 使用 `gofmt` 格式化代码 + +--- + +## 阶段 2: 套餐和订单模型修复 + +### Task 2.1: 修复套餐系列模型 (PackageSeries) + +**文件**: `internal/model/package.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 表名从 `package_series` 改为 `tb_package_series` +- `SeriesCode` 唯一索引添加 `where:deleted_at IS NULL` +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 + +--- + +### Task 2.2: 修复套餐模型 (Package) + +**文件**: `internal/model/package.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 金额字段(`Price`)从 `float64` 改为 `int64`,数据库类型从 `decimal(10,2)` 改为 `bigint` +- 表名从 `packages` 改为 `tb_package` +- `PackageCode` 唯一索引添加 `where:deleted_at IS NULL` +- 关联 ID 字段(`SeriesID`)添加 `index` 标签 +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 + +--- + +### Task 2.3: 修复代理套餐分配模型 (AgentPackageAllocation) + +**文件**: `internal/model/package.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 金额字段(`CostPrice`、`RetailPrice`)从 `float64` 改为 `int64` +- 表名从 `agent_package_allocations` 改为 `tb_agent_package_allocation` +- 关联 ID 字段(`AgentID`、`PackageID`)添加 `index` 标签 +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 + +--- + +### Task 2.4: 修复套餐使用情况模型 (PackageUsage) + +**文件**: `internal/model/package.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 表名从 `package_usages` 改为 `tb_package_usage` +- 关联 ID 字段(`OrderID`、`PackageID`、`IotCardID`、`DeviceID`)添加 `index` 标签 +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 + +--- + +### Task 2.5: 修复设备-SIM 卡绑定模型 (DeviceSimBinding) + +**文件**: `internal/model/package.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 表名从 `device_sim_bindings` 改为 `tb_device_sim_binding` +- 添加复合索引:`DeviceID` 和 `SlotPosition` 使用 `index:idx_device_slot` +- 关联 ID 字段(`IotCardID`)添加独立 `index` 标签 +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 + +--- + +### Task 2.6: 修复订单模型 (Order) + +**文件**: `internal/model/order.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 金额字段(`Amount`)从 `float64` 改为 `int64` +- `CarrierOrderData` 从 `pq.StringArray` 改为 `datatypes.JSON`,添加 `import "gorm.io/datatypes"` +- 表名从 `orders` 改为 `tb_order` +- `OrderNo` 唯一索引添加 `where:deleted_at IS NULL` +- 关联 ID 字段(`IotCardID`、`DeviceID`、`NumberCardID`、`PackageID`、`UserID`、`AgentID`)添加 `index` 标签 +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 +- 检查 `datatypes.JSON` 导入是否正确 + +--- + +## 阶段 3: 分佣系统模型修复 + +### Task 3.1: 修复代理层级模型 (AgentHierarchy) + +**文件**: `internal/model/commission.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 表名从 `agent_hierarchies` 改为 `tb_agent_hierarchy` +- `AgentID` 唯一索引添加 `where:deleted_at IS NULL` +- 关联 ID 字段(`ParentAgentID`)添加 `index` 标签 +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 + +--- + +### Task 3.2: 修复分佣规则模型 (CommissionRule) + +**文件**: `internal/model/commission.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 金额字段(`CommissionValue`)从 `float64` 改为 `int64` +- 表名从 `commission_rules` 改为 `tb_commission_rule` +- 关联 ID 字段(`AgentID`、`SeriesID`、`PackageID`)添加 `index` 标签 +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 + +--- + +### Task 3.3: 修复阶梯分佣配置模型 (CommissionLadder) + +**文件**: `internal/model/commission.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 金额字段(`CommissionValue`)从 `float64` 改为 `int64` +- 表名从 `commission_ladder` 改为 `tb_commission_ladder` +- 关联 ID 字段(`RuleID`)添加 `index` 标签 +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 + +--- + +### Task 3.4: 修复组合分佣条件模型 (CommissionCombinedCondition) + +**文件**: `internal/model/commission.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 金额字段(`OneTimeCommissionValue`、`LongTermCommissionValue`)从 `float64` 改为 `int64` +- 表名从 `commission_combined_conditions` 改为 `tb_commission_combined_condition` +- `RuleID` 唯一索引添加 `where:deleted_at IS NULL` +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 + +--- + +### Task 3.5: 修复分佣记录模型 (CommissionRecord) + +**文件**: `internal/model/commission.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 金额字段(`Amount`)从 `float64` 改为 `int64` +- 表名从 `commission_records` 改为 `tb_commission_record` +- 关联 ID 字段(`AgentID`、`OrderID`、`RuleID`)添加 `index` 标签 +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 + +--- + +### Task 3.6: 修复分佣审批模型 (CommissionApproval) + +**文件**: `internal/model/commission.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 表名从 `commission_approvals` 改为 `tb_commission_approval` +- 关联 ID 字段(`CommissionRecordID`、`ApproverID`)添加 `index` 标签 +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 + +--- + +### Task 3.7: 修复分佣模板模型 (CommissionTemplate) + +**文件**: `internal/model/commission.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 金额字段(`CommissionValue`)从 `float64` 改为 `int64` +- 表名从 `commission_templates` 改为 `tb_commission_template` +- `TemplateName` 唯一索引添加 `where:deleted_at IS NULL` +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 + +--- + +### Task 3.8: 修复运营商结算模型 (CarrierSettlement) + +**文件**: `internal/model/commission.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 金额字段(`SettlementAmount`)从 `float64` 改为 `int64`,数据库类型从 `decimal(18,2)` 改为 `bigint` +- 表名从 `carrier_settlements` 改为 `tb_carrier_settlement` +- `CommissionRecordID` 唯一索引添加 `where:deleted_at IS NULL` +- 关联 ID 字段(`AgentID`)添加 `index` 标签 +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 + +--- + +## 阶段 4: 财务和系统模型修复 + +### Task 4.1: 修复佣金提现申请模型 (CommissionWithdrawalRequest) + +**文件**: `internal/model/financial.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 金额字段(`Amount`、`Fee`、`ActualAmount`)从 `float64` 改为 `int64`,数据库类型从 `decimal(18,2)` 改为 `bigint` +- `AccountInfo` 从 `pq.StringArray` 改为 `datatypes.JSON` +- 表名从 `commission_withdrawal_requests` 改为 `tb_commission_withdrawal_request` +- 关联 ID 字段(`AgentID`、`ApprovedBy`)添加 `index` 标签 +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 +- 检查 `datatypes.JSON` 导入是否正确 + +--- + +### Task 4.2: 修复佣金提现设置模型 (CommissionWithdrawalSetting) + +**文件**: `internal/model/financial.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 金额字段(`MinWithdrawalAmount`)从 `float64` 改为 `int64`,数据库类型从 `decimal(10,2)` 改为 `bigint` +- 表名从 `commission_withdrawal_settings` 改为 `tb_commission_withdrawal_setting` +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 + +--- + +### Task 4.3: 修复收款商户设置模型 (PaymentMerchantSetting) + +**文件**: `internal/model/financial.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 表名从 `payment_merchant_settings` 改为 `tb_payment_merchant_setting` +- 关联 ID 字段(`UserID`)添加 `index` 标签 +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 + +--- + +### Task 4.4: 修复开发能力配置模型 (DevCapabilityConfig) + +**文件**: `internal/model/system.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 表名从 `dev_capability_configs` 改为 `tb_dev_capability_config` +- `AppID` 唯一索引添加 `where:deleted_at IS NULL` +- 关联 ID 字段(`UserID`)添加 `index` 标签 +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 + +--- + +### Task 4.5: 修复换卡申请模型 (CardReplacementRequest) + +**文件**: `internal/model/system.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 表名从 `card_replacement_requests` 改为 `tb_card_replacement_request` +- 关联 ID 字段(`UserID`、`ApprovedBy`)添加 `index` 标签 +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 + +--- + +### Task 4.6: 修复轮询配置模型 (PollingConfig) + +**文件**: `internal/model/polling.go` + +**修改内容:** +- 嵌入 `gorm.Model` 和 `BaseModel` +- 移除手动定义的 `ID`、`CreatedAt`、`UpdatedAt` 字段 +- 所有字段显式指定 `column` 标签 +- 表名从 `polling_configs` 改为 `tb_polling_config` +- `ConfigName` 唯一索引添加 `where:deleted_at IS NULL` +- 关联 ID 字段(`CarrierID`)添加 `index` 标签 +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 + +--- + +### Task 4.7: 修复流量使用记录模型 (DataUsageRecord) + +**文件**: `internal/model/data_usage.go` + +**修改内容:** +- **不嵌入** `gorm.Model`(简化模型,只包含 ID 和 CreatedAt) +- **不嵌入** `BaseModel`(日志表不需要审计) +- 保留 `ID`、`CreatedAt` 字段,移除 `UpdatedAt` +- 所有字段显式指定 `column` 标签 +- 表名从 `data_usage_records` 改为 `tb_data_usage_record` +- 关联 ID 字段(`IotCardID`)添加 `index` 标签 +- 完善所有字段的中文注释 + +**验证方法:** +- 运行 `go build` 确保编译通过 +- 确认模型不包含 `UpdatedAt` 和 `DeletedAt` + +--- + +## 阶段 5: 验证和测试 + +### Task 5.1: 编译验证 + +**内容:** +- 运行 `go build ./...` 确保所有模型文件编译通过 +- 运行 `gofmt -w internal/model/` 格式化所有模型文件 +- 运行 `go vet ./internal/model/` 静态分析检查 + +**依赖**: 所有模型修复任务完成 + +**验证方法:** +- 无编译错误 +- 无静态分析警告 + +--- + +### Task 5.2: 模型定义一致性检查 + +**内容:** +- 手动检查所有模型是否遵循规范(参考验证清单) +- 对比 `Account` 模型,确保风格一致 +- 检查所有金额字段是否使用 `int64` 类型 +- 检查所有表名是否使用 `tb_` 前缀 + 单数 +- 检查所有唯一索引是否包含 `where:deleted_at IS NULL` + +**依赖**: Task 5.1 + +**验证方法:** +- 完成验证清单(设计文档第 8 节) + +--- + +### Task 5.3: 生成数据库迁移脚本(可选) + +**内容:** +- 如果 IoT 模块尚未创建迁移脚本,跳过此任务 +- 如果已有迁移脚本,生成新的迁移脚本或修改现有脚本 +- 包含表重命名、字段修改、索引创建等 SQL 语句 + +**依赖**: Task 5.2 + +**验证方法:** +- 在开发环境测试迁移脚本 +- 确认所有表和字段正确创建 + +--- + +### Task 5.4: 文档更新 + +**内容:** +- 更新 IoT SIM 管理提案(`openspec/changes/archive/2026-01-12-iot-sim-management/`)的模型定义部分(可选) +- 在 `docs/` 目录创建模型修复总结文档(可选) +- 更新 `README.md` 添加模型规范说明(可选) + +**依赖**: Task 5.2 + +**验证方法:** +- 文档清晰易懂,准确反映当前实现 + +--- + +### Task 5.5: 更新全局规范文档 + +**内容:** +- 更新 `CLAUDE.md` 中的数据库设计原则和模型规范部分 +- 确保 CLAUDE.md 中的示例代码与修复后的模型风格完全一致 +- 如果需要,更新 `openspec/AGENTS.md`(如果其中包含模型相关指导) +- 添加或完善以下规范内容: + - GORM 模型字段规范(显式 column 标签、类型定义、注释要求) + - 金额字段使用整数类型(分为单位)的详细说明和示例 + - 表名命名规范(`tb_` 前缀 + 单数) + - BaseModel 嵌入和审计字段使用说明 + - 唯一索引软删除兼容性(`where:deleted_at IS NULL`) + - JSONB 字段使用 `datatypes.JSON` 类型的说明 + +**具体修改位置(CLAUDE.md):** + +1. **数据库设计原则** 部分: + - 补充完整的 GORM 模型字段定义规范 + - 添加金额字段整数存储的要求和理由 + - 添加字段标签完整性要求(显式 column、type、comment) + +2. **GORM 模型字段规范** 新增小节: + ```markdown + **GORM 模型字段规范:** + - 数据库字段名必须使用下划线命名法(snake_case),如 `user_id`、`email_address`、`created_at` + - Go 结构体字段名必须使用驼峰命名法(PascalCase),如 `UserID`、`EmailAddress`、`CreatedAt` + - **所有字段必须显式指定数据库列名**:使用 `gorm:"column:字段名"` 标签明确指定数据库字段名,不依赖 GORM 的自动转换 + - 示例:`UserID uint gorm:"column:user_id;not null" json:"user_id"` + - 禁止省略 `column:` 标签,即使 GORM 能自动推断字段名 + - 这确保了 Go 字段名和数据库字段名的映射关系清晰可见,避免命名歧义 + - 字符串字段长度必须明确定义且保持一致性: + - 短文本(名称、标题等):`VARCHAR(255)` 或 `VARCHAR(100)` + - 中等文本(描述、备注等):`VARCHAR(500)` 或 `VARCHAR(1000)` + - 长文本(内容、详情等):`TEXT` 类型 + - 货币金额字段必须使用 `int64` 类型,数据库类型为 `bigint`,单位为"分"(1元 = 100分) + - 所有字段必须添加中文注释,说明字段用途和业务含义 + ``` + +3. **示例代码更新**: + - 将现有的模型示例(如果有)更新为包含完整字段标签的版本 + +**依赖**: Task 5.2 + +**验证方法:** +- CLAUDE.md 中的规范描述与实际实现的模型完全一致 +- 所有示例代码可以直接复制使用,无需修改 +- 规范描述清晰、完整、无歧义 +- 运行 `git diff CLAUDE.md` 检查修改内容 + +--- + +## 依赖关系图 + +``` +阶段 1 (核心模型) + ├─ Task 1.1: IotCard + ├─ Task 1.2: Device + ├─ Task 1.3: NumberCard + └─ Task 1.4: Carrier + ↓ +阶段 2 (套餐和订单) + ├─ Task 2.1: PackageSeries + ├─ Task 2.2: Package (依赖 Task 2.1) + ├─ Task 2.3: AgentPackageAllocation (依赖 Task 2.2) + ├─ Task 2.4: PackageUsage (依赖 Task 2.2) + ├─ Task 2.5: DeviceSimBinding (依赖 Task 1.1, Task 1.2) + └─ Task 2.6: Order (依赖 Task 1.1, Task 1.2, Task 1.3, Task 2.2) + ↓ +阶段 3 (分佣系统) + ├─ Task 3.1: AgentHierarchy + ├─ Task 3.2: CommissionRule + ├─ Task 3.3: CommissionLadder (依赖 Task 3.2) + ├─ Task 3.4: CommissionCombinedCondition (依赖 Task 3.2) + ├─ Task 3.5: CommissionRecord (依赖 Task 3.2) + ├─ Task 3.6: CommissionApproval (依赖 Task 3.5) + ├─ Task 3.7: CommissionTemplate + └─ Task 3.8: CarrierSettlement (依赖 Task 3.5) + ↓ +阶段 4 (财务和系统) + ├─ Task 4.1: CommissionWithdrawalRequest + ├─ Task 4.2: CommissionWithdrawalSetting + ├─ Task 4.3: PaymentMerchantSetting + ├─ Task 4.4: DevCapabilityConfig + ├─ Task 4.5: CardReplacementRequest + ├─ Task 4.6: PollingConfig + └─ Task 4.7: DataUsageRecord (依赖 Task 1.1) + ↓ +阶段 5 (验证和测试) + ├─ Task 5.1: 编译验证 + ├─ Task 5.2: 一致性检查 (依赖 Task 5.1) + ├─ Task 5.3: 生成迁移脚本 (依赖 Task 5.2, 可选) + ├─ Task 5.4: 文档更新 (依赖 Task 5.2, 可选) + └─ Task 5.5: 更新全局规范文档 (依赖 Task 5.2, 必需) +``` + +## 估算工作量 + +- **阶段 1**: 约 2-3 小时(4 个核心模型) +- **阶段 2**: 约 3-4 小时(6 个套餐和订单模型) +- **阶段 3**: 约 4-5 小时(8 个分佣系统模型) +- **阶段 4**: 约 3-4 小时(7 个财务和系统模型) +- **阶段 5**: 约 2-3 小时(验证、测试和全局规范文档更新) + +**总计**: 约 14-19 小时(~2-3 个工作日) + +## 注意事项 + +1. **并行执行**: 阶段内的任务可以并行执行(除非明确依赖) +2. **增量提交**: 建议每完成一个阶段提交一次 Git commit +3. **回归测试**: 修复完成后需要运行完整的单元测试套件(如有) +4. **代码审查**: 修复完成后需要进行 Code Review,确保符合项目规范