// 余额账户仓储的「老带新结算」增量(AC-08)——`total_recommended_amount` 列 + 107 流水定位。 // // 为什么要独立文件:`expense_repo.go` 是 AC-03/AC-04/AC-05 的共用文件,本批只**新增** // 一个列名常量与两个只读/统计方法(对既有行为零改动),集中在本文件便于评审与回滚。 // // dump 依据(`sp_courses_activity_use_activity_recommend`,yuansiwei_finance.sql:763344-763366): // // UPDATE t_user_expense_school SET balance=balance+m, total_recommended_amount=total_recommended_amount+m // WHERE userid=? AND schoolid=? AND campusid=?; // UPDATE t_user_expense SET balance=balance+m, total_recommended_amount=total_recommended_amount+m // WHERE userid=?; // INSERT INTO t_user_expense_log(userid,type,logid,money,create_userid,create_time,remark) // VALUES(?,107,inputOrder,m,...); // // 两条 UPDATE 都是「balance 与 total_recommended_amount 同一语句双增」——拆开会出现 // "余额加了、累计推荐额没加"的中间态(对账/双跑会读到半成品),故走 `ColumnDelta` 多列原子增减。 package repository import ( "context" "errors" "strings" "gorm.io/gorm" "yuansiwei/unified-backend/internal/academic/model" infraerrors "yuansiwei/unified-backend/internal/infra/errors" ) // ExpenseColumnTotalRecommendedAmount 累计推荐金额列(老带新双边入账累加)。 // // 该列**不在** AC-03 的初始白名单里(当时无写入场景),AC-08 结算需要按 dump 与 // `balance` 同一语句双增,故补入白名单;`isExpenseColumn` 已同步(见 expense_repo.go)。 const ExpenseColumnTotalRecommendedAmount = "total_recommended_amount" // FindLogByLogID 按「用户 + 流水 logid + type」取一条余额流水(老带新 107 的幂等回读)。 // // 为什么不用 `FindLogByRequest`:老带新 dump 写的是 `logid=inputOrder`, // `request_id` 为 NULL(无幂等键语义)→ 幂等回放只能靠 `(user_id, log_id, type)` 定位。 // // 入参: // - ctx 请求上下文; // - tx 事务句柄(可 nil:nil 时用连接池或 ctx 内事务); // - userID 用户(空 → ErrBadParam);logID 外部单号(空 → ErrBadParam);typ 流水类型。 // // 出参:(*model.UserExpenseLog, nil) 命中;(**nil**, nil) 无该流水;ErrBadParam / ErrDBError。 func (r *ExpenseRepo) FindLogByLogID( ctx context.Context, tx *gorm.DB, userID, logID string, typ int32, ) (*model.UserExpenseLog, error) { if strings.TrimSpace(userID) == "" || strings.TrimSpace(logID) == "" { return nil, infraerrors.WithCause(infraerrors.ErrBadParam, errString("repository: FindLogByLogID 需要 user_id 与 log_id")) } db, err := r.queryDB(ctx, tx) if err != nil { return nil, err } var out model.UserExpenseLog err = db.Model(&model.UserExpenseLog{}). Where("user_id = ? AND log_id = ? AND type = ?", userID, logID, typ). Order("id ASC"). Take(&out).Error if err != nil { if isRecordNotFound(err) { return nil, nil } return nil, infraerrors.WithCause(infraerrors.ErrDBError, err) } return &out, nil } // isRecordNotFound 判定 gorm 的"无行"错误(errors.Is 兼容驱动/包装差异)。 func isRecordNotFound(err error) bool { return errors.Is(err, gorm.ErrRecordNotFound) }