load.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. package fixture
  2. import (
  3. "context"
  4. "database/sql"
  5. "fmt"
  6. "strings"
  7. )
  8. // 本文件是**唯一**往 yuansiwei_2026 写数据的地方,且只写 5 张表:
  9. // t_user / t_legacy_mapping / t_user_expense / t_user_expense_school / t_user_expense_log
  10. //
  11. // 所有写入都用幂等写法(ON DUPLICATE KEY UPDATE),理由:
  12. // · map/verify/clean 是 3 个独立命令,中途失败要能重跑;
  13. // · 变异实验要跑多轮「红→绿」,每轮都从同一起点开始。
  14. // upsertUser 插入 fixture 用的 t_user 行(最小列集)。
  15. //
  16. // t_user 的 NOT NULL 列只有 canonical_user_id / person_type / status / created_at / updated_at,
  17. // 后三者都有默认值,因此最少只需给 canonical_user_id(+ 便于识别的 display_name)。
  18. func upsertUser(ctx context.Context, db *sql.DB, canonicalUserID, displayName string) error {
  19. const q = `
  20. INSERT INTO t_user (canonical_user_id, username, display_name, person_type, status)
  21. VALUES (?, NULL, ?, 'student', 'active')
  22. ON DUPLICATE KEY UPDATE display_name = VALUES(display_name)`
  23. if _, err := db.ExecContext(ctx, q, canonicalUserID, displayName); err != nil {
  24. return fmt.Errorf("写 t_user(%s) 失败:%w", canonicalUserID, err)
  25. }
  26. return nil
  27. }
  28. // upsertMapping 插入 t_legacy_mapping 行(legacy int id → ULID)。
  29. //
  30. // 唯一键是 uk_canonical(canonical_user_id),因此重复执行只会更新同一行。
  31. func upsertMapping(ctx context.Context, db *sql.DB, m MappingEntry) error {
  32. const q = `
  33. INSERT INTO t_legacy_mapping
  34. (canonical_user_id, legacy_academic_user_id, institution_id, campus_id,
  35. role, match_source, match_confidence, merged_at)
  36. VALUES (?, ?, ?, ?, ?, ?, ?, NOW(3))
  37. ON DUPLICATE KEY UPDATE
  38. legacy_academic_user_id = VALUES(legacy_academic_user_id),
  39. institution_id = VALUES(institution_id),
  40. campus_id = VALUES(campus_id),
  41. role = VALUES(role),
  42. match_source = VALUES(match_source),
  43. match_confidence = VALUES(match_confidence)`
  44. if _, err := db.ExecContext(ctx, q, m.CanonicalUserID, m.LegacyAcademicUserID,
  45. m.InstitutionID, m.CampusID, nullIfEmpty(m.Role), m.MatchSource, m.MatchConfidence); err != nil {
  46. return fmt.Errorf("写 t_legacy_mapping(legacy=%d) 失败:%w", m.LegacyAcademicUserID, err)
  47. }
  48. return nil
  49. }
  50. // upsertExpense 写全局账户行(P):金额以十进制字符串写入 DECIMAL(12,2)。
  51. func upsertExpense(ctx context.Context, db *sql.DB, r TargetExpense) error {
  52. const q = `
  53. INSERT INTO t_user_expense
  54. (user_id, balance, spend, total_income, total_expenditure,
  55. total_recommended_amount, special_expenses)
  56. VALUES (?, ?, ?, ?, ?, ?, ?)
  57. ON DUPLICATE KEY UPDATE
  58. balance = VALUES(balance),
  59. spend = VALUES(spend),
  60. total_income = VALUES(total_income),
  61. total_expenditure = VALUES(total_expenditure),
  62. total_recommended_amount = VALUES(total_recommended_amount),
  63. special_expenses = VALUES(special_expenses)`
  64. _, err := db.ExecContext(ctx, q, r.UserID,
  65. CentsToDecimal(r.BalanceCents),
  66. CentsToDecimal(r.SpendCents),
  67. CentsToDecimal(r.TotalIncomeCents),
  68. CentsToDecimal(r.TotalExpenditureCents),
  69. CentsToDecimal(r.TotalRecommendedCents),
  70. CentsToDecimal(r.SpecialExpensesCents))
  71. if err != nil {
  72. return fmt.Errorf("写 t_user_expense(user=%s) 失败:%w", r.UserID, err)
  73. }
  74. return nil
  75. }
  76. // upsertExpenseSchool 写校区账户行。
  77. //
  78. // id 不显式指定:该表的对齐键是 uk_user_inst_campus,靠唯一键做幂等。
  79. func upsertExpenseSchool(ctx context.Context, db *sql.DB, r TargetExpenseSchool) error {
  80. const q = `
  81. INSERT INTO t_user_expense_school
  82. (user_id, institution_id, campus_id, balance, spend, total_income,
  83. total_expenditure, total_recommended_amount, special_expenses)
  84. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
  85. ON DUPLICATE KEY UPDATE
  86. balance = VALUES(balance),
  87. spend = VALUES(spend),
  88. total_income = VALUES(total_income),
  89. total_expenditure = VALUES(total_expenditure),
  90. total_recommended_amount = VALUES(total_recommended_amount),
  91. special_expenses = VALUES(special_expenses)`
  92. _, err := db.ExecContext(ctx, q, r.UserID, r.InstitutionID, r.CampusID,
  93. CentsToDecimal(r.BalanceCents),
  94. CentsToDecimal(r.SpendCents),
  95. CentsToDecimal(r.TotalIncomeCents),
  96. CentsToDecimal(r.TotalExpenditureCents),
  97. CentsToDecimal(r.TotalRecommendedCents),
  98. CentsToDecimal(r.SpecialExpensesCents))
  99. if err != nil {
  100. return fmt.Errorf("写 t_user_expense_school(user=%s,campus=%d) 失败:%w", r.UserID, r.CampusID, err)
  101. }
  102. return nil
  103. }
  104. // insertExpenseLog 写流水行(D1:**显式插入 legacy id**)。
  105. //
  106. // 幂等键是 PRIMARY KEY(id)=legacy id,天然可重跑。
  107. // 注意:这里刻意**不**依赖 AUTO_INCREMENT 生成 id——id 由 legacy 决定。
  108. func insertExpenseLog(ctx context.Context, db *sql.DB, r TargetExpenseLog) error {
  109. const q = `
  110. INSERT INTO t_user_expense_log
  111. (id, user_id, type, log_id, request_id, money, balance_after, create_user_id, create_time, remark)
  112. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  113. ON DUPLICATE KEY UPDATE
  114. user_id = VALUES(user_id),
  115. type = VALUES(type),
  116. log_id = VALUES(log_id),
  117. request_id = VALUES(request_id),
  118. money = VALUES(money),
  119. balance_after = VALUES(balance_after),
  120. create_user_id = VALUES(create_user_id),
  121. create_time = VALUES(create_time),
  122. remark = VALUES(remark)`
  123. _, err := db.ExecContext(ctx, q,
  124. r.ID, r.UserID, r.Type,
  125. nullStr(r.LogID), nullStr(r.RequestID),
  126. CentsToDecimal(r.MoneyCents),
  127. nullStr(r.BalanceAfter), nullStr(r.CreateUserID),
  128. r.CreateTime.Format("2006-01-02 15:04:05"),
  129. nullStr(r.Remark))
  130. if err != nil {
  131. return fmt.Errorf("写 t_user_expense_log(id=%d) 失败:%w", r.ID, err)
  132. }
  133. return nil
  134. }
  135. // nullIfEmpty 把空串转 NULL(role 之类的可选列用)。
  136. func nullIfEmpty(s string) any {
  137. if strings.TrimSpace(s) == "" {
  138. return nil
  139. }
  140. return s
  141. }
  142. // nullStr 把 sql.NullString 转成可交给驱动的 any(NULL 或 string)。
  143. func nullStr(v sql.NullString) any {
  144. if !v.Valid {
  145. return nil
  146. }
  147. return v.String
  148. }