// 课程域 6 表 model(t_courses / t_courses_classify / t_courses_class / t_courses_student / // t_courses_record / t_courses_record_student)——与 academic-model-ddl.sql「第四部分」逐列对齐。 // // 本期范围(设计 §11-3 复盘结论):只做「排班生成 + 打卡 + 结束扣课时」最小闭环; // 调课/插班/结课申请/上课通知/学员档案列 P2(表未建)。 // // 表语义分层: // // t_courses(课程模板:周规则+次数+价格+时长) // → t_courses_class(排班规则:某课在某班由某老师按 week+时分 排课) // → t_courses_record(排班实例:一次具体上课,含 income/teacher_reward) // → t_courses_record_student(打卡/考勤/扣课时:一次打卡一行) // → t_courses_student(报课订单 = 学费账户/课次账户) // // ⚠️ 时间列语义特例:t_courses_class.start_time/end_time 旧库为"时分"(date 部分无意义), // 目标态保留 datetime(3);新写入统一以 1970-01-01 日期 + 时分承载(设计 §11-3)。 // // 字段注释五维口径:语义与单位 | 枚举全量取值 | 写入时机 | 主外键/关联语义 | 安全红线。 package model import "time" // t_courses.status 课程状态编码。 const ( CoursesStatusUnpublished int32 = 0 // 未发布 CoursesStatusPublished int32 = 1 // 发布未开始 CoursesStatusOngoing int32 = 2 // 进行中 CoursesStatusFinished int32 = 3 // 已结束 ) // t_courses_class.status / t_courses_class.confirm 编码。 const ( CoursesClassStatusOngoing int32 = 1 // 进行中 CoursesClassStatusEnded int32 = 2 // 结束 CoursesClassConfirmNo int8 = 0 // 未确认 CoursesClassConfirmYes int8 = 1 // 已确认 ) // t_courses_student.type / status 编码(报课类型与状态)。 const ( CoursesStudentTypeNormal int8 = 1 // 正常上课 CoursesStudentTypeTransfer int8 = 2 // 插班 CoursesStudentStatusNormal int8 = 1 // 正常 CoursesStudentStatusEnded int8 = 2 // 结束 ) // t_courses_record.status / 调课标记 编码。 const ( CoursesRecordStatusNotStarted int32 = 0 // 未开始 CoursesRecordStatusOngoing int32 = 1 // 正在进行 CoursesRecordStatusFinished int32 = 2 // 已结束 CoursesRecordChangingCourseNo int8 = 0 // 非调课 CoursesRecordChangingCourseYes int8 = 1 // 调课 CoursesRecordTransferStudentNo int8 = 0 // 无插班生 CoursesRecordTransferStudentYes int8 = 1 // 有插班生 CoursesRecordRewardTypeNormal int8 = 1 // 正常结算 CoursesRecordRewardTypeNoMinimum int8 = 2 // 按原课程老师课酬无最低课酬 ) // t_courses_record_student.type / pay 编码。 const ( CoursesRecordStudentTypeNormal int32 = 1 // 正常 CoursesRecordStudentTypeTransfer int32 = 2 // 插班 CoursesRecordStudentTypeGive int32 = 3 // 赠送 CoursesRecordStudentPayUnpaid int8 = 0 // 未支付 CoursesRecordStudentPayPaid int8 = 1 // 已支付 CoursesWeekAll string = "-1" // week 列的特殊取值:全部周 ) // Courses 课程信息表 t_courses(课程模板:价格/次数/周规则/时长)。 // // week 语义(字符串原样保留,不做位图化,保迁移稳定): // "-1" 全部周 / "0" 周日 / "1".."6" 周一..周六 / 多值英文逗号分隔(如 "1,3,5")。 type Courses struct { ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // 课程 ID(D3 自增)|枚举:无|写入:INSERT 自动|FK:被 classify/class/student/record 引用|红线:已排课的课程禁止物理删除 SubjectID int32 `gorm:"index:idx_subject_grade,priority:1;column:subject_id;not null;default:0" json:"subjectId"` // 科目 ID(0 混合科)|枚举:科目字典(跨域)|写入:创建/编辑|FK:→ 教学域科目(无物理 FK)|红线:仅分类维度 GradeID int32 `gorm:"index:idx_subject_grade,priority:2;column:grade_id;not null;default:0" json:"gradeId"` // 年级 ID(0 混合年级)|枚举:年级字典(跨域)|写入:创建/编辑|FK:→ 教学域年级|红线:仅分类维度 Type int32 `gorm:"column:type;not null;default:1" json:"type"` // 类型(1000 集体班/2000 VIP…)|枚举:班型编码(与 classify.class_type 对应)|写入:创建/编辑|FK:无|红线:影响课酬匹配,改动须评审 InstitutionID int64 `gorm:"index:idx_inst_campus_status,priority:1;column:institution_id;not null;default:0" json:"institutionId"` // 机构 ID(原 schoolid;作用域列)|枚举:无|写入:创建时锁定|FK:→ t_institution|红线:scoping 强依赖 CampusID int64 `gorm:"index:idx_inst_campus_status,priority:2;column:campus_id;not null;default:0" json:"campusId"` // 校区 ID(原 campusid;作用域列)|枚举:无|写入:创建/回填(campus_id=0 的多校区课程不更新)|FK:→ t_campus|红线:backfill 仅唯一校区时回填(设计 §3.5 #29) ClassifyID int64 `gorm:"index:idx_classify;column:classify_id;not null;default:0" json:"classifyId"` // 课程分类 ID(原 classifyid)|枚举:无|写入:创建/编辑|FK:→ t_courses_classify.id|红线:回填 max_people 依赖本列 // ⚠ 本列**故意不写 `default:1`**:GORM 对"有 default tag 且字段为零值"的列会**跳过该列**、改由 DB 默认值填充, // 而 0(未发布)恰恰是本列最常用的零值 ⇒ 新建课程会被静默写成 status=1("新建即发布")。 // 去掉 tag 后 GORM 总是显式写 status,0 能落库;DDL 的 `DEFAULT 1` 保持不变(只影响裸 SQL 插入)。 // 同类坑本项目已踩两次(另一次是 `create_time`):凡有 `default:` 又可能被赋零值的列,先问"零值能落库吗"。 Status int32 `gorm:"index:idx_inst_campus_status,priority:3;column:status;not null" json:"status"` // 状态(0 未发布/1 发布未开始/2 进行中/3 已结束)|枚举:CoursesStatus*|写入:发布/结课任务|FK:无|红线:状态机单向推进 Name string `gorm:"column:name;size:500;not null" json:"name"` // 课程名称|枚举:无|写入:创建/编辑|FK:无|红线:用户输入,展示转义 Number int32 `gorm:"column:number;not null;default:0" json:"number"` // 上课次数(课次)|枚举:无|写入:创建/编辑|FK:无|红线:与 record 生成条数关联,改动须校验已开课 Duration string `gorm:"column:duration;type:decimal(5,1);not null;default:0.0" json:"duration"` // 单节课时长(课时,1 位小数,保留旧精度)|枚举:无|写入:创建/编辑|FK:无|红线:精度例外列(非 12,2) Week string `gorm:"column:week;size:50;not null;default:'-1'" json:"week"` // 周规则("-1" 全部/"0" 周日/"1".."6"/多值逗号分隔)|枚举:见上方 week 语义|写入:创建/编辑|FK:无|红线:解析逻辑须与旧 PHP 一致(排班展开依赖) StartTime *time.Time `gorm:"column:start_time" json:"startTime,omitempty"` // 课程开始时间(datetime(3))|枚举:无|写入:创建/编辑|FK:无|红线:<= end_time EndTime *time.Time `gorm:"column:end_time" json:"endTime,omitempty"` // 课程结束时间(datetime(3))|枚举:无|写入:创建/编辑|FK:无|红线:同上 Address *string `gorm:"column:address;type:longtext" json:"address,omitempty"` // 上课地址|枚举:无|写入:创建/编辑|FK:无|红线:展示转义 OriginalCost string `gorm:"column:original_cost;type:decimal(12,2);not null;default:0.00" json:"originalCost"` // 原价(元;旧 float(10,3) → 2 位收敛)|枚举:无|写入:创建/编辑|FK:无|红线:报课快照进 courses_student,事后改不影响存量 RealPrice string `gorm:"column:real_price;type:decimal(12,2);not null;default:0.00" json:"realPrice"` // 实价(元)|枚举:无|写入:创建/编辑|FK:无|红线:同上 DiscountCash string `gorm:"column:discount_cash;type:decimal(12,2);not null;default:0.00" json:"discountCash"` // 现金折扣(元)|枚举:无|写入:创建/编辑|FK:无|红线:可为负(加价场景),勿强校验非负 DiscountRatio int32 `gorm:"column:discount_ratio;not null;default:0" json:"discountRatio"` // 折扣率(百分比数字部分,如 85 表示 85%)|枚举:整数百分比|写入:创建/编辑|FK:无|红线:仅展示/试算,实际金额以 real_price 为准 Remark *string `gorm:"column:remark;type:longtext" json:"remark,omitempty"` // 备注|枚举:无|写入:创建/编辑|FK:无|红线:脱敏 MaxPeople int32 `gorm:"column:max_people;not null;default:0" json:"maxPeople"` // 最多人数(0 时由 courses_update 从 classify 继承)|枚举:无|写入:创建/编辑/修复 SP|FK:无|红线:回填幂等(仅 max_people=0 行) UpdateUserID *string `gorm:"column:update_user_id;size:26" json:"updateUserId,omitempty"` // 最后修改人(ULID;原 update_userid)|枚举:无|写入:每次编辑|FK:→ t_user(逻辑)|红线:禁止旧 int id UpdateTime *time.Time `gorm:"column:update_time" json:"updateTime,omitempty"` // 最后修改时间(datetime(3))|枚举:无|写入:每次编辑|FK:无|红线:与 update_user_id 同写 CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP" json:"createdAt"` // 创建时间(datetime(3))|枚举:无|写入:DB 默认|FK:无|红线:勿手改 } // TableName 指定表名。 func (Courses) TableName() string { return "t_courses" } // CoursesClassify 课程分类表 t_courses_classify(班级类型/单价/人数区间 + 课酬方案绑定)。 type CoursesClassify struct { ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // 分类 ID(D3 自增)|枚举:无|写入:INSERT 自动|FK:被 t_courses.classify_id 引用|红线:被引用后禁删 InstitutionID int64 `gorm:"index:idx_inst_campus,priority:1;column:institution_id;not null;default:0" json:"institutionId"` // 机构 ID(原 schoolid;作用域列)|枚举:无|写入:创建时锁定|FK:→ t_institution|红线:scoping 强依赖 CampusID int64 `gorm:"index:idx_inst_campus,priority:2;column:campus_id;not null;default:0" json:"campusId"` // 校区 ID(原 campusid;作用域列)|枚举:无|写入:创建时锁定|FK:→ t_campus|红线:同上 ClassType int32 `gorm:"column:class_type;not null;default:1000" json:"classType"` // 班级类型(1000 集体班/1001 小班…)|枚举:班型编码(与 course_payment_detail.class_type 对应)|写入:创建/编辑|FK:无|红线:课酬匹配维度 CoursePaymentID int64 `gorm:"index:idx_payment;column:course_payment_id;not null;default:0" json:"coursePaymentId"` // 课酬方案 ID(原 course_paymentid)|枚举:无|写入:创建/编辑|FK:→ t_course_payment.id|红线:0=未绑定方案(课酬试算回退通用档) GradeID int32 `gorm:"column:grade_id;not null;default:0" json:"gradeId"` // 年级 ID(0 混合)|枚举:年级字典|写入:创建/编辑|FK:→ 教学域年级|红线:仅分类维度 SubjectID int32 `gorm:"column:subject_id;not null;default:0" json:"subjectId"` // 科目 ID(0 混合)|枚举:科目字典|写入:创建/编辑|FK:→ 教学域科目|红线:同上 Name string `gorm:"column:name;size:500;not null" json:"name"` // 分类名称|枚举:无|写入:创建/编辑|FK:无|红线:展示转义 UnitPrice string `gorm:"column:unit_price;type:decimal(12,2);not null;default:0.00" json:"unitPrice"` // 单价(元/一次课;旧 float(10,3) 收敛)|枚举:无|写入:创建/编辑|FK:无|红线:报课计费依据,改动须评估存量课程 MinPeople int32 `gorm:"column:min_people;not null;default:1" json:"minPeople"` // 最少人数|枚举:无|写入:创建/编辑|FK:无|红线:<= max_people MaxPeople int32 `gorm:"column:max_people;not null;default:1000" json:"maxPeople"` // 最多人数|枚举:无|写入:创建/编辑|FK:无|红线:同上 } // TableName 指定表名。 func (CoursesClassify) TableName() string { return "t_courses_classify" } // CoursesClass 课程班级关系表 t_courses_class(排班规则:某课在某班由某老师按 week+时分 排课)。 // // ⚠️ start_time/end_time 旧库仅"时分"语义;目标态保留 datetime(3) 兼容旧数据, // 新写入统一 1970-01-01 + 时分(设计 §11-3)。 type CoursesClass struct { ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // 关系 ID(D3 自增)|枚举:无|写入:INSERT 自动|FK:无|红线:排班规则行,被 record 生成依赖 Status int32 `gorm:"column:status;not null;default:1" json:"status"` // 状态(1 进行中/2 结束)|枚举:CoursesClassStatus*|写入:创建/结课|FK:无|红线:结束后不再生成 record CoursesID int64 `gorm:"index:idx_courses;column:courses_id;not null" json:"coursesId"` // 课程 ID(原 coursesid)|枚举:无|写入:创建|FK:→ t_courses.id|红线:禁止改绑 ClassID int64 `gorm:"index:idx_class;column:class_id;not null" json:"classId"` // 班级 ID(原 classid;旧库为外部班级体系 id,本期仅存值)|枚举:无|写入:创建|FK:外部班级体系(无物理 FK)|红线:不做业务联查 TeacherID *string `gorm:"index:idx_teacher;column:teacher_id;size:26" json:"teacherId,omitempty"` // 老师(ULID;原 teacherid;0→NULL)|枚举:无|写入:排课时|FK:→ t_user(逻辑)|红线:课酬计算输入,禁止旧 int id InstitutionID int64 `gorm:"index:idx_inst_campus,priority:1;column:institution_id;not null;default:0" json:"institutionId"` // 机构 ID(原 schoolid;作用域列)|枚举:无|写入:创建时锁定|FK:→ t_institution|红线:scoping 强依赖 CampusID int64 `gorm:"index:idx_inst_campus,priority:2;column:campus_id;not null;default:0" json:"campusId"` // 校区 ID(原 campusid;作用域列)|枚举:无|写入:创建时锁定|FK:→ t_campus|红线:同上 StandardLessonCost string `gorm:"column:standard_lesson_cost;type:decimal(12,2);not null;default:0.00" json:"standardLessonCost"` // 课程标准单价(元;旧 float(10,3) 收敛)|枚举:无|写入:排课时|FK:无|红线:与 classify.unit_price 口径区分(本列=排班时快照) Number int32 `gorm:"column:number;not null;default:0" json:"number"` // 开课总量(课次)|枚举:无|写入:排课时|FK:无|红线:与 finished_number 比较判结课 LessonCost string `gorm:"column:lesson_cost;type:decimal(12,2);not null;default:0.00" json:"lessonCost"` // 一次课的课费(元)|枚举:无|写入:排课时|FK:无|红线:打卡扣课时依据 DiscountCash string `gorm:"column:discount_cash;type:decimal(12,2);not null;default:0.00" json:"discountCash"` // 课程优惠(元)|枚举:无|写入:排课时|FK:无|红线:可为负 FinishedNumber int32 `gorm:"column:finished_number;not null;default:0" json:"finishedNumber"` // 已上课量(课次)|枚举:无|写入:结束上课时 +1|FK:无|红线:不得超过 number Confirm int8 `gorm:"column:confirm;not null;default:0" json:"confirm"` // 是否确认(0 未确认/1 确认)|枚举:CoursesClassConfirm*|写入:确认动作|FK:无|红线:未确认不得开始上课(业务校验) Week string `gorm:"column:week;size:50;not null;default:'-1'" json:"week"` // 周规则("-1" 全部/"0" 周日/"1".."6"/多值逗号分隔)|枚举:同 t_courses.week|写入:排课时|FK:无|红线:排班展开依据 StartTime *time.Time `gorm:"column:start_time" json:"startTime,omitempty"` // 上课时间(时分语义;旧 NOT NULL,目标态放宽)|枚举:无|写入:排课时(1970-01-01+时分)|FK:无|红线:date 部分无业务含义,勿用于区间查询 EndTime *time.Time `gorm:"column:end_time" json:"endTime,omitempty"` // 下课时间(时分语义)|枚举:无|写入:排课时|FK:无|红线:同上 Address *string `gorm:"column:address;type:longtext" json:"address,omitempty"` // 上课地址|枚举:无|写入:排课时|FK:无|红线:展示转义 Remark *string `gorm:"column:remark;type:longtext" json:"remark,omitempty"` // 备注|枚举:无|写入:排课时|FK:无|红线:脱敏 CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP" json:"createdAt"` // 创建时间(datetime(3))|枚举:无|写入:DB 默认|FK:无|红线:勿手改 } // TableName 指定表名。 func (CoursesClass) TableName() string { return "t_courses_class" } // CoursesStudent 学生报课信息表 t_courses_student(学费账户/课次账户;旧 UUID 主键 → int64 + biz_no)。 // // 该表同时是"报课订单"载体:t_courses_activity_user_discount_log.courses_student_id / // t_courses_activity_used_log.order_no 均指向本表;旧 UUID 必须保留在 biz_no(设计 §2「19.」)。 // // ⚠️ activity_ids 旧为"多活动英文逗号串":目标态保留 varchar(200) 原样,**不拆关联表** // (拆表引入不可逆迁移风险且旧报表依赖该列,设计 §2.3)。 // ⚠️ confirm_discount_user_id 为空/NULL 表示"不可充值"(业务语义,设计 §4.2)。 type CoursesStudent struct { ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // 报课记录 ID(D3 自增)|枚举:无|写入:INSERT 自动|FK:被 activity 两张 log 引用|红线:对外单号是 biz_no BizNo string `gorm:"uniqueIndex:uk_biz_no;column:biz_no;size:32;not null" json:"bizNo"` // 业务单号(旧 UUID 原样保留;UNIQUE)|枚举:无|写入:报课时|FK:迁移幂等/回滚锚点|红线:不可变(旧 UUID 引用列靠它重映射) CoursesID int64 `gorm:"index:idx_courses;column:courses_id;not null" json:"coursesId"` // 课程 ID(原 coursesid)|枚举:无|写入:报课时|FK:→ t_courses.id|红线:与课程作用域须一致 ClassID int64 `gorm:"column:class_id;not null;default:0" json:"classId"` // 班级 ID(原 classid)|枚举:无|写入:报课时|FK:外部/本域班级(无物理 FK)|红线:仅存值 InstitutionID int64 `gorm:"index:idx_inst_campus,priority:1;column:institution_id;not null;default:0" json:"institutionId"` // 机构 ID(原 schoolid;作用域列)|枚举:无|写入:报课时|FK:→ t_institution|红线:scoping 强依赖 CampusID int64 `gorm:"index:idx_inst_campus,priority:2;column:campus_id;not null;default:0" json:"campusId"` // 校区 ID(原 campusid;作用域列)|枚举:无|写入:报课时|FK:→ t_campus|红线:同上 StudentID string `gorm:"index:idx_student_status,priority:1;index:idx_unpaid,priority:1;column:student_id;size:26;not null" json:"studentId"` // 学生(ULID;原 studentid)|枚举:无|写入:报课时|FK:→ t_user(逻辑)|红线:欠费核销按本列过滤 Type int8 `gorm:"column:type;not null;default:1" json:"type"` // 上课类型(1 正常/2 插班)|枚举:CoursesStudentType*|写入:报课时|FK:无|红线:影响扣课时规则 Status int8 `gorm:"index:idx_student_status,priority:2;column:status;not null;default:1" json:"status"` // 状态(1 正常/2 结束)|枚举:CoursesStudentStatus*|写入:报课/结课|FK:无|红线:结束后不再参与打卡 Receivable string `gorm:"column:receivable;type:decimal(12,2);not null;default:0.00" json:"receivable"` // 应收(元)|枚举:无|写入:报课时|FK:无|红线:= net_receipts + unpaid(对账恒等式) NetReceipts string `gorm:"column:net_receipts;type:decimal(12,2);not null;default:0.00" json:"netReceipts"` // 实收(元;学生实际缴纳)|枚举:无|写入:缴费/退费|FK:无|红线:与资金流水累计一致(对账) Unpaid string `gorm:"index:idx_unpaid,priority:2;column:unpaid;type:decimal(12,2);not null;default:0.00" json:"unpaid"` // 欠费(元)|枚举:无|写入:缴费/核销|FK:无|红线:>0 即进入欠费核销候选(idx_unpaid) LessonCost string `gorm:"column:lesson_cost;type:decimal(12,2);not null;default:0.00" json:"lessonCost"` // 一节课的费用(元)|枚举:无|写入:报课时快照|FK:无|红线:打卡扣费依据,扣课时不得改本列 Number int32 `gorm:"column:number;not null;default:0" json:"number"` // 总课次|枚举:无|写入:报课时|FK:无|红线:含 give_number(赠送)口径差异见旧 PHP FinishedNumber int32 `gorm:"column:finished_number;not null;default:0" json:"finishedNumber"` // 已上课次|枚举:无|写入:打卡 +1|FK:无|红线:不得超过 number+give_number DiscountCash string `gorm:"column:discount_cash;type:decimal(12,2);not null;default:0.00" json:"discountCash"` // 现金折扣(元)|枚举:无|写入:报课时|FK:无|红线:可为负 ActivityIDs *string `gorm:"column:activity_ids;size:200" json:"activityIds,omitempty"` // 报课使用的活动 ID(英文逗号分隔;原 activityid)|枚举:活动 ID 列表串|写入:报课时|FK:弱关联 t_courses_activity.id(不拆表)|红线:勿当 JSON 解析,按英文逗号 split GiveNumber int32 `gorm:"column:give_number;not null;default:0" json:"giveNumber"` // 赠送课次|枚举:无|写入:报课时|FK:无|红线:扣课时先扣赠送还是实付须按旧口径 GiveCost string `gorm:"column:give_cost;type:decimal(12,2);not null;default:0.00" json:"giveCost"` // 使用的赠送金额(元)|枚举:无|写入:报课时|FK:无|红线:与 discount_log 累计一致 ChangingCourseNumber int32 `gorm:"column:changing_course_number;not null;default:0" json:"changingCourseNumber"` // 调课次数|枚举:无|写入:调课(P2)|FK:无|红线:本期只读 RemainderNumber int32 `gorm:"column:remainder_number;not null;default:0" json:"remainderNumber"` // 剩余课次|枚举:无|写入:报课/打卡|FK:无|红线:>=0(负数=数据异常,须修复) RemainderCost string `gorm:"column:remainder_cost;type:decimal(12,2);not null;default:0.00" json:"remainderCost"` // 剩余费用(元;缴费扣除每次课费后余额)|枚举:无|写入:报课/打卡|FK:无|红线:与 net_receipts−已扣课费 一致(对账) RemainderReceivable string `gorm:"column:remainder_receivable;type:decimal(12,2);not null;default:0.00" json:"remainderReceivable"` // 剩余应收(元)|枚举:无|写入:报课/打卡|FK:无|红线:<= receivable BadDebt string `gorm:"column:bad_debt;type:decimal(12,2);not null;default:0.00" json:"badDebt"` // 坏账(元)|枚举:无|写入:退费差额/坏账处理|FK:无|红线:与 special_expenses 同源口径 ConfirmDiscountUserID *string `gorm:"column:confirm_discount_user_id;size:26" json:"confirmDiscountUserId,omitempty"` // 确认学生折扣的操作人(原 confirm_discount_userid;空/NULL=不可充值)|枚举:无|写入:折扣确认动作|FK:→ t_user(逻辑)|红线:**NULL 语义=不可充值**,勿用 0 替代 ConfirmTime *time.Time `gorm:"column:confirm_time" json:"confirmTime,omitempty"` // 确认时间(datetime(3))|枚举:无|写入:折扣确认|FK:无|红线:与 confirm_discount_user_id 同写 RecommenderUserID *string `gorm:"column:recommender_user_id;size:26" json:"recommenderUserId,omitempty"` // 推荐人(原 recommender_userid)|枚举:无|写入:报课时|FK:→ t_user(逻辑)|红线:分账树上行起点(设计 §3.3) RecommenderTime *time.Time `gorm:"column:recommender_time" json:"recommenderTime,omitempty"` // 推荐时间(datetime(3))|枚举:无|写入:报课时|FK:无|红线:勿手改 CreateTime time.Time `gorm:"column:create_time;not null;default:CURRENT_TIMESTAMP" json:"createTime"` // 创建时间(datetime(3))|枚举:无|写入:DB 默认|FK:无|红线:欠费核销排序键(create_time ASC) Remark *string `gorm:"column:remark;type:longtext" json:"remark,omitempty"` // 备注|枚举:无|写入:报课/编辑|FK:无|红线:脱敏 } // TableName 指定表名。 func (CoursesStudent) TableName() string { return "t_courses_student" } // CoursesRecord 课程记录 t_courses_record(课表/排班实例;旧 UUID 主键 → int64 + biz_no)。 // // 流转(设计 §7.3 流程外延):开始上课写 actual_start_time;结束上课写 actual_end_time + income // + teacher_reward,并联动 t_courses_record_student / 余额 spend / 分账 / 课酬(同事务)。 type CoursesRecord struct { ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // 上课记录 ID(D3 自增)|枚举:无|写入:INSERT 自动|FK:被 record_student.record_id 引用|红线:对外单号是 biz_no BizNo string `gorm:"uniqueIndex:uk_biz_no;column:biz_no;size:32;not null" json:"bizNo"` // 业务单号(旧 UUID 原样保留;UNIQUE)|枚举:无|写入:排班生成时|FK:迁移/回滚锚点|红线:不可变 CoursesID int64 `gorm:"index:idx_courses_start,priority:1;column:courses_id;not null" json:"coursesId"` // 课程 ID(原 coursesid)|枚举:无|写入:排班生成|FK:→ t_courses.id|红线:与 start_time 组排班唯一性判据(3122) ClassID int64 `gorm:"column:class_id;not null" json:"classId"` // 班级 ID(原 classid)|枚举:无|写入:排班生成|FK:弱关联 t_courses_class.class_id|红线:仅存值 TeacherID *string `gorm:"index:idx_teacher_start,priority:1;column:teacher_id;size:26" json:"teacherId,omitempty"` // 老师(ULID;原 teacherid)|枚举:无|写入:排班/调课|FK:→ t_user(逻辑)|红线:课酬归属人,禁止改绑已结束记录 InstitutionID int64 `gorm:"index:idx_inst_campus,priority:1;column:institution_id;not null;default:0" json:"institutionId"` // 机构 ID(原 schoolid;作用域列)|枚举:无|写入:排班生成|FK:→ t_institution|红线:scoping 强依赖 CampusID int64 `gorm:"index:idx_inst_campus,priority:2;column:campus_id;not null;default:0" json:"campusId"` // 校区 ID(原 campusid;作用域列)|枚举:无|写入:排班生成|FK:→ t_campus|红线:同上 Status int32 `gorm:"index:idx_status;column:status;not null;default:0" json:"status"` // 状态(0 未开始/1 正在进行/2 已结束)|枚举:CoursesRecordStatus*|写入:开始/结束上课|FK:无|红线:状态机校验(非法迁移 → 3121) ChangingCourse int8 `gorm:"column:changing_course;not null;default:0" json:"changingCourse"` // 是否调课(1 调课)|枚举:CoursesRecordChangingCourse*|写入:调课(P2)|FK:无|红线:本期只读 StudentChangingCourse int32 `gorm:"column:student_changing_course;not null;default:0" json:"studentChangingCourse"` // 单学生调课数|枚举:无|写入:调课(P2)|FK:无|红线:本期只读 TransferStudent int8 `gorm:"column:transfer_student;not null;default:0" json:"transferStudent"` // 是否有插班生(1 有)|枚举:CoursesRecordTransferStudent*|写入:插班(P2)|FK:无|红线:本期只读 OriginalRecordNo *string `gorm:"column:original_record_no;size:32" json:"originalRecordNo,omitempty"` // 原上课记录单号(原 original_recordid UUID)|枚举:无|写入:调课生成|FK:弱关联本表 biz_no|红线:字符串保留(非 int64 FK) StartTime *time.Time `gorm:"index:idx_courses_start,priority:2;index:idx_teacher_start,priority:2;column:start_time" json:"startTime,omitempty"` // 上课时间(datetime(3))|枚举:无|写入:排班生成|FK:无|红线:排班唯一性判据之一 EndTime *time.Time `gorm:"column:end_time" json:"endTime,omitempty"` // 下课时间(datetime(3))|枚举:无|写入:排班生成|FK:无|红线:>= start_time Address *string `gorm:"column:address;type:longtext" json:"address,omitempty"` // 上课地点|枚举:无|写入:排班生成/编辑|FK:无|红线:展示转义 ActualStartTime *time.Time `gorm:"column:actual_start_time" json:"actualStartTime,omitempty"` // 实际上课时间(datetime(3))|枚举:无|写入:开始上课|FK:无|红线:只写一次(重复开始 → 3121) ActualEndTime *time.Time `gorm:"column:actual_end_time" json:"actualEndTime,omitempty"` // 实际下课时间(datetime(3))|枚举:无|写入:结束上课|FK:无|红线:结束上课的幂等判据(非空即已结束) StudentNumber int32 `gorm:"column:student_number;not null;default:0" json:"studentNumber"` // 上课人数|枚举:无|写入:结束上课汇总|FK:无|红线:与 record_student 行数一致 Income string `gorm:"column:income;type:decimal(12,2);not null;default:0.00" json:"income"` // 课程收入(元)|枚举:无|写入:结束上课|FK:无|红线:= Σ record_student.cost(对账) TeacherReward string `gorm:"column:teacher_reward;type:decimal(12,2);not null;default:0.00" json:"teacherReward"` // 老师课酬(元)|枚举:无|写入:结束上课(课酬试算)|FK:无|红线:**条件式**恒等 —— 有非赠送生时 = Σ(record_student.teacher_reward WHERE type∈{1,2});全班赠送(type 全为 3)时学生行全 0、本列仍写课酬(PHP `if($stuCount)` 语义,见 `applyTeacherRewards`) TeacherRewardType int8 `gorm:"column:teacher_reward_type;not null;default:1" json:"teacherRewardType"` // 课酬结算方式(1 正常/2 按原课程老师课酬无最低课酬)|枚举:CoursesRecordRewardType*|写入:结束上课|FK:无|红线:影响试算分支 ExpectStudentIDs *string `gorm:"column:expect_student_ids;size:500" json:"expectStudentIds,omitempty"` // 要求上课的学生 id 列表(逗号分隔;原 expect_studentids)|枚举:ULID 列表串|写入:排班生成|FK:弱关联 t_user|红线:字符串保留,勿当数组入 SQL Remark *string `gorm:"column:remark;type:longtext" json:"remark,omitempty"` // 备注|枚举:无|写入:编辑|FK:无|红线:脱敏 CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP" json:"createdAt"` // 创建时间(datetime(3))|枚举:无|写入:DB 默认|FK:无|红线:勿手改 } // TableName 指定表名。 func (CoursesRecord) TableName() string { return "t_courses_record" } // CoursesRecordStudent 课程上课记录 t_courses_record_student(打卡/考勤/扣课时;旧 UUID → int64 + biz_no)。 // // 语义:一次打卡 = 一行;cost 为本次扣除课费;give_cost 为本次使用的赠送金额; // `divide_into_log_delete` 用本表"首次 cost>0 且 end_time 非空"的记录判定分成归属(设计 §3.3 #23)。 type CoursesRecordStudent struct { ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // 记录 ID(D3 自增)|枚举:无|写入:INSERT 自动|FK:无|红线:对外单号是 biz_no BizNo string `gorm:"uniqueIndex:uk_biz_no;column:biz_no;size:32;not null" json:"bizNo"` // 业务单号(旧 UUID 原样保留;UNIQUE)|枚举:无|写入:打卡时|FK:迁移/回滚锚点|红线:不可变 CoursesID int64 `gorm:"column:courses_id;not null" json:"coursesId"` // 课程 ID(原 coursesid)|枚举:无|写入:打卡时冗余|FK:→ t_courses.id|红线:与 record 的课程一致 ClassID int64 `gorm:"column:class_id;not null;default:0" json:"classId"` // 班级 ID(原 classid)|枚举:无|写入:打卡时冗余|FK:弱关联|红线:仅存值 InstitutionID int64 `gorm:"index:idx_inst_campus,priority:1;column:institution_id;not null;default:0" json:"institutionId"` // 机构 ID(原 schoolid;作用域列)|枚举:无|写入:打卡时冗余|FK:→ t_institution|红线:scoping 强依赖 CampusID int64 `gorm:"index:idx_inst_campus,priority:2;column:campus_id;not null;default:0" json:"campusId"` // 校区 ID(原 campusid;作用域列)|枚举:无|写入:打卡时冗余|FK:→ t_campus|红线:同上 RecordID int64 `gorm:"index:idx_record;column:record_id;not null" json:"recordId"` // 上课记录 ID(原 recordid UUID → 迁移 JOIN biz_no 重映射)|枚举:无|写入:打卡时|FK:→ t_courses_record.id|红线:分账/课酬汇总按本列分组 StudentID string `gorm:"index:idx_student_time,priority:1;column:student_id;size:26;not null" json:"studentId"` // 学生(ULID;原 studentid)|枚举:无|写入:打卡时|FK:→ t_user(逻辑)|红线:首次扣费判定按 (student, start_time) Type int32 `gorm:"column:type;not null;default:1" json:"type"` // 上课类型(1 正常/2 插班/3 赠送)|枚举:CoursesRecordStudentType*|写入:打卡时|FK:无|红线:赠送课次不动余额(只动 give_cost) StartTime time.Time `gorm:"index:idx_student_time,priority:2;column:start_time;not null" json:"startTime"` // 上课时间(datetime(3))|枚举:无|写入:打卡时|FK:无|红线:只增不改(考勤凭证) EndTime *time.Time `gorm:"column:end_time" json:"endTime,omitempty"` // 下课时间(datetime(3))|枚举:无|写入:结束上课|FK:无|红线:非空 = 已完成(分成归属判据) Cost string `gorm:"column:cost;type:decimal(12,2);not null;default:0.00" json:"cost"` // 本次课程费用(元;扣课时金额)|枚举:无|写入:结束上课|FK:无|红线:Σ cost = record.income(对账) GiveCost string `gorm:"column:give_cost;type:decimal(12,2);not null;default:0.00" json:"giveCost"` // 本次使用的赠送金额(元)|枚举:无|写入:结束上课|FK:无|红线:与 discount_log 累计一致 TeacherReward string `gorm:"column:teacher_reward;type:decimal(12,2);not null;default:0.00" json:"teacherReward"` // 本次课老师课酬(元)|枚举:无|写入:结束上课(课酬试算)|FK:无|红线:**`type=3`(赠送)行恒为 0**(PHP 只写 `type IN (1,2)`);非赠送行之和 = record.teacher_reward Pay int8 `gorm:"column:pay;not null;default:0" json:"pay"` // 是否已支付(0 未/1 已支付)|枚举:CoursesRecordStudentPay*|写入:扣费成功/欠费核销|FK:无|红线:未支付即欠费候选 Remark *string `gorm:"column:remark;type:longtext" json:"remark,omitempty"` // 备注|枚举:无|写入:打卡/结束|FK:无|红线:脱敏 } // TableName 指定表名。 func (CoursesRecordStudent) TableName() string { return "t_courses_record_student" }