models_course.go 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. // 课程域 6 表 model(t_courses / t_courses_classify / t_courses_class / t_courses_student /
  2. // t_courses_record / t_courses_record_student)——与 academic-model-ddl.sql「第四部分」逐列对齐。
  3. //
  4. // 本期范围(设计 §11-3 复盘结论):只做「排班生成 + 打卡 + 结束扣课时」最小闭环;
  5. // 调课/插班/结课申请/上课通知/学员档案列 P2(表未建)。
  6. //
  7. // 表语义分层:
  8. //
  9. // t_courses(课程模板:周规则+次数+价格+时长)
  10. // → t_courses_class(排班规则:某课在某班由某老师按 week+时分 排课)
  11. // → t_courses_record(排班实例:一次具体上课,含 income/teacher_reward)
  12. // → t_courses_record_student(打卡/考勤/扣课时:一次打卡一行)
  13. // → t_courses_student(报课订单 = 学费账户/课次账户)
  14. //
  15. // ⚠️ 时间列语义特例:t_courses_class.start_time/end_time 旧库为"时分"(date 部分无意义),
  16. // 目标态保留 datetime(3);新写入统一以 1970-01-01 日期 + 时分承载(设计 §11-3)。
  17. //
  18. // 字段注释五维口径:语义与单位 | 枚举全量取值 | 写入时机 | 主外键/关联语义 | 安全红线。
  19. package model
  20. import "time"
  21. // t_courses.status 课程状态编码。
  22. const (
  23. CoursesStatusUnpublished int32 = 0 // 未发布
  24. CoursesStatusPublished int32 = 1 // 发布未开始
  25. CoursesStatusOngoing int32 = 2 // 进行中
  26. CoursesStatusFinished int32 = 3 // 已结束
  27. )
  28. // t_courses_class.status / t_courses_class.confirm 编码。
  29. const (
  30. CoursesClassStatusOngoing int32 = 1 // 进行中
  31. CoursesClassStatusEnded int32 = 2 // 结束
  32. CoursesClassConfirmNo int8 = 0 // 未确认
  33. CoursesClassConfirmYes int8 = 1 // 已确认
  34. )
  35. // t_courses_student.type / status 编码(报课类型与状态)。
  36. const (
  37. CoursesStudentTypeNormal int8 = 1 // 正常上课
  38. CoursesStudentTypeTransfer int8 = 2 // 插班
  39. CoursesStudentStatusNormal int8 = 1 // 正常
  40. CoursesStudentStatusEnded int8 = 2 // 结束
  41. )
  42. // t_courses_record.status / 调课标记 编码。
  43. const (
  44. CoursesRecordStatusNotStarted int32 = 0 // 未开始
  45. CoursesRecordStatusOngoing int32 = 1 // 正在进行
  46. CoursesRecordStatusFinished int32 = 2 // 已结束
  47. CoursesRecordChangingCourseNo int8 = 0 // 非调课
  48. CoursesRecordChangingCourseYes int8 = 1 // 调课
  49. CoursesRecordTransferStudentNo int8 = 0 // 无插班生
  50. CoursesRecordTransferStudentYes int8 = 1 // 有插班生
  51. CoursesRecordRewardTypeNormal int8 = 1 // 正常结算
  52. CoursesRecordRewardTypeNoMinimum int8 = 2 // 按原课程老师课酬无最低课酬
  53. )
  54. // t_courses_record_student.type / pay 编码。
  55. const (
  56. CoursesRecordStudentTypeNormal int32 = 1 // 正常
  57. CoursesRecordStudentTypeTransfer int32 = 2 // 插班
  58. CoursesRecordStudentTypeGive int32 = 3 // 赠送
  59. CoursesRecordStudentPayUnpaid int8 = 0 // 未支付
  60. CoursesRecordStudentPayPaid int8 = 1 // 已支付
  61. CoursesWeekAll string = "-1" // week 列的特殊取值:全部周
  62. )
  63. // Courses 课程信息表 t_courses(课程模板:价格/次数/周规则/时长)。
  64. //
  65. // week 语义(字符串原样保留,不做位图化,保迁移稳定):
  66. // "-1" 全部周 / "0" 周日 / "1".."6" 周一..周六 / 多值英文逗号分隔(如 "1,3,5")。
  67. type Courses struct {
  68. ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // 课程 ID(D3 自增)|枚举:无|写入:INSERT 自动|FK:被 classify/class/student/record 引用|红线:已排课的课程禁止物理删除
  69. SubjectID int32 `gorm:"index:idx_subject_grade,priority:1;column:subject_id;not null;default:0" json:"subjectId"` // 科目 ID(0 混合科)|枚举:科目字典(跨域)|写入:创建/编辑|FK:→ 教学域科目(无物理 FK)|红线:仅分类维度
  70. GradeID int32 `gorm:"index:idx_subject_grade,priority:2;column:grade_id;not null;default:0" json:"gradeId"` // 年级 ID(0 混合年级)|枚举:年级字典(跨域)|写入:创建/编辑|FK:→ 教学域年级|红线:仅分类维度
  71. Type int32 `gorm:"column:type;not null;default:1" json:"type"` // 类型(1000 集体班/2000 VIP…)|枚举:班型编码(与 classify.class_type 对应)|写入:创建/编辑|FK:无|红线:影响课酬匹配,改动须评审
  72. 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 强依赖
  73. 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)
  74. ClassifyID int64 `gorm:"index:idx_classify;column:classify_id;not null;default:0" json:"classifyId"` // 课程分类 ID(原 classifyid)|枚举:无|写入:创建/编辑|FK:→ t_courses_classify.id|红线:回填 max_people 依赖本列
  75. // ⚠ 本列**故意不写 `default:1`**:GORM 对"有 default tag 且字段为零值"的列会**跳过该列**、改由 DB 默认值填充,
  76. // 而 0(未发布)恰恰是本列最常用的零值 ⇒ 新建课程会被静默写成 status=1("新建即发布")。
  77. // 去掉 tag 后 GORM 总是显式写 status,0 能落库;DDL 的 `DEFAULT 1` 保持不变(只影响裸 SQL 插入)。
  78. // 同类坑本项目已踩两次(另一次是 `create_time`):凡有 `default:` 又可能被赋零值的列,先问"零值能落库吗"。
  79. Status int32 `gorm:"index:idx_inst_campus_status,priority:3;column:status;not null" json:"status"` // 状态(0 未发布/1 发布未开始/2 进行中/3 已结束)|枚举:CoursesStatus*|写入:发布/结课任务|FK:无|红线:状态机单向推进
  80. Name string `gorm:"column:name;size:500;not null" json:"name"` // 课程名称|枚举:无|写入:创建/编辑|FK:无|红线:用户输入,展示转义
  81. Number int32 `gorm:"column:number;not null;default:0" json:"number"` // 上课次数(课次)|枚举:无|写入:创建/编辑|FK:无|红线:与 record 生成条数关联,改动须校验已开课
  82. Duration string `gorm:"column:duration;type:decimal(5,1);not null;default:0.0" json:"duration"` // 单节课时长(课时,1 位小数,保留旧精度)|枚举:无|写入:创建/编辑|FK:无|红线:精度例外列(非 12,2)
  83. Week string `gorm:"column:week;size:50;not null;default:'-1'" json:"week"` // 周规则("-1" 全部/"0" 周日/"1".."6"/多值逗号分隔)|枚举:见上方 week 语义|写入:创建/编辑|FK:无|红线:解析逻辑须与旧 PHP 一致(排班展开依赖)
  84. StartTime *time.Time `gorm:"column:start_time" json:"startTime,omitempty"` // 课程开始时间(datetime(3))|枚举:无|写入:创建/编辑|FK:无|红线:<= end_time
  85. EndTime *time.Time `gorm:"column:end_time" json:"endTime,omitempty"` // 课程结束时间(datetime(3))|枚举:无|写入:创建/编辑|FK:无|红线:同上
  86. Address *string `gorm:"column:address;type:longtext" json:"address,omitempty"` // 上课地址|枚举:无|写入:创建/编辑|FK:无|红线:展示转义
  87. OriginalCost string `gorm:"column:original_cost;type:decimal(12,2);not null;default:0.00" json:"originalCost"` // 原价(元;旧 float(10,3) → 2 位收敛)|枚举:无|写入:创建/编辑|FK:无|红线:报课快照进 courses_student,事后改不影响存量
  88. RealPrice string `gorm:"column:real_price;type:decimal(12,2);not null;default:0.00" json:"realPrice"` // 实价(元)|枚举:无|写入:创建/编辑|FK:无|红线:同上
  89. DiscountCash string `gorm:"column:discount_cash;type:decimal(12,2);not null;default:0.00" json:"discountCash"` // 现金折扣(元)|枚举:无|写入:创建/编辑|FK:无|红线:可为负(加价场景),勿强校验非负
  90. DiscountRatio int32 `gorm:"column:discount_ratio;not null;default:0" json:"discountRatio"` // 折扣率(百分比数字部分,如 85 表示 85%)|枚举:整数百分比|写入:创建/编辑|FK:无|红线:仅展示/试算,实际金额以 real_price 为准
  91. Remark *string `gorm:"column:remark;type:longtext" json:"remark,omitempty"` // 备注|枚举:无|写入:创建/编辑|FK:无|红线:脱敏
  92. MaxPeople int32 `gorm:"column:max_people;not null;default:0" json:"maxPeople"` // 最多人数(0 时由 courses_update 从 classify 继承)|枚举:无|写入:创建/编辑/修复 SP|FK:无|红线:回填幂等(仅 max_people=0 行)
  93. UpdateUserID *string `gorm:"column:update_user_id;size:26" json:"updateUserId,omitempty"` // 最后修改人(ULID;原 update_userid)|枚举:无|写入:每次编辑|FK:→ t_user(逻辑)|红线:禁止旧 int id
  94. UpdateTime *time.Time `gorm:"column:update_time" json:"updateTime,omitempty"` // 最后修改时间(datetime(3))|枚举:无|写入:每次编辑|FK:无|红线:与 update_user_id 同写
  95. CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP" json:"createdAt"` // 创建时间(datetime(3))|枚举:无|写入:DB 默认|FK:无|红线:勿手改
  96. }
  97. // TableName 指定表名。
  98. func (Courses) TableName() string { return "t_courses" }
  99. // CoursesClassify 课程分类表 t_courses_classify(班级类型/单价/人数区间 + 课酬方案绑定)。
  100. type CoursesClassify struct {
  101. ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // 分类 ID(D3 自增)|枚举:无|写入:INSERT 自动|FK:被 t_courses.classify_id 引用|红线:被引用后禁删
  102. InstitutionID int64 `gorm:"index:idx_inst_campus,priority:1;column:institution_id;not null;default:0" json:"institutionId"` // 机构 ID(原 schoolid;作用域列)|枚举:无|写入:创建时锁定|FK:→ t_institution|红线:scoping 强依赖
  103. CampusID int64 `gorm:"index:idx_inst_campus,priority:2;column:campus_id;not null;default:0" json:"campusId"` // 校区 ID(原 campusid;作用域列)|枚举:无|写入:创建时锁定|FK:→ t_campus|红线:同上
  104. ClassType int32 `gorm:"column:class_type;not null;default:1000" json:"classType"` // 班级类型(1000 集体班/1001 小班…)|枚举:班型编码(与 course_payment_detail.class_type 对应)|写入:创建/编辑|FK:无|红线:课酬匹配维度
  105. 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=未绑定方案(课酬试算回退通用档)
  106. GradeID int32 `gorm:"column:grade_id;not null;default:0" json:"gradeId"` // 年级 ID(0 混合)|枚举:年级字典|写入:创建/编辑|FK:→ 教学域年级|红线:仅分类维度
  107. SubjectID int32 `gorm:"column:subject_id;not null;default:0" json:"subjectId"` // 科目 ID(0 混合)|枚举:科目字典|写入:创建/编辑|FK:→ 教学域科目|红线:同上
  108. Name string `gorm:"column:name;size:500;not null" json:"name"` // 分类名称|枚举:无|写入:创建/编辑|FK:无|红线:展示转义
  109. UnitPrice string `gorm:"column:unit_price;type:decimal(12,2);not null;default:0.00" json:"unitPrice"` // 单价(元/一次课;旧 float(10,3) 收敛)|枚举:无|写入:创建/编辑|FK:无|红线:报课计费依据,改动须评估存量课程
  110. MinPeople int32 `gorm:"column:min_people;not null;default:1" json:"minPeople"` // 最少人数|枚举:无|写入:创建/编辑|FK:无|红线:<= max_people
  111. MaxPeople int32 `gorm:"column:max_people;not null;default:1000" json:"maxPeople"` // 最多人数|枚举:无|写入:创建/编辑|FK:无|红线:同上
  112. }
  113. // TableName 指定表名。
  114. func (CoursesClassify) TableName() string { return "t_courses_classify" }
  115. // CoursesClass 课程班级关系表 t_courses_class(排班规则:某课在某班由某老师按 week+时分 排课)。
  116. //
  117. // ⚠️ start_time/end_time 旧库仅"时分"语义;目标态保留 datetime(3) 兼容旧数据,
  118. // 新写入统一 1970-01-01 + 时分(设计 §11-3)。
  119. type CoursesClass struct {
  120. ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // 关系 ID(D3 自增)|枚举:无|写入:INSERT 自动|FK:无|红线:排班规则行,被 record 生成依赖
  121. Status int32 `gorm:"column:status;not null;default:1" json:"status"` // 状态(1 进行中/2 结束)|枚举:CoursesClassStatus*|写入:创建/结课|FK:无|红线:结束后不再生成 record
  122. CoursesID int64 `gorm:"index:idx_courses;column:courses_id;not null" json:"coursesId"` // 课程 ID(原 coursesid)|枚举:无|写入:创建|FK:→ t_courses.id|红线:禁止改绑
  123. ClassID int64 `gorm:"index:idx_class;column:class_id;not null" json:"classId"` // 班级 ID(原 classid;旧库为外部班级体系 id,本期仅存值)|枚举:无|写入:创建|FK:外部班级体系(无物理 FK)|红线:不做业务联查
  124. TeacherID *string `gorm:"index:idx_teacher;column:teacher_id;size:26" json:"teacherId,omitempty"` // 老师(ULID;原 teacherid;0→NULL)|枚举:无|写入:排课时|FK:→ t_user(逻辑)|红线:课酬计算输入,禁止旧 int id
  125. InstitutionID int64 `gorm:"index:idx_inst_campus,priority:1;column:institution_id;not null;default:0" json:"institutionId"` // 机构 ID(原 schoolid;作用域列)|枚举:无|写入:创建时锁定|FK:→ t_institution|红线:scoping 强依赖
  126. CampusID int64 `gorm:"index:idx_inst_campus,priority:2;column:campus_id;not null;default:0" json:"campusId"` // 校区 ID(原 campusid;作用域列)|枚举:无|写入:创建时锁定|FK:→ t_campus|红线:同上
  127. 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 口径区分(本列=排班时快照)
  128. Number int32 `gorm:"column:number;not null;default:0" json:"number"` // 开课总量(课次)|枚举:无|写入:排课时|FK:无|红线:与 finished_number 比较判结课
  129. LessonCost string `gorm:"column:lesson_cost;type:decimal(12,2);not null;default:0.00" json:"lessonCost"` // 一次课的课费(元)|枚举:无|写入:排课时|FK:无|红线:打卡扣课时依据
  130. DiscountCash string `gorm:"column:discount_cash;type:decimal(12,2);not null;default:0.00" json:"discountCash"` // 课程优惠(元)|枚举:无|写入:排课时|FK:无|红线:可为负
  131. FinishedNumber int32 `gorm:"column:finished_number;not null;default:0" json:"finishedNumber"` // 已上课量(课次)|枚举:无|写入:结束上课时 +1|FK:无|红线:不得超过 number
  132. Confirm int8 `gorm:"column:confirm;not null;default:0" json:"confirm"` // 是否确认(0 未确认/1 确认)|枚举:CoursesClassConfirm*|写入:确认动作|FK:无|红线:未确认不得开始上课(业务校验)
  133. Week string `gorm:"column:week;size:50;not null;default:'-1'" json:"week"` // 周规则("-1" 全部/"0" 周日/"1".."6"/多值逗号分隔)|枚举:同 t_courses.week|写入:排课时|FK:无|红线:排班展开依据
  134. StartTime *time.Time `gorm:"column:start_time" json:"startTime,omitempty"` // 上课时间(时分语义;旧 NOT NULL,目标态放宽)|枚举:无|写入:排课时(1970-01-01+时分)|FK:无|红线:date 部分无业务含义,勿用于区间查询
  135. EndTime *time.Time `gorm:"column:end_time" json:"endTime,omitempty"` // 下课时间(时分语义)|枚举:无|写入:排课时|FK:无|红线:同上
  136. Address *string `gorm:"column:address;type:longtext" json:"address,omitempty"` // 上课地址|枚举:无|写入:排课时|FK:无|红线:展示转义
  137. Remark *string `gorm:"column:remark;type:longtext" json:"remark,omitempty"` // 备注|枚举:无|写入:排课时|FK:无|红线:脱敏
  138. CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP" json:"createdAt"` // 创建时间(datetime(3))|枚举:无|写入:DB 默认|FK:无|红线:勿手改
  139. }
  140. // TableName 指定表名。
  141. func (CoursesClass) TableName() string { return "t_courses_class" }
  142. // CoursesStudent 学生报课信息表 t_courses_student(学费账户/课次账户;旧 UUID 主键 → int64 + biz_no)。
  143. //
  144. // 该表同时是"报课订单"载体:t_courses_activity_user_discount_log.courses_student_id /
  145. // t_courses_activity_used_log.order_no 均指向本表;旧 UUID 必须保留在 biz_no(设计 §2「19.」)。
  146. //
  147. // ⚠️ activity_ids 旧为"多活动英文逗号串":目标态保留 varchar(200) 原样,**不拆关联表**
  148. // (拆表引入不可逆迁移风险且旧报表依赖该列,设计 §2.3)。
  149. // ⚠️ confirm_discount_user_id 为空/NULL 表示"不可充值"(业务语义,设计 §4.2)。
  150. type CoursesStudent struct {
  151. ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // 报课记录 ID(D3 自增)|枚举:无|写入:INSERT 自动|FK:被 activity 两张 log 引用|红线:对外单号是 biz_no
  152. BizNo string `gorm:"uniqueIndex:uk_biz_no;column:biz_no;size:32;not null" json:"bizNo"` // 业务单号(旧 UUID 原样保留;UNIQUE)|枚举:无|写入:报课时|FK:迁移幂等/回滚锚点|红线:不可变(旧 UUID 引用列靠它重映射)
  153. CoursesID int64 `gorm:"index:idx_courses;column:courses_id;not null" json:"coursesId"` // 课程 ID(原 coursesid)|枚举:无|写入:报课时|FK:→ t_courses.id|红线:与课程作用域须一致
  154. ClassID int64 `gorm:"column:class_id;not null;default:0" json:"classId"` // 班级 ID(原 classid)|枚举:无|写入:报课时|FK:外部/本域班级(无物理 FK)|红线:仅存值
  155. InstitutionID int64 `gorm:"index:idx_inst_campus,priority:1;column:institution_id;not null;default:0" json:"institutionId"` // 机构 ID(原 schoolid;作用域列)|枚举:无|写入:报课时|FK:→ t_institution|红线:scoping 强依赖
  156. CampusID int64 `gorm:"index:idx_inst_campus,priority:2;column:campus_id;not null;default:0" json:"campusId"` // 校区 ID(原 campusid;作用域列)|枚举:无|写入:报课时|FK:→ t_campus|红线:同上
  157. 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(逻辑)|红线:欠费核销按本列过滤
  158. Type int8 `gorm:"column:type;not null;default:1" json:"type"` // 上课类型(1 正常/2 插班)|枚举:CoursesStudentType*|写入:报课时|FK:无|红线:影响扣课时规则
  159. Status int8 `gorm:"index:idx_student_status,priority:2;column:status;not null;default:1" json:"status"` // 状态(1 正常/2 结束)|枚举:CoursesStudentStatus*|写入:报课/结课|FK:无|红线:结束后不再参与打卡
  160. Receivable string `gorm:"column:receivable;type:decimal(12,2);not null;default:0.00" json:"receivable"` // 应收(元)|枚举:无|写入:报课时|FK:无|红线:= net_receipts + unpaid(对账恒等式)
  161. NetReceipts string `gorm:"column:net_receipts;type:decimal(12,2);not null;default:0.00" json:"netReceipts"` // 实收(元;学生实际缴纳)|枚举:无|写入:缴费/退费|FK:无|红线:与资金流水累计一致(对账)
  162. 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)
  163. LessonCost string `gorm:"column:lesson_cost;type:decimal(12,2);not null;default:0.00" json:"lessonCost"` // 一节课的费用(元)|枚举:无|写入:报课时快照|FK:无|红线:打卡扣费依据,扣课时不得改本列
  164. Number int32 `gorm:"column:number;not null;default:0" json:"number"` // 总课次|枚举:无|写入:报课时|FK:无|红线:含 give_number(赠送)口径差异见旧 PHP
  165. FinishedNumber int32 `gorm:"column:finished_number;not null;default:0" json:"finishedNumber"` // 已上课次|枚举:无|写入:打卡 +1|FK:无|红线:不得超过 number+give_number
  166. DiscountCash string `gorm:"column:discount_cash;type:decimal(12,2);not null;default:0.00" json:"discountCash"` // 现金折扣(元)|枚举:无|写入:报课时|FK:无|红线:可为负
  167. ActivityIDs *string `gorm:"column:activity_ids;size:200" json:"activityIds,omitempty"` // 报课使用的活动 ID(英文逗号分隔;原 activityid)|枚举:活动 ID 列表串|写入:报课时|FK:弱关联 t_courses_activity.id(不拆表)|红线:勿当 JSON 解析,按英文逗号 split
  168. GiveNumber int32 `gorm:"column:give_number;not null;default:0" json:"giveNumber"` // 赠送课次|枚举:无|写入:报课时|FK:无|红线:扣课时先扣赠送还是实付须按旧口径
  169. GiveCost string `gorm:"column:give_cost;type:decimal(12,2);not null;default:0.00" json:"giveCost"` // 使用的赠送金额(元)|枚举:无|写入:报课时|FK:无|红线:与 discount_log 累计一致
  170. ChangingCourseNumber int32 `gorm:"column:changing_course_number;not null;default:0" json:"changingCourseNumber"` // 调课次数|枚举:无|写入:调课(P2)|FK:无|红线:本期只读
  171. RemainderNumber int32 `gorm:"column:remainder_number;not null;default:0" json:"remainderNumber"` // 剩余课次|枚举:无|写入:报课/打卡|FK:无|红线:>=0(负数=数据异常,须修复)
  172. RemainderCost string `gorm:"column:remainder_cost;type:decimal(12,2);not null;default:0.00" json:"remainderCost"` // 剩余费用(元;缴费扣除每次课费后余额)|枚举:无|写入:报课/打卡|FK:无|红线:与 net_receipts−已扣课费 一致(对账)
  173. RemainderReceivable string `gorm:"column:remainder_receivable;type:decimal(12,2);not null;default:0.00" json:"remainderReceivable"` // 剩余应收(元)|枚举:无|写入:报课/打卡|FK:无|红线:<= receivable
  174. BadDebt string `gorm:"column:bad_debt;type:decimal(12,2);not null;default:0.00" json:"badDebt"` // 坏账(元)|枚举:无|写入:退费差额/坏账处理|FK:无|红线:与 special_expenses 同源口径
  175. ConfirmDiscountUserID *string `gorm:"column:confirm_discount_user_id;size:26" json:"confirmDiscountUserId,omitempty"` // 确认学生折扣的操作人(原 confirm_discount_userid;空/NULL=不可充值)|枚举:无|写入:折扣确认动作|FK:→ t_user(逻辑)|红线:**NULL 语义=不可充值**,勿用 0 替代
  176. ConfirmTime *time.Time `gorm:"column:confirm_time" json:"confirmTime,omitempty"` // 确认时间(datetime(3))|枚举:无|写入:折扣确认|FK:无|红线:与 confirm_discount_user_id 同写
  177. RecommenderUserID *string `gorm:"column:recommender_user_id;size:26" json:"recommenderUserId,omitempty"` // 推荐人(原 recommender_userid)|枚举:无|写入:报课时|FK:→ t_user(逻辑)|红线:分账树上行起点(设计 §3.3)
  178. RecommenderTime *time.Time `gorm:"column:recommender_time" json:"recommenderTime,omitempty"` // 推荐时间(datetime(3))|枚举:无|写入:报课时|FK:无|红线:勿手改
  179. CreateTime time.Time `gorm:"column:create_time;not null;default:CURRENT_TIMESTAMP" json:"createTime"` // 创建时间(datetime(3))|枚举:无|写入:DB 默认|FK:无|红线:欠费核销排序键(create_time ASC)
  180. Remark *string `gorm:"column:remark;type:longtext" json:"remark,omitempty"` // 备注|枚举:无|写入:报课/编辑|FK:无|红线:脱敏
  181. }
  182. // TableName 指定表名。
  183. func (CoursesStudent) TableName() string { return "t_courses_student" }
  184. // CoursesRecord 课程记录 t_courses_record(课表/排班实例;旧 UUID 主键 → int64 + biz_no)。
  185. //
  186. // 流转(设计 §7.3 流程外延):开始上课写 actual_start_time;结束上课写 actual_end_time + income
  187. // + teacher_reward,并联动 t_courses_record_student / 余额 spend / 分账 / 课酬(同事务)。
  188. type CoursesRecord struct {
  189. ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // 上课记录 ID(D3 自增)|枚举:无|写入:INSERT 自动|FK:被 record_student.record_id 引用|红线:对外单号是 biz_no
  190. BizNo string `gorm:"uniqueIndex:uk_biz_no;column:biz_no;size:32;not null" json:"bizNo"` // 业务单号(旧 UUID 原样保留;UNIQUE)|枚举:无|写入:排班生成时|FK:迁移/回滚锚点|红线:不可变
  191. 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)
  192. ClassID int64 `gorm:"column:class_id;not null" json:"classId"` // 班级 ID(原 classid)|枚举:无|写入:排班生成|FK:弱关联 t_courses_class.class_id|红线:仅存值
  193. TeacherID *string `gorm:"index:idx_teacher_start,priority:1;column:teacher_id;size:26" json:"teacherId,omitempty"` // 老师(ULID;原 teacherid)|枚举:无|写入:排班/调课|FK:→ t_user(逻辑)|红线:课酬归属人,禁止改绑已结束记录
  194. InstitutionID int64 `gorm:"index:idx_inst_campus,priority:1;column:institution_id;not null;default:0" json:"institutionId"` // 机构 ID(原 schoolid;作用域列)|枚举:无|写入:排班生成|FK:→ t_institution|红线:scoping 强依赖
  195. CampusID int64 `gorm:"index:idx_inst_campus,priority:2;column:campus_id;not null;default:0" json:"campusId"` // 校区 ID(原 campusid;作用域列)|枚举:无|写入:排班生成|FK:→ t_campus|红线:同上
  196. Status int32 `gorm:"index:idx_status;column:status;not null;default:0" json:"status"` // 状态(0 未开始/1 正在进行/2 已结束)|枚举:CoursesRecordStatus*|写入:开始/结束上课|FK:无|红线:状态机校验(非法迁移 → 3121)
  197. ChangingCourse int8 `gorm:"column:changing_course;not null;default:0" json:"changingCourse"` // 是否调课(1 调课)|枚举:CoursesRecordChangingCourse*|写入:调课(P2)|FK:无|红线:本期只读
  198. StudentChangingCourse int32 `gorm:"column:student_changing_course;not null;default:0" json:"studentChangingCourse"` // 单学生调课数|枚举:无|写入:调课(P2)|FK:无|红线:本期只读
  199. TransferStudent int8 `gorm:"column:transfer_student;not null;default:0" json:"transferStudent"` // 是否有插班生(1 有)|枚举:CoursesRecordTransferStudent*|写入:插班(P2)|FK:无|红线:本期只读
  200. OriginalRecordNo *string `gorm:"column:original_record_no;size:32" json:"originalRecordNo,omitempty"` // 原上课记录单号(原 original_recordid UUID)|枚举:无|写入:调课生成|FK:弱关联本表 biz_no|红线:字符串保留(非 int64 FK)
  201. 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:无|红线:排班唯一性判据之一
  202. EndTime *time.Time `gorm:"column:end_time" json:"endTime,omitempty"` // 下课时间(datetime(3))|枚举:无|写入:排班生成|FK:无|红线:>= start_time
  203. Address *string `gorm:"column:address;type:longtext" json:"address,omitempty"` // 上课地点|枚举:无|写入:排班生成/编辑|FK:无|红线:展示转义
  204. ActualStartTime *time.Time `gorm:"column:actual_start_time" json:"actualStartTime,omitempty"` // 实际上课时间(datetime(3))|枚举:无|写入:开始上课|FK:无|红线:只写一次(重复开始 → 3121)
  205. ActualEndTime *time.Time `gorm:"column:actual_end_time" json:"actualEndTime,omitempty"` // 实际下课时间(datetime(3))|枚举:无|写入:结束上课|FK:无|红线:结束上课的幂等判据(非空即已结束)
  206. StudentNumber int32 `gorm:"column:student_number;not null;default:0" json:"studentNumber"` // 上课人数|枚举:无|写入:结束上课汇总|FK:无|红线:与 record_student 行数一致
  207. Income string `gorm:"column:income;type:decimal(12,2);not null;default:0.00" json:"income"` // 课程收入(元)|枚举:无|写入:结束上课|FK:无|红线:= Σ record_student.cost(对账)
  208. 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`)
  209. TeacherRewardType int8 `gorm:"column:teacher_reward_type;not null;default:1" json:"teacherRewardType"` // 课酬结算方式(1 正常/2 按原课程老师课酬无最低课酬)|枚举:CoursesRecordRewardType*|写入:结束上课|FK:无|红线:影响试算分支
  210. ExpectStudentIDs *string `gorm:"column:expect_student_ids;size:500" json:"expectStudentIds,omitempty"` // 要求上课的学生 id 列表(逗号分隔;原 expect_studentids)|枚举:ULID 列表串|写入:排班生成|FK:弱关联 t_user|红线:字符串保留,勿当数组入 SQL
  211. Remark *string `gorm:"column:remark;type:longtext" json:"remark,omitempty"` // 备注|枚举:无|写入:编辑|FK:无|红线:脱敏
  212. CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP" json:"createdAt"` // 创建时间(datetime(3))|枚举:无|写入:DB 默认|FK:无|红线:勿手改
  213. }
  214. // TableName 指定表名。
  215. func (CoursesRecord) TableName() string { return "t_courses_record" }
  216. // CoursesRecordStudent 课程上课记录 t_courses_record_student(打卡/考勤/扣课时;旧 UUID → int64 + biz_no)。
  217. //
  218. // 语义:一次打卡 = 一行;cost 为本次扣除课费;give_cost 为本次使用的赠送金额;
  219. // `divide_into_log_delete` 用本表"首次 cost>0 且 end_time 非空"的记录判定分成归属(设计 §3.3 #23)。
  220. type CoursesRecordStudent struct {
  221. ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // 记录 ID(D3 自增)|枚举:无|写入:INSERT 自动|FK:无|红线:对外单号是 biz_no
  222. BizNo string `gorm:"uniqueIndex:uk_biz_no;column:biz_no;size:32;not null" json:"bizNo"` // 业务单号(旧 UUID 原样保留;UNIQUE)|枚举:无|写入:打卡时|FK:迁移/回滚锚点|红线:不可变
  223. CoursesID int64 `gorm:"column:courses_id;not null" json:"coursesId"` // 课程 ID(原 coursesid)|枚举:无|写入:打卡时冗余|FK:→ t_courses.id|红线:与 record 的课程一致
  224. ClassID int64 `gorm:"column:class_id;not null;default:0" json:"classId"` // 班级 ID(原 classid)|枚举:无|写入:打卡时冗余|FK:弱关联|红线:仅存值
  225. InstitutionID int64 `gorm:"index:idx_inst_campus,priority:1;column:institution_id;not null;default:0" json:"institutionId"` // 机构 ID(原 schoolid;作用域列)|枚举:无|写入:打卡时冗余|FK:→ t_institution|红线:scoping 强依赖
  226. CampusID int64 `gorm:"index:idx_inst_campus,priority:2;column:campus_id;not null;default:0" json:"campusId"` // 校区 ID(原 campusid;作用域列)|枚举:无|写入:打卡时冗余|FK:→ t_campus|红线:同上
  227. RecordID int64 `gorm:"index:idx_record;column:record_id;not null" json:"recordId"` // 上课记录 ID(原 recordid UUID → 迁移 JOIN biz_no 重映射)|枚举:无|写入:打卡时|FK:→ t_courses_record.id|红线:分账/课酬汇总按本列分组
  228. 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)
  229. Type int32 `gorm:"column:type;not null;default:1" json:"type"` // 上课类型(1 正常/2 插班/3 赠送)|枚举:CoursesRecordStudentType*|写入:打卡时|FK:无|红线:赠送课次不动余额(只动 give_cost)
  230. StartTime time.Time `gorm:"index:idx_student_time,priority:2;column:start_time;not null" json:"startTime"` // 上课时间(datetime(3))|枚举:无|写入:打卡时|FK:无|红线:只增不改(考勤凭证)
  231. EndTime *time.Time `gorm:"column:end_time" json:"endTime,omitempty"` // 下课时间(datetime(3))|枚举:无|写入:结束上课|FK:无|红线:非空 = 已完成(分成归属判据)
  232. Cost string `gorm:"column:cost;type:decimal(12,2);not null;default:0.00" json:"cost"` // 本次课程费用(元;扣课时金额)|枚举:无|写入:结束上课|FK:无|红线:Σ cost = record.income(对账)
  233. GiveCost string `gorm:"column:give_cost;type:decimal(12,2);not null;default:0.00" json:"giveCost"` // 本次使用的赠送金额(元)|枚举:无|写入:结束上课|FK:无|红线:与 discount_log 累计一致
  234. 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
  235. Pay int8 `gorm:"column:pay;not null;default:0" json:"pay"` // 是否已支付(0 未/1 已支付)|枚举:CoursesRecordStudentPay*|写入:扣费成功/欠费核销|FK:无|红线:未支付即欠费候选
  236. Remark *string `gorm:"column:remark;type:longtext" json:"remark,omitempty"` // 备注|枚举:无|写入:打卡/结束|FK:无|红线:脱敏
  237. }
  238. // TableName 指定表名。
  239. func (CoursesRecordStudent) TableName() string { return "t_courses_record_student" }