All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m42s
297 lines
12 KiB
Go
297 lines
12 KiB
Go
// Package businessusergroup 提供业务用户组及其成员规模的只读投影。
|
|
// Query 只做筛选、分页与 DTO 投影,不修改任何状态。
|
|
package businessusergroup
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
|
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
|
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
|
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
|
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
|
"github.com/break/junhong_cmp_fiber/pkg/sanitizer"
|
|
)
|
|
|
|
// Query 查询业务用户组及其成员。
|
|
type Query struct {
|
|
db *gorm.DB
|
|
store *postgres.BusinessUserGroupStore
|
|
}
|
|
|
|
// NewQuery 创建业务用户组查询。
|
|
func NewQuery(db *gorm.DB, groupStore *postgres.BusinessUserGroupStore) *Query {
|
|
return &Query{db: db, store: groupStore}
|
|
}
|
|
|
|
// List 分页查询业务用户组,供维护与筛选下拉使用。
|
|
func (q *Query) List(ctx context.Context, request dto.BusinessUserGroupListRequest) (*dto.BusinessUserGroupPageResult, error) {
|
|
if q == nil || q.db == nil {
|
|
return nil, errors.New(errors.CodeServiceUnavailable, "业务用户组查询尚未配置")
|
|
}
|
|
page, pageSize := normalizePage(request.Page, request.PageSize)
|
|
status := enabledToStatus(request.Enabled)
|
|
query := q.db.WithContext(ctx).Model(&model.BusinessUserGroup{})
|
|
if status != nil {
|
|
query = query.Where("status = ?", *status)
|
|
}
|
|
if request.BusinessLine != nil {
|
|
query = query.Where("business_line = ?", *request.BusinessLine)
|
|
}
|
|
if keyword := strings.TrimSpace(request.Keyword); keyword != "" {
|
|
like := "%" + keyword + "%"
|
|
query = query.Where("name ILIKE ? OR code ILIKE ?", like, like)
|
|
}
|
|
var total int64
|
|
if err := query.Count(&total).Error; err != nil {
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询业务用户组总数失败")
|
|
}
|
|
var groups []*model.BusinessUserGroup
|
|
if err := query.Order("sort_order ASC, id ASC").
|
|
Offset((page - 1) * pageSize).Limit(pageSize).Find(&groups).Error; err != nil {
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询业务用户组列表失败")
|
|
}
|
|
counts, err := q.memberCounts(ctx, groupIDs(groups))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := make([]*dto.BusinessUserGroupResponse, 0, len(groups))
|
|
for _, group := range groups {
|
|
item := toResponse(group)
|
|
item.MemberCount = counts[group.ID]
|
|
items = append(items, item)
|
|
}
|
|
return &dto.BusinessUserGroupPageResult{Items: items, Total: total, Page: page, Size: pageSize}, nil
|
|
}
|
|
|
|
// Detail 查询单个业务用户组详情。
|
|
func (q *Query) Detail(ctx context.Context, groupID uint) (*dto.BusinessUserGroupResponse, error) {
|
|
if q == nil || q.store == nil {
|
|
return nil, errors.New(errors.CodeServiceUnavailable, "业务用户组查询尚未配置")
|
|
}
|
|
if groupID == 0 {
|
|
return nil, errors.New(errors.CodeInvalidParam)
|
|
}
|
|
group, err := q.store.GetByID(ctx, groupID)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, errors.New(errors.CodeNotFound, "业务用户组不存在")
|
|
}
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询业务用户组失败")
|
|
}
|
|
count, err := q.store.CountMembers(ctx, groupID)
|
|
if err != nil {
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "统计业务用户组成员失败")
|
|
}
|
|
response := toResponse(group)
|
|
response.MemberCount = count
|
|
return response, nil
|
|
}
|
|
|
|
// ListMembers 分页查询指定业务用户组保留的全部成员关系。
|
|
func (q *Query) ListMembers(ctx context.Context, groupID uint, request dto.BusinessUserGroupMemberListRequest) (*dto.BusinessUserGroupMemberPageResult, error) {
|
|
if q == nil || q.db == nil || q.store == nil {
|
|
return nil, errors.New(errors.CodeServiceUnavailable, "业务用户组成员查询尚未配置")
|
|
}
|
|
if groupID == 0 {
|
|
return nil, errors.New(errors.CodeInvalidParam)
|
|
}
|
|
if _, err := q.store.GetByID(ctx, groupID); err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, errors.New(errors.CodeNotFound, "业务用户组不存在")
|
|
}
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询业务用户组失败")
|
|
}
|
|
page, pageSize := normalizePage(request.Page, request.PageSize)
|
|
base := q.db.WithContext(ctx).Unscoped().Model(&model.BusinessUserGroupMember{}).
|
|
Joins("JOIN tb_account a ON a.id = tb_business_user_group_member.account_id").
|
|
Joins("JOIN tb_business_user_group g ON g.id = tb_business_user_group_member.business_user_group_id").
|
|
Where("tb_business_user_group_member.business_user_group_id = ? AND tb_business_user_group_member.deleted_at IS NULL AND g.deleted_at IS NULL", groupID)
|
|
if keyword := strings.TrimSpace(request.Keyword); keyword != "" {
|
|
like := "%" + keyword + "%"
|
|
base = base.Where("a.username ILIKE ? OR a.phone ILIKE ?", like, like)
|
|
}
|
|
switch request.AccountStatus {
|
|
case "enabled":
|
|
base = base.Where("a.deleted_at IS NULL AND a.status = ?", constants.StatusEnabled)
|
|
case "disabled":
|
|
base = base.Where("a.deleted_at IS NULL AND a.status = ?", constants.StatusDisabled)
|
|
case "deleted":
|
|
base = base.Where("a.deleted_at IS NOT NULL")
|
|
}
|
|
var total int64
|
|
if err := base.Count(&total).Error; err != nil {
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询业务用户组成员总数失败")
|
|
}
|
|
var rows []memberRow
|
|
if err := base.Select("tb_business_user_group_member.account_id, a.username, a.phone, a.status, a.deleted_at, tb_business_user_group_member.created_at AS member_created_at, g.id AS group_id, g.code AS group_code, g.name AS group_name, g.status AS group_status").
|
|
Order("tb_business_user_group_member.account_id ASC").
|
|
Offset((page - 1) * pageSize).Limit(pageSize).Scan(&rows).Error; err != nil {
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询业务用户组成员失败")
|
|
}
|
|
items := make([]*dto.BusinessUserGroupMemberResponse, 0, len(rows))
|
|
for _, row := range rows {
|
|
items = append(items, &dto.BusinessUserGroupMemberResponse{
|
|
AccountID: row.AccountID, Username: row.Username, Phone: sanitizer.MaskPhone(row.Phone),
|
|
Enabled: row.Status == constants.StatusEnabled && !row.DeletedAt.Valid,
|
|
StatusName: constants.GetStatusName(row.Status), Deleted: row.DeletedAt.Valid,
|
|
MemberCreatedAt: row.MemberCreatedAt.Format(time.RFC3339), CurrentGroupID: row.GroupID,
|
|
CurrentGroupCode: row.GroupCode, CurrentGroupName: row.GroupName,
|
|
CurrentGroupEnabled: row.GroupStatus == constants.StatusEnabled,
|
|
})
|
|
}
|
|
return &dto.BusinessUserGroupMemberPageResult{Items: items, Total: total, Page: page, Size: pageSize}, nil
|
|
}
|
|
|
|
// ListMemberCandidates 分页查询指定业务用户组的启用平台用户候选。
|
|
func (q *Query) ListMemberCandidates(ctx context.Context, groupID uint, request dto.BusinessUserGroupMemberCandidateRequest) (*dto.BusinessUserGroupMemberCandidatePageResult, error) {
|
|
if q == nil || q.db == nil || q.store == nil {
|
|
return nil, errors.New(errors.CodeServiceUnavailable, "业务用户组候选查询尚未配置")
|
|
}
|
|
if groupID == 0 {
|
|
return nil, errors.New(errors.CodeInvalidParam)
|
|
}
|
|
group, err := q.store.GetByID(ctx, groupID)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, errors.New(errors.CodeNotFound, "业务用户组不存在")
|
|
}
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询业务用户组失败")
|
|
}
|
|
page, pageSize := normalizePage(request.Page, request.PageSize)
|
|
base := q.db.WithContext(ctx).Model(&model.Account{}).
|
|
Where("tb_account.user_type = ? AND tb_account.status = ?", constants.UserTypePlatform, constants.StatusEnabled).
|
|
Joins("LEFT JOIN tb_business_user_group_member m ON m.account_id = tb_account.id AND m.deleted_at IS NULL").
|
|
Joins("LEFT JOIN tb_business_user_group g ON g.id = m.business_user_group_id AND g.deleted_at IS NULL")
|
|
if keyword := strings.TrimSpace(request.Keyword); keyword != "" {
|
|
like := "%" + keyword + "%"
|
|
base = base.Where("tb_account.username ILIKE ? OR tb_account.phone ILIKE ?", like, like)
|
|
}
|
|
switch request.GroupFilter {
|
|
case "ungrouped":
|
|
base = base.Where("m.account_id IS NULL")
|
|
case "current":
|
|
base = base.Where("m.business_user_group_id = ?", groupID)
|
|
case "other":
|
|
base = base.Where("m.business_user_group_id IS NOT NULL AND m.business_user_group_id <> ?", groupID)
|
|
}
|
|
var total int64
|
|
if err := base.Count(&total).Error; err != nil {
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询业务用户组候选总数失败")
|
|
}
|
|
var rows []candidateRow
|
|
if err := base.Select("tb_account.id AS account_id, tb_account.username, tb_account.phone, m.business_user_group_id AS group_id, g.code AS group_code, g.name AS group_name, g.status AS group_status").
|
|
Order("tb_account.id ASC").Offset((page - 1) * pageSize).Limit(pageSize).Scan(&rows).Error; err != nil {
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询业务用户组候选失败")
|
|
}
|
|
canAdd := group.Status == constants.StatusEnabled
|
|
items := make([]*dto.BusinessUserGroupMemberCandidateResponse, 0, len(rows))
|
|
for _, row := range rows {
|
|
action := "add"
|
|
if row.GroupID != nil {
|
|
action = "move"
|
|
if *row.GroupID == groupID {
|
|
action = "already_member"
|
|
}
|
|
}
|
|
items = append(items, &dto.BusinessUserGroupMemberCandidateResponse{
|
|
AccountID: row.AccountID, Username: row.Username, Phone: sanitizer.MaskPhone(row.Phone),
|
|
CurrentGroupID: row.GroupID, CurrentGroupCode: row.GroupCode, CurrentGroupName: row.GroupName,
|
|
CurrentGroupEnabled: row.GroupStatus != nil && *row.GroupStatus == constants.StatusEnabled,
|
|
Action: action, CanAdd: canAdd,
|
|
})
|
|
}
|
|
return &dto.BusinessUserGroupMemberCandidatePageResult{Items: items, Total: total, Page: page, Size: pageSize}, nil
|
|
}
|
|
|
|
type memberRow struct {
|
|
AccountID uint
|
|
Username string
|
|
Phone string
|
|
Status int
|
|
DeletedAt gorm.DeletedAt
|
|
MemberCreatedAt time.Time
|
|
GroupID uint `gorm:"column:group_id"`
|
|
GroupCode string
|
|
GroupName string
|
|
GroupStatus int
|
|
}
|
|
|
|
type candidateRow struct {
|
|
AccountID uint
|
|
Username string
|
|
Phone string
|
|
GroupID *uint
|
|
GroupCode string
|
|
GroupName string
|
|
GroupStatus *int
|
|
}
|
|
|
|
func (q *Query) memberCounts(ctx context.Context, ids []uint) (map[uint]int64, error) {
|
|
counts := make(map[uint]int64, len(ids))
|
|
if len(ids) == 0 {
|
|
return counts, nil
|
|
}
|
|
var rows []struct {
|
|
GroupID uint
|
|
Count int64
|
|
}
|
|
if err := q.db.WithContext(ctx).Model(&model.BusinessUserGroupMember{}).
|
|
Select("business_user_group_id AS group_id, COUNT(*) AS count").
|
|
Where("business_user_group_id IN ?", ids).Group("business_user_group_id").Scan(&rows).Error; err != nil {
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "统计业务用户组成员失败")
|
|
}
|
|
for _, row := range rows {
|
|
counts[row.GroupID] = row.Count
|
|
}
|
|
return counts, nil
|
|
}
|
|
|
|
func groupIDs(groups []*model.BusinessUserGroup) []uint {
|
|
ids := make([]uint, 0, len(groups))
|
|
for _, group := range groups {
|
|
ids = append(ids, group.ID)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// toResponse 将业务用户组投影为对外响应。
|
|
func toResponse(group *model.BusinessUserGroup) *dto.BusinessUserGroupResponse {
|
|
return &dto.BusinessUserGroupResponse{
|
|
ID: group.ID, Code: group.Code, Name: group.Name,
|
|
BusinessLine: group.BusinessLine, BusinessLineName: constants.GetBusinessLineName(group.BusinessLine),
|
|
Sort: group.SortOrder, Enabled: group.Status == constants.StatusEnabled, Remark: group.Remark,
|
|
CreatedAt: group.CreatedAt.Format(time.RFC3339), UpdatedAt: group.UpdatedAt.Format(time.RFC3339),
|
|
}
|
|
}
|
|
|
|
// enabledToStatus 把对外启停布尔映射为既有状态值;未提交时返回 nil 表示不过滤。
|
|
func enabledToStatus(enabled *bool) *int {
|
|
if enabled == nil {
|
|
return nil
|
|
}
|
|
status := constants.StatusDisabled
|
|
if *enabled {
|
|
status = constants.StatusEnabled
|
|
}
|
|
return &status
|
|
}
|
|
|
|
// normalizePage 归一化分页参数,执行 DefaultPageSize 与 MaxPageSize 上限。
|
|
func normalizePage(page, pageSize int) (int, int) {
|
|
if page <= 0 {
|
|
page = constants.DefaultPage
|
|
}
|
|
if pageSize <= 0 {
|
|
pageSize = constants.DefaultPageSize
|
|
}
|
|
if pageSize > constants.MaxPageSize {
|
|
pageSize = constants.MaxPageSize
|
|
}
|
|
return page, pageSize
|
|
}
|