batch3_model.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. package fixture
  2. import (
  3. "database/sql"
  4. "time"
  5. )
  6. // 本文件是**第三批(org 域)的数据形状**:机构 t_institution + 校区 t_campus。
  7. //
  8. // 口径(已拍板):
  9. // - **两张表都走 D1**:`t_school.id` → `t_institution.institution_id`、`t_school_campus.id`
  10. // → `t_campus.campus_id`,**原值插入**。理由:两者都是 int 自然主键、数据量极小,
  11. // 而 institution_id / campus_id 在几十张表里是裸 int,双跑要靠 id 直接对齐;
  12. // 走 biz_no 反而要多一跳。
  13. // - **只迁数据用得到的机构**,不全量迁 t_school(61905 行里 61900 行与本项目无关)。
  14. // - `campus_id = 0` **不建行**:0 是"机构级/未分配"的约定值(DDL 里 `campus_id ... DEFAULT 0
  15. // COMMENT '校区ID(0=平台级/机构级)'`),建了会让前端校区下拉框多出一个"未知校区"。
  16. // OrgOptions 是第三批的取样参数。
  17. type OrgOptions struct {
  18. // InstitutionIDs 是要迁移的机构 id 集合(D1 原值)。
  19. //
  20. // 这是**显式决策**,不是从数据里自动推导的:多迁一个机构 = 多一个孤儿机构;
  21. // 少迁一个 = 引用它的一整批数据变成孤儿引用。集合必须由人确认。
  22. InstitutionIDs []int64
  23. // WithAllCampuses 是否迁移所选机构的**全部**校区(true)
  24. // 还是只迁数据里出现过的校区(false)。默认 true:机构迁了、校区不迁会让
  25. // 前端校区下拉框与机构对不上,且属于 org 域自身的不一致。
  26. WithAllCampuses bool
  27. OrgSourceDB string
  28. }
  29. // DefaultOrgOptions 是默认取样 = **7 个机构**。
  30. //
  31. // ⚠ 初版统计给的是 5 个(`SELECT DISTINCT schoolid FROM t_courses` + campus 1/2 的 school_id=1),
  32. // 实测逐来源清点后发现**漏了 2 个**,各自都带孤儿引用:
  33. //
  34. // 63332(用户注册)← finance_bk.t_user_balance_request 1 行(schoolid=63332, campusid=57)
  35. // 63330(弘毅教育)← finance_bk.t_courses_classify 1 行(schoolid=63330)
  36. // —— 这张表不在原统计的 7 个来源清单里,是靠"按列名反查"找出来的
  37. //
  38. // 只迁 5 个会让这两个来源各留 1 条孤儿机构 + 1 条孤儿校区(实测见报告)。
  39. func DefaultOrgOptions() OrgOptions {
  40. return OrgOptions{
  41. InstitutionIDs: []int64{1, 63317, 63323, 63324, 63325, 63330, 63332},
  42. WithAllCampuses: true,
  43. OrgSourceDB: "yuansiwei_i",
  44. }
  45. }
  46. // ---------- legacy(yuansiwei_i) ----------
  47. // LegacySchool 对应 `yuansiwei_i.t_school`。
  48. type LegacySchool struct {
  49. ID int64
  50. Name string
  51. ProvinceID int64
  52. CityID int64
  53. DistrictID int64
  54. GradePeriod string
  55. Status int8 // tinyint(1):1=正常 0=停用(业务口径,见 mapping 注释)
  56. IsOpenUserLogin int8
  57. }
  58. // LegacyCampus 对应 `yuansiwei_i.t_school_campus`。
  59. type LegacyCampus struct {
  60. ID int64
  61. Name string
  62. SchoolID int64
  63. ProvinceID int64
  64. CityID int64
  65. DistrictID int64
  66. Status int8
  67. Code sql.NullString
  68. SelfSupport int8 // bit(1) → 0/1
  69. IsOpenUserLogin int8
  70. }
  71. // ---------- target ----------
  72. // TargetInstitution 对应 `t_institution`(16 列里只用得到的那些)。
  73. type TargetInstitution struct {
  74. InstitutionID int64
  75. InstitutionName string
  76. InstitutionCode sql.NullString // legacy 无对应列 → NULL(uk_institution_code 允许多 NULL)
  77. Address sql.NullString // legacy 只有省市区 id,没有地址文本 → NULL
  78. Phone sql.NullString // legacy 无 → NULL
  79. Status string // active / disabled
  80. CreatedAt time.Time // legacy 无时间列 → epoch 哨兵
  81. UpdatedAt time.Time
  82. }
  83. // TargetCampus 对应 `t_campus`。
  84. type TargetCampus struct {
  85. CampusID int64
  86. InstitutionID int64
  87. CampusName string
  88. CampusCode sql.NullString // legacy code(varchar(16))→ campus_code
  89. Address sql.NullString
  90. Phone sql.NullString
  91. Status string
  92. SelfSupport int8 // bit(1) → TINYINT(1)
  93. CreatedAt time.Time
  94. UpdatedAt time.Time
  95. }
  96. // ---------- 引用完整性 ----------
  97. // RefSource 是一个「带 institution_id / campus_id 的数据来源」。
  98. type RefSource struct {
  99. Table string // finance_bk 里的表名
  100. InstCol string // 机构列
  101. CampusCol string // 校区列(空 = 该表没有校区列)
  102. Note string
  103. }
  104. // DisabledCampusStat 是一个**停用校区**(status=0)的影响面。
  105. //
  106. // 这一块单列的理由:迁过去之后这些校区是 `disabled`,挂在它们上面的历史数据
  107. // 在"按启用校区筛选"时看不到;而 `assertInScope` 会拒绝"给停用校区新增数据"(正确)。
  108. // 两者叠加 = 业务侧会看到"历史数据消失",必须让业务知情,不能靠"把 status 改成 1"掩盖。
  109. type DisabledCampusStat struct {
  110. CampusID int64
  111. Name string
  112. InstitutionID int64
  113. Courses int64
  114. Records int64
  115. ExpenseSchool int64
  116. BalanceRequest int64
  117. ExpenditureLog int64
  118. }
  119. // CrossOrgStat 是**一类**跨机构统计。
  120. //
  121. // 跨机构有三个不同层次的来源,混在一起报会让人看不出"到底是哪一层不一致":
  122. //
  123. // A 课程级:t_courses.schoolid vs 该课程 campusid 的校区真实所属
  124. // B 记录级:t_courses_record.schoolid vs 该记录 campusid 的校区真实所属
  125. // C 记录 vs 课程:t_courses_record.schoolid vs 其 coursesid 对应课程的 schoolid
  126. //
  127. // A/B 是"数据挂的机构 vs 校区真实所属",C 是"记录与课程的机构是否一致"(性质不同:
  128. // 即使校区所属一致,记录也可能挂到了另一个机构下)。
  129. type CrossOrgStat struct {
  130. Key string // A / B / C
  131. Name string
  132. Source string // 参与比较的两列
  133. Total int64
  134. Skipped int64 // campusid=0(机构级)或 join 不到,不参与判定
  135. Consistent int64
  136. Inconsistent int64
  137. Samples []string
  138. Note string
  139. }
  140. // RefStat 是一个来源的引用统计结果。
  141. type RefStat struct {
  142. Source string
  143. Rows int64 // 该表总行数
  144. DistinctInst int // distinct institution_id
  145. DistinctCampus int // distinct campus_id(不含 0)
  146. OrphanInst int64 // institution_id 在 target 里找不到的行数
  147. OrphanCampus int64 // campus_id != 0 但在 target 里找不到的行数
  148. CampusZeroRows int64 // campus_id = 0(机构级,不建行)的行数
  149. OrphanInstList []string
  150. OrphanCampusList []string
  151. }