package service import ( "context" "crypto/sha256" "fmt" "net/url" "os" "strings" "time" "github.com/aliyun/aliyun-oss-go-sdk/oss" "github.com/aliyun/credentials-go/credentials" "github.com/google/uuid" appErrors "github.com/topfans/backend/pkg/errors" "github.com/topfans/backend/pkg/logger" "github.com/topfans/backend/pkg/models" pb "github.com/topfans/backend/pkg/proto/asset" pbCommon "github.com/topfans/backend/pkg/proto/common" "github.com/topfans/backend/pkg/validator" "github.com/topfans/backend/services/assetService/client" "github.com/topfans/backend/services/assetService/config" "github.com/topfans/backend/services/assetService/repository" "github.com/topfans/backend/services/assetService/util" starbookRepo "github.com/topfans/backend/services/starbookService/repository" "go.uber.org/zap" "gorm.io/gorm" ) // MintService 铸造服务接口 type MintService interface { // InitMintOrder 阶段一:初始化订单(仅落库 order_id,status=PENDING,幂等) InitMintOrder(orderID string, userID, starID int64) (*pb.InitMintOrderResponse, error) // PreCreateMintOrder 阶段一:预创建订单(生成 order_id) PreCreateMintOrder(req *pb.PreCreateMintOrderRequest, userID, starID int64) (*pb.PreCreateMintOrderResponse, error) // CreateMintOrder 创建铸造订单 CreateMintOrder(req *pb.CreateMintOrderRequest, userID, starID int64) (*pb.CreateMintOrderResponse, error) // GetMintOrder 查询铸造订单状态 GetMintOrder(orderID string, userID, starID int64) (*pb.GetMintOrderResponse, error) // CancelMintOrder 取消铸造订单 CancelMintOrder(orderID string, userID, starID int64) error // GetMintCost 获取铸造消耗配置 GetMintCost(mintCount int32) (*models.MintCostConfig, error) // GetUserMintCount 获取用户累计铸爱次数 GetUserMintCount(userID, starID int64) (int32, error) // UpdateMintCountAndBoost 更新铸爱次数和收益提升 UpdateMintCountAndBoost(ctx context.Context, tx *gorm.DB, userID, starID int64, boostBps int32) error } // mintService 铸造服务实现 type mintService struct { assetRepo repository.AssetRepository mintOrderRepo repository.MintOrderRepository userClient client.UserServiceClient db *gorm.DB config *config.AssetConfig registryRepo starbookRepo.AssetRegistryRepository // 资产索引仓库(用于星册体系) mintCostRepo repository.MintCostRepository // 铸造消耗配置仓库 userMintCountRepo repository.UserMintCountRepository // 用户铸爱累计仓库 } // NewMintService 创建铸造服务实例 func NewMintService( assetRepo repository.AssetRepository, mintOrderRepo repository.MintOrderRepository, userClient client.UserServiceClient, db *gorm.DB, cfg *config.AssetConfig, registryRepo starbookRepo.AssetRegistryRepository, mintCostRepo repository.MintCostRepository, userMintCountRepo repository.UserMintCountRepository, ) MintService { return &mintService{ assetRepo: assetRepo, mintOrderRepo: mintOrderRepo, userClient: userClient, db: db, config: cfg, registryRepo: registryRepo, mintCostRepo: mintCostRepo, userMintCountRepo: userMintCountRepo, } } // InitMintOrder 阶段一:初始化订单(仅落库 order_id,status=PENDING,幂等) func (s *mintService) InitMintOrder(orderID string, userID, starID int64) (*pb.InitMintOrderResponse, error) { if orderID == "" { return nil, fmt.Errorf("order_id is required") } if !validator.ValidateUserID(userID) { return nil, appErrors.ErrInvalidUserID } if !validator.ValidateStarID(starID) { return nil, appErrors.ErrInvalidStarID } // 已存在则直接返回(幂等) existing, err := s.mintOrderRepo.GetByOrderIDAndUser(orderID, userID, starID) if err == nil && existing != nil { return &pb.InitMintOrderResponse{ Base: &pbCommon.BaseResponse{ Code: pbCommon.StatusCode_STATUS_OK, Message: "", Timestamp: time.Now().UnixMilli(), }, Order: ModelToProtoMintOrder(existing), }, nil } order := &models.MintOrder{ OrderID: orderID, UserID: userID, StarID: starID, Status: models.MintOrderStatusPending, CostCrystal: 0, ErrorMessage: nil, RetryCount: 0, } if err := s.mintOrderRepo.Create(order); err != nil { // 处理并发下重复创建:再次查询返回 existing2, err2 := s.mintOrderRepo.GetByOrderIDAndUser(orderID, userID, starID) if err2 == nil && existing2 != nil { return &pb.InitMintOrderResponse{ Base: &pbCommon.BaseResponse{ Code: pbCommon.StatusCode_STATUS_OK, Message: "", Timestamp: time.Now().UnixMilli(), }, Order: ModelToProtoMintOrder(existing2), }, nil } return nil, fmt.Errorf("failed to init mint order: %w", err) } return &pb.InitMintOrderResponse{ Base: &pbCommon.BaseResponse{ Code: pbCommon.StatusCode_STATUS_OK, Message: "", Timestamp: time.Now().UnixMilli(), }, Order: ModelToProtoMintOrder(order), }, nil } // PreCreateMintOrder 阶段一:预创建订单(生成 order_id,状态=PENDING) func (s *mintService) PreCreateMintOrder(req *pb.PreCreateMintOrderRequest, userID, starID int64) (*pb.PreCreateMintOrderResponse, error) { if !validator.ValidateUserID(userID) { return nil, appErrors.ErrInvalidUserID } if !validator.ValidateStarID(starID) { return nil, appErrors.ErrInvalidStarID } if req.MaterialUrl == "" { return nil, fmt.Errorf("material_url is required") } orderID := uuid.New().String() materialType := req.MaterialType if materialType == "" { materialType = "new" } order := &models.MintOrder{ OrderID: orderID, UserID: userID, StarID: starID, Status: models.MintOrderStatusPending, CostCrystal: 0, ErrorMessage: nil, RetryCount: 0, MaterialURL: stringToPtr(req.MaterialUrl), Name: stringToPtr(req.Name), Description: stringToPtr(req.Description), MaterialType: stringToPtr(materialType), Event: stringToPtr(req.Event), } if err := s.db.Create(order).Error; err != nil { return nil, fmt.Errorf("failed to create mint order draft: %w", err) } return &pb.PreCreateMintOrderResponse{ Base: &pbCommon.BaseResponse{ Code: pbCommon.StatusCode_STATUS_OK, Message: "", Timestamp: time.Now().UnixMilli(), }, Order: ModelToProtoMintOrder(order), }, nil } // CreateMintOrder 创建铸造订单 func (s *mintService) CreateMintOrder(req *pb.CreateMintOrderRequest, userID, starID int64) (*pb.CreateMintOrderResponse, error) { // 1. 参数验证 if !validator.ValidateUserID(userID) { logger.Logger.Warn("Invalid user_id", zap.Int64("user_id", userID), ) return nil, appErrors.ErrInvalidUserID } if !validator.ValidateStarID(starID) { logger.Logger.Warn("Invalid star_id", zap.Int64("star_id", starID), ) return nil, appErrors.ErrInvalidStarID } // 按新流程:order_id 必填,所有后续操作以 order_id 为唯一标识 if req.OrderId == "" { return nil, fmt.Errorf("order_id is required(请先调用 /api/v1/assets/mints/precreate 获取)") } // 2. 获取当前累计铸爱次数,用于计算阶梯费用 currentMintCount, err := s.GetUserMintCount(userID, starID) if err != nil { logger.Logger.Warn("Failed to get user mint count, using 0", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Error(err)) currentMintCount = 0 } // 3. 使用事务创建铸造订单(或将阶段一订单推进到 PROCESSING) var mintOrder *models.MintOrder var asset *models.Asset err = s.db.Transaction(func(tx *gorm.DB) error { // 3.0 取出阶段一订单,并校验状态/所有者 existing, err := s.mintOrderRepo.GetByOrderIDAndUser(req.OrderId, userID, starID) if err != nil { return fmt.Errorf("order not found: %w", err) } if existing.Status != models.MintOrderStatusPending { return fmt.Errorf("订单状态为%s,不能继续铸造", existing.Status) } mintOrder = existing // 若阶段二传了字段,则覆盖阶段一的存储值(允许再次编辑元数据) if req.MaterialUrl != "" { mintOrder.MaterialURL = stringToPtr(req.MaterialUrl) } if req.Name != "" { mintOrder.Name = stringToPtr(req.Name) } if req.Description != "" { mintOrder.Description = stringToPtr(req.Description) } if req.MaterialType != "" { mintOrder.MaterialType = stringToPtr(req.MaterialType) } else { mintOrder.MaterialType = stringToPtr("new") } if req.Info != "" { mintOrder.Info = stringToPtr(req.Info) } // 继续铸造时必须有素材(可来自阶段一或阶段二) if getStringValue(mintOrder.MaterialURL) == "" { return fmt.Errorf("material_url is required") } // info 为必填 if getStringValue(mintOrder.Info) == "" { return fmt.Errorf("info is required") } // 如果没有提供名称,使用默认名称 if getStringValue(mintOrder.Name) == "" { mintOrder.Name = stringToPtr("未命名藏品") } // 3.1 获取铸造消耗配置(阶梯计价) // 本次铸造是第 currentMintCount+1 次 mintCost, err := s.GetMintCost(currentMintCount + 1) if err != nil { return fmt.Errorf("获取铸造消耗配置失败: %w", err) } // 3.2 扣除水晶余额(调用 User Service RPC) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() newBalance, err := s.userClient.UpdateCrystalBalance(ctx, userID, starID, -mintCost.CostCrystal, "mint_cost", req.OrderId, fmt.Sprintf("铸造藏品 #%s", req.OrderId)) if err != nil { logger.Logger.Error("Failed to deduct crystal balance", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Int64("cost", mintCost.CostCrystal), zap.Error(err), ) return fmt.Errorf("水晶余额不足或扣除失败: %w", err) } logger.Logger.Info("Crystal balance deducted with tiered cost", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Int64("cost", mintCost.CostCrystal), zap.Int32("mint_count", currentMintCount+1), zap.Int64("new_balance", newBalance), ) // 3.3 检查是否触发保底(概率触发) var boostBps int32 = 0 if mintCost.Probability > 0 && mintCost.RewardValue > 0 { // 随机判断是否触发 randomValue := time.Now().UnixNano() % 100 if randomValue < mintCost.Probability { boostBps = int32(mintCost.RewardValue) // reward_value 单位是 bps logger.Logger.Info("Mint guarantee triggered", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Int32("mint_count", currentMintCount+1), zap.Int32("boost_bps", boostBps), zap.Int64("probability", mintCost.Probability)) } } // 3.4 更新用户铸爱次数和收益提升 if err := s.UpdateMintCountAndBoost(ctx, tx, userID, starID, boostBps); err != nil { logger.Logger.Error("Failed to update mint count and boost", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Error(err)) // 不阻断主流程,只记录错误 } // 3.5 创建资产记录(状态:Active,CoverURL 直接使用 MaterialURL) materialURLValue := getStringValue(mintOrder.MaterialURL) mintedAt := time.Now().UnixMilli() mockTxHash := fmt.Sprintf("0x%x", sha256.Sum256([]byte(fmt.Sprintf("%s-%d-%d-%d", mintOrder.OrderID, userID, starID, time.Now().UnixNano())))) mockBlockNumber := int64(time.Now().Unix()) // 使用当前时间戳作为模拟区块号 asset = &models.Asset{ OwnerUID: userID, StarID: starID, Name: getStringValue(mintOrder.Name), CoverURL: materialURLValue, // 直接使用素材图作为封面 MaterialURL: &materialURLValue, // 使用指针 Description: mintOrder.Description, Visibility: models.AssetVisibilityPrivate, Status: models.AssetStatusActive, // 直接设为 Active LikeCount: 0, Info: getStringValue(mintOrder.Info), TxHash: &mockTxHash, BlockNumber: &mockBlockNumber, MintedAt: &mintedAt, } // 可选字段 if req.Grade != 0 { asset.Grade = int32ToPtr(req.Grade) } if len(req.Tags) > 0 { asset.Tags = models.StringArray(req.Tags) } if err := tx.Create(asset).Error; err != nil { logger.Logger.Error("Failed to create asset", zap.Int64("user_id", userID), zap.Error(err), ) return fmt.Errorf("failed to create asset: %w", err) } logger.Logger.Info("Asset created", zap.Int64("asset_id", asset.ID), zap.Int64("user_id", userID), ) // 3.3 同步写入 asset_registry(普通藏品纳入星册体系) grade := int32(1) // 普通藏品初始等级为1 registry := &models.AssetRegistry{ AssetID: asset.ID, AssetType: models.AssetTypeRegular, OwnerUID: userID, StarID: starID, Grade: &grade, Status: models.AssetRegistryStatusActive, LikeCount: 0, CreatedAt: time.Now().UnixMilli(), UpdatedAt: time.Now().UnixMilli(), } if err := tx.Create(registry).Error; err != nil { logger.Logger.Error("Failed to create asset registry", zap.Int64("asset_id", asset.ID), zap.Int64("user_id", userID), zap.Error(err), ) return fmt.Errorf("failed to create asset registry: %w", err) } logger.Logger.Info("Asset registry created", zap.Int64("asset_id", asset.ID), zap.Int64("user_id", userID), zap.Int32("grade", grade), ) // 3.5 推进阶段一订单到 SUCCESS,并关联资产(铸造同步完成) mintedAt = time.Now().UnixMilli() updates := map[string]interface{}{ "asset_id": asset.ID, "status": models.MintOrderStatusSuccess, // 直接设为成功,无需异步处理 "cost_crystal": mintCost.CostCrystal, "error_message": nil, "material_url": getStringValue(mintOrder.MaterialURL), "name": getStringValue(mintOrder.Name), "description": getStringValue(mintOrder.Description), "material_type": getStringValue(mintOrder.MaterialType), "event": getStringValue(mintOrder.Event), "info": getStringValue(mintOrder.Info), "minted_at": mintedAt, // 同步设置上链时间 } if err := tx.Model(&models.MintOrder{}). Where("order_id = ? AND user_id = ? AND star_id = ?", mintOrder.OrderID, userID, starID). Updates(updates).Error; err != nil { return fmt.Errorf("failed to update mint order: %w", err) } assetID := asset.ID mintOrder.AssetID = &assetID mintOrder.Status = models.MintOrderStatusSuccess mintOrder.CostCrystal = mintCost.CostCrystal logger.Logger.Info("Mint order created", zap.String("order_id", mintOrder.OrderID), zap.Int64("asset_id", asset.ID), ) return nil }) if err != nil { return nil, err } // 4. 无需异步 AI 处理,cover_url 已在步骤 3.2 中直接设置 // 5. 获取所有者的昵称(创建时所有者就是当前用户) var ownerNickname string profile, err := s.userClient.GetFanProfile(context.Background(), userID, starID) if err != nil { logger.Logger.Warn("Failed to get owner fan profile, will return without nickname", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Error(err), ) ownerNickname = "" } else { ownerNickname = profile.Nickname } // 6. 构建响应 response := &pb.CreateMintOrderResponse{ Base: &pbCommon.BaseResponse{ Code: pbCommon.StatusCode_STATUS_OK, Message: "", Timestamp: time.Now().UnixMilli(), }, Order: ModelToProtoMintOrder(mintOrder), Asset: ModelToProtoAssetDetail(asset, ownerNickname, false, 0, 0, 0), // 新创建的资产,is_liked 为 false,display_status 默认为 0,earnings 和 exhibitionExpireAt 为 0 } logger.Logger.Info("Create mint order successful", zap.String("order_id", mintOrder.OrderID), zap.Int64("asset_id", asset.ID), zap.Int64("user_id", userID), ) return response, nil } // ModelToProtoMintOrder 将数据库模型转换为Proto格式 func ModelToProtoMintOrder(order *models.MintOrder) *pb.MintOrder { if order == nil { return nil } return &pb.MintOrder{ OrderId: order.OrderID, UserId: order.UserID, AssetId: getInt64Value(order.AssetID), StarId: order.StarID, Status: order.Status, CostCrystal: order.CostCrystal, ErrorMessage: getStringValue(order.ErrorMessage), RetryCount: order.RetryCount, MaterialUrl: getStringValue(order.MaterialURL), Name: getStringValue(order.Name), Description: getStringValue(order.Description), MaterialType: getStringValue(order.MaterialType), Event: getStringValue(order.Event), CreatedAt: order.CreatedAt, UpdatedAt: order.UpdatedAt, MintedAt: getInt64Value(order.MintedAt), Info: getStringValue(order.Info), } } // stringToPtr 将字符串转换为指针 func stringToPtr(s string) *string { if s == "" { return nil } return &s } // int32ToPtr 将int32转换为指针 func int32ToPtr(i int32) *int32 { return &i } // GetMintOrder 查询铸造订单状态 func (s *mintService) GetMintOrder(orderID string, userID, starID int64) (*pb.GetMintOrderResponse, error) { // 1. 参数验证 if orderID == "" { logger.Logger.Warn("Invalid order_id (empty)") return nil, fmt.Errorf("order_id不能为空") } if !validator.ValidateUserID(userID) { logger.Logger.Warn("Invalid user_id", zap.Int64("user_id", userID), ) return nil, appErrors.ErrInvalidUserID } if !validator.ValidateStarID(starID) { logger.Logger.Warn("Invalid star_id", zap.Int64("star_id", starID), ) return nil, appErrors.ErrInvalidStarID } // 2. 查询订单(验证权限) order, err := s.mintOrderRepo.GetByOrderIDAndUser(orderID, userID, starID) if err != nil { logger.Logger.Error("Failed to get mint order", zap.String("order_id", orderID), zap.Int64("user_id", userID), zap.Error(err), ) return nil, fmt.Errorf("订单不存在或无权访问: %w", err) } // 3. 查询关联的资产(如果存在) var assetProto *pb.Asset if order.AssetID != nil { asset, err := s.assetRepo.GetByID(*order.AssetID) if err == nil && asset != nil { // 获取所有者昵称 var ownerNickname string profile, err := s.userClient.GetFanProfile(context.Background(), asset.OwnerUID, asset.StarID) if err == nil && profile != nil { ownerNickname = profile.Nickname } // 转换为 Proto(这里需要调用 ModelToProtoAssetDetail,但需要 is_liked 参数) // 由于是查询自己的订单,is_liked 设为 false(简化处理) // 获取 display_status displayStatus, _ := s.assetRepo.GetDisplayStatusByAssetID(asset.ID) assetProto = ModelToProtoAssetDetail(asset, ownerNickname, false, displayStatus, 0, 0) // 新创建的资产,earnings 和 exhibitionExpireAt 为 0 // 如果 cover_url 存在,生成预签名 URL if assetProto.CoverUrl != "" { signedURL, err := s.generatePresignedURL(assetProto.CoverUrl, 3600) if err == nil { // 将预签名 URL 添加到 asset 的扩展字段中 // 注意:proto 定义中可能没有 cover_url_signed 字段,需要检查 // 这里先记录日志,后续可以在 DTO 层处理 logger.Logger.Debug("生成预签名URL成功", zap.String("cover_url", assetProto.CoverUrl), zap.String("signed_url", signedURL), ) // TODO: 将 signedURL 添加到响应中(需要在 proto 中添加字段或通过 DTO 处理) } } } } // 4. 构建响应 response := &pb.GetMintOrderResponse{ Base: &pbCommon.BaseResponse{ Code: pbCommon.StatusCode_STATUS_OK, Message: "", Timestamp: time.Now().UnixMilli(), }, Order: ModelToProtoMintOrder(order), Asset: assetProto, } logger.Logger.Info("GetMintOrder successful", zap.String("order_id", orderID), zap.Int64("user_id", userID), zap.String("status", order.Status), ) return response, nil } // generatePresignedURL 生成预签名 URL(复用 Gateway 的逻辑) func (s *mintService) generatePresignedURL(filePath string, expiresInSeconds int64) (string, error) { // 从环境变量读取 OSS 配置(必须设置,无默认值) region := os.Getenv("OSS_REGION") bucketName := os.Getenv("OSS_BUCKET_NAME") roleArn := os.Getenv("OSS_STS_ROLE_ARN") accessKeyID := os.Getenv("OSS_ACCESS_KEY_ID") accessKeySecret := os.Getenv("OSS_ACCESS_KEY_SECRET") // 验证必需的配置项 if region == "" || bucketName == "" || roleArn == "" || accessKeyID == "" || accessKeySecret == "" { return "", fmt.Errorf("OSS 配置不完整,请设置环境变量: OSS_REGION, OSS_BUCKET_NAME, OSS_STS_ROLE_ARN, OSS_ACCESS_KEY_ID, OSS_ACCESS_KEY_SECRET") } // 获取 STS 临时凭证 credConfig := new(credentials.Config). SetType("ram_role_arn"). SetAccessKeyId(accessKeyID). SetAccessKeySecret(accessKeySecret). SetRoleArn(roleArn). SetRoleSessionName("topfans-download-session"). SetPolicy(""). SetRoleSessionExpiration(int(expiresInSeconds)) provider, err := credentials.NewCredential(credConfig) if err != nil { return "", fmt.Errorf("创建凭证提供器失败: %w", err) } cred, err := provider.GetCredential() if err != nil { return "", fmt.Errorf("获取临时凭证失败: %w", err) } // 创建 OSS 客户端 endpoint := fmt.Sprintf("https://oss-%s.aliyuncs.com", region) client, err := oss.New(endpoint, *cred.AccessKeyId, *cred.AccessKeySecret, oss.SecurityToken(*cred.SecurityToken)) if err != nil { return "", fmt.Errorf("创建OSS客户端失败: %w", err) } // 获取 Bucket bucket, err := client.Bucket(bucketName) if err != nil { return "", fmt.Errorf("获取Bucket失败: %w", err) } // 从完整 URL 中提取 OSS key // 格式: https://bucket.oss-region.aliyuncs.com/key ossKey := filePath if strings.HasPrefix(filePath, "https://") { // 提取 key 部分 // 例如: https://top-fans-test.oss-cn-shanghai.aliyuncs.com/asset/7/88/covers/123_1234567890.png // 需要提取: asset/7/88/covers/123_1234567890.png parts := strings.SplitN(filePath, ".oss-", 2) if len(parts) == 2 { // parts[1] = "cn-shanghai.aliyuncs.com/asset/7/88/covers/123_1234567890.png" keyParts := strings.SplitN(parts[1], "/", 2) if len(keyParts) == 2 { ossKey = keyParts[1] // asset/7/88/covers/123_1234567890.png } } } // 生成预签名 URL signedURL, err := bucket.SignURL(ossKey, oss.HTTPGet, expiresInSeconds) if err != nil { return "", fmt.Errorf("生成预签名URL失败: %w", err) } // 修复 path 的 URL 编码:OSS SDK 的 buildURL 会把 / 编成 %2F,导致 OSS 按字面 key 查找失败(403)。 // 只把 path 段(? 之前)的 %2F 改回 /。 if idx := strings.Index(signedURL, "?"); idx >= 0 { signedURL = strings.ReplaceAll(signedURL[:idx], "%2F", "/") + signedURL[idx:] } else { signedURL = strings.ReplaceAll(signedURL, "%2F", "/") } // 若 SDK 未把 STS 的 security-token 加入 URL,则手动追加(使用 STS 时预签名必须带此参数,否则 403) if !strings.Contains(signedURL, "security-token") && cred.SecurityToken != nil && *cred.SecurityToken != "" { signedURL = signedURL + "&security-token=" + url.QueryEscape(*cred.SecurityToken) } return signedURL, nil } // CancelMintOrder 取消铸造订单 func (s *mintService) CancelMintOrder(orderID string, userID, starID int64) error { // 1. 参数验证 if orderID == "" { logger.Logger.Warn("Invalid order_id (empty)") return fmt.Errorf("order_id不能为空") } if !validator.ValidateUserID(userID) { logger.Logger.Warn("Invalid user_id", zap.Int64("user_id", userID), ) return appErrors.ErrInvalidUserID } if !validator.ValidateStarID(starID) { logger.Logger.Warn("Invalid star_id", zap.Int64("star_id", starID), ) return appErrors.ErrInvalidStarID } // 2. 查询订单 order, err := s.mintOrderRepo.GetByOrderID(orderID) if err != nil { logger.Logger.Error("Failed to get mint order", zap.String("order_id", orderID), zap.Error(err), ) return fmt.Errorf("failed to get mint order: %w", err) } // 3. 验证订单所有者 if order.UserID != userID || order.StarID != starID { logger.Logger.Warn("Unauthorized to cancel this order", zap.String("order_id", orderID), zap.Int64("order_user_id", order.UserID), zap.Int64("order_star_id", order.StarID), zap.Int64("request_user_id", userID), zap.Int64("request_star_id", starID), ) return appErrors.ErrMintOrderAccessDenied } // 4. 检查订单状态(新流程:PENDING 才允许“取消并清理素材与订单”) if order.Status != models.MintOrderStatusPending && order.Status != models.MintOrderStatusFailed { logger.Logger.Warn("Cannot cancel order in current status", zap.String("order_id", orderID), zap.String("status", order.Status), ) return fmt.Errorf("订单状态为%s,不能取消", order.Status) } // 5. PENDING:删除素材 + 删除订单(按 list.txt 要求) if order.Status == models.MintOrderStatusPending { // 删除 OSS 素材(best-effort) if mu := getStringValue(order.MaterialURL); mu != "" { ossKey := extractOSSKeyFromURLForService(mu) if ossKey != "" { cfg := util.OSSConfig{ Region: os.Getenv("OSS_REGION"), BucketName: os.Getenv("OSS_BUCKET_NAME"), RoleArn: os.Getenv("OSS_STS_ROLE_ARN"), AccessKeyID: os.Getenv("OSS_ACCESS_KEY_ID"), AccessKeySecret: os.Getenv("OSS_ACCESS_KEY_SECRET"), } _ = util.DeleteObjectFromOSS(cfg, ossKey) } } // 删除订单记录 if err := s.mintOrderRepo.DeleteByOrderID(orderID); err != nil { return fmt.Errorf("failed to delete mint order: %w", err) } return nil } // 6. FAILED:保留兼容旧逻辑(只改状态为 CANCELLED) if err := s.mintOrderRepo.CancelOrder(orderID); err != nil { return fmt.Errorf("failed to cancel mint order: %w", err) } // 注意:不需要退回水晶,因为水晶是在创建订单时扣除的 // 取消订单不会退款 logger.Logger.Info("Mint order cancelled successfully", zap.String("order_id", orderID), zap.Int64("user_id", userID), zap.Int64("star_id", starID), ) return nil } // GetMintCost 获取铸造消耗配置 // 根据当前累计铸爱次数获取下次铸造的消耗配置 func (s *mintService) GetMintCost(mintCount int32) (*models.MintCostConfig, error) { // 铸爱次数从1开始,最大10 if mintCount < 1 { mintCount = 1 } if mintCount > 10 { mintCount = 10 } config, err := s.mintCostRepo.GetByMintCount(mintCount) if err != nil { logger.Logger.Error("Failed to get mint cost config", zap.Int32("mint_count", mintCount), zap.Error(err)) return nil, fmt.Errorf("获取铸造配置失败: %w", err) } return config, nil } // GetUserMintCount 获取用户累计铸爱次数 func (s *mintService) GetUserMintCount(userID, starID int64) (int32, error) { record, err := s.userMintCountRepo.Get(userID, starID) if err != nil { // 如果记录不存在,返回0 return 0, nil } return record.MintCount, nil } // UpdateMintCountAndBoost 更新铸爱次数和收益提升 // 在事务内调用,tx 为nil时会创建新事务 func (s *mintService) UpdateMintCountAndBoost(ctx context.Context, tx *gorm.DB, userID, starID int64, boostBps int32) error { // 获取或创建用户铸爱累计记录 record, isNew, err := s.userMintCountRepo.GetOrCreate(tx, userID, starID) if err != nil { logger.Logger.Error("Failed to get or create user mint count", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Error(err)) return fmt.Errorf("获取用户铸爱累计失败: %w", err) } // 计算新的铸爱次数 newMintCount := record.MintCount + 1 // 如果达到10次,重置为0 if newMintCount > 10 { newMintCount = 0 } // 更新记录 record.MintCount = newMintCount if boostBps > 0 { record.RevenueBoostBps += boostBps } record.UpdatedAt = time.Now().UnixMilli() if isNew { if err := tx.Create(record).Error; err != nil { return fmt.Errorf("创建用户铸爱累计记录失败: %w", err) } } else { if err := tx.Save(record).Error; err != nil { return fmt.Errorf("更新用户铸爱累计记录失败: %w", err) } } logger.Logger.Info("Updated mint count and boost", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Int32("new_mint_count", newMintCount), zap.Int32("revenue_boost_bps", record.RevenueBoostBps)) return nil } // extractOSSKeyFromURLForService 从 OSS URL 提取 key(服务内使用) func extractOSSKeyFromURLForService(filePath string) string { ossKey := filePath if strings.HasPrefix(filePath, "https://") { parts := strings.SplitN(filePath, ".oss-", 2) if len(parts) == 2 { keyParts := strings.SplitN(parts[1], "/", 2) if len(keyParts) == 2 { ossKey = keyParts[1] } } } return ossKey }