378 lines
11 KiB
Go
378 lines
11 KiB
Go
package resource
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"hyapp-admin-server/internal/appctx"
|
||
"hyapp-admin-server/internal/model"
|
||
"hyapp-admin-server/internal/modules/shared"
|
||
)
|
||
|
||
const (
|
||
grantSourceAdmin = "admin"
|
||
grantSourceManagerCenter = "manager_center"
|
||
)
|
||
|
||
type adminOperator struct {
|
||
UserID uint
|
||
Username string
|
||
Name string
|
||
}
|
||
|
||
type managerOperator struct {
|
||
UserID int64
|
||
DisplayUserID string
|
||
Username string
|
||
Avatar string
|
||
}
|
||
|
||
func (h *Handler) enrichGrantTargets(ctx context.Context, grants []grantDTO) error {
|
||
ids := collectGrantTargetIDs(grants)
|
||
if len(ids) == 0 {
|
||
return nil
|
||
}
|
||
users := make(map[int64]grantUserDTO, len(ids))
|
||
if h.userDB != nil {
|
||
var err error
|
||
users, err = h.queryGrantTargetUsers(ctx, ids)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
applyGrantTargetUsers(grants, users)
|
||
return nil
|
||
}
|
||
|
||
func (h *Handler) queryGrantTargetUsers(ctx context.Context, ids []int64) (map[int64]grantUserDTO, error) {
|
||
out := make(map[int64]grantUserDTO, len(ids))
|
||
if len(ids) == 0 || h.userDB == nil {
|
||
return out, nil
|
||
}
|
||
nowMs := time.Now().UnixMilli()
|
||
args := append([]any{nowMs, nowMs, appctx.FromContext(ctx)}, int64Args(ids)...)
|
||
rows, err := h.userDB.QueryContext(ctx, `
|
||
SELECT user_id,
|
||
current_display_user_id,
|
||
COALESCE(default_display_user_id, ''),
|
||
COALESCE((
|
||
SELECT lease.display_user_id
|
||
FROM pretty_display_user_id_leases lease
|
||
WHERE lease.app_code = users.app_code
|
||
AND lease.user_id = users.user_id
|
||
AND lease.status = 'active'
|
||
AND (COALESCE(lease.expires_at_ms, 0) = 0 OR lease.expires_at_ms > ?)
|
||
ORDER BY lease.created_at_ms DESC, lease.lease_id DESC
|
||
LIMIT 1
|
||
), ''),
|
||
COALESCE((
|
||
SELECT pdi.pretty_id
|
||
FROM pretty_display_user_id_leases lease
|
||
JOIN pretty_display_ids pdi
|
||
ON pdi.app_code = lease.app_code
|
||
AND pdi.assigned_lease_id = lease.lease_id
|
||
AND pdi.status = 'assigned'
|
||
WHERE lease.app_code = users.app_code
|
||
AND lease.user_id = users.user_id
|
||
AND lease.status = 'active'
|
||
AND (COALESCE(lease.expires_at_ms, 0) = 0 OR lease.expires_at_ms > ?)
|
||
ORDER BY lease.created_at_ms DESC, lease.lease_id DESC
|
||
LIMIT 1
|
||
), ''),
|
||
COALESCE(username, ''),
|
||
COALESCE(avatar, '')
|
||
FROM users
|
||
WHERE app_code = ? AND user_id IN (`+placeholders(len(ids))+`)
|
||
`, args...)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
for rows.Next() {
|
||
var user grantUserDTO
|
||
if err := rows.Scan(&user.UserID, &user.DisplayUserID, &user.DefaultDisplayUserID, &user.PrettyDisplayUserID, &user.PrettyID, &user.Username, &user.Avatar); err != nil {
|
||
return nil, err
|
||
}
|
||
out[user.UserID] = user
|
||
}
|
||
return out, rows.Err()
|
||
}
|
||
|
||
func applyGrantTargetUsers(grants []grantDTO, users map[int64]grantUserDTO) {
|
||
for index := range grants {
|
||
targetUserID := grants[index].TargetUserID
|
||
if targetUserID <= 0 {
|
||
continue
|
||
}
|
||
if user, ok := users[targetUserID]; ok {
|
||
grants[index].TargetUser = &user
|
||
continue
|
||
}
|
||
grants[index].TargetUser = &grantUserDTO{UserID: targetUserID}
|
||
}
|
||
}
|
||
|
||
func (h *Handler) resolveResourceGrantListTargetUserID(ctx context.Context, appCode string, raw string) (int64, bool, bool, error) {
|
||
raw = strings.TrimSpace(raw)
|
||
if raw == "" {
|
||
return 0, false, false, nil
|
||
}
|
||
// 资源赠送记录的最终 owner 是 wallet-service,wallet 只接受内部 user_id 过滤;
|
||
// 后台列表输入框展示的是“用户信息”,运营会输入默认短号或靓号,所以这里先按 user-service 身份投影解析。
|
||
// 解析不到时保留旧版数字 target_user_id 行为,避免用户资料缺失时历史赠送记录完全查不到。
|
||
fallbackUserID, parseErr := strconv.ParseInt(raw, 10, 64)
|
||
if h != nil && h.userDB != nil {
|
||
resolvedUserID, filtered, err := h.resolveResourceShopPurchaseUserID(ctx, appCode, raw)
|
||
switch {
|
||
case err == nil && filtered:
|
||
return resolvedUserID, true, false, nil
|
||
case err == sql.ErrNoRows:
|
||
// 没有用户身份投影时继续尝试按内部 user_id 过滤,兼容历史孤儿赠送记录。
|
||
case err != nil:
|
||
return 0, true, false, err
|
||
}
|
||
}
|
||
if parseErr != nil || fallbackUserID < 0 {
|
||
return 0, true, true, fmt.Errorf("target_user_id 参数不正确")
|
||
}
|
||
return fallbackUserID, true, false, nil
|
||
}
|
||
|
||
func (h *Handler) enrichResourceShopPurchaseUsers(ctx context.Context, orders []resourceShopPurchaseOrderDTO) error {
|
||
ids := collectResourceShopPurchaseUserIDs(orders)
|
||
if len(ids) == 0 {
|
||
return nil
|
||
}
|
||
users := map[int64]grantUserDTO{}
|
||
if h.userDB != nil {
|
||
var err error
|
||
users, err = h.queryGrantTargetUsers(ctx, ids)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
for index := range orders {
|
||
userID := orders[index].UserID
|
||
if userID <= 0 {
|
||
continue
|
||
}
|
||
if user, ok := users[userID]; ok {
|
||
orders[index].User = &user
|
||
continue
|
||
}
|
||
orders[index].User = &grantUserDTO{UserID: userID}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func collectResourceShopPurchaseUserIDs(orders []resourceShopPurchaseOrderDTO) []int64 {
|
||
seen := make(map[int64]struct{}, len(orders))
|
||
ids := make([]int64, 0, len(orders))
|
||
for _, order := range orders {
|
||
if order.UserID <= 0 {
|
||
continue
|
||
}
|
||
if _, exists := seen[order.UserID]; exists {
|
||
continue
|
||
}
|
||
seen[order.UserID] = struct{}{}
|
||
ids = append(ids, order.UserID)
|
||
}
|
||
return ids
|
||
}
|
||
|
||
func (h *Handler) resolveResourceShopPurchaseUserID(ctx context.Context, appCode string, keyword string) (int64, bool, error) {
|
||
keyword = strings.TrimSpace(keyword)
|
||
if keyword == "" {
|
||
return 0, false, nil
|
||
}
|
||
if h == nil || h.userDB == nil {
|
||
return 0, false, fmt.Errorf("user mysql is not configured")
|
||
}
|
||
nowMs := time.Now().UnixMilli()
|
||
matchSQL, matchArgs := shared.UserIdentityExactSQL("u", "u.user_id", keyword, nowMs)
|
||
orderSQL, orderArgs := shared.UserIdentityExactOrderSQL("u", "u.user_id", keyword, nowMs)
|
||
args := append([]any{appCode}, matchArgs...)
|
||
args = append(args, orderArgs...)
|
||
row := h.userDB.QueryRowContext(ctx, `
|
||
SELECT u.user_id
|
||
FROM users u
|
||
WHERE u.app_code = ? AND `+matchSQL+`
|
||
ORDER BY
|
||
`+orderSQL+`,
|
||
u.user_id DESC
|
||
LIMIT 1`,
|
||
args...,
|
||
)
|
||
var userID int64
|
||
if err := row.Scan(&userID); err != nil {
|
||
return 0, true, err
|
||
}
|
||
return userID, true, nil
|
||
}
|
||
|
||
func (h *Handler) enrichGrantOperators(ctx context.Context, grants []grantDTO) error {
|
||
// operator_user_id 在 wallet 发放事实里只保存稳定 ID;后台列表按来源补展示资料,避免把 admin 用户和 app 用户混成同一张表。
|
||
adminIDs, managerIDs := collectGrantOperatorIDs(grants)
|
||
admins, err := h.queryAdminOperators(adminIDs)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
managers, err := h.queryManagerOperators(ctx, managerIDs)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
applyGrantOperators(grants, admins, managers)
|
||
return nil
|
||
}
|
||
|
||
func (h *Handler) queryAdminOperators(ids []uint) (map[uint]adminOperator, error) {
|
||
out := make(map[uint]adminOperator, len(ids))
|
||
if len(ids) == 0 || h.store == nil {
|
||
return out, nil
|
||
}
|
||
users, err := h.store.UsersByIDs(ids)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for id, user := range users {
|
||
out[id] = adminOperatorFromModel(user)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func (h *Handler) queryManagerOperators(ctx context.Context, ids []int64) (map[int64]managerOperator, error) {
|
||
out := make(map[int64]managerOperator, len(ids))
|
||
if len(ids) == 0 || h.userDB == nil {
|
||
return out, nil
|
||
}
|
||
// 经理中心发放人是 app 用户,只读 user-service 的用户库投影;资源发放事实仍然以 wallet-service 返回为准。
|
||
args := append([]any{appctx.FromContext(ctx)}, int64Args(ids)...)
|
||
rows, err := h.userDB.QueryContext(ctx, `
|
||
SELECT user_id,
|
||
current_display_user_id,
|
||
COALESCE(username, ''),
|
||
COALESCE(avatar, '')
|
||
FROM users
|
||
WHERE app_code = ? AND user_id IN (`+placeholders(len(ids))+`)
|
||
`, args...)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
for rows.Next() {
|
||
var operator managerOperator
|
||
if err := rows.Scan(&operator.UserID, &operator.DisplayUserID, &operator.Username, &operator.Avatar); err != nil {
|
||
return nil, err
|
||
}
|
||
out[operator.UserID] = operator
|
||
}
|
||
return out, rows.Err()
|
||
}
|
||
|
||
func applyGrantOperators(grants []grantDTO, admins map[uint]adminOperator, managers map[int64]managerOperator) {
|
||
for index := range grants {
|
||
source := normalizeGrantSource(grants[index].GrantSource)
|
||
operatorID := grants[index].OperatorUserID
|
||
if operatorID <= 0 {
|
||
continue
|
||
}
|
||
// 即使资料缺失也返回 source + user_id,前端仍能展示可追溯的操作人线索。
|
||
operator := &operatorDTO{
|
||
Source: source,
|
||
UserID: operatorID,
|
||
}
|
||
switch source {
|
||
case grantSourceAdmin:
|
||
if admin, ok := admins[uint(operatorID)]; ok {
|
||
operator.Username = admin.Username
|
||
operator.Name = admin.Name
|
||
}
|
||
case grantSourceManagerCenter:
|
||
if manager, ok := managers[operatorID]; ok {
|
||
operator.DisplayUserID = manager.DisplayUserID
|
||
operator.Username = manager.Username
|
||
operator.Avatar = manager.Avatar
|
||
}
|
||
}
|
||
grants[index].Operator = operator
|
||
}
|
||
}
|
||
|
||
func collectGrantOperatorIDs(grants []grantDTO) ([]uint, []int64) {
|
||
adminSet := make(map[uint]struct{})
|
||
managerSet := make(map[int64]struct{})
|
||
for _, grant := range grants {
|
||
if grant.OperatorUserID <= 0 {
|
||
continue
|
||
}
|
||
switch normalizeGrantSource(grant.GrantSource) {
|
||
case grantSourceAdmin:
|
||
adminSet[uint(grant.OperatorUserID)] = struct{}{}
|
||
case grantSourceManagerCenter:
|
||
managerSet[grant.OperatorUserID] = struct{}{}
|
||
}
|
||
}
|
||
adminIDs := make([]uint, 0, len(adminSet))
|
||
for id := range adminSet {
|
||
adminIDs = append(adminIDs, id)
|
||
}
|
||
managerIDs := make([]int64, 0, len(managerSet))
|
||
for id := range managerSet {
|
||
managerIDs = append(managerIDs, id)
|
||
}
|
||
return adminIDs, managerIDs
|
||
}
|
||
|
||
func collectGrantTargetIDs(grants []grantDTO) []int64 {
|
||
targetSet := make(map[int64]struct{})
|
||
for _, grant := range grants {
|
||
if grant.TargetUserID <= 0 {
|
||
continue
|
||
}
|
||
targetSet[grant.TargetUserID] = struct{}{}
|
||
}
|
||
targetIDs := make([]int64, 0, len(targetSet))
|
||
for id := range targetSet {
|
||
targetIDs = append(targetIDs, id)
|
||
}
|
||
return targetIDs
|
||
}
|
||
|
||
func adminOperatorFromModel(user model.User) adminOperator {
|
||
return adminOperator{
|
||
UserID: user.ID,
|
||
Username: user.Username,
|
||
Name: user.Name,
|
||
}
|
||
}
|
||
|
||
func normalizeGrantSource(source string) string {
|
||
return strings.ToLower(strings.TrimSpace(source))
|
||
}
|
||
|
||
func placeholders(count int) string {
|
||
if count <= 0 {
|
||
return ""
|
||
}
|
||
items := make([]string, count)
|
||
for index := range items {
|
||
items[index] = "?"
|
||
}
|
||
return strings.Join(items, ",")
|
||
}
|
||
|
||
func int64Args(values []int64) []any {
|
||
args := make([]any, 0, len(values))
|
||
for _, value := range values {
|
||
args = append(args, value)
|
||
}
|
||
return args
|
||
}
|