package logic import ( "context" "encoding/json" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" adminv1 "tool-api/api/admin/v1" userv1 "tool-api/api/user/v1" "tool-api/internal/consts" "tool-api/internal/model/entity" ) // ============================================================================ // 管理端「用户详情抽屉」聚合(A-new1):GET /user/detail // // 一次拉全:基础信息 / 额度 / 订单 / 笔记(仅标题·时间·提醒态) / 等级变更 / 优惠券。 // 笔记**不含正文**(只读抽屉,避免把用户隐私正文带到后台)。 // ============================================================================ // AdminUserDetail 用户详情聚合 func AdminUserDetail(ctx context.Context, userId int64) (*adminv1.UserDetailRes, error) { record, err := g.Model(consts.TableUsers).Where("id", userId).WhereNull("deleted_at").One() if err != nil { return nil, err } if record.IsEmpty() { return nil, gerror.New("用户不存在") } user := &entity.Users{} if err = record.Struct(user); err != nil { return nil, err } res := &adminv1.UserDetailRes{ User: buildUserDetailBase(ctx, user), Quotas: []userv1.QuotaToolOut{}, Orders: []adminv1.OrderBrief{}, Notes: []adminv1.NoteBrief{}, LevelLogs: []adminv1.LevelLogItem{}, Coupons: []userv1.UserCouponItem{}, } // 额度 if quotas, qErr := quotaListForUser(ctx, user); qErr == nil { res.Quotas = quotas } // 订单 if orders, oErr := userDetailOrders(ctx, userId); oErr == nil { res.Orders = orders } // 笔记(仅标题/时间/提醒态) if notes, nErr := userDetailNotes(ctx, userId); nErr == nil { res.Notes = notes } // 等级变更(操作审计) if logs, lErr := userDetailLevelLogs(ctx, userId); lErr == nil { res.LevelLogs = logs } // 优惠券(读取态) if coupons, cErr := userDetailCoupons(ctx, userId); cErr == nil { res.Coupons = coupons } return res, nil } func buildUserDetailBase(ctx context.Context, user *entity.Users) adminv1.UserDetailBase { base := adminv1.UserDetailBase{ Id: user.Id, Nickname: user.Nickname, AvatarUrl: user.AvatarUrl, Openid: user.Openid, LevelKey: user.LevelKey, Status: user.Status, CreatedAt: timeStr(user.CreatedAt), } if names, err := levelNameMap(ctx); err == nil { base.LevelName = names[user.LevelKey] } if user.LevelExpireAt != nil && !user.LevelExpireAt.IsZero() { base.LevelExpireAt = timeStr(user.LevelExpireAt) } if c, err := g.Model(consts.TableUsageLogs).Where("user_id", user.Id).Count(); err == nil { base.UsageCount = int64(c) } return base } func userDetailOrders(ctx context.Context, userId int64) ([]adminv1.OrderBrief, error) { records, err := g.Model(consts.TableMemberOrders). Where("user_id", userId).OrderDesc("id").Limit(20).All() if err != nil { return nil, err } planNames, _ := planNameMap(ctx) packNames, _ := quotaPackNameMap(ctx) toolNames, _ := toolNameMap(ctx) if toolNames == nil { toolNames = map[string]string{} } list := make([]adminv1.OrderBrief, 0, len(records)) for _, r := range records { orderType := r["order_type"].Int() if orderType == 0 { orderType = consts.OrderTypeMember } title := planNames[r["plan_key"].String()] if orderType == consts.OrderTypeQuota { title = packNames[r["pack_key"].String()] if tn := toolNames[r["tool_key"].String()]; tn != "" && title != "" { title = tn + " · " + title } } list = append(list, adminv1.OrderBrief{ OutTradeNo: r["out_trade_no"].String(), Title: title, OrderType: orderType, OriginPriceCents: r["origin_price_cents"].Int64(), PaidPriceCents: r["paid_price_cents"].Int64(), DiscountCents: r["discount_cents"].Int64(), PromoKind: r["promo_kind"].Int(), Status: r["status"].Int(), StatusText: orderStatusText(r["status"].Int()), CreatedAt: timeStr(r["created_at"].GTime()), DeliveredAt: timeStr(r["delivered_at"].GTime()), }) } return list, nil } func userDetailNotes(ctx context.Context, userId int64) ([]adminv1.NoteBrief, error) { records, err := g.Model(consts.TableNotes). Where("user_id", userId).WhereNull("deleted_at").OrderDesc("id").Limit(20).All() if err != nil { return nil, err } list := make([]adminv1.NoteBrief, 0, len(records)) for _, r := range records { list = append(list, adminv1.NoteBrief{ Id: r["id"].Int64(), Title: r["title"].String(), RemindStatus: r["remind_status"].Int(), RemindAt: timeStr(r["remind_at"].GTime()), CreatedAt: timeStr(r["created_at"].GTime()), }) } return list, nil } func userDetailLevelLogs(ctx context.Context, userId int64) ([]adminv1.LevelLogItem, error) { records, err := g.Model(consts.TableAdminAuditLog). Where("action", "user.set_level"). Where("target_id", auditId(userId)). OrderDesc("id").Limit(20).All() if err != nil { return nil, err } list := make([]adminv1.LevelLogItem, 0, len(records)) for _, r := range records { list = append(list, adminv1.LevelLogItem{ At: timeStr(r["created_at"].GTime()), BeforeLevel: levelKeyFromAuditJSON(r["before_json"].String()), AfterLevel: levelKeyFromAuditJSON(r["after_json"].String()), Operator: r["admin_account"].String(), Remark: r["remark"].String(), }) } return list, nil } // levelKeyFromAuditJSON 从 {"level_key":"v2"} 中提取等级 key(解析失败返回原串)。 func levelKeyFromAuditJSON(raw string) string { if raw == "" { return "" } obj := map[string]interface{}{} if err := json.Unmarshal([]byte(raw), &obj); err != nil { return "" } if v, ok := obj["level_key"]; ok { if s, ok := v.(string); ok { return s } } return "" } func userDetailCoupons(ctx context.Context, userId int64) ([]userv1.UserCouponItem, error) { // expire_at 的过期判定在 SQL 侧(绑定 gtime 参数,与写入同源;TZ-01),读侧不再比较时间。 records, err := g.DB().GetAll(ctx, "SELECT uc.*, "+couponExpirePassedExpr+" AS expire_passed "+ "FROM "+consts.TableUserCoupons+" uc WHERE uc.user_id = ? ORDER BY uc.id DESC LIMIT 50", gtime.Now(), userId) if err != nil { return nil, err } list := make([]userv1.UserCouponItem, 0, len(records)) for _, r := range records { c := &entity.UserCoupons{} if err = r.Struct(c); err != nil { continue } eff := effectiveCouponStatus(c, r["expire_passed"].Bool()) list = append(list, userv1.UserCouponItem{ Id: c.Id, CouponId: c.CouponId, Title: c.CouponTitle, SubTitle: couponSubTitleOf(ctx, c.CouponId), CouponType: c.CouponType, Kind: c.Kind, Value: c.Value, ThresholdCents: c.ThresholdCents, Status: eff, ExpireAt: timeStr(c.ExpireAt), GrantedAt: timeStr(c.GrantedAt), UsedAt: timeStr(c.UsedAt), }) } return list, nil }