2026-06-24 18:56:14 +08:00

228 lines
8.4 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 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,
}
}