package business import ( "context" "errors" "time" "github.com/google/uuid" "gorm.io/gorm" "kra/internal/biz/business" ) // accountPO 是 sys_users 的商务投影,表结构仍由 system 模块维护。 type accountPO struct { ID uint `gorm:"primaryKey"` CreatedAt, UpdatedAt time.Time DeletedAt gorm.DeletedAt UUID, Username, Password, NickName, RealName, Phone, Email string QQNumber string `gorm:"column:qq_number"` Level int ParentID uint IDPath string `gorm:"column:id_path"` BusinessStatus int RejectReason string AuthorityID uint Enable int OriginSetting string PasswordUpdatedAt *time.Time ParentUserName string `gorm:"->;-:migration"` } func (accountPO) TableName() string { return "sys_users" } func newAccount(a *business.Account) accountPO { now := time.Now() enable := 0 if a.Status == 0 { enable = 1 } return accountPO{ID: a.ID, UUID: uuid.NewString(), Username: a.Username, NickName: a.NickName, Password: a.Password, RealName: a.RealName, Phone: a.Phone, Email: a.Email, QQNumber: a.QQNumber, Level: a.Level, ParentID: a.ParentID, IDPath: a.IDPath, BusinessStatus: a.Status, RejectReason: a.RejectReason, AuthorityID: a.AuthorityID, Enable: enable, OriginSetting: "{}", PasswordUpdatedAt: &now} } func toBizAccount(po accountPO) *business.Account { return &business.Account{ID: po.ID, Username: po.Username, NickName: po.NickName, RealName: po.RealName, Phone: po.Phone, Email: po.Email, QQNumber: po.QQNumber, Level: po.Level, ParentID: po.ParentID, IDPath: po.IDPath, Status: po.BusinessStatus, RejectReason: po.RejectReason, AuthorityID: po.AuthorityID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, ParentUserName: po.ParentUserName} } type accountRepo struct{ data Provider } func NewAccountRepo(data Provider) business.AccountRepo { return &accountRepo{data: data} } func (r *accountRepo) Find(ctx context.Context, id uint) (*business.Account, error) { var po accountPO if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, business.ErrAccountNotFound } return nil, business.ErrStorage } return toBizAccount(po), nil } func applyUserScope(db *gorm.DB, scope business.UserScope, columnPrefix string) *gorm.DB { if scope.All { return db } if scope.Empty || scope.SelfID == 0 { return db.Where("1=0") } id, path, parent := columnPrefix+"id", columnPrefix+"id_path", columnPrefix+"parent_id" if scope.OnlySelf { return db.Where(id+" = ?", scope.SelfID) } query := "(" + path + " = ? OR " + path + " LIKE ? OR " + parent + " = ?)" args := []any{scope.DescendantPath, scope.DescendantPath + ",%", scope.SelfID} if scope.IncludeSelf { query = "(" + query + " OR " + id + " = ?)" args = append(args, scope.SelfID) } else { db = db.Where(id+" <> ?", scope.SelfID) } return db.Where(query, args...) } func (r *accountRepo) List(ctx context.Context, scope business.UserScope, options ...business.ListOption) ([]*business.Account, int64, error) { o := business.NewListOptions(options...) filter := o.Filter db := r.data.DB().WithContext(ctx).Model(&accountPO{}).Table("sys_users AS u").Where("u.deleted_at IS NULL") db = applyUserScope(db, scope, "u.") if filter.UserID != 0 { db = db.Where("u.id = ?", filter.UserID) } for _, item := range []struct{ column, value string }{{"username", filter.Username}, {"nick_name", filter.NickName}, {"real_name", filter.RealName}, {"phone", filter.Phone}} { if item.value != "" { db = db.Where("u."+item.column+" LIKE ?", "%"+item.value+"%") } } if filter.Level != nil { db = db.Where("u.level = ?", *filter.Level) } if filter.Status != nil { db = db.Where("u.business_status = ?", *filter.Status) } if filter.BeginTime != nil && filter.EndTime != nil { db = db.Where("u.created_at BETWEEN ? AND ?", filter.BeginTime, filter.EndTime) } var total int64 if err := db.Count(&total).Error; err != nil { return nil, 0, business.ErrStorage } columns := map[string]string{"userId": "id", "userName": "username", "realName": "real_name", "level": "level", "status": "business_status", "createTime": "created_at"} column := columns[o.OrderBy] if column == "" { column = "id" } order := "u." + column if o.Desc { order += " DESC" } else { order += " ASC" } var pos []accountPO if err := db.Select("u.*, p.username AS parent_user_name").Joins("LEFT JOIN sys_users p ON p.id=u.parent_id AND p.deleted_at IS NULL").Order(order).Order("u.id ASC").Offset(o.Offset).Limit(o.Limit).Find(&pos).Error; err != nil { return nil, 0, business.ErrStorage } rows := make([]*business.Account, 0, len(pos)) for _, po := range pos { rows = append(rows, toBizAccount(po)) } return rows, total, nil } func checkAccountUnique(tx *gorm.DB, a *business.Account) error { for _, item := range []struct { column, value string err error }{{"username", a.Username, business.ErrAccountExists}, {"phone", a.Phone, business.ErrPhoneExists}, {"email", a.Email, business.ErrEmailExists}} { if item.value == "" { continue } var count int64 if err := tx.Model(&accountPO{}).Where(item.column+" = ? AND id <> ?", item.value, a.ID).Count(&count).Error; err != nil { return business.ErrStorage } if count > 0 { return item.err } } return nil } func (r *accountRepo) Create(ctx context.Context, a *business.Account) error { err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := checkAccountUnique(tx, a); err != nil { return err } po := newAccount(a) if err := tx.Create(&po).Error; err != nil { return business.ErrStorage } if a.HireDate != "" { if err := tx.Create(&businessMemberProfilePO{UserID: po.ID, HireDate: a.HireDate, Employment: "employed"}).Error; err != nil { return business.ErrStorage } } if err := tx.Table("sys_user_authority").Create(map[string]any{"sys_user_id": po.ID, "sys_authority_authority_id": po.AuthorityID}).Error; err != nil { return business.ErrStorage } on := 1 if err := tx.Create(&preferencePO{UserID: po.ID, IncludeSubPromote: &on, IDCardDedup: &on, UpdateTime: time.Now()}).Error; err != nil { return business.ErrStorage } a.ID, a.CreatedAt, a.UpdatedAt = po.ID, po.CreatedAt, po.UpdatedAt return nil }) return storageError(err) } func (r *accountRepo) Update(ctx context.Context, a *business.Account) error { err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := checkAccountUnique(tx, a); err != nil { return err } enable := 0 if a.Status == 0 { enable = 1 } result := tx.Model(&accountPO{}).Where("id = ?", a.ID).Updates(map[string]any{ "username": a.Username, "nick_name": a.NickName, "real_name": a.RealName, "phone": a.Phone, "email": a.Email, "qq_number": a.QQNumber, "business_status": a.Status, "enable": enable, "reject_reason": a.RejectReason, }) if result.Error != nil { return business.ErrStorage } if result.RowsAffected == 0 { return business.ErrAccountNotFound } return nil }) return storageError(err) } func (r *accountRepo) AuthorityForLevel(ctx context.Context, level int) (uint, error) { // 原角色 ID 仅用于查迁移映射,不作为 Kra 角色 ID 使用。 legacy := map[int]string{1: "2083497078545207297", 2: "2087435436573081601", 3: "2087361690139152385"} oldID, ok := legacy[level] if !ok { return 0, business.ErrBusinessRole } db := r.data.DB().WithContext(ctx) var id uint if db.Migrator().HasTable("sw_kra_id_map") { if err := db.Table("sw_kra_id_map m").Select("m.new_id").Joins("JOIN sys_authorities a ON a.authority_id=m.new_id AND a.deleted_at IS NULL").Where("m.entity = ? AND m.old_id = ?", "role", oldID).Scan(&id).Error; err != nil { return 0, business.ErrStorage } if id != 0 && id != 888 { return id, nil } } // 新安装可使用对应中文角色;匹配不唯一时停止创建,避免错授权限。 names := map[int]string{1: "一级商务", 2: "二级商务", 3: "三级商务"} var ids []uint if err := db.Table("sys_authorities").Where("authority_name = ? AND deleted_at IS NULL AND authority_id <> ?", names[level], 888).Pluck("authority_id", &ids).Error; err != nil { return 0, business.ErrStorage } if len(ids) != 1 { return 0, business.ErrBusinessRole } return ids[0], nil } func (r *accountRepo) DirectCounts(ctx context.Context, ids []uint) (map[uint]business.AccountCounts, error) { result := map[uint]business.AccountCounts{} if len(ids) == 0 { return result, nil } db := r.data.DB().WithContext(ctx) for _, id := range ids { zero := int64(0) result[id] = business.AccountCounts{PromoteCount: &zero} } var binds []struct { UserID uint Total int64 } if err := db.Table("sys_user_bind_platform").Select("user_id, COUNT(*) AS total").Where("user_id IN ? AND status = ?", ids, "0").Group("user_id").Scan(&binds).Error; err != nil { return nil, business.ErrStorage } if len(binds) == 0 { return result, nil } for _, b := range binds { zero := int64(0) result[b.UserID] = business.AccountCounts{PromoteCount: &zero, BindCount: b.Total} } // 系统数据演练库尚未包含渠道表时,基础资料与已通过绑定数仍可使用。 available, err := businessTableAvailable(db, "tab_promote") if err != nil { return nil, business.ErrStorage } if !available { for _, b := range binds { value := result[b.UserID] value.PromoteCount = nil result[b.UserID] = value } return result, nil } var counts []struct { UserID uint Total int64 } if err := db.Table("sys_user_bind_platform b").Select("b.user_id, COUNT(*) AS total").Joins("JOIN tab_promote p ON p.source_db=b.source_db AND p.busier_id=b.platform_busier_id AND p.promote_level=1").Where("b.user_id IN ? AND b.status = ?", ids, "0").Group("b.user_id").Scan(&counts).Error; err != nil { return nil, business.ErrStorage } for _, c := range counts { value := result[c.UserID] count := c.Total value.PromoteCount = &count result[c.UserID] = value } return result, nil }