feat: expand superadmin user detail views
This commit is contained in:
@@ -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("缺少操作者信息")
|
||||
|
||||
Reference in New Issue
Block a user