package model import ( "errors" "fmt" "strings" "github.com/QuantumNous/new-api/common" "gorm.io/gorm" ) // 分销商申请状态:1=待审核 2=已通过 3=已驳回 const ( DistributorAppStatusPending = 1 DistributorAppStatusApproved = 2 DistributorAppStatusRejected = 3 DistributorApplyTypePersonal = 1 // 个人申请:real_name=姓名,id_card_no=身份证 DistributorApplyTypeEnterprise = 2 // 企业申请:real_name=企业名称,id_card_no=统一社会信用代码 ) // DistributorApplication 分销商入驻申请(每个用户最多一条记录,驳回后可更新重新提交) type DistributorApplication struct { Id int `json:"id" gorm:"primaryKey;autoIncrement"` UserId int `json:"user_id" gorm:"not null;uniqueIndex:idx_dist_app_user"` ApplyType int `json:"apply_type" gorm:"type:int;not null;default:1;column:apply_type"` // 1=个人 2=企业 RealName string `json:"real_name" gorm:"type:varchar(64);not null;column:real_name"` IdCardNo string `json:"id_card_no" gorm:"type:varchar(32);not null;column:id_card_no"` QualificationUrls string `json:"qualification_urls" gorm:"type:text;not null;column:qualification_urls"` // JSON 数组 URL 字符串 Contact string `json:"contact" gorm:"type:varchar(128);not null;column:contact"` Status int `json:"status" gorm:"type:int;not null;default:1;index:idx_dist_app_status"` RejectReason string `json:"reject_reason" gorm:"type:varchar(512);column:reject_reason"` ReviewerId int `json:"reviewer_id" gorm:"column:reviewer_id"` ReviewedAt int64 `json:"reviewed_at" gorm:"column:reviewed_at"` CreatedAt int64 `json:"created_at" gorm:"autoCreateTime;bigint"` UpdatedAt int64 `json:"updated_at" gorm:"autoUpdateTime;bigint"` } func (DistributorApplication) TableName() string { return "distributor_applications" } func distributorQualificationURLsNonEmpty(jsonStr string) bool { raw := strings.TrimSpace(jsonStr) if raw == "" { return false } var urls []string if common.UnmarshalJsonStr(raw, &urls) != nil { return false } for _, u := range urls { if strings.TrimSpace(u) != "" { return true } } return false } // NormalizeDistributorQualificationURLsJSON 解析并规范化资格证书 JSON 数组字符串 func NormalizeDistributorQualificationURLsJSON(raw string) (string, error) { raw = strings.TrimSpace(raw) if raw == "" { return "[]", nil } var urls []string if err := common.UnmarshalJsonStr(raw, &urls); err != nil { return "", errors.New("资格证书格式无效") } out := make([]string, 0, len(urls)) for _, u := range urls { u = strings.TrimSpace(u) if u != "" { out = append(out, u) } } b, err := common.Marshal(out) if err != nil { return "", err } return string(b), nil } // UpsertDistributorApplication 用户提交或驳回后重新提交 func UpsertDistributorApplication(userId, applyType int, realName, idCardNo, qualificationUrlsJSON, contact string) error { if userId <= 0 { return errors.New("invalid user") } if applyType != DistributorApplyTypePersonal && applyType != DistributorApplyTypeEnterprise { return errors.New("申请类型无效") } realName = strings.TrimSpace(realName) idCardNo = strings.TrimSpace(idCardNo) contact = strings.TrimSpace(contact) qualJSON, err := NormalizeDistributorQualificationURLsJSON(qualificationUrlsJSON) if err != nil { return err } if !distributorQualificationURLsNonEmpty(qualJSON) { return errors.New("请上传资格证书") } if realName == "" || idCardNo == "" || contact == "" { return errors.New("请填写完整资料") } u, err := GetUserById(userId, false) if err != nil { return err } if UserIsDistributor(u) { return errors.New("您已是分销商") } if u.Role >= common.RoleAdminUser { return errors.New("管理员无需申请") } var app DistributorApplication err = DB.Where("user_id = ?", userId).First(&app).Error ts := common.GetTimestamp() if errors.Is(err, gorm.ErrRecordNotFound) { app = DistributorApplication{ UserId: userId, ApplyType: applyType, RealName: realName, IdCardNo: idCardNo, QualificationUrls: qualJSON, Contact: contact, Status: DistributorAppStatusPending, CreatedAt: ts, UpdatedAt: ts, } return DB.Create(&app).Error } if err != nil { return err } if app.Status == DistributorAppStatusPending { return errors.New("申请正在审核中,请耐心等待") } if app.Status == DistributorAppStatusApproved { // 记录仍为「已通过」,但账号已被取消分销商资格时需允许再次提交(与驳回后重提相同,重置为待审核) if UserIsDistributor(u) { return errors.New("申请已通过") } app.ApplyType = applyType app.RealName = realName app.IdCardNo = idCardNo app.QualificationUrls = qualJSON app.Contact = contact app.Status = DistributorAppStatusPending app.RejectReason = "" app.ReviewerId = 0 app.ReviewedAt = 0 app.UpdatedAt = ts return DB.Save(&app).Error } // rejected -> resubmit app.ApplyType = applyType app.RealName = realName app.IdCardNo = idCardNo app.QualificationUrls = qualJSON app.Contact = contact app.Status = DistributorAppStatusPending app.RejectReason = "" app.ReviewerId = 0 app.ReviewedAt = 0 app.UpdatedAt = ts return DB.Save(&app).Error } // GetDistributorWithdrawAccountType 提现账户类型与主体名称(来自入驻申请,无申请时默认个人) func GetDistributorWithdrawAccountType(userId int) (accountType int, subjectName string, err error) { accountType = DistributorApplyTypePersonal app, err := GetDistributorApplicationByUserId(userId) if err != nil { return 0, "", err } if app == nil { return accountType, "", nil } if app.ApplyType == DistributorApplyTypeEnterprise { accountType = DistributorApplyTypeEnterprise } return accountType, strings.TrimSpace(app.RealName), nil } // GetDistributorApplicationByUserId 当前用户申请记录 func GetDistributorApplicationByUserId(userId int) (*DistributorApplication, error) { if userId <= 0 { return nil, errors.New("invalid user") } var apps []DistributorApplication if err := DB.Where("user_id = ?", userId).Limit(1).Find(&apps).Error; err != nil { return nil, err } if len(apps) == 0 { return nil, nil } return &apps[0], nil } // DistributorApplicationListQuery 管理端筛选 type DistributorApplicationListQuery struct { Keyword string Status int // 0 = 全部 ApplyType int // 0 = 全部 1=个人 2=企业 DateFrom int64 DateTo int64 PageInfo *common.PageInfo } // ListDistributorApplicationsAdmin 分页列表(keyword 匹配姓名、用户名、联系方式) func ListDistributorApplicationsAdmin(q DistributorApplicationListQuery) ([]DistributorApplication, []string, int64, error) { tx := DB.Model(&DistributorApplication{}).Joins("LEFT JOIN users ON users.id = distributor_applications.user_id") if q.Status > 0 { tx = tx.Where("distributor_applications.status = ?", q.Status) } if q.ApplyType == DistributorApplyTypePersonal || q.ApplyType == DistributorApplyTypeEnterprise { tx = tx.Where("distributor_applications.apply_type = ?", q.ApplyType) } if q.DateFrom > 0 { tx = tx.Where("distributor_applications.created_at >= ?", q.DateFrom) } if q.DateTo > 0 { tx = tx.Where("distributor_applications.created_at <= ?", q.DateTo) } kw := strings.TrimSpace(q.Keyword) if kw != "" { pattern := "%" + kw + "%" tx = tx.Where( "(distributor_applications.real_name LIKE ? OR distributor_applications.contact LIKE ? OR distributor_applications.id_card_no LIKE ? OR users.username LIKE ?)", pattern, pattern, pattern, pattern, ) } var total int64 if err := tx.Count(&total).Error; err != nil { return nil, nil, 0, err } var rows []DistributorApplication pi := q.PageInfo if pi == nil { pi = &common.PageInfo{} } err := tx.Select("distributor_applications.*"). Order("distributor_applications.id desc"). Limit(pi.GetPageSize()). Offset(pi.GetStartIdx()). Find(&rows).Error if err != nil { return nil, nil, 0, err } usernames := make([]string, len(rows)) for i := range rows { var u User if e := DB.Select("username").Where("id = ?", rows[i].UserId).First(&u).Error; e == nil { usernames[i] = u.Username } } return rows, usernames, total, nil } // GetDistributorApplicationByIdAdmin 详情 func GetDistributorApplicationByIdAdmin(id int) (*DistributorApplication, string, error) { if id <= 0 { return nil, "", errors.New("invalid id") } var app DistributorApplication if err := DB.Where("id = ?", id).First(&app).Error; err != nil { return nil, "", err } var u User _ = DB.Select("username").Where("id = ?", app.UserId).First(&u).Error return &app, u.Username, nil } // ApproveDistributorApplication 通过:用户角色改为分销商,申请状态已通过。 // distributorCommissionBps 非 nil 时写入该用户的 distributor_commission_bps(0~10000,万分之一;0 表示跟随系统默认);nil 表示不修改该字段(兼容无请求体调用)。 func ApproveDistributorApplication(appId, reviewerId int, distributorCommissionBps *int) error { if appId <= 0 || reviewerId <= 0 { return errors.New("invalid params") } return DB.Transaction(func(tx *gorm.DB) error { var app DistributorApplication if err := tx.Where("id = ?", appId).First(&app).Error; err != nil { return err } if app.Status != DistributorAppStatusPending { return errors.New("申请状态不是待审核") } var u User if err := tx.Where("id = ?", app.UserId).First(&u).Error; err != nil { return err } if u.Role >= common.RoleAdminUser { return errors.New("不能将管理员设为分销商") } if UserIsDistributor(&u) { return errors.New("用户已是分销商") } ts := common.GetTimestamp() app.Status = DistributorAppStatusApproved app.ReviewerId = reviewerId app.ReviewedAt = ts app.RejectReason = "" if err := tx.Save(&app).Error; err != nil { return err } if err := tx.Model(&User{}).Where("id = ?", app.UserId).Update("is_distributor", common.DistributorFlagYes).Error; err != nil { return err } if distributorCommissionBps != nil { b := *distributorCommissionBps if b < 0 || b > 10000 { return fmt.Errorf("commission bps must be 0..10000") } if err := tx.Model(&User{}).Where("id = ?", app.UserId).Update("distributor_commission_bps", b).Error; err != nil { return err } } return nil }) } // RejectDistributorApplication 驳回 func RejectDistributorApplication(appId, reviewerId int, reason string) error { reason = strings.TrimSpace(reason) if appId <= 0 || reviewerId <= 0 { return errors.New("invalid params") } if reason == "" { return errors.New("请填写驳回原因") } if len(reason) > 500 { return errors.New("驳回原因过长") } var app DistributorApplication if err := DB.Where("id = ?", appId).First(&app).Error; err != nil { return err } if app.Status != DistributorAppStatusPending { return errors.New("申请状态不是待审核") } ts := common.GetTimestamp() app.Status = DistributorAppStatusRejected app.RejectReason = reason app.ReviewerId = reviewerId app.ReviewedAt = ts return DB.Save(&app).Error } // DistributorAdminListItem 管理端分销商列表行(含申请真实姓名、是否需补录资料) type DistributorAdminListItem struct { User ApplicationRealName string `json:"application_real_name"` ApplicationApplyType int `json:"application_apply_type"` // 无申请记录时为 0 NeedsSupplement bool `json:"needs_supplement"` } // DistributorListAdminQuery 管理端代理人员列表筛选 type DistributorListAdminQuery struct { Keyword string ApplyType int // 0=全部 1=个人 2=企业 PageInfo *common.PageInfo } // ListDistributorsAdmin 分销商用户列表(LEFT JOIN 申请资料;关键字可搜用户名、显示名、申请姓名、联系方式、身份证) func ListDistributorsAdmin(param DistributorListAdminQuery) ([]DistributorAdminListItem, int64, error) { tx := DB.Table("users"). Joins("LEFT JOIN distributor_applications ON distributor_applications.user_id = users.id"). Where("users.is_distributor = ? AND users.role < ?", common.DistributorFlagYes, common.RoleAdminUser) if param.ApplyType == DistributorApplyTypePersonal || param.ApplyType == DistributorApplyTypeEnterprise { tx = tx.Where("distributor_applications.apply_type = ?", param.ApplyType) } kw := strings.TrimSpace(param.Keyword) if kw != "" { pattern := "%" + kw + "%" tx = tx.Where( "(users.username LIKE ? OR users.display_name LIKE ? OR distributor_applications.real_name LIKE ? OR distributor_applications.contact LIKE ? OR distributor_applications.id_card_no LIKE ?)", pattern, pattern, pattern, pattern, pattern, ) } var total int64 if err := tx.Count(&total).Error; err != nil { return nil, 0, err } pageInfo := param.PageInfo if pageInfo == nil { pageInfo = &common.PageInfo{} } type distAdminScan struct { User AppRealName string `gorm:"column:app_rn"` AppIdCard string `gorm:"column:app_ic"` AppContact string `gorm:"column:app_ct"` AppQual string `gorm:"column:app_ql"` AppApplyType int `gorm:"column:app_at"` } var scans []distAdminScan err := tx.Select(`users.*, distributor_applications.real_name AS app_rn, distributor_applications.id_card_no AS app_ic, distributor_applications.contact AS app_ct, distributor_applications.qualification_urls AS app_ql, COALESCE(distributor_applications.apply_type, 1) AS app_at`). Order("users.id DESC"). Limit(pageInfo.GetPageSize()). Offset(pageInfo.GetStartIdx()). Scan(&scans).Error if err != nil { return nil, 0, err } out := make([]DistributorAdminListItem, 0, len(scans)) for i := range scans { s := scans[i] fake := &DistributorApplication{ ApplyType: s.AppApplyType, RealName: s.AppRealName, IdCardNo: s.AppIdCard, Contact: s.AppContact, QualificationUrls: s.AppQual, } rn := strings.TrimSpace(s.AppRealName) out = append(out, DistributorAdminListItem{ User: s.User, ApplicationRealName: rn, ApplicationApplyType: s.AppApplyType, NeedsSupplement: !IsDistributorApplicationProfileComplete(fake), }) } return out, total, nil } // SetUserDistributorCommissionBps 管理员设置单个分销商默认分成比例(万分之一) func SetUserDistributorCommissionBps(userId, bps int) error { if userId <= 0 { return errors.New("invalid user") } if bps < 0 || bps > 10000 { return fmt.Errorf("commission bps must be 0..10000") } var u User if err := DB.Where("id = ?", userId).First(&u).Error; err != nil { return err } if !UserIsDistributor(&u) { return errors.New("用户不是分销商") } return DB.Model(&User{}).Where("id = ?", userId).Update("distributor_commission_bps", bps).Error } // AdminSettleDistributorAffQuota 结账:清空待结算分销收益额度 aff_quota func AdminSettleDistributorAffQuota(userId int) error { if userId <= 0 { return errors.New("invalid user") } var u User if err := DB.Where("id = ?", userId).First(&u).Error; err != nil { return err } if !UserIsDistributor(&u) { return errors.New("用户不是分销商") } return DB.Model(&User{}).Where("id = ?", userId).Update("aff_quota", 0).Error } // IsDistributorApplicationProfileComplete 判断分销商申请资料是否已完整(用于手工开通后补录提示) func IsDistributorApplicationProfileComplete(app *DistributorApplication) bool { if app == nil { return false } if strings.TrimSpace(app.RealName) == "" || strings.TrimSpace(app.IdCardNo) == "" || strings.TrimSpace(app.Contact) == "" { return false } return distributorQualificationURLsNonEmpty(app.QualificationUrls) } // GetDistributorApplicationProfileByUserIdAdmin 管理端:某分销商的申请资料;无记录或资料不全时 needsManualEntry 为 true func GetDistributorApplicationProfileByUserIdAdmin(userId int) (username string, app *DistributorApplication, needsManualEntry bool, err error) { if userId <= 0 { return "", nil, false, errors.New("invalid id") } u, err := GetUserById(userId, false) if err != nil { return "", nil, false, err } if !UserIsDistributor(u) { return "", nil, false, errors.New("用户不是分销商") } app, err = GetDistributorApplicationByUserId(userId) if err != nil { return "", nil, false, err } needsManualEntry = !IsDistributorApplicationProfileComplete(app) return u.Username, app, needsManualEntry, nil } // AdminUpsertDistributorApplicationByUser 管理端补录/修改分销商申请资料(无记录时创建为已通过) func AdminUpsertDistributorApplicationByUser(userId, reviewerId, applyType int, realName, idCardNo, qualificationUrlsJSON, contact string) error { if userId <= 0 || reviewerId <= 0 { return errors.New("invalid params") } if applyType != DistributorApplyTypePersonal && applyType != DistributorApplyTypeEnterprise { return errors.New("申请类型无效") } realName = strings.TrimSpace(realName) idCardNo = strings.TrimSpace(idCardNo) contact = strings.TrimSpace(contact) qualJSON, err := NormalizeDistributorQualificationURLsJSON(qualificationUrlsJSON) if err != nil { return err } if !distributorQualificationURLsNonEmpty(qualJSON) { return errors.New("请上传资格证书") } if realName == "" || idCardNo == "" || contact == "" { return errors.New("请填写完整资料") } u, err := GetUserById(userId, false) if err != nil { return err } if !UserIsDistributor(u) { return errors.New("用户不是分销商") } if u.Role >= common.RoleAdminUser { return errors.New("管理员账号无需维护申请资料") } return DB.Transaction(func(tx *gorm.DB) error { var app DistributorApplication err := tx.Where("user_id = ?", userId).First(&app).Error ts := common.GetTimestamp() if errors.Is(err, gorm.ErrRecordNotFound) { app = DistributorApplication{ UserId: userId, ApplyType: applyType, RealName: realName, IdCardNo: idCardNo, QualificationUrls: qualJSON, Contact: contact, Status: DistributorAppStatusApproved, RejectReason: "", ReviewerId: reviewerId, ReviewedAt: ts, CreatedAt: ts, UpdatedAt: ts, } return tx.Create(&app).Error } if err != nil { return err } app.ApplyType = applyType app.RealName = realName app.IdCardNo = idCardNo app.QualificationUrls = qualJSON app.Contact = contact app.RejectReason = "" app.ReviewerId = reviewerId app.ReviewedAt = ts if app.Status != DistributorAppStatusApproved { app.Status = DistributorAppStatusApproved } app.UpdatedAt = ts return tx.Save(&app).Error }) } // migrateDropDistributorApplicationIsStudentColumn 删除 distributor_applications 表中已废弃的 is_student 列(模型已不再映射该字段)。 func migrateDropDistributorApplicationIsStudentColumn() error { if DB == nil { return nil } var stmt string switch { case common.UsingPostgreSQL: stmt = `ALTER TABLE distributor_applications DROP COLUMN IF EXISTS is_student` case common.UsingSQLite: stmt = `ALTER TABLE distributor_applications DROP COLUMN is_student` default: stmt = `ALTER TABLE distributor_applications DROP COLUMN is_student` } err := DB.Exec(stmt).Error if err == nil { common.SysLog("migrate: dropped distributor_applications.is_student") 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, "check that 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 is_student (SQLite may not support DROP COLUMN): " + err.Error()) return nil } return err }