models_course.go 40 KB

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