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, } }