package model import ( "errors" "fmt" "sort" "strconv" "strings" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/setting/ratio_setting" "gorm.io/gorm" ) // AffInviteRelation 邀请人与被邀请人关系表:为每个被邀请人单独配置充值分销比例。 // CommissionRatioBps 存储单位为万分之一(相对于「百分比」):1 表示 0.01%,100 表示 1%,10000 表示 100%。 type AffInviteRelation struct { Id int `json:"id" gorm:"primaryKey;autoIncrement"` InviterId int `json:"inviter_id" gorm:"not null;uniqueIndex:idx_aff_inv_pair"` InviteeUserId int `json:"invitee_user_id" gorm:"not null;uniqueIndex:idx_aff_inv_pair;column:invitee_user_id"` CommissionRatioBps int `json:"commission_ratio_bps" gorm:"not null;default:0;column:commission_ratio_bps"` CommissionEarnedQuota int `json:"commission_earned_quota" gorm:"not null;default:0;column:commission_earned_quota"` // 该被邀请人为邀请人累计贡献的分销额度(与 aff_quota 增量一致) // ProfitShareEarnedQuota 利润分成模式下,该被邀请人用量加价切片累计为邀请人贡献的收益(与 aff_quota 中对应增量一致;与 commission_earned_quota 分列统计)。 ProfitShareEarnedQuota int `json:"profit_share_earned_quota" gorm:"not null;default:0;column:profit_share_earned_quota"` // 被邀请用户模型加价折扣率:JSON 数组 [{model_name, channel_id, markup_discount_rate}],仅存与渠道默认不同的项。 ModelMarkupDiscountRate string `json:"model_markup_discount_rate" gorm:"type:text;column:model_markup_discount_rate;comment:被邀请用户模型加价折扣率(JSON数组)"` // 自动时间戳:创建/更新时 GORM 自动赋值 CreatedAt int64 `json:"created_at" gorm:"autoCreateTime;bigint;comment:创建时间"` UpdatedAt int64 `json:"updated_at" gorm:"autoUpdateTime;bigint;comment:更新时间"` } func (AffInviteRelation) TableName() string { return "aff_invite_relations" } const maxAffiliateCommissionBps = 10000 // AffInviteeListItem 邀请人视角下的被邀请人列表项 type AffInviteeListItem struct { InviteeId int `json:"invitee_id"` Username string `json:"username"` DisplayName string `json:"display_name"` CommissionRatioBps int `json:"commission_ratio_bps"` // 万分之一单位(1=0.01%),前端展示为百分比 CommissionEarnedQuota int `json:"commission_earned_quota"` ProfitShareEarnedQuota int `json:"profit_share_earned_quota"` CreatedAt int64 `json:"created_at"` // 邀请关系建立时间(aff_invite_relations.created_at) } func defaultCommissionBpsForNewInviteRelation(inviterId int) int { var inviter User err := DB.Select("id", "role", "distributor_commission_bps", "is_distributor").Where("id = ?", inviterId).First(&inviter).Error if err != nil { return common.AffiliateDefaultCommissionBps } if UserIsDistributor(&inviter) && inviter.DistributorCommissionBps > 0 { return inviter.DistributorCommissionBps } return common.AffiliateDefaultCommissionBps } // EnsureAffInviteRelation 注册成功后建立关系行,比例初始为系统默认或分销商单独默认。 func EnsureAffInviteRelation(inviterId, inviteeUserId int) error { if inviterId <= 0 || inviteeUserId <= 0 { return nil } var cnt int64 err := DB.Model(&AffInviteRelation{}).Where("inviter_id = ? AND invitee_user_id = ?", inviterId, inviteeUserId).Count(&cnt).Error if err != nil { return err } if cnt > 0 { return nil } ts := common.GetTimestamp() bps := defaultCommissionBpsForNewInviteRelation(inviterId) rel := AffInviteRelation{ InviterId: inviterId, InviteeUserId: inviteeUserId, CommissionRatioBps: bps, CommissionEarnedQuota: 0, ProfitShareEarnedQuota: 0, ModelMarkupDiscountRate: "[]", CreatedAt: ts, UpdatedAt: ts, } return DB.Create(&rel).Error } // BackfillAffInviteRelationsIfNeeded 表为空时执行一次历史数据补全,避免每次启动全表扫描。 func BackfillAffInviteRelationsIfNeeded() error { var cnt int64 if err := DB.Model(&AffInviteRelation{}).Count(&cnt).Error; err != nil { return err } if cnt > 0 { return nil } return BackfillAffInviteRelationsFromUsers() } // BackfillAffInviteRelationsFromUsers 为历史数据补全关系行。 func BackfillAffInviteRelationsFromUsers() error { var users []User err := DB.Unscoped().Model(&User{}).Select("id", "inviter_id").Where("inviter_id > ?", 0).Find(&users).Error if err != nil { return err } for i := range users { if err := EnsureAffInviteRelation(users[i].InviterId, users[i].Id); err != nil { common.SysError("backfill aff_invite_relations: " + err.Error()) } } return nil } // EffectiveAffiliateCommissionBps 计算邀请人对某一被邀请人生效的分销比例(万分之一)。 // 与充值分成、利润分成(加价切片分润)共用同一套优先级:分销商账号 distributor_commission_bps > 0 优先, // 否则 aff_invite_relations.commission_ratio_bps(>0),否则系统 AffiliateDefaultCommissionBps。 func EffectiveAffiliateCommissionBps(inviter *User, inviteeUserId int) int { return effectiveAffiliateCommissionBps(inviter, inviteeUserId) } // effectiveAffiliateCommissionBps(内部):充值与利润分成逻辑一致。 func effectiveAffiliateCommissionBps(inviter *User, inviteeUserId int) int { if inviter == nil || inviter.Id <= 0 { return common.AffiliateDefaultCommissionBps } if UserIsDistributor(inviter) && inviter.DistributorCommissionBps > 0 { bps := inviter.DistributorCommissionBps if bps > maxAffiliateCommissionBps { bps = maxAffiliateCommissionBps } return bps } var rel AffInviteRelation err := DB.Where("inviter_id = ? AND invitee_user_id = ?", inviter.Id, inviteeUserId).First(&rel).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return common.AffiliateDefaultCommissionBps } common.SysError("effectiveAffiliateCommissionBps: " + err.Error()) return common.AffiliateDefaultCommissionBps } if rel.CommissionRatioBps <= 0 { return common.AffiliateDefaultCommissionBps } return rel.CommissionRatioBps } // ApplyAffiliateTopupReward 被邀请用户获得充值额度 quotaAdded 后,按 effectiveAffiliateCommissionBps 将提成记入邀请人 aff_quota / aff_history(不增加 quota)。 // 须在支付回调完成入账后调用,与订单事务解耦。 func ApplyAffiliateTopupReward(inviteeUserId int, quotaAdded int) { if common.IsDistributorProfitShareMode() { return } if inviteeUserId <= 0 || quotaAdded <= 0 { return } invitee, err := GetUserById(inviteeUserId, false) if err != nil { return } inviterId := invitee.InviterId if inviterId <= 0 { return } inviterUser, errInv := GetUserById(inviterId, false) if errInv != nil || !UserIsDistributor(inviterUser) { return } bps := effectiveAffiliateCommissionBps(inviterUser, inviteeUserId) if bps <= 0 { return } if bps > maxAffiliateCommissionBps { bps = maxAffiliateCommissionBps } reward := int(int64(quotaAdded) * int64(bps) / int64(maxAffiliateCommissionBps)) if reward <= 0 { return } if err := IncreaseUserAffCommissionQuota(inviterId, reward); err != nil { common.SysError(fmt.Sprintf("ApplyAffiliateTopupReward: inviter=%d invitee=%d reward=%d err=%v", inviterId, inviteeUserId, reward, err)) return } if err := InsertAffInviteCommissionLog(inviterId, inviteeUserId, quotaAdded, bps, reward); err != nil { common.SysError(fmt.Sprintf("ApplyAffiliateTopupReward commission log: inviter=%d invitee=%d err=%v", inviterId, inviteeUserId, err)) } if err := DB.Model(&AffInviteRelation{}). Where("inviter_id = ? AND invitee_user_id = ?", inviterId, inviteeUserId). UpdateColumn("commission_earned_quota", gorm.Expr("commission_earned_quota + ?", reward)).Error; err != nil { common.SysError(fmt.Sprintf("ApplyAffiliateTopupReward update earned: inviter=%d invitee=%d err=%v", inviterId, inviteeUserId, err)) } inviteeLabel := strings.TrimSpace(invitee.Username) if inviteeLabel == "" { inviteeLabel = strings.TrimSpace(invitee.DisplayName) } if inviteeLabel == "" { inviteeLabel = fmt.Sprintf("ID:%d", invitee.Id) } pct := logger.FormatCommissionRatioAsPercent(bps) amt := logger.LogQuotaConcise(reward) RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请分销奖励(被邀请用户 %s 充值)%s,分成比例 %s", inviteeLabel, amt, pct)) } // ListAffInvitees 分页返回当前用户邀请注册的用户(含关系表累计分成等;单笔明细见 aff_invite_commission_logs)。 // keyword 非空时按用户名、显示名模糊匹配;若 keyword 为十进制正整数则同时匹配被邀请用户 id。 func ListAffInvitees(inviterId int, keyword string, pageInfo *common.PageInfo) ([]AffInviteeListItem, int64, error) { if inviterId <= 0 { return nil, 0, errors.New("invalid inviter") } kw := strings.TrimSpace(keyword) inviteesScope := func(db *gorm.DB) *gorm.DB { db = db.Where("inviter_id = ?", inviterId) if kw != "" { pattern := "%" + kw + "%" if uid, err := strconv.Atoi(kw); err == nil && uid > 0 { db = db.Where("(id = ? OR username LIKE ? OR display_name LIKE ?)", uid, pattern, pattern) } else { db = db.Where("(username LIKE ? OR display_name LIKE ?)", pattern, pattern) } } return db } var total int64 if err := DB.Model(&User{}).Scopes(inviteesScope).Count(&total).Error; err != nil { return nil, 0, err } var users []User err := DB.Model(&User{}).Scopes(inviteesScope). Order("id desc"). Limit(pageInfo.GetPageSize()). Offset(pageInfo.GetStartIdx()). Find(&users).Error if err != nil { return nil, 0, err } if len(users) == 0 { return []AffInviteeListItem{}, total, nil } ids := make([]int, 0, len(users)) for _, u := range users { ids = append(ids, u.Id) } var rels []AffInviteRelation _ = DB.Where("inviter_id = ? AND invitee_user_id IN ?", inviterId, ids).Find(&rels).Error bpsMap := make(map[int]int, len(rels)) earnedMap := make(map[int]int, len(rels)) profitEarnedMap := make(map[int]int, len(rels)) relCreatedMap := make(map[int]int64, len(rels)) for _, r := range rels { bpsMap[r.InviteeUserId] = r.CommissionRatioBps earnedMap[r.InviteeUserId] = r.CommissionEarnedQuota profitEarnedMap[r.InviteeUserId] = r.ProfitShareEarnedQuota relCreatedMap[r.InviteeUserId] = r.CreatedAt } defaultBps := common.AffiliateDefaultCommissionBps items := make([]AffInviteeListItem, 0, len(users)) for _, u := range users { bps, ok := bpsMap[u.Id] if !ok { bps = defaultBps } else if bps <= 0 { bps = defaultBps } earned := earnedMap[u.Id] profitEarned := profitEarnedMap[u.Id] relAt := relCreatedMap[u.Id] items = append(items, AffInviteeListItem{ InviteeId: u.Id, Username: u.Username, DisplayName: u.DisplayName, CommissionRatioBps: bps, CommissionEarnedQuota: earned, ProfitShareEarnedQuota: profitEarned, CreatedAt: relAt, }) } return items, total, nil } // UpdateAffInviteeCommission 邀请人修改某一被邀请人的分销比例(验证被邀请人确实属于当前邀请人)。 func UpdateAffInviteeCommission(inviterId, inviteeUserId, commissionBps int) error { if inviterId <= 0 || inviteeUserId <= 0 { return errors.New("invalid id") } if commissionBps < 0 || commissionBps > maxAffiliateCommissionBps { return fmt.Errorf("commission_ratio_bps must be 0..%d (万分之一单位,1=0.01%%)", maxAffiliateCommissionBps) } invitee, err := GetUserById(inviteeUserId, false) if err != nil { return errors.New("user not found") } if invitee.InviterId != inviterId { return errors.New("not your invitee") } ts := common.GetTimestamp() var rel AffInviteRelation err = DB.Where("inviter_id = ? AND invitee_user_id = ?", inviterId, inviteeUserId).First(&rel).Error if errors.Is(err, gorm.ErrRecordNotFound) { rel = AffInviteRelation{ InviterId: inviterId, InviteeUserId: inviteeUserId, CommissionRatioBps: commissionBps, ProfitShareEarnedQuota: 0, CreatedAt: ts, UpdatedAt: ts, } return DB.Create(&rel).Error } if err != nil { return err } rel.CommissionRatioBps = commissionBps rel.UpdatedAt = ts return DB.Save(&rel).Error } // InviteeModelMarkupDiscountRateItem 定价页可见的模型×渠道加价折扣配置项(API 列表元素)。 type InviteeModelMarkupDiscountRateItem struct { ModelName string `json:"model_name"` ChannelID int `json:"channel_id"` ChannelPath string `json:"channel_path"` // 与定价页通道列表「复制」一致:model/route_slug 或 alias/model/channel_no SupplierType string `json:"supplier_type"` ChannelName string `json:"channel_name"` DefaultMarkupDiscountRate float64 `json:"default_markup_discount_rate"` // 渠道默认官方价加价折扣率(%) CurrentMarkupDiscountRate float64 `json:"current_markup_discount_rate"` // 对该被邀请用户生效的加价折扣率(%) } // inviteePricingChannelPath 与前端 ModelChannelList 复制通道路径格式一致。 func inviteePricingChannelPath(modelName string, ch PricingChannelItem) string { modelName = strings.TrimSpace(modelName) if slug := strings.TrimSpace(ch.RouteSlug); slug != "" && modelName != "" { return modelName + "/" + slug } return strings.TrimSpace(ch.SupplierAlias) + "/" + modelName + "/" + strings.TrimSpace(ch.ChannelNo) } type inviteeModelMarkupDiscountRateEntry struct { ModelName string `json:"model_name"` ChannelID int `json:"channel_id"` MarkupDiscountRate float64 `json:"markup_discount_rate"` } type inviteeModelMarkupDiscountRateEntryRaw struct { ModelName string `json:"model_name"` ChannelID int `json:"channel_id"` MarkupDiscountRate *float64 `json:"markup_discount_rate"` Discount *float64 `json:"discount"` } func inviteeModelMarkupKey(channelID int, modelName string) string { return fmt.Sprintf("%d:%s", channelID, strings.TrimSpace(modelName)) } func parseInviteeModelMarkupDiscountRates(raw string) ([]inviteeModelMarkupDiscountRateEntry, error) { raw = strings.TrimSpace(raw) if raw == "" || raw == "[]" || raw == "{}" { return nil, nil } var list []inviteeModelMarkupDiscountRateEntryRaw if err := common.UnmarshalJsonStr(raw, &list); err != nil { return nil, err } out := make([]inviteeModelMarkupDiscountRateEntry, 0, len(list)) for _, item := range list { modelName := strings.TrimSpace(item.ModelName) if item.ChannelID <= 0 || modelName == "" { continue } rate := 0.0 if item.MarkupDiscountRate != nil { rate = *item.MarkupDiscountRate } else if item.Discount != nil { rate = *item.Discount } out = append(out, inviteeModelMarkupDiscountRateEntry{ ModelName: modelName, ChannelID: item.ChannelID, MarkupDiscountRate: rate, }) } return out, nil } func inviteeModelMarkupDiscountRateMap(list []inviteeModelMarkupDiscountRateEntry) map[string]float64 { m := make(map[string]float64, len(list)) for _, item := range list { m[inviteeModelMarkupKey(item.ChannelID, item.ModelName)] = item.MarkupDiscountRate } return m } func listPricingVisibleMarkupDiscountRateItems() ([]InviteeModelMarkupDiscountRateItem, map[string]float64, error) { pricing := GetPricing() filtered := make([]Pricing, 0, len(pricing)) for _, p := range pricing { if ratio_setting.ModelHasConfiguredPricing(p.ModelName) { filtered = append(filtered, p) } } pricingChannels, err := ListChannelsForPricing() if err != nil { return nil, nil, err } visibleChannelIDs := make(map[int]struct{}, len(pricingChannels)) channelNames := make(map[int]string, len(pricingChannels)) channelSupplierTypes := make(map[int]string, len(pricingChannels)) for _, pch := range pricingChannels { visibleChannelIDs[pch.ChannelID] = struct{}{} channelNames[pch.ChannelID] = strings.TrimSpace(pch.ChannelName) channelSupplierTypes[pch.ChannelID] = strings.TrimSpace(pch.SupplierType) } metas, err := ListChannelPricingMeta() if err != nil { return nil, nil, err } pricingItems := BuildPricingAPIItems(filtered, visibleChannelIDs, metas, false) items := make([]InviteeModelMarkupDiscountRateItem, 0, len(pricingItems)) defaultRates := make(map[string]float64, len(pricingItems)) for _, p := range pricingItems { if len(p.ChannelList) == 0 { continue } ch := p.ChannelList[0] modelName := strings.TrimSpace(p.ModelName) if modelName == "" || ch.ChannelID <= 0 { continue } key := inviteeModelMarkupKey(ch.ChannelID, modelName) if _, exists := defaultRates[key]; exists { continue } defaultRate := ch.MarkupDiscountRate defaultRates[key] = defaultRate items = append(items, InviteeModelMarkupDiscountRateItem{ ModelName: modelName, ChannelID: ch.ChannelID, ChannelPath: inviteePricingChannelPath(modelName, ch), SupplierType: channelSupplierTypes[ch.ChannelID], ChannelName: channelNames[ch.ChannelID], DefaultMarkupDiscountRate: defaultRate, CurrentMarkupDiscountRate: defaultRate, }) } sort.Slice(items, func(i, j int) bool { if items[i].ChannelPath != items[j].ChannelPath { return items[i].ChannelPath < items[j].ChannelPath } return items[i].ChannelID < items[j].ChannelID }) return items, defaultRates, nil } // GetInviteeModelDiscounts 获取被邀请用户的模型加价折扣率配置(列表口径与 /api/pricing 一致)。 func GetInviteeModelDiscounts(inviterId, inviteeUserId int) ([]InviteeModelMarkupDiscountRateItem, float64, error) { if inviterId <= 0 || inviteeUserId <= 0 { return nil, 0, errors.New("invalid id") } // 验证被邀请人确实属于当前邀请人 invitee, err := GetUserById(inviteeUserId, false) if err != nil { return nil, 0, errors.New("user not found") } if invitee.InviterId != inviterId { return nil, 0, errors.New("not your invitee") } items, _, err := listPricingVisibleMarkupDiscountRateItems() if err != nil { return nil, 0, err } var rel AffInviteRelation err = DB.Where("inviter_id = ? AND invitee_user_id = ?", inviterId, inviteeUserId).First(&rel).Error savedByKey := map[string]float64{} if err == nil { savedList, parseErr := parseInviteeModelMarkupDiscountRates(rel.ModelMarkupDiscountRate) if parseErr != nil { return nil, 0, parseErr } savedByKey = inviteeModelMarkupDiscountRateMap(savedList) } for i := range items { key := inviteeModelMarkupKey(items[i].ChannelID, items[i].ModelName) if rate, ok := savedByKey[key]; ok { items[i].CurrentMarkupDiscountRate = rate } } return items, 0, nil } // UpdateInviteeModelDiscounts 更新被邀请用户的模型加价折扣率(仅存与渠道默认不同的项,JSON 数组全量覆盖)。 type ModelMarkupDiscountRateUpdateRequest struct { ModelName string `json:"model_name"` ChannelID int `json:"channel_id"` MarkupDiscountRate float64 `json:"markup_discount_rate"` } func UpdateInviteeModelDiscounts(inviterId, inviteeUserId int, updates []ModelMarkupDiscountRateUpdateRequest) error { if inviterId <= 0 || inviteeUserId <= 0 { return errors.New("invalid id") } for _, u := range updates { if u.MarkupDiscountRate < 0 || u.MarkupDiscountRate > 100 { return fmt.Errorf("markup_discount_rate for model %s must be between 0 and 100", u.ModelName) } } // 验证被邀请人确实属于当前邀请人 invitee, err := GetUserById(inviteeUserId, false) if err != nil { return errors.New("user not found") } if invitee.InviterId != inviterId { return errors.New("not your invitee") } _, defaultRates, err := listPricingVisibleMarkupDiscountRateItems() if err != nil { return err } ratesToSave := make([]inviteeModelMarkupDiscountRateEntry, 0) for _, u := range updates { modelName := strings.TrimSpace(u.ModelName) key := inviteeModelMarkupKey(u.ChannelID, modelName) defaultRate, hasModel := defaultRates[key] if !hasModel { continue } if u.MarkupDiscountRate != defaultRate { ratesToSave = append(ratesToSave, inviteeModelMarkupDiscountRateEntry{ ModelName: modelName, ChannelID: u.ChannelID, MarkupDiscountRate: u.MarkupDiscountRate, }) } } discountsJSON, err := common.Marshal(ratesToSave) if err != nil { return err } // 更新或创建关系记录 ts := common.GetTimestamp() var rel AffInviteRelation err = DB.Where("inviter_id = ? AND invitee_user_id = ?", inviterId, inviteeUserId).First(&rel).Error if errors.Is(err, gorm.ErrRecordNotFound) { rel = AffInviteRelation{ InviterId: inviterId, InviteeUserId: inviteeUserId, ModelMarkupDiscountRate: string(discountsJSON), CreatedAt: ts, UpdatedAt: ts, } return DB.Create(&rel).Error } if err != nil { return err } rel.ModelMarkupDiscountRate = string(discountsJSON) rel.UpdatedAt = ts return DB.Save(&rel).Error } func affInviteRelationColumnExists(column string) bool { if DB == nil { return false } var count int64 var err error switch { case common.UsingPostgreSQL: err = DB.Raw(`SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = ? AND column_name = ?`, "aff_invite_relations", column).Scan(&count).Error case common.UsingSQLite: err = DB.Raw(`SELECT COUNT(*) FROM pragma_table_info('aff_invite_relations') WHERE name = ?`, column).Scan(&count).Error default: err = DB.Raw(`SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?`, "aff_invite_relations", column).Scan(&count).Error } return err == nil && count > 0 } // migrateAffInviteRelationModelMarkupDiscountRateColumn 将废弃列 model_discounts 重命名为 model_markup_discount_rate。 func migrateAffInviteRelationModelMarkupDiscountRateColumn() error { if DB == nil || !DB.Migrator().HasTable(&AffInviteRelation{}) { return nil } hasNew := affInviteRelationColumnExists("model_markup_discount_rate") hasOld := affInviteRelationColumnExists("model_discounts") if !hasOld { return nil } if hasNew { // 两列并存时把旧数据拷到新列(新列为空时) if common.UsingPostgreSQL { _ = DB.Exec(`UPDATE aff_invite_relations SET model_markup_discount_rate = model_discounts WHERE (model_markup_discount_rate IS NULL OR TRIM(model_markup_discount_rate) = '' OR model_markup_discount_rate = '[]') AND model_discounts IS NOT NULL AND TRIM(model_discounts) <> '' AND model_discounts <> '[]'`).Error } else { _ = DB.Exec(`UPDATE aff_invite_relations SET model_markup_discount_rate = model_discounts WHERE (model_markup_discount_rate IS NULL OR TRIM(model_markup_discount_rate) = '' OR model_markup_discount_rate = '[]') AND model_discounts IS NOT NULL AND TRIM(model_discounts) <> '' AND model_discounts <> '[]'`).Error } } else { var stmt string switch { case common.UsingPostgreSQL: stmt = `ALTER TABLE aff_invite_relations RENAME COLUMN model_discounts TO model_markup_discount_rate` case common.UsingSQLite: stmt = `ALTER TABLE aff_invite_relations RENAME COLUMN model_discounts TO model_markup_discount_rate` default: stmt = `ALTER TABLE aff_invite_relations CHANGE COLUMN model_discounts model_markup_discount_rate TEXT` } if err := DB.Exec(stmt).Error; err != nil { return err } common.SysLog("migrate: renamed aff_invite_relations.model_discounts -> model_markup_discount_rate") return nil } var dropStmt string switch { case common.UsingPostgreSQL: dropStmt = `ALTER TABLE aff_invite_relations DROP COLUMN IF EXISTS model_discounts` default: dropStmt = `ALTER TABLE aff_invite_relations DROP COLUMN model_discounts` } err := DB.Exec(dropStmt).Error if err == nil { common.SysLog("migrate: dropped aff_invite_relations.model_discounts") return nil } msg := strings.ToLower(err.Error()) if strings.Contains(msg, "unknown column") || strings.Contains(msg, "doesn't exist") || strings.Contains(msg, "no such column") || strings.Contains(msg, "does not exist") { return nil } if common.UsingSQLite && (strings.Contains(msg, "syntax error") || strings.Contains(msg, "near \"drop\"")) { common.SysLog("migrate: skip DROP model_discounts (SQLite may not support DROP COLUMN): " + err.Error()) return nil } return err }