224 lines
6.9 KiB
Go
224 lines
6.9 KiB
Go
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
|
||
}
|