expense_repo_recommend.go 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. // 余额账户仓储的「老带新结算」增量(AC-08)——`total_recommended_amount` 列 + 107 流水定位。
  2. //
  3. // 为什么要独立文件:`expense_repo.go` 是 AC-03/AC-04/AC-05 的共用文件,本批只**新增**
  4. // 一个列名常量与两个只读/统计方法(对既有行为零改动),集中在本文件便于评审与回滚。
  5. //
  6. // dump 依据(`sp_courses_activity_use_activity_recommend`,yuansiwei_finance.sql:763344-763366):
  7. //
  8. // UPDATE t_user_expense_school SET balance=balance+m, total_recommended_amount=total_recommended_amount+m
  9. // WHERE userid=? AND schoolid=? AND campusid=?;
  10. // UPDATE t_user_expense SET balance=balance+m, total_recommended_amount=total_recommended_amount+m
  11. // WHERE userid=?;
  12. // INSERT INTO t_user_expense_log(userid,type,logid,money,create_userid,create_time,remark)
  13. // VALUES(?,107,inputOrder,m,...);
  14. //
  15. // 两条 UPDATE 都是「balance 与 total_recommended_amount 同一语句双增」——拆开会出现
  16. // "余额加了、累计推荐额没加"的中间态(对账/双跑会读到半成品),故走 `ColumnDelta` 多列原子增减。
  17. package repository
  18. import (
  19. "context"
  20. "errors"
  21. "strings"
  22. "gorm.io/gorm"
  23. "yuansiwei/unified-backend/internal/academic/model"
  24. infraerrors "yuansiwei/unified-backend/internal/infra/errors"
  25. )
  26. // ExpenseColumnTotalRecommendedAmount 累计推荐金额列(老带新双边入账累加)。
  27. //
  28. // 该列**不在** AC-03 的初始白名单里(当时无写入场景),AC-08 结算需要按 dump 与
  29. // `balance` 同一语句双增,故补入白名单;`isExpenseColumn` 已同步(见 expense_repo.go)。
  30. const ExpenseColumnTotalRecommendedAmount = "total_recommended_amount"
  31. // FindLogByLogID 按「用户 + 流水 logid + type」取一条余额流水(老带新 107 的幂等回读)。
  32. //
  33. // 为什么不用 `FindLogByRequest`:老带新 dump 写的是 `logid=inputOrder`,
  34. // `request_id` 为 NULL(无幂等键语义)→ 幂等回放只能靠 `(user_id, log_id, type)` 定位。
  35. //
  36. // 入参:
  37. // - ctx 请求上下文;
  38. // - tx 事务句柄(可 nil:nil 时用连接池或 ctx 内事务);
  39. // - userID 用户(空 → ErrBadParam);logID 外部单号(空 → ErrBadParam);typ 流水类型。
  40. //
  41. // 出参:(*model.UserExpenseLog, nil) 命中;(**nil**, nil) 无该流水;ErrBadParam / ErrDBError。
  42. func (r *ExpenseRepo) FindLogByLogID(
  43. ctx context.Context, tx *gorm.DB, userID, logID string, typ int32,
  44. ) (*model.UserExpenseLog, error) {
  45. if strings.TrimSpace(userID) == "" || strings.TrimSpace(logID) == "" {
  46. return nil, infraerrors.WithCause(infraerrors.ErrBadParam,
  47. errString("repository: FindLogByLogID 需要 user_id 与 log_id"))
  48. }
  49. db, err := r.queryDB(ctx, tx)
  50. if err != nil {
  51. return nil, err
  52. }
  53. var out model.UserExpenseLog
  54. err = db.Model(&model.UserExpenseLog{}).
  55. Where("user_id = ? AND log_id = ? AND type = ?", userID, logID, typ).
  56. Order("id ASC").
  57. Take(&out).Error
  58. if err != nil {
  59. if isRecordNotFound(err) {
  60. return nil, nil
  61. }
  62. return nil, infraerrors.WithCause(infraerrors.ErrDBError, err)
  63. }
  64. return &out, nil
  65. }
  66. // isRecordNotFound 判定 gorm 的"无行"错误(errors.Is 兼容驱动/包装差异)。
  67. func isRecordNotFound(err error) bool {
  68. return errors.Is(err, gorm.ErrRecordNotFound)
  69. }