228 lines
8.4 KiB
Go
228 lines
8.4 KiB
Go
package gamemanagement
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||
)
|
||
|
||
const (
|
||
robotResourceDurationDays = 9999
|
||
robotResourceDurationMS = int64(robotResourceDurationDays) * int64(24*time.Hour/time.Millisecond)
|
||
robotMinDisplayLevel = 5
|
||
robotMaxDisplayLevel = 30
|
||
robotMaxAvatarFrameCoins = 2_000_000
|
||
)
|
||
|
||
var excludedRobotAvatarFrameResourceIDs = map[int64]struct{}{
|
||
165: {},
|
||
175: {},
|
||
}
|
||
|
||
type robotAppearanceCatalog struct {
|
||
avatarFrames []*walletv1.Resource
|
||
vehicles []*walletv1.Resource
|
||
longBadges []*walletv1.Resource
|
||
shortBadges []*walletv1.Resource
|
||
}
|
||
|
||
func (h *Handler) initializeGeneratedRobotAppearance(ctx context.Context, requestID string, appCode string, actorUserID int64, userID int64) error {
|
||
if h.wallet == nil || h.activity == nil {
|
||
return fmt.Errorf("机器人装扮初始化依赖未配置")
|
||
}
|
||
catalog, err := h.loadRobotAppearanceCatalog(ctx, requestID, appCode)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
avatarFrame := randomRobotResource(catalog.avatarFrames)
|
||
vehicle := randomRobotResource(catalog.vehicles)
|
||
longBadge := randomRobotResource(catalog.longBadges)
|
||
shortBadge := randomRobotResource(catalog.shortBadges)
|
||
if avatarFrame == nil || vehicle == nil || longBadge == nil || shortBadge == nil {
|
||
return fmt.Errorf("机器人装扮资源库不完整: 头像框=%d 座驾=%d 长徽章=%d 短徽章=%d", len(catalog.avatarFrames), len(catalog.vehicles), len(catalog.longBadges), len(catalog.shortBadges))
|
||
}
|
||
if err := h.grantAndEquipRobotResource(ctx, requestID, appCode, actorUserID, userID, "avatar_frame", avatarFrame.GetResourceId(), true); err != nil {
|
||
return err
|
||
}
|
||
if err := h.grantAndEquipRobotResource(ctx, requestID, appCode, actorUserID, userID, "vehicle", vehicle.GetResourceId(), true); err != nil {
|
||
return err
|
||
}
|
||
if err := h.grantAndEquipRobotResource(ctx, requestID, appCode, actorUserID, userID, "long_badge", longBadge.GetResourceId(), false); err != nil {
|
||
return err
|
||
}
|
||
if err := h.grantAndEquipRobotResource(ctx, requestID, appCode, actorUserID, userID, "short_badge", shortBadge.GetResourceId(), false); err != nil {
|
||
return err
|
||
}
|
||
// 财富和魅力等级由 activity-service 按等级规则写入账户、展示投影和等级奖励 job;
|
||
// admin-server 只给出目标等级,避免绕过成长系统直接伪造资料卡字段。
|
||
if err := h.setRobotDisplayLevel(ctx, requestID, appCode, actorUserID, userID, "wealth", int32(robotMinDisplayLevel+randomRobotInt(robotMaxDisplayLevel-robotMinDisplayLevel+1))); err != nil {
|
||
return err
|
||
}
|
||
if err := h.setRobotDisplayLevel(ctx, requestID, appCode, actorUserID, userID, "charm", int32(robotMinDisplayLevel+randomRobotInt(robotMaxDisplayLevel-robotMinDisplayLevel+1))); err != nil {
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (h *Handler) loadRobotAppearanceCatalog(ctx context.Context, requestID string, appCode string) (robotAppearanceCatalog, error) {
|
||
avatarFrames, err := h.listRobotResources(ctx, requestID, appCode, "avatar_frame")
|
||
if err != nil {
|
||
return robotAppearanceCatalog{}, err
|
||
}
|
||
vehicles, err := h.listRobotResources(ctx, requestID, appCode, "vehicle")
|
||
if err != nil {
|
||
return robotAppearanceCatalog{}, err
|
||
}
|
||
badges, err := h.listRobotResources(ctx, requestID, appCode, "badge")
|
||
if err != nil {
|
||
return robotAppearanceCatalog{}, err
|
||
}
|
||
catalog := robotAppearanceCatalog{avatarFrames: avatarFrames, vehicles: vehicles}
|
||
for _, badge := range badges {
|
||
if !isRobotVIPBadgeResource(badge) {
|
||
// 机器人批量账号只使用 VIP 体系徽章;普通活动、成就和运营徽章仍留给真实用户或明确活动链路,
|
||
// 避免机器人大量占用非 VIP 资源造成客户端误判身份来源。
|
||
continue
|
||
}
|
||
switch robotBadgeForm(badge.GetMetadataJson()) {
|
||
case "strip", "long":
|
||
catalog.longBadges = append(catalog.longBadges, badge)
|
||
case "tile", "short":
|
||
catalog.shortBadges = append(catalog.shortBadges, badge)
|
||
}
|
||
}
|
||
return catalog, nil
|
||
}
|
||
|
||
func (h *Handler) listRobotResources(ctx context.Context, requestID string, appCode string, resourceType string) ([]*walletv1.Resource, error) {
|
||
resp, err := h.wallet.ListResources(ctx, &walletv1.ListResourcesRequest{
|
||
RequestId: requestID,
|
||
AppCode: appCode,
|
||
ResourceType: resourceType,
|
||
Page: 1,
|
||
PageSize: 500,
|
||
ActiveOnly: true,
|
||
})
|
||
if err != nil {
|
||
return nil, fmt.Errorf("读取机器人%s资源失败: %w", resourceType, err)
|
||
}
|
||
resources := make([]*walletv1.Resource, 0, len(resp.GetResources()))
|
||
for _, item := range resp.GetResources() {
|
||
if item == nil || item.GetResourceId() <= 0 || item.GetStatus() != "active" {
|
||
continue
|
||
}
|
||
if isExcludedRobotAppearanceResource(resourceType, item.GetResourceId()) {
|
||
// 机器人装扮随机池只过滤明确不适合机器人批量露出的资源;资源仍保留在 wallet owner 中,
|
||
// 真人发放、商城和其他后台场景不受这个创建机器人专用策略影响。
|
||
continue
|
||
}
|
||
if resourceType == "avatar_frame" && item.GetCoinPrice() > robotMaxAvatarFrameCoins {
|
||
// 头像框用金币价格做机器人随机池硬边界;价格高于 2000000 的资源通常是稀有或运营保留展示,
|
||
// 创建机器人时不应自动发放,线上批量修复脚本也复用同一条规则。
|
||
continue
|
||
}
|
||
resources = append(resources, item)
|
||
}
|
||
return resources, nil
|
||
}
|
||
|
||
func isExcludedRobotAppearanceResource(resourceType string, resourceID int64) bool {
|
||
if resourceType != "avatar_frame" {
|
||
return false
|
||
}
|
||
_, excluded := excludedRobotAvatarFrameResourceIDs[resourceID]
|
||
return excluded
|
||
}
|
||
|
||
func isRobotVIPBadgeResource(resource *walletv1.Resource) bool {
|
||
if resource == nil || resource.GetResourceType() != "badge" {
|
||
return false
|
||
}
|
||
code := strings.ToLower(strings.TrimSpace(resource.GetResourceCode()))
|
||
name := strings.ToLower(strings.TrimSpace(resource.GetName()))
|
||
return strings.HasPrefix(code, "vip") || strings.HasPrefix(name, "vip")
|
||
}
|
||
|
||
func (h *Handler) grantAndEquipRobotResource(ctx context.Context, requestID string, appCode string, actorUserID int64, userID int64, slot string, resourceID int64, equip bool) error {
|
||
grant, err := h.wallet.GrantResource(ctx, &walletv1.GrantResourceRequest{
|
||
CommandId: fmt.Sprintf("game_robot_init:%s:%d:%s:%d", requestID, userID, slot, resourceID),
|
||
AppCode: appCode,
|
||
TargetUserId: userID,
|
||
ResourceId: resourceID,
|
||
Quantity: 1,
|
||
DurationMs: robotResourceDurationMS,
|
||
Reason: "game_robot_init",
|
||
OperatorUserId: actorUserID,
|
||
GrantSource: "game_robot_init",
|
||
})
|
||
if err != nil {
|
||
return fmt.Errorf("发放机器人%s失败: %w", slot, err)
|
||
}
|
||
if !equip {
|
||
return nil
|
||
}
|
||
entitlementID := ""
|
||
for _, item := range grant.GetGrant().GetItems() {
|
||
if item.GetResourceId() == resourceID {
|
||
entitlementID = item.GetEntitlementId()
|
||
break
|
||
}
|
||
}
|
||
if _, err := h.wallet.EquipUserResource(ctx, &walletv1.EquipUserResourceRequest{
|
||
RequestId: fmt.Sprintf("%s:%s:equip", requestID, slot),
|
||
AppCode: appCode,
|
||
UserId: userID,
|
||
ResourceId: resourceID,
|
||
EntitlementId: entitlementID,
|
||
}); err != nil {
|
||
return fmt.Errorf("装备机器人%s失败: %w", slot, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (h *Handler) setRobotDisplayLevel(ctx context.Context, requestID string, appCode string, actorUserID int64, userID int64, track string, level int32) error {
|
||
_, err := h.activity.SetUserLevel(ctx, &activityv1.SetUserLevelRequest{
|
||
Meta: activityRequestMeta(requestID, appCode),
|
||
CommandId: fmt.Sprintf("game_robot_init:%s:%d:%s:%d", requestID, userID, track, level),
|
||
UserId: userID,
|
||
Track: track,
|
||
Level: level,
|
||
OperatorUserId: actorUserID,
|
||
Reason: "game_robot_init",
|
||
})
|
||
if err != nil {
|
||
return fmt.Errorf("设置机器人%s等级失败: %w", track, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func robotBadgeForm(metadataJSON string) string {
|
||
var payload map[string]any
|
||
if err := json.Unmarshal([]byte(strings.TrimSpace(metadataJSON)), &payload); err != nil {
|
||
return ""
|
||
}
|
||
value, _ := payload["badge_form"].(string)
|
||
return strings.ToLower(strings.TrimSpace(value))
|
||
}
|
||
|
||
func randomRobotResource(items []*walletv1.Resource) *walletv1.Resource {
|
||
if len(items) == 0 {
|
||
return nil
|
||
}
|
||
return items[randomRobotInt(int64(len(items)))]
|
||
}
|
||
|
||
func activityRequestMeta(requestID string, appCode string) *activityv1.RequestMeta {
|
||
return &activityv1.RequestMeta{
|
||
RequestId: requestID,
|
||
Caller: "admin-server",
|
||
SentAtMs: time.Now().UnixMilli(),
|
||
AppCode: appCode,
|
||
}
|
||
}
|