Files
qitongxue-api/internal/logic/user_detail.go
2026-09-29 10:57:01 +08:00

224 lines
6.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}