509 lines
26 KiB
Go
509 lines
26 KiB
Go
package wallet
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
"time"
|
||
|
||
"hyapp/pkg/appcode"
|
||
"hyapp/pkg/xerr"
|
||
"hyapp/services/wallet-service/internal/domain/ledger"
|
||
resourcedomain "hyapp/services/wallet-service/internal/domain/resource"
|
||
)
|
||
|
||
// Repository 隔离账务存储,扣费必须由 repository 保证余额和幂等。
|
||
type Repository interface {
|
||
DebitGift(ctx context.Context, command ledger.DebitGiftCommand) (ledger.Receipt, error)
|
||
GetBalances(ctx context.Context, userID int64, assetTypes []string) ([]ledger.AssetBalance, error)
|
||
AdminCreditAsset(ctx context.Context, command ledger.AdminCreditAssetCommand) (ledger.AssetBalance, string, error)
|
||
AdminCreditCoinSellerStock(ctx context.Context, command ledger.CoinSellerStockCreditCommand) (ledger.CoinSellerStockCreditReceipt, error)
|
||
TransferCoinFromSeller(ctx context.Context, command ledger.CoinSellerTransferCommand) (ledger.CoinSellerTransferReceipt, error)
|
||
CreditTaskReward(ctx context.Context, command ledger.TaskRewardCommand) (ledger.TaskRewardReceipt, error)
|
||
ApplyGameCoinChange(ctx context.Context, command ledger.GameCoinChangeCommand) (ledger.GameCoinChangeReceipt, error)
|
||
ListRechargeBills(ctx context.Context, query ledger.ListRechargeBillsQuery) ([]ledger.RechargeBill, int64, error)
|
||
GetWalletOverview(ctx context.Context, userID int64) (ledger.WalletOverview, error)
|
||
GetWalletValueSummary(ctx context.Context, userID int64) (ledger.WalletValueSummary, error)
|
||
GetUserGiftWall(ctx context.Context, userID int64) (ledger.UserGiftWall, error)
|
||
ListRechargeProducts(ctx context.Context, userID int64, regionID int64, platform string) ([]ledger.RechargeProduct, []string, error)
|
||
ListAdminRechargeProducts(ctx context.Context, query ledger.ListRechargeProductsQuery) ([]ledger.RechargeProduct, int64, error)
|
||
CreateRechargeProduct(ctx context.Context, command ledger.RechargeProductCommand) (ledger.RechargeProduct, error)
|
||
UpdateRechargeProduct(ctx context.Context, command ledger.RechargeProductCommand) (ledger.RechargeProduct, error)
|
||
DeleteRechargeProduct(ctx context.Context, appCode string, productID int64) error
|
||
GetDiamondExchangeConfig(ctx context.Context, userID int64) ([]ledger.DiamondExchangeRule, error)
|
||
ListWalletTransactions(ctx context.Context, query ledger.ListWalletTransactionsQuery) ([]ledger.WalletTransaction, int64, error)
|
||
ApplyWithdrawal(ctx context.Context, command ledger.WithdrawalCommand) (ledger.WithdrawalRequest, ledger.AssetBalance, error)
|
||
ListVipPackages(ctx context.Context, userID int64) (ledger.UserVip, []ledger.VipLevel, error)
|
||
GetMyVip(ctx context.Context, userID int64) (ledger.UserVip, error)
|
||
PurchaseVip(ctx context.Context, command ledger.PurchaseVipCommand) (ledger.PurchaseVipReceipt, error)
|
||
ListResources(ctx context.Context, query resourcedomain.ListResourcesQuery) ([]resourcedomain.Resource, int64, error)
|
||
GetResource(ctx context.Context, resourceID int64) (resourcedomain.Resource, error)
|
||
CreateResource(ctx context.Context, command resourcedomain.ResourceCommand) (resourcedomain.Resource, error)
|
||
UpdateResource(ctx context.Context, command resourcedomain.ResourceCommand) (resourcedomain.Resource, error)
|
||
SetResourceStatus(ctx context.Context, command resourcedomain.StatusCommand) (resourcedomain.Resource, error)
|
||
ListResourceGroups(ctx context.Context, query resourcedomain.ListResourceGroupsQuery) ([]resourcedomain.ResourceGroup, int64, error)
|
||
GetResourceGroup(ctx context.Context, groupID int64) (resourcedomain.ResourceGroup, error)
|
||
CreateResourceGroup(ctx context.Context, command resourcedomain.ResourceGroupCommand) (resourcedomain.ResourceGroup, error)
|
||
UpdateResourceGroup(ctx context.Context, command resourcedomain.ResourceGroupCommand) (resourcedomain.ResourceGroup, error)
|
||
SetResourceGroupStatus(ctx context.Context, command resourcedomain.StatusCommand) (resourcedomain.ResourceGroup, error)
|
||
ListGiftConfigs(ctx context.Context, query resourcedomain.ListGiftConfigsQuery) ([]resourcedomain.GiftConfig, int64, error)
|
||
CreateGiftConfig(ctx context.Context, command resourcedomain.GiftConfigCommand) (resourcedomain.GiftConfig, error)
|
||
UpdateGiftConfig(ctx context.Context, command resourcedomain.GiftConfigCommand) (resourcedomain.GiftConfig, error)
|
||
SetGiftConfigStatus(ctx context.Context, command resourcedomain.StatusCommand) (resourcedomain.GiftConfig, error)
|
||
GrantResource(ctx context.Context, command resourcedomain.GrantResourceCommand) (resourcedomain.ResourceGrant, error)
|
||
GrantResourceGroup(ctx context.Context, command resourcedomain.GrantResourceGroupCommand) (resourcedomain.ResourceGrant, error)
|
||
ListUserResources(ctx context.Context, query resourcedomain.ListUserResourcesQuery) ([]resourcedomain.UserResourceEntitlement, error)
|
||
EquipUserResource(ctx context.Context, command resourcedomain.EquipUserResourceCommand) (resourcedomain.UserResourceEntitlement, error)
|
||
ListResourceGrants(ctx context.Context, query resourcedomain.ListResourceGrantsQuery) ([]resourcedomain.ResourceGrant, int64, error)
|
||
ProjectPendingGiftWallEvents(ctx context.Context, workerID string, limit int, lockTTL time.Duration) (int, error)
|
||
}
|
||
|
||
// Service 承载钱包账务用例。
|
||
type Service struct {
|
||
repository Repository
|
||
}
|
||
|
||
// New 创建钱包服务。
|
||
func New(repository Repository) *Service {
|
||
return &Service{repository: repository}
|
||
}
|
||
|
||
// DebitGift 校验送礼扣费命令,并交给 repository 做原子落账和幂等。
|
||
func (s *Service) DebitGift(ctx context.Context, command ledger.DebitGiftCommand) (ledger.Receipt, error) {
|
||
if command.CommandID == "" || command.RoomID == "" || command.SenderUserID <= 0 || command.TargetUserID <= 0 || command.GiftID == "" {
|
||
return ledger.Receipt{}, xerr.New(xerr.InvalidArgument, "billing command is incomplete")
|
||
}
|
||
if command.GiftCount <= 0 {
|
||
return ledger.Receipt{}, xerr.New(xerr.InvalidArgument, "gift_count must be positive")
|
||
}
|
||
if s.repository == nil {
|
||
return ledger.Receipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
command.AppCode = appcode.Normalize(command.AppCode)
|
||
ctx = appcode.WithContext(ctx, command.AppCode)
|
||
|
||
return s.repository.DebitGift(ctx, command)
|
||
}
|
||
|
||
// GetBalances 返回用户多资产余额;鉴权范围由 gateway 或后台入口控制。
|
||
func (s *Service) GetBalances(ctx context.Context, userID int64, assetTypes []string) ([]ledger.AssetBalance, error) {
|
||
if userID <= 0 {
|
||
return nil, xerr.New(xerr.InvalidArgument, "user_id is required")
|
||
}
|
||
if s.repository == nil {
|
||
return nil, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
ctx = appcode.WithContext(ctx, appcode.FromContext(ctx))
|
||
normalized := make([]string, 0, len(assetTypes))
|
||
for _, assetType := range assetTypes {
|
||
assetType = strings.ToUpper(strings.TrimSpace(assetType))
|
||
if assetType == "" {
|
||
continue
|
||
}
|
||
if !ledger.ValidAssetType(assetType) {
|
||
return nil, xerr.New(xerr.InvalidArgument, "asset_type is invalid")
|
||
}
|
||
normalized = append(normalized, assetType)
|
||
}
|
||
|
||
return s.repository.GetBalances(ctx, userID, normalized)
|
||
}
|
||
|
||
// AdminCreditAsset 执行后台手动入账;所有审计字段必须随交易一起落库。
|
||
func (s *Service) AdminCreditAsset(ctx context.Context, command ledger.AdminCreditAssetCommand) (ledger.AssetBalance, string, error) {
|
||
if command.CommandID == "" || command.TargetUserID <= 0 || command.OperatorUserID <= 0 || command.Amount <= 0 {
|
||
return ledger.AssetBalance{}, "", xerr.New(xerr.InvalidArgument, "admin credit command is incomplete")
|
||
}
|
||
command.AssetType = strings.ToUpper(strings.TrimSpace(command.AssetType))
|
||
if !ledger.ValidAssetType(command.AssetType) {
|
||
return ledger.AssetBalance{}, "", xerr.New(xerr.InvalidArgument, "asset_type is invalid")
|
||
}
|
||
if command.Reason == "" || command.EvidenceRef == "" {
|
||
return ledger.AssetBalance{}, "", xerr.New(xerr.InvalidArgument, "reason and evidence_ref are required")
|
||
}
|
||
if s.repository == nil {
|
||
return ledger.AssetBalance{}, "", xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
command.AppCode = appcode.Normalize(command.AppCode)
|
||
ctx = appcode.WithContext(ctx, command.AppCode)
|
||
|
||
return s.repository.AdminCreditAsset(ctx, command)
|
||
}
|
||
|
||
// AdminCreditCoinSellerStock 只给 COIN_SELLER_COIN 库存入账;USDT 进货和金币补偿使用不同账务类型。
|
||
func (s *Service) AdminCreditCoinSellerStock(ctx context.Context, command ledger.CoinSellerStockCreditCommand) (ledger.CoinSellerStockCreditReceipt, error) {
|
||
if command.CommandID == "" || command.SellerUserID <= 0 || command.OperatorUserID <= 0 {
|
||
return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.InvalidArgument, "coin seller stock command is incomplete")
|
||
}
|
||
if len(command.CommandID) > 128 {
|
||
return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.InvalidArgument, "command_id is too long")
|
||
}
|
||
if command.CoinAmount <= 0 {
|
||
return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.CoinSellerStockAmountInvalid, "coin_amount must be positive")
|
||
}
|
||
command.StockType = ledger.NormalizeCoinSellerStockType(command.StockType)
|
||
if command.StockType == "" {
|
||
return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.CoinSellerStockTypeInvalid, "stock_type is invalid")
|
||
}
|
||
command.PaidCurrencyCode = strings.ToUpper(strings.TrimSpace(command.PaidCurrencyCode))
|
||
command.PaymentRef = strings.TrimSpace(command.PaymentRef)
|
||
command.EvidenceRef = strings.TrimSpace(command.EvidenceRef)
|
||
command.Reason = strings.TrimSpace(command.Reason)
|
||
if len(command.PaidCurrencyCode) > 8 || len(command.PaymentRef) > 128 || len(command.EvidenceRef) > 256 || len(command.Reason) > 512 {
|
||
return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.InvalidArgument, "coin seller stock text fields are too long")
|
||
}
|
||
switch command.StockType {
|
||
case ledger.StockTypeUSDTPurchase:
|
||
if command.PaidCurrencyCode == "" {
|
||
command.PaidCurrencyCode = ledger.PaidCurrencyUSDT
|
||
}
|
||
if command.PaidCurrencyCode != ledger.PaidCurrencyUSDT || command.PaidAmountMicro <= 0 {
|
||
return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.CoinSellerStockAmountInvalid, "usdt purchase requires paid amount")
|
||
}
|
||
case ledger.StockTypeCoinCompensation:
|
||
if command.PaidCurrencyCode != "" || command.PaidAmountMicro != 0 || command.PaymentRef != "" {
|
||
return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.CoinSellerStockAmountInvalid, "coin compensation must not include paid amount")
|
||
}
|
||
default:
|
||
return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.CoinSellerStockTypeInvalid, "stock_type is invalid")
|
||
}
|
||
if s.repository == nil {
|
||
return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
command.AppCode = appcode.Normalize(command.AppCode)
|
||
ctx = appcode.WithContext(ctx, command.AppCode)
|
||
|
||
return s.repository.AdminCreditCoinSellerStock(ctx, command)
|
||
}
|
||
|
||
// TransferCoinFromSeller 执行币商向玩家转金币;身份和区域都由 gateway/user-service 校验并传入。
|
||
func (s *Service) TransferCoinFromSeller(ctx context.Context, command ledger.CoinSellerTransferCommand) (ledger.CoinSellerTransferReceipt, error) {
|
||
if command.CommandID == "" || command.SellerUserID <= 0 || command.TargetUserID <= 0 || command.Amount <= 0 {
|
||
return ledger.CoinSellerTransferReceipt{}, xerr.New(xerr.InvalidArgument, "coin seller transfer command is incomplete")
|
||
}
|
||
if command.SellerUserID == command.TargetUserID {
|
||
return ledger.CoinSellerTransferReceipt{}, xerr.New(xerr.InvalidArgument, "seller and target must be different")
|
||
}
|
||
if command.SellerRegionID <= 0 || command.TargetRegionID <= 0 {
|
||
return ledger.CoinSellerTransferReceipt{}, xerr.New(xerr.InvalidArgument, "seller and target region are required")
|
||
}
|
||
if command.SellerRegionID != command.TargetRegionID {
|
||
return ledger.CoinSellerTransferReceipt{}, xerr.New(xerr.Conflict, "seller and target region must match")
|
||
}
|
||
if strings.TrimSpace(command.Reason) == "" {
|
||
return ledger.CoinSellerTransferReceipt{}, xerr.New(xerr.InvalidArgument, "reason is required")
|
||
}
|
||
if s.repository == nil {
|
||
return ledger.CoinSellerTransferReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
command.AppCode = appcode.Normalize(command.AppCode)
|
||
command.Reason = strings.TrimSpace(command.Reason)
|
||
ctx = appcode.WithContext(ctx, command.AppCode)
|
||
|
||
return s.repository.TransferCoinFromSeller(ctx, command)
|
||
}
|
||
|
||
// CreditTaskReward 发放任务奖励金币;任务完成判断属于 activity-service,这里只负责账本幂等入账。
|
||
func (s *Service) CreditTaskReward(ctx context.Context, command ledger.TaskRewardCommand) (ledger.TaskRewardReceipt, error) {
|
||
if command.CommandID == "" || command.TargetUserID <= 0 || command.Amount <= 0 || command.TaskID == "" || command.CycleKey == "" {
|
||
return ledger.TaskRewardReceipt{}, xerr.New(xerr.InvalidArgument, "task reward command is incomplete")
|
||
}
|
||
command.TaskType = strings.ToLower(strings.TrimSpace(command.TaskType))
|
||
if command.TaskType != "daily" && command.TaskType != "exclusive" {
|
||
return ledger.TaskRewardReceipt{}, xerr.New(xerr.InvalidArgument, "task_type is invalid")
|
||
}
|
||
command.Reason = strings.TrimSpace(command.Reason)
|
||
if command.Reason == "" {
|
||
return ledger.TaskRewardReceipt{}, xerr.New(xerr.InvalidArgument, "reason is required")
|
||
}
|
||
if s.repository == nil {
|
||
return ledger.TaskRewardReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
command.AppCode = appcode.Normalize(command.AppCode)
|
||
ctx = appcode.WithContext(ctx, command.AppCode)
|
||
|
||
return s.repository.CreditTaskReward(ctx, command)
|
||
}
|
||
|
||
// ApplyGameCoinChange 是游戏平台唯一的 COIN 改账入口,方向由 op_type 控制。
|
||
func (s *Service) ApplyGameCoinChange(ctx context.Context, command ledger.GameCoinChangeCommand) (ledger.GameCoinChangeReceipt, error) {
|
||
if command.CommandID == "" || command.UserID <= 0 || command.PlatformCode == "" || command.GameID == "" || command.ProviderOrderID == "" {
|
||
return ledger.GameCoinChangeReceipt{}, xerr.New(xerr.InvalidArgument, "game coin command is incomplete")
|
||
}
|
||
command.OpType = ledger.NormalizeGameOpType(command.OpType)
|
||
if command.OpType == "" {
|
||
return ledger.GameCoinChangeReceipt{}, xerr.New(xerr.InvalidArgument, "game op_type is invalid")
|
||
}
|
||
if command.CoinAmount <= 0 {
|
||
return ledger.GameCoinChangeReceipt{}, xerr.New(xerr.InvalidArgument, "coin_amount must be positive")
|
||
}
|
||
command.RequestHash = strings.TrimSpace(command.RequestHash)
|
||
if command.RequestHash == "" {
|
||
return ledger.GameCoinChangeReceipt{}, xerr.New(xerr.InvalidArgument, "request_hash is required")
|
||
}
|
||
if s.repository == nil {
|
||
return ledger.GameCoinChangeReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
command.AppCode = appcode.Normalize(command.AppCode)
|
||
command.PlatformCode = strings.TrimSpace(command.PlatformCode)
|
||
command.GameID = strings.TrimSpace(command.GameID)
|
||
command.ProviderOrderID = strings.TrimSpace(command.ProviderOrderID)
|
||
command.ProviderRoundID = strings.TrimSpace(command.ProviderRoundID)
|
||
command.RoomID = strings.TrimSpace(command.RoomID)
|
||
ctx = appcode.WithContext(ctx, command.AppCode)
|
||
|
||
return s.repository.ApplyGameCoinChange(ctx, command)
|
||
}
|
||
|
||
// ListRechargeBills 返回充值账单只读列表;所有充值来源必须先在 wallet-service 落充值记录。
|
||
func (s *Service) ListRechargeBills(ctx context.Context, query ledger.ListRechargeBillsQuery) ([]ledger.RechargeBill, int64, error) {
|
||
if s.repository == nil {
|
||
return nil, 0, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
query.AppCode = appcode.Normalize(query.AppCode)
|
||
ctx = appcode.WithContext(ctx, query.AppCode)
|
||
|
||
return s.repository.ListRechargeBills(ctx, query)
|
||
}
|
||
|
||
// GetWalletOverview 返回钱包首页摘要,余额和能力开关都由 wallet-service 统一给出。
|
||
func (s *Service) GetWalletOverview(ctx context.Context, userID int64) (ledger.WalletOverview, error) {
|
||
if userID <= 0 {
|
||
return ledger.WalletOverview{}, xerr.New(xerr.InvalidArgument, "user_id is required")
|
||
}
|
||
if s.repository == nil {
|
||
return ledger.WalletOverview{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
ctx = appcode.WithContext(ctx, appcode.FromContext(ctx))
|
||
return s.repository.GetWalletOverview(ctx, userID)
|
||
}
|
||
|
||
// GetWalletValueSummary 返回我的页钱包卡片摘要,只读取 COIN 账户。
|
||
func (s *Service) GetWalletValueSummary(ctx context.Context, userID int64) (ledger.WalletValueSummary, error) {
|
||
if userID <= 0 {
|
||
return ledger.WalletValueSummary{}, xerr.New(xerr.InvalidArgument, "user_id is required")
|
||
}
|
||
if s.repository == nil {
|
||
return ledger.WalletValueSummary{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
ctx = appcode.WithContext(ctx, appcode.FromContext(ctx))
|
||
return s.repository.GetWalletValueSummary(ctx, userID)
|
||
}
|
||
|
||
// GetUserGiftWall 返回用户收礼聚合投影;收礼事实只来自成功送礼账务,不读取房间运行态。
|
||
func (s *Service) GetUserGiftWall(ctx context.Context, userID int64) (ledger.UserGiftWall, error) {
|
||
if userID <= 0 {
|
||
return ledger.UserGiftWall{}, xerr.New(xerr.InvalidArgument, "user_id is required")
|
||
}
|
||
if s.repository == nil {
|
||
return ledger.UserGiftWall{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
ctx = appcode.WithContext(ctx, appcode.FromContext(ctx))
|
||
return s.repository.GetUserGiftWall(ctx, userID)
|
||
}
|
||
|
||
// ProjectPendingGiftWallEvents 消费钱包 outbox 中的送礼成功事件,异步维护用户礼物墙投影。
|
||
func (s *Service) ProjectPendingGiftWallEvents(ctx context.Context, workerID string, limit int, lockTTL time.Duration) (int, error) {
|
||
workerID = strings.TrimSpace(workerID)
|
||
if workerID == "" {
|
||
return 0, xerr.New(xerr.InvalidArgument, "worker_id is required")
|
||
}
|
||
if s.repository == nil {
|
||
return 0, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
return s.repository.ProjectPendingGiftWallEvents(ctx, workerID, limit, lockTTL)
|
||
}
|
||
|
||
// ListRechargeProducts 返回区域化充值档位;region_id 必须由 gateway 从 user-service 资料解析。
|
||
func (s *Service) ListRechargeProducts(ctx context.Context, userID int64, regionID int64, platform string) ([]ledger.RechargeProduct, []string, error) {
|
||
if userID <= 0 || regionID <= 0 {
|
||
return nil, nil, xerr.New(xerr.InvalidArgument, "user_id and region_id are required")
|
||
}
|
||
if strings.TrimSpace(platform) != "" {
|
||
platform = ledger.NormalizeRechargeProductPlatform(platform)
|
||
if platform == "" {
|
||
return nil, nil, xerr.New(xerr.InvalidArgument, "platform is invalid")
|
||
}
|
||
}
|
||
if s.repository == nil {
|
||
return nil, nil, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
ctx = appcode.WithContext(ctx, appcode.FromContext(ctx))
|
||
return s.repository.ListRechargeProducts(ctx, userID, regionID, platform)
|
||
}
|
||
|
||
// ListAdminRechargeProducts 返回后台配置列表;后台入口负责 RBAC 和审计。
|
||
func (s *Service) ListAdminRechargeProducts(ctx context.Context, query ledger.ListRechargeProductsQuery) ([]ledger.RechargeProduct, int64, error) {
|
||
if s.repository == nil {
|
||
return nil, 0, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
query.AppCode = appcode.Normalize(query.AppCode)
|
||
if strings.TrimSpace(query.Status) != "" {
|
||
query.Status = ledger.NormalizeRechargeProductStatus(query.Status)
|
||
if query.Status == "" {
|
||
return nil, 0, xerr.New(xerr.InvalidArgument, "status is invalid")
|
||
}
|
||
}
|
||
if strings.TrimSpace(query.Platform) != "" {
|
||
query.Platform = ledger.NormalizeRechargeProductPlatform(query.Platform)
|
||
if query.Platform == "" {
|
||
return nil, 0, xerr.New(xerr.InvalidArgument, "platform is invalid")
|
||
}
|
||
}
|
||
ctx = appcode.WithContext(ctx, query.AppCode)
|
||
return s.repository.ListAdminRechargeProducts(ctx, query)
|
||
}
|
||
|
||
// CreateRechargeProduct 创建内购商品配置;首版充值资源只允许发 COIN。
|
||
func (s *Service) CreateRechargeProduct(ctx context.Context, command ledger.RechargeProductCommand) (ledger.RechargeProduct, error) {
|
||
if s.repository == nil {
|
||
return ledger.RechargeProduct{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
command.AppCode = appcode.Normalize(command.AppCode)
|
||
if err := validateRechargeProductCommand(command, false); err != nil {
|
||
return ledger.RechargeProduct{}, err
|
||
}
|
||
ctx = appcode.WithContext(ctx, command.AppCode)
|
||
return s.repository.CreateRechargeProduct(ctx, command)
|
||
}
|
||
|
||
// UpdateRechargeProduct 覆盖内购商品配置和支持区域,避免状态散落在后台库。
|
||
func (s *Service) UpdateRechargeProduct(ctx context.Context, command ledger.RechargeProductCommand) (ledger.RechargeProduct, error) {
|
||
if s.repository == nil {
|
||
return ledger.RechargeProduct{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
command.AppCode = appcode.Normalize(command.AppCode)
|
||
if err := validateRechargeProductCommand(command, true); err != nil {
|
||
return ledger.RechargeProduct{}, err
|
||
}
|
||
ctx = appcode.WithContext(ctx, command.AppCode)
|
||
return s.repository.UpdateRechargeProduct(ctx, command)
|
||
}
|
||
|
||
// DeleteRechargeProduct 删除尚未接入 provider order 的配置事实;订单实现后这里需要先做引用检查。
|
||
func (s *Service) DeleteRechargeProduct(ctx context.Context, appCode string, productID int64) error {
|
||
if productID <= 0 {
|
||
return xerr.New(xerr.InvalidArgument, "product_id is required")
|
||
}
|
||
if s.repository == nil {
|
||
return xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
ctx = appcode.WithContext(ctx, appcode.Normalize(appCode))
|
||
return s.repository.DeleteRechargeProduct(ctx, appcode.FromContext(ctx), productID)
|
||
}
|
||
|
||
// GetDiamondExchangeConfig 返回当前 App 支持的钻石兑换规则。
|
||
func (s *Service) GetDiamondExchangeConfig(ctx context.Context, userID int64) ([]ledger.DiamondExchangeRule, error) {
|
||
if userID <= 0 {
|
||
return nil, xerr.New(xerr.InvalidArgument, "user_id is required")
|
||
}
|
||
if s.repository == nil {
|
||
return nil, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
ctx = appcode.WithContext(ctx, appcode.FromContext(ctx))
|
||
return s.repository.GetDiamondExchangeConfig(ctx, userID)
|
||
}
|
||
|
||
// ListWalletTransactions 返回当前用户钱包流水,避免我的页首屏扫描流水表。
|
||
func (s *Service) ListWalletTransactions(ctx context.Context, query ledger.ListWalletTransactionsQuery) ([]ledger.WalletTransaction, int64, error) {
|
||
if query.UserID <= 0 {
|
||
return nil, 0, xerr.New(xerr.InvalidArgument, "user_id is required")
|
||
}
|
||
query.AssetType = strings.ToUpper(strings.TrimSpace(query.AssetType))
|
||
if query.AssetType != "" && !ledger.ValidAssetType(query.AssetType) {
|
||
return nil, 0, xerr.New(xerr.InvalidArgument, "asset_type is invalid")
|
||
}
|
||
if s.repository == nil {
|
||
return nil, 0, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
query.AppCode = appcode.Normalize(query.AppCode)
|
||
ctx = appcode.WithContext(ctx, query.AppCode)
|
||
return s.repository.ListWalletTransactions(ctx, query)
|
||
}
|
||
|
||
// ApplyWithdrawal 冻结主播美元余额并创建待人工审核提现申请。
|
||
func (s *Service) ApplyWithdrawal(ctx context.Context, command ledger.WithdrawalCommand) (ledger.WithdrawalRequest, ledger.AssetBalance, error) {
|
||
if command.CommandID == "" || command.UserID <= 0 || command.Amount <= 0 {
|
||
return ledger.WithdrawalRequest{}, ledger.AssetBalance{}, xerr.New(xerr.InvalidArgument, "withdrawal command is incomplete")
|
||
}
|
||
command.PayoutAccount = strings.TrimSpace(command.PayoutAccount)
|
||
command.Reason = strings.TrimSpace(command.Reason)
|
||
if command.PayoutAccount == "" || len(command.PayoutAccount) > 256 || len(command.Reason) > 512 {
|
||
return ledger.WithdrawalRequest{}, ledger.AssetBalance{}, xerr.New(xerr.InvalidArgument, "withdrawal payout account is invalid")
|
||
}
|
||
if s.repository == nil {
|
||
return ledger.WithdrawalRequest{}, ledger.AssetBalance{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
command.AppCode = appcode.Normalize(command.AppCode)
|
||
ctx = appcode.WithContext(ctx, command.AppCode)
|
||
return s.repository.ApplyWithdrawal(ctx, command)
|
||
}
|
||
|
||
// ListVipPackages 返回可购买 VIP 包和当前会员状态。
|
||
func (s *Service) ListVipPackages(ctx context.Context, userID int64) (ledger.UserVip, []ledger.VipLevel, error) {
|
||
if userID <= 0 {
|
||
return ledger.UserVip{}, nil, xerr.New(xerr.InvalidArgument, "user_id is required")
|
||
}
|
||
if s.repository == nil {
|
||
return ledger.UserVip{}, nil, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
ctx = appcode.WithContext(ctx, appcode.FromContext(ctx))
|
||
return s.repository.ListVipPackages(ctx, userID)
|
||
}
|
||
|
||
// GetMyVip 返回当前用户会员状态;过期会员会按 active=false 投影。
|
||
func (s *Service) GetMyVip(ctx context.Context, userID int64) (ledger.UserVip, error) {
|
||
if userID <= 0 {
|
||
return ledger.UserVip{}, xerr.New(xerr.InvalidArgument, "user_id is required")
|
||
}
|
||
if s.repository == nil {
|
||
return ledger.UserVip{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
ctx = appcode.WithContext(ctx, appcode.FromContext(ctx))
|
||
return s.repository.GetMyVip(ctx, userID)
|
||
}
|
||
|
||
// PurchaseVip 执行 VIP 购买、升级或续期;降级由 repository 在锁内按当前会员状态拒绝。
|
||
func (s *Service) PurchaseVip(ctx context.Context, command ledger.PurchaseVipCommand) (ledger.PurchaseVipReceipt, error) {
|
||
if command.CommandID == "" || command.UserID <= 0 || command.Level <= 0 {
|
||
return ledger.PurchaseVipReceipt{}, xerr.New(xerr.InvalidArgument, "vip purchase command is incomplete")
|
||
}
|
||
if s.repository == nil {
|
||
return ledger.PurchaseVipReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||
}
|
||
command.AppCode = appcode.Normalize(command.AppCode)
|
||
ctx = appcode.WithContext(ctx, command.AppCode)
|
||
return s.repository.PurchaseVip(ctx, command)
|
||
}
|
||
|
||
func validateRechargeProductCommand(command ledger.RechargeProductCommand, requireProductID bool) error {
|
||
if requireProductID && command.ProductID <= 0 {
|
||
return xerr.New(xerr.InvalidArgument, "product_id is required")
|
||
}
|
||
if command.AmountMicro <= 0 || command.CoinAmount <= 0 || command.OperatorUserID <= 0 {
|
||
return xerr.New(xerr.InvalidArgument, "recharge product amount and operator are required")
|
||
}
|
||
if strings.TrimSpace(command.ProductName) == "" || len([]rune(strings.TrimSpace(command.ProductName))) > 128 {
|
||
return xerr.New(xerr.InvalidArgument, "product_name is invalid")
|
||
}
|
||
if len([]rune(strings.TrimSpace(command.Description))) > 512 {
|
||
return xerr.New(xerr.InvalidArgument, "description is too long")
|
||
}
|
||
if ledger.NormalizeRechargeProductPlatform(command.Platform) == "" {
|
||
return xerr.New(xerr.InvalidArgument, "platform is invalid")
|
||
}
|
||
if len(command.RegionIDs) == 0 {
|
||
return xerr.New(xerr.InvalidArgument, "region_ids are required")
|
||
}
|
||
seen := make(map[int64]struct{}, len(command.RegionIDs))
|
||
for _, regionID := range command.RegionIDs {
|
||
if regionID <= 0 {
|
||
return xerr.New(xerr.InvalidArgument, "region_id is invalid")
|
||
}
|
||
if _, ok := seen[regionID]; ok {
|
||
return xerr.New(xerr.InvalidArgument, "region_id is duplicated")
|
||
}
|
||
seen[regionID] = struct{}{}
|
||
}
|
||
return nil
|
||
}
|