package service import ( "fmt" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/setting/system_setting" ) const BillingSourceSyncedWallet = "synced_wallet" // SyncedUserFunding 同步用户的资金来源实现 // 同步用户的余额存储在 master 节点,本地通过原子 SQL 管理 synced_quota 快照 // 预扣时原子扣减 synced_quota 并记录到 PendingSyncRecord,结算时同步到 master type SyncedUserFunding struct { userId int remoteUserId int requestId string consumed int // 实际预扣额度 syncedQuota int // master 同步过来的余额快照(PreConsume 后为最新值) } func NewSyncedUserFunding(userId, remoteUserId int, requestId string, syncedQuota int) *SyncedUserFunding { return &SyncedUserFunding{ userId: userId, remoteUserId: remoteUserId, requestId: requestId, syncedQuota: syncedQuota, } } func (s *SyncedUserFunding) Source() string { return BillingSourceSyncedWallet } func (s *SyncedUserFunding) PreConsume(amount int) error { if amount <= 0 { common.SysLog(fmt.Sprintf("[RegionSync] PreConsume skipped: userId=%d, amount=%d (<=0)", s.userId, amount)) return nil } settings := system_setting.GetRegionSyncSettings() common.SysLog(fmt.Sprintf("[RegionSync] PreConsume: userId=%d, remoteUserId=%d, amount=%d, syncedQuota=%d, threshold=%d, requestId=%s", s.userId, s.remoteUserId, amount, s.syncedQuota, settings.MinBalanceThreshold, s.requestId)) // 原子 SQL:检查余额 + 扣减 synced_quota newQuota, ok, err := model.AtomicDecreaseSyncedQuota(s.userId, amount, settings.MinBalanceThreshold) if err != nil { return fmt.Errorf("扣减同步额度失败: %w", err) } if !ok { return fmt.Errorf("同步用户余额不足 (userId=%d, syncedQuota=%d, need=%d, threshold=%d)", s.userId, newQuota, amount, settings.MinBalanceThreshold) } // 创建待同步记录(预估扣费) err = model.CreatePendingSyncRecord(s.userId, s.remoteUserId, s.requestId, amount, amount) if err != nil { // 回滚:退还已扣减的额度 common.SysError(fmt.Sprintf("[RegionSync] PreConsume failed to create pending record, rolling back: userId=%d, requestId=%s, err=%v", s.userId, s.requestId, err)) model.IncreaseSyncedQuota(s.userId, amount) return fmt.Errorf("创建同步扣费记录失败: %w", err) } s.consumed = amount s.syncedQuota = newQuota common.SysLog(fmt.Sprintf("[RegionSync] PreConsume success: userId=%d, remoteUserId=%d, consumed=%d, syncedQuota=%d, requestId=%s", s.userId, s.remoteUserId, s.consumed, s.syncedQuota, s.requestId)) return nil } func (s *SyncedUserFunding) Settle(delta int) error { if delta == 0 { return nil } // 结算时更新待同步记录的实际扣费额度 if delta > 0 { // 需要额外扣减 s.consumed += delta } else { // 需要退还部分额度 s.consumed += delta // delta 是负数 } // 更新本地同步余额快照 s.syncedQuota -= delta common.SysLog(fmt.Sprintf("[RegionSync] Settle: userId=%d, delta=%d, consumed=%d, syncedQuota=%d, requestId=%s", s.userId, delta, s.consumed, s.syncedQuota, s.requestId)) // 尝试更新已有的 PendingSyncRecord;如果不存在(PreConsume 时 amount=0 跳过),则创建新记录 updated := model.UpdatePendingSyncRecordQuota(s.requestId, s.consumed) if updated == 0 { // PreConsume 跳过时没有创建记录,此处用实际消耗量创建 if err := model.CreatePendingSyncRecord(s.userId, s.remoteUserId, s.requestId, s.consumed, s.consumed); err != nil { common.SysError(fmt.Sprintf("[RegionSync] Settle: failed to create pending record, requestId=%s, err=%v", s.requestId, err)) } else { common.SysLog(fmt.Sprintf("[RegionSync] Settle: created pending record (PreConsume skipped), userId=%d, quota=%d, requestId=%s", s.userId, s.consumed, s.requestId)) } } // 更新数据库中用户的 synced_quota 快照 if err := model.UpdateSyncedQuota(s.userId, s.syncedQuota); err != nil { common.SysError(fmt.Sprintf("[RegionSync] Settle: failed to update synced_quota, userId=%d, err=%v", s.userId, err)) } return nil } func (s *SyncedUserFunding) Refund() error { if s.consumed <= 0 { return nil } common.SysLog(fmt.Sprintf("[RegionSync] Refund: userId=%d, consumed=%d, requestId=%s", s.userId, s.consumed, s.requestId)) // 取消待同步记录 var record model.PendingSyncRecord err := model.DB.Where("request_id = ? AND status = ?", s.requestId, model.PendingSyncStatusPending).First(&record).Error if err != nil { common.SysLog(fmt.Sprintf("[RegionSync] Refund: no pending record found for requestId=%s", s.requestId)) return nil // 记录不存在,无需处理 } // 标记为已同步(实际上已取消) if err := model.MarkRecordSynced(record.Id); err != nil { common.SysError(fmt.Sprintf("[RegionSync] Refund: failed to mark record synced, recordId=%d, err=%v", record.Id, err)) return err } // 原子退还 DB synced_quota if err := model.IncreaseSyncedQuota(s.userId, s.consumed); err != nil { common.SysError(fmt.Sprintf("[RegionSync] Refund: failed to increase synced_quota, userId=%d, err=%v", s.userId, err)) return err } common.SysLog(fmt.Sprintf("[RegionSync] Refund success: userId=%d, recordId=%d, refunded=%d, requestId=%s", s.userId, record.Id, s.consumed, s.requestId)) return nil }