course_student_repo.go 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. // 报课记录的作用域回查(AC-08 老带新结算用)——`t_courses_student` 的只读投影。
  2. //
  3. // 为什么独立文件:`course_repo.go` 是 AC-11 的交付文件,本批只**新增**一个只读方法
  4. // (对既有行为零改动),集中在本文件便于评审与回滚。
  5. //
  6. // dump 依据(`sp_courses_activity_use_activity_recommend`,yuansiwei_finance.sql:763339-763347):
  7. //
  8. // IF (selectSchoolid<=0) THEN
  9. // SELECT(SELECT schoolid FROM t_courses_student WHERE studentid=inputUserid AND id=inputOrder) INTO selectSchoolid;
  10. // END IF;
  11. // IF (selectCampusid<=0) THEN
  12. // SELECT(SELECT campusid FROM t_courses_student WHERE studentid=inputUserid AND id=inputOrder) INTO selectCampusid;
  13. // END IF;
  14. // IF(selectCampusid<=0 || selectSchoolid<=0) THEN LEAVE readLoop; END IF;
  15. //
  16. // 语义:活动行上的 `schoolid/campusid` 为 0("全机构池"活动)时,用**报课记录**的机构/校区
  17. // 兜底——钱必须落到具体校区账户上。
  18. package repository
  19. import (
  20. "context"
  21. "strings"
  22. infraerrors "yuansiwei/unified-backend/internal/infra/errors"
  23. )
  24. // StudentCourseScope 报课记录的作用域投影(机构 + 校区)。
  25. //
  26. // 字段:StudentCourseID 报课记录 ID;InstitutionID 原 schoolid;CampusID 原 campusid。
  27. type StudentCourseScope struct {
  28. // StudentCourseID t_courses_student.id。
  29. StudentCourseID int64 `gorm:"column:student_course_id"`
  30. // InstitutionID 机构 ID(原 schoolid)。
  31. InstitutionID int64 `gorm:"column:institution_id"`
  32. // CampusID 校区 ID(原 campusid)。
  33. CampusID int64 `gorm:"column:campus_id"`
  34. }
  35. // GetStudentCourseScope 按 (报课记录 id, 学生) 取机构/校区(dump 763339 的两次子查询合并为一次)。
  36. //
  37. // 入参:
  38. // - ctx 请求上下文;
  39. // - studentCourseID 报课记录 ID(<=0 → ErrBadParam);
  40. // - studentID 学生(ULID;空 → ErrBadParam;**必须与报课记录归属人一致**,否则 ErrNotFound)。
  41. //
  42. // 出参:(*StudentCourseScope, nil) 命中;ErrNotFound(记录不存在或不属于该学生)/ ErrBadParam / ErrDBError。
  43. //
  44. // 作用域:与 dump 一致**按 (id, student_id) 精确定位、不叠作用域过滤**——
  45. // dump 的子查询同样没有机构/校区条件;越权风险为 0(`student_id` 来自登录态,
  46. // 且本方法只读两个作用域列,写入口账户时仍由活动/报课记录的机构决定)。
  47. func (r *CourseRepo) GetStudentCourseScope(
  48. ctx context.Context, studentCourseID int64, studentID string,
  49. ) (*StudentCourseScope, error) {
  50. if studentCourseID <= 0 {
  51. return nil, infraerrors.WithCause(infraerrors.ErrBadParam,
  52. errString("repository: GetStudentCourseScope 需要 student_course_id>0"))
  53. }
  54. if strings.TrimSpace(studentID) == "" {
  55. return nil, infraerrors.WithCause(infraerrors.ErrBadParam,
  56. errString("repository: GetStudentCourseScope 需要 student_id"))
  57. }
  58. db, err := r.db(ctx)
  59. if err != nil {
  60. return nil, err
  61. }
  62. var out StudentCourseScope
  63. q := db.Table("t_courses_student").
  64. Select("id AS student_course_id, institution_id, campus_id").
  65. Where("id = ? AND student_id = ?", studentCourseID, strings.TrimSpace(studentID))
  66. if err := q.Take(&out).Error; err != nil {
  67. return nil, wrapQueryErr(err, "GetStudentCourseScope(t_courses_student)")
  68. }
  69. return &out, nil
  70. }