feat: expand superadmin user detail views

This commit is contained in:
2026-01-15 12:29:52 +08:00
parent 8419ddede7
commit bec984b959
10 changed files with 2874 additions and 12 deletions

View File

@@ -2,6 +2,7 @@ package services
import (
"context"
"encoding/json"
"errors"
"strconv"
"strings"
@@ -286,6 +287,7 @@ func (s *super) GetUser(ctx context.Context, id int64) (*super_dto.UserItem, err
Roles: u.Roles,
Status: u.Status,
StatusDescription: u.Status.Description(),
VerifiedAt: s.formatTime(u.VerifiedAt),
CreatedAt: u.CreatedAt.Format(time.RFC3339),
UpdatedAt: u.UpdatedAt.Format(time.RFC3339),
},
@@ -382,6 +384,301 @@ func (s *super) GetUserWallet(ctx context.Context, userID int64) (*super_dto.Sup
}, nil
}
func (s *super) GetUserRealName(ctx context.Context, userID int64) (*super_dto.SuperUserRealNameResponse, error) {
if userID == 0 {
return nil, errorx.ErrBadRequest.WithMsg("用户ID不能为空")
}
tbl, q := models.UserQuery.QueryContext(ctx)
u, err := q.Where(tbl.ID.Eq(userID)).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errorx.ErrRecordNotFound
}
return nil, errorx.ErrDatabaseError.WithCause(err)
}
// 从用户元数据中读取实名字段,便于超管展示。
meta := make(map[string]interface{})
if len(u.Metas) > 0 {
if err := json.Unmarshal(u.Metas, &meta); err != nil {
return nil, errorx.ErrInternalError.WithCause(err).WithMsg("解析实名认证信息失败")
}
}
realName := ""
if value, ok := meta["real_name"].(string); ok {
realName = value
}
idCard := ""
if value, ok := meta["id_card"].(string); ok {
idCard = value
}
return &super_dto.SuperUserRealNameResponse{
IsRealNameVerified: u.IsRealNameVerified,
VerifiedAt: s.formatTime(u.VerifiedAt),
RealName: realName,
IDCardMasked: s.maskIDCard(idCard),
}, nil
}
func (s *super) ListUserNotifications(ctx context.Context, userID int64, filter *super_dto.SuperUserNotificationListFilter) (*requests.Pager, error) {
if userID == 0 {
return nil, errorx.ErrBadRequest.WithMsg("用户ID不能为空")
}
if filter == nil {
filter = &super_dto.SuperUserNotificationListFilter{}
}
tbl, q := models.NotificationQuery.QueryContext(ctx)
q = q.Where(tbl.UserID.Eq(userID))
if filter.TenantID != nil && *filter.TenantID > 0 {
q = q.Where(tbl.TenantID.Eq(*filter.TenantID))
}
if filter.Type != nil && strings.TrimSpace(*filter.Type) != "" {
q = q.Where(tbl.Type.Eq(strings.TrimSpace(*filter.Type)))
}
if filter.Read != nil {
q = q.Where(tbl.IsRead.Is(*filter.Read))
}
if filter.CreatedAtFrom != nil {
from, err := s.parseFilterTime(filter.CreatedAtFrom)
if err != nil {
return nil, err
}
if from != nil {
q = q.Where(tbl.CreatedAt.Gte(*from))
}
}
if filter.CreatedAtTo != nil {
to, err := s.parseFilterTime(filter.CreatedAtTo)
if err != nil {
return nil, err
}
if to != nil {
q = q.Where(tbl.CreatedAt.Lte(*to))
}
}
filter.Pagination.Format()
total, err := q.Count()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
list, err := q.Order(tbl.CreatedAt.Desc()).
Offset(int(filter.Pagination.Offset())).
Limit(int(filter.Pagination.Limit)).
Find()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
// 补齐租户信息,便于跨租户展示。
tenantIDs := make([]int64, 0, len(list))
tenantSet := make(map[int64]struct{})
for _, n := range list {
if n.TenantID > 0 {
if _, ok := tenantSet[n.TenantID]; !ok {
tenantSet[n.TenantID] = struct{}{}
tenantIDs = append(tenantIDs, n.TenantID)
}
}
}
tenantMap := make(map[int64]*models.Tenant, len(tenantIDs))
if len(tenantIDs) > 0 {
tenantTbl, tenantQuery := models.TenantQuery.QueryContext(ctx)
tenants, err := tenantQuery.Where(tenantTbl.ID.In(tenantIDs...)).Find()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
for _, tenant := range tenants {
tenantMap[tenant.ID] = tenant
}
}
items := make([]super_dto.SuperUserNotificationItem, 0, len(list))
for _, n := range list {
tenant := tenantMap[n.TenantID]
tenantCode := ""
tenantName := ""
if tenant != nil {
tenantCode = tenant.Code
tenantName = tenant.Name
}
items = append(items, super_dto.SuperUserNotificationItem{
ID: n.ID,
TenantID: n.TenantID,
TenantCode: tenantCode,
TenantName: tenantName,
Type: n.Type,
Title: n.Title,
Content: n.Content,
Read: n.IsRead,
CreatedAt: s.formatTime(n.CreatedAt),
})
}
return &requests.Pager{
Pagination: filter.Pagination,
Total: total,
Items: items,
}, nil
}
func (s *super) ListUserCoupons(ctx context.Context, userID int64, filter *super_dto.SuperUserCouponListFilter) (*requests.Pager, error) {
if userID == 0 {
return nil, errorx.ErrBadRequest.WithMsg("用户ID不能为空")
}
if filter == nil {
filter = &super_dto.SuperUserCouponListFilter{}
}
couponIDs, couponFilter, err := s.filterCouponIDs(ctx, filter)
if err != nil {
return nil, err
}
tbl, q := models.UserCouponQuery.QueryContext(ctx)
q = q.Where(tbl.UserID.Eq(userID))
if filter.Status != nil && *filter.Status != "" {
q = q.Where(tbl.Status.Eq(*filter.Status))
}
if filter.CreatedAtFrom != nil {
from, err := s.parseFilterTime(filter.CreatedAtFrom)
if err != nil {
return nil, err
}
if from != nil {
q = q.Where(tbl.CreatedAt.Gte(*from))
}
}
if filter.CreatedAtTo != nil {
to, err := s.parseFilterTime(filter.CreatedAtTo)
if err != nil {
return nil, err
}
if to != nil {
q = q.Where(tbl.CreatedAt.Lte(*to))
}
}
if couponFilter {
if len(couponIDs) == 0 {
filter.Pagination.Format()
return &requests.Pager{
Pagination: filter.Pagination,
Total: 0,
Items: []super_dto.SuperUserCouponItem{},
}, nil
}
q = q.Where(tbl.CouponID.In(couponIDs...))
}
filter.Pagination.Format()
total, err := q.Count()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
list, err := q.Order(tbl.CreatedAt.Desc()).
Offset(int(filter.Pagination.Offset())).
Limit(int(filter.Pagination.Limit)).
Find()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
if len(list) == 0 {
return &requests.Pager{
Pagination: filter.Pagination,
Total: total,
Items: []super_dto.SuperUserCouponItem{},
}, nil
}
// 读取券模板与租户信息,便于聚合展示。
couponIDSet := make(map[int64]struct{})
couponIDs = make([]int64, 0, len(list))
for _, uc := range list {
if uc.CouponID > 0 {
if _, ok := couponIDSet[uc.CouponID]; !ok {
couponIDSet[uc.CouponID] = struct{}{}
couponIDs = append(couponIDs, uc.CouponID)
}
}
}
couponMap := make(map[int64]*models.Coupon, len(couponIDs))
tenantIDs := make([]int64, 0)
tenantSet := make(map[int64]struct{})
if len(couponIDs) > 0 {
couponTbl, couponQuery := models.CouponQuery.QueryContext(ctx)
coupons, err := couponQuery.Where(couponTbl.ID.In(couponIDs...)).Find()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
for _, coupon := range coupons {
couponMap[coupon.ID] = coupon
if coupon.TenantID > 0 {
if _, ok := tenantSet[coupon.TenantID]; !ok {
tenantSet[coupon.TenantID] = struct{}{}
tenantIDs = append(tenantIDs, coupon.TenantID)
}
}
}
}
tenantMap := make(map[int64]*models.Tenant, len(tenantIDs))
if len(tenantIDs) > 0 {
tenantTbl, tenantQuery := models.TenantQuery.QueryContext(ctx)
tenants, err := tenantQuery.Where(tenantTbl.ID.In(tenantIDs...)).Find()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
for _, tenant := range tenants {
tenantMap[tenant.ID] = tenant
}
}
items := make([]super_dto.SuperUserCouponItem, 0, len(list))
for _, uc := range list {
item := super_dto.SuperUserCouponItem{
ID: uc.ID,
CouponID: uc.CouponID,
Status: uc.Status,
StatusDescription: uc.Status.Description(),
OrderID: uc.OrderID,
UsedAt: s.formatTime(uc.UsedAt),
CreatedAt: s.formatTime(uc.CreatedAt),
}
coupon := couponMap[uc.CouponID]
if coupon != nil {
item.TenantID = coupon.TenantID
item.Title = coupon.Title
item.Description = coupon.Description
item.Type = coupon.Type
item.TypeDescription = coupon.Type.Description()
item.Value = coupon.Value
item.MinOrderAmount = coupon.MinOrderAmount
item.MaxDiscount = coupon.MaxDiscount
item.StartAt = s.formatTime(coupon.StartAt)
item.EndAt = s.formatTime(coupon.EndAt)
if tenant := tenantMap[coupon.TenantID]; tenant != nil {
item.TenantCode = tenant.Code
item.TenantName = tenant.Name
}
}
items = append(items, item)
}
return &requests.Pager{
Pagination: filter.Pagination,
Total: total,
Items: items,
}, nil
}
func (s *super) UpdateUserStatus(ctx context.Context, id int64, form *super_dto.UserStatusUpdateForm) error {
tbl, q := models.UserQuery.QueryContext(ctx)
_, err := q.Where(tbl.ID.Eq(id)).Update(tbl.Status, consts.UserStatus(form.Status))
@@ -3400,6 +3697,75 @@ func (s *super) formatTime(t time.Time) string {
return t.Format(time.RFC3339)
}
func (s *super) maskIDCard(raw string) string {
text := strings.TrimSpace(raw)
if text == "" {
return ""
}
text = strings.TrimPrefix(text, "ENC:")
if text == "" {
return ""
}
length := len(text)
if length <= 4 {
return strings.Repeat("*", length)
}
if length <= 8 {
return text[:2] + strings.Repeat("*", length-4) + text[length-2:]
}
return text[:3] + strings.Repeat("*", length-7) + text[length-4:]
}
func (s *super) filterCouponIDs(ctx context.Context, filter *super_dto.SuperUserCouponListFilter) ([]int64, bool, error) {
if filter == nil {
return nil, false, nil
}
couponTbl, couponQuery := models.CouponQuery.QueryContext(ctx)
applied := false
if filter.TenantID != nil && *filter.TenantID > 0 {
applied = true
couponQuery = couponQuery.Where(couponTbl.TenantID.Eq(*filter.TenantID))
} else {
tenantIDs, tenantFilter, err := s.lookupTenantIDs(ctx, filter.TenantCode, filter.TenantName)
if err != nil {
return nil, true, err
}
if tenantFilter {
applied = true
if len(tenantIDs) == 0 {
return []int64{}, true, nil
}
couponQuery = couponQuery.Where(couponTbl.TenantID.In(tenantIDs...))
}
}
if filter.Type != nil && *filter.Type != "" {
applied = true
couponQuery = couponQuery.Where(couponTbl.Type.Eq(*filter.Type))
}
if filter.Keyword != nil && strings.TrimSpace(*filter.Keyword) != "" {
applied = true
keyword := "%" + strings.TrimSpace(*filter.Keyword) + "%"
couponQuery = couponQuery.Where(field.Or(couponTbl.Title.Like(keyword), couponTbl.Description.Like(keyword)))
}
if !applied {
return nil, false, nil
}
coupons, err := couponQuery.Select(couponTbl.ID).Find()
if err != nil {
return nil, true, errorx.ErrDatabaseError.WithCause(err)
}
ids := make([]int64, 0, len(coupons))
for _, coupon := range coupons {
ids = append(ids, coupon.ID)
}
return ids, true, nil
}
func (s *super) RejectWithdrawal(ctx context.Context, operatorID, id int64, reason string) error {
if operatorID == 0 {
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")