extract.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. package fixture
  2. import (
  3. "context"
  4. "database/sql"
  5. "fmt"
  6. )
  7. // 本文件只发 SELECT(legacy 侧),且所有金额列一律 `CAST(... AS DECIMAL(20,3))`。
  8. //
  9. // 为什么不直接 `SELECT balance`:
  10. // balance 是 float(10,3)。go-sql-driver 会把它当 float64 交给 Go,
  11. // 于是 0.788 变成 0.78800000000000003,后面任何 Round 都在误差上做决策。
  12. // `CAST(x AS DECIMAL(20,3))` 让 MySQL 先把该列按「3 位小数的十进制」输出,
  13. // 我们再按字符串精确换成分——误差在源头就被掐掉。
  14. // extractExpense 读 1 个账户行。
  15. func extractExpense(ctx context.Context, db *sql.DB, userID int64) (*LegacyExpense, error) {
  16. const q = `
  17. SELECT userid,
  18. CAST(balance AS DECIMAL(20,3)),
  19. CAST(spend AS DECIMAL(20,3)),
  20. CAST(total_income AS DECIMAL(20,3)),
  21. CAST(total_expenditure AS DECIMAL(20,3)),
  22. CAST(total_recommended_amount AS DECIMAL(20,3)),
  23. CAST(special_expenses AS DECIMAL(20,3))
  24. FROM t_user_expense
  25. WHERE userid = ?`
  26. row := db.QueryRowContext(ctx, q, userID)
  27. var out LegacyExpense
  28. if err := row.Scan(&out.UserID, &out.Balance, &out.Spend, &out.TotalIncome,
  29. &out.TotalExpenditure, &out.TotalRecommendedAmount, &out.SpecialExpenses); err != nil {
  30. if err == sql.ErrNoRows {
  31. return nil, fmt.Errorf("legacy t_user_expense 无 userid=%d 的行", userID)
  32. }
  33. return nil, err
  34. }
  35. return &out, nil
  36. }
  37. // extractExpenseSchools 读该用户的全部校区账户行(按 legacy id 升序,保证可复现)。
  38. func extractExpenseSchools(ctx context.Context, db *sql.DB, userID int64) ([]LegacyExpenseSchool, error) {
  39. const q = `
  40. SELECT id, userid, schoolid, cast(ifnull(campusid,0) as signed),
  41. CAST(balance AS DECIMAL(20,3)),
  42. CAST(spend AS DECIMAL(20,3)),
  43. CAST(total_income AS DECIMAL(20,3)),
  44. CAST(total_expenditure AS DECIMAL(20,3)),
  45. CAST(total_recommended_amount AS DECIMAL(20,3)),
  46. CAST(special_expenses AS DECIMAL(20,3))
  47. FROM t_user_expense_school
  48. WHERE userid = ?
  49. ORDER BY id`
  50. rows, err := db.QueryContext(ctx, q, userID)
  51. if err != nil {
  52. return nil, err
  53. }
  54. defer rows.Close()
  55. var out []LegacyExpenseSchool
  56. for rows.Next() {
  57. var r LegacyExpenseSchool
  58. if err := rows.Scan(&r.ID, &r.UserID, &r.SchoolID, &r.CampusID,
  59. &r.Balance, &r.Spend, &r.TotalIncome, &r.TotalExpenditure,
  60. &r.TotalRecommendedAmount, &r.SpecialExpenses); err != nil {
  61. return nil, err
  62. }
  63. out = append(out, r)
  64. }
  65. return out, rows.Err()
  66. }
  67. // extractExpenseLogs 读该用户的全部流水(按 legacy id 升序)。
  68. func extractExpenseLogs(ctx context.Context, db *sql.DB, userID int64) ([]LegacyExpenseLog, error) {
  69. const q = `
  70. SELECT id, userid, cast(ifnull(type,0) as signed), logid,
  71. CAST(money AS DECIMAL(20,3)),
  72. create_userid, create_time, remark
  73. FROM t_user_expense_log
  74. WHERE userid = ?
  75. ORDER BY id`
  76. rows, err := db.QueryContext(ctx, q, userID)
  77. if err != nil {
  78. return nil, err
  79. }
  80. defer rows.Close()
  81. var out []LegacyExpenseLog
  82. for rows.Next() {
  83. var r LegacyExpenseLog
  84. if err := rows.Scan(&r.ID, &r.UserID, &r.Type, &r.LogID,
  85. &r.Money, &r.CreateUserID, &r.CreateTime, &r.Remark); err != nil {
  86. return nil, err
  87. }
  88. out = append(out, r)
  89. }
  90. return out, rows.Err()
  91. }
  92. // pickLegacyUser 在未显式指定用户时,按验收要求挑一个用户:
  93. // 有账户行 + 至少 1 个校区账户 + 至少 3 条流水 + balance 带 3 位小数(覆盖取整边界)。
  94. //
  95. // 只用 SELECT,且条件全部来自 information 之外的业务表本身。
  96. func pickLegacyUser(ctx context.Context, db *sql.DB) (int64, error) {
  97. const q = `
  98. SELECT e.userid
  99. FROM t_user_expense e
  100. JOIN (SELECT userid, COUNT(*) n FROM t_user_expense_school GROUP BY userid) s ON s.userid = e.userid
  101. JOIN (SELECT userid, COUNT(*) n FROM t_user_expense_log GROUP BY userid HAVING n >= 3) l ON l.userid = e.userid
  102. WHERE ABS(CAST(e.balance * 1000 AS DECIMAL(20,0))) % 100 >= 1
  103. AND ABS(e.balance) > 0
  104. ORDER BY e.userid
  105. LIMIT 1`
  106. var id int64
  107. if err := db.QueryRowContext(ctx, q).Scan(&id); err != nil {
  108. return 0, fmt.Errorf("自动挑选 legacy 用户失败(无满足条件的用户):%w", err)
  109. }
  110. return id, nil
  111. }
  112. // countLegacyRows 统计该用户在 3 张 legacy 表里的行数(报告里展示「搬了几行」)。
  113. func countLegacyRows(ctx context.Context, db *sql.DB, userID int64) (map[string]int, error) {
  114. out := make(map[string]int, 3)
  115. for _, q := range []struct {
  116. name string
  117. sql string
  118. }{
  119. {"t_user_expense", `SELECT COUNT(*) FROM t_user_expense WHERE userid = ?`},
  120. {"t_user_expense_school", `SELECT COUNT(*) FROM t_user_expense_school WHERE userid = ?`},
  121. {"t_user_expense_log", `SELECT COUNT(*) FROM t_user_expense_log WHERE userid = ?`},
  122. } {
  123. var n int
  124. if err := db.QueryRowContext(ctx, q.sql, userID).Scan(&n); err != nil {
  125. return nil, err
  126. }
  127. out[q.name] = n
  128. }
  129. return out, nil
  130. }