2026-05-13 10:58:33 +08:00

512 lines
27 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 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.SenderUserID == command.TargetUserID {
return ledger.Receipt{}, xerr.New(xerr.InvalidArgument, "sender and target must be different")
}
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
}