// 分账域 2 表 model(t_user_divide_into / t_user_divide_into_log)——Nested Set 左右值树。 // // 与 academic-model-ddl.sql「第三部分」逐列对齐;覆盖 4 个 SP: // divide_into_add(加入下线)/ divide_into_log_add(上行分成,递归→迭代 BFS)/ // divide_into_log_delete(撤销/改绑)/ divide_into_update_quota(额度级联降)。 // // 作用域说明(设计 §1.7 例外二 + §11.1 拍板 11-12):本域 2 表**无** institution_id/campus_id // (旧库为全局推广体系),**不继承 BaseRepo**;越权防线 = Handler 角色白名单(super_admin/academic) // + 显式数据范围参数,代码评审须在此显式标注"平台级/全局数据"。 // // 字段注释五维口径:语义与单位 | 枚举全量取值 | 写入时机 | 主外键/关联语义 | 安全红线。 package model import "time" // t_user_divide_into.type / t_user_divide_into_log.type 分成类型编码。 const ( DivideTypeOneOff int8 = 1 // 一次付清(同 user+student 只生成一条) DivideTypePerLesson int8 = 2 // 按课次(同 user+student+order_no 只生成一条) ) // UserDivideInto 用户分账表 t_user_divide_into(Nested Set 左右值树·一人一节点·自然键 user_id)。 // // 树不变式:root_user_id 标识所属树;left_val/right_val 为嵌套集区间(left < right); // layer 为层级(根=1)。下线 quota 不得超过上线(update_quota 级联约束,超限 → 3111)。 type UserDivideInto struct { UserID string `gorm:"column:user_id;primaryKey;size:26;not null" json:"userId"` // 节点用户(ULID;原 userid,主键)|枚举:无|写入:加入分账体系时|FK:→ t_user(逻辑)|红线:一人一节点,禁止重复建节点 Type int8 `gorm:"column:type;not null;default:1" json:"type"` // 分成类型(1 一次付清/2 按课次)|枚举:DivideType*|写入:加入/调整时|FK:无|红线:决定幂等维度(per student / per order),运行期改须评审 Quota string `gorm:"column:quota;type:decimal(12,2);not null;default:0.00" json:"quota"` // 分成额度(元/次)|枚举:无|写入:加入时配置/update_quota|FK:无|红线:子树 quota 不得高于上线(级联降) TotalIncome string `gorm:"column:total_income;type:decimal(12,2);not null;default:0.00" json:"totalIncome"` // 分成总收入(元,累积)|枚举:无|写入:上行分成累加/撤销回滚|FK:无|红线:须与 divide_into_log.money 累计一致(撤销是回滚本列) TotalIncomePayed string `gorm:"column:total_income_payed;type:decimal(12,2);not null;default:0.00" json:"totalIncomePayed"` // 已支付的分成收入(元)|枚举:无|写入:结算打款时|FK:无|红线:<= total_income RootUserID *string `gorm:"index:idx_root;index:idx_root_left_right,priority:1;column:root_user_id;size:26" json:"rootUserId,omitempty"` // 树根用户(ULID;自身为根时=user_id)|枚举:无|写入:加入时|FK:→ 本表 user_id(自引用)|红线:★旧 SP 在 subid 已存在分支未初始化 root → NULL,Go 侧须显式取父 root ParentUserID *string `gorm:"index:idx_parent;column:parent_user_id;size:26" json:"parentUserId,omitempty"` // 上级用户(ULID;根为 NULL)|枚举:无|写入:加入时|FK:→ 本表 user_id(自引用)|红线:BFS 上行链依据,禁止成环 LeftVal int32 `gorm:"index:idx_root_left_right,priority:2;column:left_val;not null;default:1" json:"leftVal"` // 左值(Nested Set)|枚举:无|写入:加入/插入新节点时右值扩张|FK:无|红线:区间扩张须同事务(left>=parentRight 全体 +2) RightVal int32 `gorm:"index:idx_root_left_right,priority:3;column:right_val;not null;default:2" json:"rightVal"` // 右值(Nested Set)|枚举:无|写入:同上|FK:无|红线:必须恒有 left < right Layer int32 `gorm:"column:layer;not null;default:1" json:"layer"` // 层级(根=1)|枚举:无|写入:加入时=父+1|FK:无|红线:**两道限制,不是同一个概念**——① **业务规则:最多 3 层**(`service.MaxDivideBizDepth`,第 4 层 → 3111,2026-11-10 拍板"硬规则")② **结构上限:100**(`service.MaxDivideDepth`,防 BFS 不终止/递归爆栈);判序先 3 后 100,**两个数不得合并**(否则下一个看到 3 与 100 并存的人一定会删掉一个) CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP" json:"createdAt"` // 创建时间(datetime(3))|枚举:无|写入:DB 默认|FK:无|红线:勿手改 UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP" json:"updatedAt"` // 更新时间(datetime(3),ON UPDATE)|枚举:无|写入:DB 自动|FK:无|红线:勿手改 } // TableName 指定表名。 func (UserDivideInto) TableName() string { return "t_user_divide_into" } // UserDivideIntoLog 分账明细表 t_user_divide_into_log(只增;两级幂等)。 // // 幂等口径(divide_into_log_add,设计 §3.3 #22): // - type=1(一次付清):同 user+student 只生成一条(不区分 order); // - type=2(按课次) :同 user+student+order_no 只生成一条。 // // 撤销语义(log_delete,设计 §3.3 #23):学生已有首次扣费上课记录 → 改 order_no 为首次 record // (保留分成);否则回滚 total_income 并物理删除该行。 type UserDivideIntoLog struct { ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // 分成记录 ID(D3 自增)|枚举:无|写入:INSERT 自动|FK:无|红线:撤销路径允许物理删除(旧语义),须与 total_income 回滚同事务 Type int8 `gorm:"index:idx_user_student_type,priority:3;column:type;not null;default:1" json:"type"` // 分成类型(1 一次付清/2 按课次;旧 tinyint ZEROFILL → tinyint)|枚举:DivideType*|写入:上行分成时|FK:无|红线:决定幂等维度,与节点 type 一致 UserID string `gorm:"index:idx_user_student_type,priority:1;column:user_id;size:26;not null" json:"userId"` // 获得分成的用户(ULID;原 userid)|枚举:无|写入:上行分成时|FK:→ t_user_divide_into.user_id|红线:必须是分账树节点(否则不入账) StudentID string `gorm:"index:idx_user_student_type,priority:2;index:idx_student_order,priority:1;column:student_id;size:26;not null" json:"studentId"` // 报课学生(ULID;原 studentid)|枚举:无|写入:上行分成时|FK:→ t_user(逻辑)|红线:幂等键组成列 OrderNo *string `gorm:"index:idx_student_order,priority:2;column:order_no;size:50" json:"orderNo,omitempty"` // 订单号(原 orderid:type=2 时=上课记录 id)|枚举:无|写入:上行分成/撤销改绑|FK:弱关联 t_courses_record.id|红线:幂等键组成列(type=2),改绑即改幂等语义,须审计 Money string `gorm:"column:money;type:decimal(12,2);not null;default:0.00" json:"money"` // 分成金额(元;旧 float(10,2))|枚举:无|写入:上行分成时取节点 quota|FK:无|红线:与 total_income 累加同事务 CreateTime time.Time `gorm:"index:idx_create_time;column:create_time;not null;default:CURRENT_TIMESTAMP" json:"createTime"` // 创建时间(datetime(3))|枚举:无|写入:DB 默认|FK:无|红线:勿手改(对账依据) Remark *string `gorm:"column:remark;type:longtext" json:"remark,omitempty"` // 备注(撤销改绑时写"推广分成")|枚举:无|写入:上行/撤销|FK:无|红线:脱敏 } // TableName 指定表名。 func (UserDivideIntoLog) TableName() string { return "t_user_divide_into_log" }