package supportgroup import ( "context" "errors" "strings" "hfb_sys/backend/internal/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) type Repository struct { db *gorm.DB } func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } func (r *Repository) List(ctx context.Context) ([]GroupDTO, error) { var groups []model.ChatSupportGroup if err := r.db.WithContext(ctx).Order("sort_order ASC, id ASC").Find(&groups).Error; err != nil { return nil, err } items := make([]GroupDTO, 0, len(groups)) for _, group := range groups { dto, err := r.toDTO(ctx, group) if err != nil { return nil, err } items = append(items, dto) } return items, nil } func (r *Repository) FindByID(ctx context.Context, id uint64) (*GroupDTO, error) { var group model.ChatSupportGroup if err := r.db.WithContext(ctx).First(&group, id).Error; err != nil { return nil, err } dto, err := r.toDTO(ctx, group) if err != nil { return nil, err } return &dto, nil } func (r *Repository) Create(ctx context.Context, req CreateGroupRequest) (*GroupDTO, error) { code := strings.TrimSpace(req.Code) if code == "" { code = makeCustomCode(req.Name) } status := normalizeStatus(req.Status) group := model.ChatSupportGroup{ Code: code, Name: strings.TrimSpace(req.Name), Description: strings.TrimSpace(req.Description), Status: status, SortOrder: req.SortOrder, } err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Create(&group).Error; err != nil { return err } return replaceMembers(tx, group.ID, req.MemberIDs) }) if err != nil { return nil, err } return r.FindByID(ctx, group.ID) } func (r *Repository) Update(ctx context.Context, id uint64, req UpdateGroupRequest) (*GroupDTO, error) { var group model.ChatSupportGroup db := r.db.WithContext(ctx) if err := db.First(&group, id).Error; err != nil { return nil, err } group.Name = strings.TrimSpace(req.Name) group.Description = strings.TrimSpace(req.Description) group.Status = normalizeStatus(req.Status) group.SortOrder = req.SortOrder err := db.Transaction(func(tx *gorm.DB) error { if err := tx.Save(&group).Error; err != nil { return err } return replaceMembers(tx, group.ID, req.MemberIDs) }) if err != nil { return nil, err } return r.FindByID(ctx, id) } func (r *Repository) Delete(ctx context.Context, id uint64) error { var group model.ChatSupportGroup db := r.db.WithContext(ctx) if err := db.First(&group, id).Error; err != nil { return err } if isProtectedCode(group.Code) { return ErrProtectedGroup } return db.Transaction(func(tx *gorm.DB) error { if err := tx.Where("group_id = ?", id).Delete(&model.ChatSupportGroupMember{}).Error; err != nil { return err } return tx.Delete(&model.ChatSupportGroup{}, id).Error }) } func (r *Repository) AssignMembers(ctx context.Context, id uint64, memberIDs []uint64) error { var count int64 if err := r.db.WithContext(ctx).Model(&model.ChatSupportGroup{}).Where("id = ?", id).Count(&count).Error; err != nil { return err } if count == 0 { return gorm.ErrRecordNotFound } return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return replaceMembers(tx, id, memberIDs) }) } func (r *Repository) ListSupportAdmins(ctx context.Context) ([]MemberDTO, error) { var rows []struct { ID uint64 Username string Nickname string SupportStatus string } err := r.db.WithContext(ctx).Table("admin_users AS au"). Select("DISTINCT au.id, au.username, au.nickname, au.support_status"). Joins("JOIN admin_user_roles AS aur ON aur.admin_user_id = au.id"). Joins("JOIN roles AS r ON r.id = aur.role_id"). Where("au.status = ? AND r.code = ?", "active", "cs"). Order("CASE au.support_status WHEN 'online' THEN 0 WHEN 'busy' THEN 1 ELSE 2 END, au.id ASC"). Scan(&rows).Error if err != nil { return nil, err } items := make([]MemberDTO, 0, len(rows)) for _, row := range rows { items = append(items, MemberDTO{ ID: row.ID, Username: row.Username, Nickname: row.Nickname, SupportStatus: row.SupportStatus, }) } return items, nil } func (r *Repository) toDTO(ctx context.Context, group model.ChatSupportGroup) (GroupDTO, error) { members, err := r.members(ctx, group.ID) if err != nil { return GroupDTO{}, err } return GroupDTO{ ID: group.ID, Code: group.Code, Name: group.Name, Description: group.Description, Status: group.Status, SortOrder: group.SortOrder, Members: members, CreatedAt: group.CreatedAt, UpdatedAt: group.UpdatedAt, }, nil } func (r *Repository) members(ctx context.Context, groupID uint64) ([]MemberDTO, error) { var rows []struct { ID uint64 Username string Nickname string SupportStatus string } err := r.db.WithContext(ctx).Table("chat_support_group_members AS gm"). Select("au.id, au.username, au.nickname, au.support_status"). Joins("JOIN admin_users AS au ON au.id = gm.admin_user_id"). Where("gm.group_id = ?", groupID). Order("au.id ASC"). Scan(&rows).Error if err != nil { return nil, err } items := make([]MemberDTO, 0, len(rows)) for _, row := range rows { items = append(items, MemberDTO{ ID: row.ID, Username: row.Username, Nickname: row.Nickname, SupportStatus: row.SupportStatus, }) } return items, nil } func replaceMembers(tx *gorm.DB, groupID uint64, memberIDs []uint64) error { if err := tx.Where("group_id = ?", groupID).Delete(&model.ChatSupportGroupMember{}).Error; err != nil { return err } for _, adminID := range uniqueIDs(memberIDs) { if adminID == 0 { continue } member := model.ChatSupportGroupMember{GroupID: groupID, AdminID: adminID} if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&member).Error; err != nil { return err } } return nil } func normalizeStatus(status string) string { switch status { case "disabled": return "disabled" default: return "active" } } func isProtectedCode(code string) bool { return code == GroupCodeOwnerOnboarding || code == GroupCodeRenterHandoff || code == GroupCodeMohong } func makeCustomCode(name string) string { base := strings.TrimSpace(strings.ToLower(name)) base = strings.ReplaceAll(base, " ", "_") base = strings.ReplaceAll(base, "-", "_") if base == "" { base = "custom" } return "custom_" + base } func uniqueIDs(ids []uint64) []uint64 { seen := make(map[uint64]struct{}, len(ids)) result := make([]uint64, 0, len(ids)) for _, id := range ids { if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} result = append(result, id) } return result } func IsNotFound(err error) bool { return errors.Is(err, gorm.ErrRecordNotFound) }