batch3_io.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. package fixture
  2. import (
  3. "context"
  4. "database/sql"
  5. "fmt"
  6. "strings"
  7. )
  8. // 本文件是第三批的读写层:从 `yuansiwei_i` 只读取数 → 写入 `yuansiwei_2026`。
  9. //
  10. // 取样纪律:`t_school` 有 61905 行,**只按 id 列表取 6 行**,绝不 `SELECT *` 全表。
  11. // extractSchools 按 id 列表取机构(只取这几个 id)。
  12. func extractSchools(ctx context.Context, db *sql.DB, ids []int64) ([]LegacySchool, error) {
  13. if len(ids) == 0 {
  14. return nil, fmt.Errorf("institution id 列表为空")
  15. }
  16. ph := placeholders(len(ids))
  17. args := make([]any, len(ids))
  18. for i, v := range ids {
  19. args[i] = v
  20. }
  21. q := fmt.Sprintf(`
  22. SELECT id, name, province_id, city_id, district_id, grade_period, status, ifnull(is_open_user_login,0)
  23. FROM t_school WHERE id IN (%s) ORDER BY id`, ph)
  24. rows, err := db.QueryContext(ctx, q, args...)
  25. if err != nil {
  26. return nil, err
  27. }
  28. defer rows.Close()
  29. var out []LegacySchool
  30. for rows.Next() {
  31. var r LegacySchool
  32. if err := rows.Scan(&r.ID, &r.Name, &r.ProvinceID, &r.CityID, &r.DistrictID,
  33. &r.GradePeriod, &r.Status, &r.IsOpenUserLogin); err != nil {
  34. return nil, err
  35. }
  36. out = append(out, r)
  37. }
  38. return out, rows.Err()
  39. }
  40. // extractCampuses 取所选机构下的校区。
  41. //
  42. // self_support 是 `bit(1)`:go-sql-driver 会把它当 []byte(0x00/0x01)返回,
  43. // 直接 Scan 进 int8 会得到 48/49('0'/'1' 的 ASCII)——这里显式 `+0` 让 MySQL 先转成整数。
  44. func extractCampuses(ctx context.Context, db *sql.DB, schoolIDs []int64) ([]LegacyCampus, error) {
  45. if len(schoolIDs) == 0 {
  46. return nil, fmt.Errorf("school id 列表为空")
  47. }
  48. ph := placeholders(len(schoolIDs))
  49. args := make([]any, len(schoolIDs))
  50. for i, v := range schoolIDs {
  51. args[i] = v
  52. }
  53. q := fmt.Sprintf(`
  54. SELECT id, name, school_id, province_id, city_id, district_id, status, code,
  55. self_support+0, ifnull(is_open_user_login,0)
  56. FROM t_school_campus WHERE school_id IN (%s) ORDER BY school_id, id`, ph)
  57. rows, err := db.QueryContext(ctx, q, args...)
  58. if err != nil {
  59. return nil, err
  60. }
  61. defer rows.Close()
  62. var out []LegacyCampus
  63. for rows.Next() {
  64. var r LegacyCampus
  65. if err := rows.Scan(&r.ID, &r.Name, &r.SchoolID, &r.ProvinceID, &r.CityID, &r.DistrictID,
  66. &r.Status, &r.Code, &r.SelfSupport, &r.IsOpenUserLogin); err != nil {
  67. return nil, err
  68. }
  69. out = append(out, r)
  70. }
  71. return out, rows.Err()
  72. }
  73. // extractReferencedCampuses 取「数据里真正出现过的校区 id」集合(用于 WithAllCampuses=false)。
  74. func extractReferencedCampuses(ctx context.Context, db *sql.DB) (map[int64]bool, error) {
  75. out := map[int64]bool{}
  76. for _, q := range refCampusQueries() {
  77. rows, err := db.QueryContext(ctx, q)
  78. if err != nil {
  79. return nil, err
  80. }
  81. for rows.Next() {
  82. var id sql.NullInt64
  83. if err := rows.Scan(&id); err != nil {
  84. rows.Close()
  85. return nil, err
  86. }
  87. if id.Valid && id.Int64 != 0 {
  88. out[id.Int64] = true
  89. }
  90. }
  91. rows.Close()
  92. }
  93. return out, nil
  94. }
  95. // refCampusQueries 返回各来源的 distinct campusid 查询(只取非 0)。
  96. func refCampusQueries() []string {
  97. var out []string
  98. for _, s := range orgRefSources() {
  99. if s.CampusCol == "" {
  100. continue
  101. }
  102. out = append(out, fmt.Sprintf(
  103. "SELECT DISTINCT %s FROM `%s` WHERE %s <> 0", s.CampusCol, s.Table, s.CampusCol))
  104. }
  105. return out
  106. }
  107. // orgRefSources 是「带机构/校区引用的数据来源」清单(验收 §3 要逐个来源统计孤儿)。
  108. func orgRefSources() []RefSource {
  109. return []RefSource{
  110. {Table: "t_courses", InstCol: "schoolid", CampusCol: "campusid", Note: "课程"},
  111. {Table: "t_courses_class", InstCol: "schoolid", CampusCol: "campusid", Note: "班次"},
  112. {Table: "t_courses_record", InstCol: "schoolid", CampusCol: "campusid", Note: "上课记录"},
  113. {Table: "t_courses_activity", InstCol: "schoolid", CampusCol: "campusid", Note: "活动"},
  114. {Table: "t_user_expense_school", InstCol: "schoolid", CampusCol: "campusid", Note: "校区账户"},
  115. {Table: "t_expenditure_log", InstCol: "schoolid", CampusCol: "campusid", Note: "收支记录"},
  116. {Table: "t_user_balance_request", InstCol: "schoolid", CampusCol: "campusid", Note: "充值工单"},
  117. // 课程分类只有机构列
  118. {Table: "t_courses_classify", InstCol: "schoolid", CampusCol: "", Note: "课程分类(无校区列)"},
  119. }
  120. }
  121. // ---------- 写入(D1) ----------
  122. // upsertInstitution 写机构(D1:显式插 institution_id)。
  123. func upsertInstitution(ctx context.Context, db *sql.DB, r TargetInstitution) error {
  124. const q = `
  125. INSERT INTO t_institution
  126. (institution_id, institution_name, institution_code, address, phone, status, created_at, updated_at)
  127. VALUES (?, ?, ?, ?, ?, ?, ?, ?)
  128. ON DUPLICATE KEY UPDATE
  129. institution_name = VALUES(institution_name),
  130. institution_code = VALUES(institution_code),
  131. address = VALUES(address),
  132. phone = VALUES(phone),
  133. status = VALUES(status)`
  134. _, err := db.ExecContext(ctx, q, r.InstitutionID, r.InstitutionName,
  135. nullStr(r.InstitutionCode), nullStr(r.Address), nullStr(r.Phone), r.Status,
  136. r.CreatedAt.Format("2006-01-02 15:04:05"), r.UpdatedAt.Format("2006-01-02 15:04:05"))
  137. if err != nil {
  138. return fmt.Errorf("写 t_institution(id=%d) 失败:%w", r.InstitutionID, err)
  139. }
  140. return nil
  141. }
  142. // upsertCampus 写校区(D1:显式插 campus_id)。
  143. func upsertCampus(ctx context.Context, db *sql.DB, r TargetCampus) error {
  144. const q = `
  145. INSERT INTO t_campus
  146. (campus_id, institution_id, campus_name, campus_code, address, phone,
  147. status, self_support, created_at, updated_at)
  148. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  149. ON DUPLICATE KEY UPDATE
  150. institution_id = VALUES(institution_id),
  151. campus_name = VALUES(campus_name),
  152. campus_code = VALUES(campus_code),
  153. address = VALUES(address),
  154. phone = VALUES(phone),
  155. status = VALUES(status),
  156. self_support = VALUES(self_support)`
  157. _, err := db.ExecContext(ctx, q, r.CampusID, r.InstitutionID, r.CampusName,
  158. nullStr(r.CampusCode), nullStr(r.Address), nullStr(r.Phone), r.Status, r.SelfSupport,
  159. r.CreatedAt.Format("2006-01-02 15:04:05"), r.UpdatedAt.Format("2006-01-02 15:04:05"))
  160. if err != nil {
  161. return fmt.Errorf("写 t_campus(id=%d) 失败:%w", r.CampusID, err)
  162. }
  163. return nil
  164. }
  165. // countTargetOrg 统计 target 两张表的行数。
  166. func countTargetOrg(ctx context.Context, db *sql.DB) (inst, camp int64, err error) {
  167. if err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM t_institution").Scan(&inst); err != nil {
  168. return 0, 0, err
  169. }
  170. if err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM t_campus").Scan(&camp); err != nil {
  171. return 0, 0, err
  172. }
  173. return inst, camp, nil
  174. }
  175. // joinList 把 id 列表拼成 "1,63317" 形式(仅用于报告与 SQL 的 IN 列表构造)。
  176. func joinList(ids []int64) string {
  177. parts := make([]string, len(ids))
  178. for i, v := range ids {
  179. parts[i] = fmt.Sprint(v)
  180. }
  181. return strings.Join(parts, ",")
  182. }