package dao import ( "errors" "fmt" "time" "gorm.io/gorm" "micro-bundle/internal/model" "micro-bundle/pkg/app" ) // RunInitialTaskBalanceSync 一次性将 BundleBalance 同步到 TaskBalance // 仅在未执行过且任务余额表为空时运行;执行成功后写入标记,避免再次执行 func RunInitialTaskBalanceSync() error { // 确保标记表存在 _ = app.ModuleClients.TaskBenchDB.AutoMigrate(&model.TaskSyncStatus{}) // 已执行标记检查 var markerCount int64 if err := app.ModuleClients.TaskBenchDB.Model(&model.TaskSyncStatus{}). Where("sync_key = ?", model.InitialSyncKey).Count(&markerCount).Error; err != nil { return err } if markerCount > 0 { return nil } // 安全检查:如果任务余额表已存在数据,则不再执行,同样写入标记 var existing int64 if err := app.ModuleClients.TaskBenchDB.Model(&model.TaskBalance{}).Count(&existing).Error; err != nil { return err } if existing > 0 { _ = app.ModuleClients.TaskBenchDB.Create(&model.TaskSyncStatus{ SyncKey: model.InitialSyncKey, ExecutedAt: time.Now(), Remark: "skipped: task_balance already has data", }).Error return nil } // 获取当前有效(未过期且已支付)的艺人及其最新订单 validArtists, err := GetValidArtistList() if err != nil { return err } fmt.Println(validArtists) if len(validArtists) == 0 { // 不写入已执行标记,留待后续有数据时再次执行 fmt.Println("无数据更新") return nil } // 构造待插入的 TaskBalance 列表 tasks := make([]model.TaskBalance, 0, len(validArtists)) for _, a := range validArtists { // 根据 user_id + order_uuid 获取 BundleBalance 明细 var bb model.BundleBalance if err := app.ModuleClients.BundleDB.Where("user_id = ? AND order_uuid = ?", a.UserID, a.OrderUUID).First(&bb).Error; err != nil { // 若未查到则跳过该条 if err == gorm.ErrRecordNotFound { continue } return err } subNum, telNum, err := fetchIdentityForBundle(&bb) if err != nil { // 无法获取身份信息则跳过该条 continue } tb := model.TaskBalance{ SubNum: subNum, TelNum: telNum, Month: bb.Month, StartAt: bb.StartAt, ExpiredAt: bb.ExpiredAt, CreatedAt: time.Now(), UpdatedAt: time.Now(), } copyBundleToTaskBalance(&tb, &bb) tasks = append(tasks, tb) } // 原子写入:插入 TaskBalance + 插入标记(确保有插入才写标记) tx := app.ModuleClients.TaskBenchDB.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() if len(tasks) == 0 { // 没有可插入的数据,不写标记,直接返回 tx.Rollback() return nil } if err := tx.Create(&tasks).Error; err != nil { tx.Rollback() return err } if err := tx.Create(&model.TaskSyncStatus{ SyncKey: model.InitialSyncKey, ExecutedAt: time.Now(), Remark: "initial sync executed", }).Error; err != nil { tx.Rollback() return err } if err := tx.Commit().Error; err != nil { return err } return nil } // 用户新买套餐时使用 // SyncTaskBalanceFromBundleBalance 增量/每月:根据单条 BundleBalance 同步或更新 TaskBalance(按 sub_num + tel_num + month 唯一) func SyncTaskBalanceFromBundleBalance(bb model.BundleBalance) error { // 获取身份信息(sub_num, tel_num) subNum, telNum, err := fetchIdentityForBundle(&bb) if err != nil { return err } // 组装 TaskBalance tb := model.TaskBalance{ SubNum: subNum, TelNum: telNum, Month: bb.Month, ExpiredAt: bb.ExpiredAt, StartAt: bb.StartAt, UpdatedAt: time.Now(), CreatedAt: time.Now(), } copyBundleToTaskBalance(&tb, &bb) // 查询是否已存在(唯一:sub_num + tel_num + month) var existing model.TaskBalance err = app.ModuleClients.TaskBenchDB. Where("sub_num = ? AND tel_num = ? AND month = ?", subNum, telNum, bb.Month). First(&existing).Error if err != nil { if err == gorm.ErrRecordNotFound { // 不存在则创建 return app.ModuleClients.TaskBenchDB.Create(&tb).Error } return err } // 已存在则更新所有映射字段与时间 tb.ID = existing.ID return app.ModuleClients.TaskBenchDB.Save(&tb).Error } // fetchIdentityForBundle 根据 BundleBalance 拿到 sub_num 与 tel_num func fetchIdentityForBundle(bb *model.BundleBalance) (string, string, error) { // tel_num 来自 micro-account.user type userRow struct { Tel string } var ur userRow if err := app.ModuleClients.BundleDB.Table("`micro-account`.`user`").Unscoped(). Select("tel_num AS tel").Where("id = ?", bb.UserId).Limit(1).Scan(&ur).Error; err != nil { return "", "", err } // customer_num 来自 bundle_order_records(按 order_uuid) type orderRow struct { Customer string } var or orderRow if bb.OrderUUID == "" { return "", "", errors.New("bundle order_uuid missing") } if err := app.ModuleClients.BundleDB.Table("bundle_order_records"). Select("customer_num AS customer").Where("uuid = ?", bb.OrderUUID).Limit(1).Scan(&or).Error; err != nil { return "", "", err } return or.Customer, ur.Tel, nil } // UpdateTaskBalance 每月批量更新任务余额 // 类似于 UpdateBundleBalance 的逻辑,但针对任务余额表 func UpdateTaskBalanceEveryMon() error { // 查询需要更新的任务余额记录(最新月份且未过期的记录) tl := []model.TaskBalance{} if err := app.ModuleClients.TaskBenchDB.Raw(`select * from task_balance tb inner join ( select max(tb.month) as month , sub_num, tel_num from task_balance tb group by tb.sub_num, tb.tel_num ) newest on newest.month = tb.month and (tb.sub_num = newest.sub_num OR tb.tel_num = newest.tel_num) and tb.expired_at > now()`).Find(&tl).Error; err != nil { return err } now := time.Now() month := time.Now().Format("2006-01") for _, v := range tl { if v.Month == month { continue } // 计算本月发放的限制类型数量(与套餐余额一致) cal := func(total, limit int) int { var released int // 已释放的次数 if v.StartAt.Month() == now.Month() && v.StartAt.Year() == now.Year() { // 本月为第一个月,不计算之前的释放 } else if v.StartAt.Day() >= 16 { // 第一个月释放的(向上取整一半) released += (limit + 1) / 2 } else { released += limit } interval := now.Year()*12 + int(now.Month()) - (v.StartAt.Year()*12 + int(v.StartAt.Month())) // 后续月份释放的 released += interval * limit remaining := max(total-released, 0) // 还剩余多少次没有发放 if v.StartAt.Month() == now.Month() && v.StartAt.Year() == now.Year() && v.StartAt.Day() >= 16 { // 本月为第一个月并且16号后购买只给一半(向上取整) return min((limit+1)/2, remaining) } if v.ExpiredAt.Month() == now.Month() && v.ExpiredAt.Year() == now.Year() && v.ExpiredAt.Day() < 16 { // 本月为最后一个月并且16号前到期只给一半(向下取整) return min(limit/2, remaining) } return min(limit, remaining) } // ===== 处理视频类过期数据 ===== v.TaskMonthlyInvalidBundleVideoNumber = v.TaskMonthlyLimitVideoExpireNumber - v.TaskMonthlyLimitVideoExpireConsumptionNumber v.TaskInvalidBundleVideoNumber += v.TaskMonthlyInvalidBundleVideoNumber // ===== 处理图片类过期数据 ===== v.TaskMonthlyInvalidBundleImageNumber = v.TaskMonthlyLimitImageExpireNumber - v.TaskMonthlyLimitImageExpireConsumptionNumber v.TaskInvalidBundleImageNumber += v.TaskMonthlyInvalidBundleImageNumber // ===== 处理数据分析类过期数据 ===== v.TaskMonthlyInvalidBundleDataAnalysisNumber = v.TaskMonthlyLimitDataAnalysisExpireNumber - v.TaskMonthlyLimitDataAnalysisExpireConsumptionNumber v.TaskInvalidBundleDataAnalysisNumber += v.TaskMonthlyInvalidBundleDataAnalysisNumber // ===== 计算新月份的可用数量 ===== // 视频类 v.TaskMonthlyLimitVideoExpireNumber = cal(v.TaskBundleLimitVideoExpiredNumber, v.TaskMonthlyLimitVideoQuotaNumber) + cal(v.TaskIncreaseLimitVideoExpiredNumber, v.TaskMonthlyLimitVideoQuotaNumber) v.TaskMonthlyLimitVideoNumber = v.TaskMonthlyLimitVideoNumber - v.TaskMonthlyLimitVideoConsumptionNumber + cal(v.TaskBundleLimitVideoNumber, v.TaskMonthlyLimitVideoQuotaNumber) + cal(v.TaskIncreaseLimitVideoNumber, v.TaskMonthlyLimitVideoQuotaNumber) // 图片类 v.TaskMonthlyLimitImageExpireNumber = cal(v.TaskBundleLimitImageExpiredNumber, v.TaskMonthlyLimitImageQuotaNumber) + cal(v.TaskIncreaseLimitImageExpiredNumber, v.TaskMonthlyLimitImageQuotaNumber) v.TaskMonthlyLimitImageNumber = v.TaskMonthlyLimitImageNumber - v.TaskMonthlyLimitImageConsumptionNumber + cal(v.TaskBundleLimitImageNumber, v.TaskMonthlyLimitImageQuotaNumber) + cal(v.TaskIncreaseLimitImageNumber, v.TaskMonthlyLimitImageQuotaNumber) // 数据分析类 v.TaskMonthlyLimitDataAnalysisExpireNumber = cal(v.TaskBundleLimitDataAnalysisExpiredNumber, v.TaskMonthlyLimitDataAnalysisQuotaNumber) + cal(v.TaskIncreaseLimitDataAnalysisExpiredNumber, v.TaskMonthlyLimitDataAnalysisQuotaNumber) v.TaskMonthlyLimitDataAnalysisNumber = v.TaskMonthlyLimitDataAnalysisNumber - v.TaskMonthlyLimitDataAnalysisConsumptionNumber + cal(v.TaskBundleLimitDataAnalysisNumber, v.TaskMonthlyLimitDataAnalysisQuotaNumber) + cal(v.TaskIncreaseLimitDataAnalysisNumber, v.TaskMonthlyLimitDataAnalysisQuotaNumber) // ===== 重置月度消耗数量 ===== // 视频类月度消耗重置 v.TaskMonthlyLimitVideoConsumptionNumber = 0 v.TaskMonthlyLimitVideoExpireConsumptionNumber = 0 v.TaskMonthlyManualVideoConsumptionNumber = 0 v.TaskMonthlyBundleVideoConsumptionNumber = 0 v.TaskMonthlyIncreaseVideoConsumptionNumber = 0 // 图片类月度消耗重置 v.TaskMonthlyLimitImageConsumptionNumber = 0 v.TaskMonthlyLimitImageExpireConsumptionNumber = 0 v.TaskMonthlyManualImageConsumptionNumber = 0 v.TaskMonthlyBundleImageConsumptionNumber = 0 v.TaskMonthlyIncreaseImageConsumptionNumber = 0 // 数据分析类月度消耗重置 v.TaskMonthlyLimitDataAnalysisConsumptionNumber = 0 v.TaskMonthlyLimitDataAnalysisExpireConsumptionNumber = 0 v.TaskMonthlyManualDataAnalysisConsumptionNumber = 0 v.TaskMonthlyBundleDataAnalysisConsumptionNumber = 0 v.TaskMonthlyIncreaseDataAnalysisConsumptionNumber = 0 // 设置新月份和重置ID v.Month = month v.ID = 0 // 创建新的任务余额记录 if err := app.ModuleClients.TaskBenchDB.Create(&v).Error; err != nil { return err } } return nil } // updateTaskBalanceExpiredAt 更新任务余额表的ExpiredAt字段 func updateTaskBalanceExpiredAt(subNum, telNum string, durationNumber int) error { return app.ModuleClients.TaskBenchDB.Transaction(func(tx *gorm.DB) error { var taskBalance model.TaskBalance query := tx.Model(&model.TaskBalance{}) // 构建查询条件,优先使用 subNum if subNum != "" { query = query.Where("sub_num = ?", subNum) } else { query = query.Where("tel_num = ?", telNum) } // 查询当前有效的任务余额记录,按最新的开始时间排序 now := time.Now() err := query.Where("start_at <= ? AND expired_at >= ?", now, now).Order("start_at DESC").First(&taskBalance).Error if err != nil { return err } // 增加过期时间 taskBalance.ExpiredAt = taskBalance.ExpiredAt.Add(time.Hour * 24 * time.Duration(durationNumber)) return tx.Save(&taskBalance).Error }) } // copyBundleToTaskBalance 将 BundleBalance 的图片、视频、数据分析相关字段映射到 TaskBalance func copyBundleToTaskBalance(tb *model.TaskBalance, bb *model.BundleBalance) { // ===== 视频类 ===== tb.TaskBundleVideoNumber = bb.BundleVideoNumber tb.TaskIncreaseVideoNumber = bb.IncreaseVideoNumber tb.TaskBundleLimitVideoNumber = bb.BundleLimitVideoNumber tb.TaskIncreaseLimitVideoNumber = bb.IncreaseLimitVideoNumber tb.TaskBundleLimitVideoExpiredNumber = bb.BundleLimitVideoExpiredNumber tb.TaskIncreaseLimitVideoExpiredNumber = bb.IncreaseLimitVideoExpiredNumber tb.TaskMonthlyInvalidBundleVideoNumber = bb.MonthlyInvalidBundleVideoNumber tb.TaskInvalidBundleVideoNumber = bb.InvalidBundleVideoNumber tb.TaskMonthlyInvalidIncreaseVideoNumber = bb.MonthlyInvalidIncreaseVideoNumber tb.TaskInvalidIncreaseVideoNumber = bb.InvalidIncreaseVideoNumber tb.TaskBundleVideoConsumptionNumber = bb.BundleVideoConsumptionNumber tb.TaskIncreaseVideoConsumptionNumber = bb.IncreaseVideoConsumptionNumber tb.TaskBundleLimitVideoConsumptionNumber = bb.BundleLimitVideoConsumptionNumber tb.TaskIncreaseLimitVideoConsumptionNumber = bb.IncreaseLimitVideoConsumptionNumber tb.TaskBundleLimitVideoExpiredConsumptionNumber = bb.BundleLimitVideoExpiredConsumptionNumber tb.TaskIncreaseLimitVideoExpiredConsumptionNumber = bb.IncreaseLimitVideoExpiredConsumptionNumber tb.TaskMonthlyLimitVideoNumber = bb.MonthlyLimitVideoNumber tb.TaskMonthlyLimitVideoConsumptionNumber = bb.MonthlyLimitVideoConsumptionNumber tb.TaskMonthlyLimitVideoExpireNumber = bb.MonthlyLimitVideoExpireNumber tb.TaskMonthlyLimitVideoExpireConsumptionNumber = bb.MonthlyLimitVideoExpireConsumptionNumber tb.TaskMonthlyBundleVideoConsumptionNumber = bb.MonthlyBundleVideoConsumptionNumber tb.TaskMonthlyIncreaseVideoConsumptionNumber = bb.MonthlyIncreaseVideoConsumptionNumber tb.TaskMonthlyLimitVideoQuotaNumber = bb.MonthlyLimitVideoQuotaNumber // 手动扩展(视频) tb.TaskManualVideoNumber = bb.ManualVideoNumber tb.TaskManualVideoConsumptionNumber = bb.ManualVideoConsumptionNumber tb.TaskMonthlyNewManualVideoNumber = bb.MonthlyNewManualVideoNumber tb.TaskMonthlyManualVideoConsumptionNumber = bb.MonthlyManualVideoConsumptionNumber // ===== 图片类 ===== tb.TaskBundleImageNumber = bb.BundleImageNumber tb.TaskIncreaseImageNumber = bb.IncreaseImageNumber tb.TaskBundleLimitImageNumber = bb.BundleLimitImageNumber tb.TaskIncreaseLimitImageNumber = bb.IncreaseLimitImageNumber tb.TaskBundleLimitImageExpiredNumber = bb.BundleLimitImageExpiredNumber tb.TaskIncreaseLimitImageExpiredNumber = bb.IncreaseLimitImageExpiredNumber tb.TaskMonthlyInvalidBundleImageNumber = bb.MonthlyInvalidBundleImageNumber tb.TaskInvalidBundleImageNumber = bb.InvalidBundleImageNumber tb.TaskMonthlyInvalidIncreaseImageNumber = bb.MonthlyInvalidIncreaseImageNumber tb.TaskInvalidIncreaseImageNumber = bb.InvalidIncreaseImageNumber tb.TaskBundleImageConsumptionNumber = bb.BundleImageConsumptionNumber tb.TaskIncreaseImageConsumptionNumber = bb.IncreaseImageConsumptionNumber tb.TaskBundleLimitImageConsumptionNumber = bb.BundleLimitImageConsumptionNumber tb.TaskIncreaseLimitImageConsumptionNumber = bb.IncreaseLimitImageConsumptionNumber tb.TaskBundleLimitImageExpiredConsumptionNumber = bb.BundleLimitImageExpiredConsumptionNumber tb.TaskIncreaseLimitImageExpiredConsumptionNumber = bb.IncreaseLimitImageExpiredConsumptionNumber tb.TaskMonthlyLimitImageNumber = bb.MonthlyLimitImageNumber tb.TaskMonthlyLimitImageConsumptionNumber = bb.MonthlyLimitImageConsumptionNumber tb.TaskMonthlyLimitImageExpireNumber = bb.MonthlyLimitImageExpireNumber tb.TaskMonthlyLimitImageExpireConsumptionNumber = bb.MonthlyLimitImageExpireConsumptionNumber tb.TaskMonthlyBundleImageConsumptionNumber = bb.MonthlyBundleImageConsumptionNumber tb.TaskMonthlyIncreaseImageConsumptionNumber = bb.MonthlyIncreaseImageConsumptionNumber tb.TaskMonthlyLimitImageQuotaNumber = bb.MonthlyLimitImageQuotaNumber // 手动扩展(图片) tb.TaskManualImageNumber = bb.ManualImageNumber tb.TaskManualImageConsumptionNumber = bb.ManualImageConsumptionNumber tb.TaskMonthlyNewManualImageNumber = bb.MonthlyNewManualImageNumber tb.TaskMonthlyManualImageConsumptionNumber = bb.MonthlyManualImageConsumptionNumber // ===== 数据分析类 ===== tb.TaskBundleDataAnalysisNumber = bb.BundleDataAnalysisNumber tb.TaskIncreaseDataAnalysisNumber = bb.IncreaseDataAnalysisNumber tb.TaskBundleLimitDataAnalysisNumber = bb.BundleLimitDataAnalysisNumber tb.TaskIncreaseLimitDataAnalysisNumber = bb.IncreaseLimitDataAnalysisNumber tb.TaskBundleLimitDataAnalysisExpiredNumber = bb.BundleLimitDataAnalysisExpiredNumber tb.TaskIncreaseLimitDataAnalysisExpiredNumber = bb.IncreaseLimitDataAnalysisExpiredNumber tb.TaskMonthlyInvalidBundleDataAnalysisNumber = bb.MonthlyInvalidBundleDataAnalysisNumber tb.TaskInvalidBundleDataAnalysisNumber = bb.InvalidBundleDataAnalysisNumber tb.TaskMonthlyInvalidIncreaseDataAnalysisNumber = bb.MonthlyInvalidIncreaseDataAnalysisNumber tb.TaskInvalidIncreaseDataAnalysisNumber = bb.InvalidIncreaseDataAnalysisNumber tb.TaskBundleDataAnalysisConsumptionNumber = bb.BundleDataAnalysisConsumptionNumber tb.TaskIncreaseDataAnalysisConsumptionNumber = bb.IncreaseDataAnalysisConsumptionNumber tb.TaskBundleLimitDataAnalysisConsumptionNumber = bb.BundleLimitDataAnalysisConsumptionNumber tb.TaskIncreaseLimitDataAnalysisConsumptionNumber = bb.IncreaseLimitDataAnalysisConsumptionNumber tb.TaskBundleLimitDataAnalysisExpiredConsumptionNumber = bb.BundleLimitDataAnalysisExpiredConsumptionNumber tb.TaskIncreaseLimitDataAnalysisExpiredConsumptionNumber = bb.IncreaseLimitDataAnalysisExpiredConsumptionNumber tb.TaskMonthlyLimitDataAnalysisNumber = bb.MonthlyLimitDataAnalysisNumber tb.TaskMonthlyLimitDataAnalysisConsumptionNumber = bb.MonthlyLimitDataAnalysisConsumptionNumber tb.TaskMonthlyLimitDataAnalysisExpireNumber = bb.MonthlyLimitDataAnalysisExpireNumber tb.TaskMonthlyLimitDataAnalysisExpireConsumptionNumber = bb.MonthlyLimitDataAnalysisExpireConsumptionNumber tb.TaskMonthlyBundleDataAnalysisConsumptionNumber = bb.MonthlyBundleDataAnalysisConsumptionNumber tb.TaskMonthlyIncreaseDataAnalysisConsumptionNumber = bb.MonthlyIncreaseDataAnalysisConsumptionNumber tb.TaskMonthlyLimitDataAnalysisQuotaNumber = bb.MonthlyLimitDataAnalysisQuotaNumber // 手动扩展(数据分析) tb.TaskManualDataAnalysisNumber = bb.ManualDataAnalysisNumber tb.TaskManualDataAnalysisConsumptionNumber = bb.ManualDataAnalysisConsumptionNumber tb.TaskMonthlyNewManualDataAnalysisNumber = bb.MonthlyNewManualDataAnalysisNumber tb.TaskMonthlyManualDataAnalysisConsumptionNumber = bb.MonthlyManualDataAnalysisConsumptionNumber // 其他字段 tb.TaskMonthlyNewDurationNumber = bb.MonthlyNewDurationNumber tb.TaskExpansionPacksNumber = bb.ExpansionPacksNumber } func ExtendTaskBalanceByUserId(userId int, imageNumber int, dataAnalysisNumber int, videoNumber int, durationNumber int) error { // 根据用户ID获取其最新套餐记录,进而获取 sub_num、tel_num oldBundle := model.BundleBalance{} if err := app.ModuleClients.BundleDB.Model(&model.BundleBalance{}). Where("user_id = ?", userId). Order("created_at desc"). First(&oldBundle).Error; err != nil { return errors.New("用户还没有套餐信息") } subNum, telNum, err := fetchIdentityForBundle(&oldBundle) if err != nil { return err } // 事务更新当前有效的任务余额记录(按 start_at 最近的一条) err = app.ModuleClients.TaskBenchDB.Transaction(func(tx *gorm.DB) error { var tb model.TaskBalance now := time.Now() query := tx.Model(&model.TaskBalance{}). Where("sub_num = ? AND tel_num = ? AND start_at <= ? AND expired_at >= ?", subNum, telNum, now, now). Order("start_at DESC") if err := query.First(&tb).Error; err != nil { if err == gorm.ErrRecordNotFound { return errors.New("用户还没有任务余额信息") } return err } // 手动扩展额度与当月新增记录 tb.TaskManualImageNumber += imageNumber tb.TaskMonthlyNewManualImageNumber += imageNumber tb.TaskManualDataAnalysisNumber += dataAnalysisNumber tb.TaskMonthlyNewManualDataAnalysisNumber += dataAnalysisNumber tb.TaskManualVideoNumber += videoNumber tb.TaskMonthlyNewManualVideoNumber += videoNumber tb.TaskMonthlyNewDurationNumber += durationNumber return tx.Model(&model.TaskBalance{}).Where("id = ?", tb.ID).Save(&tb).Error }) if err != nil { return err } // 增加过期时间(按天) if durationNumber > 0 { if err := updateTaskBalanceExpiredAt(subNum, telNum, durationNumber); err != nil { return err } } return nil }