This commit is contained in:
2026-04-25 19:17:19 +08:00
parent ba463ae38d
commit 18c92584d9
10 changed files with 533 additions and 0 deletions

View File

@@ -53,3 +53,26 @@ type RemoveChildSubscriptionCommand struct {
ChildUserID string `json:"child_user_id" binding:"required"`
SubscriptionID string `json:"subscription_id" binding:"required"`
}
// PurchaseChildQuotaCommand 主账号为子账号购买调用额度
type PurchaseChildQuotaCommand struct {
ParentUserID string
ChildUserID string `json:"child_user_id" binding:"required"`
ProductID string `json:"product_id" binding:"required"`
CallCount int64 `json:"call_count" binding:"required,min=1"`
VerifyCode string `json:"verify_code" binding:"required,len=6"`
}
// ListChildQuotaPurchasesCommand 下属额度购买记录查询
type ListChildQuotaPurchasesCommand struct {
ParentUserID string
ChildUserID string `json:"child_user_id" form:"child_user_id" binding:"required"`
Page int `json:"page" form:"page"`
PageSize int `json:"page_size" form:"page_size"`
}
// ListChildQuotaAccountsCommand 下属额度账户查询
type ListChildQuotaAccountsCommand struct {
ParentUserID string
ChildUserID string `json:"child_user_id" form:"child_user_id" binding:"required"`
}

View File

@@ -55,3 +55,28 @@ type ChildSubscriptionItem struct {
UIComponentPrice string `json:"ui_component_price"`
CreatedAt time.Time `json:"created_at"`
}
// ChildQuotaPurchaseItem 下属额度购买记录
type ChildQuotaPurchaseItem struct {
ID string `json:"id"`
ProductID string `json:"product_id"`
CallCount int64 `json:"call_count"`
UnitPrice string `json:"unit_price"`
TotalAmount string `json:"total_amount"`
BusinessRef string `json:"business_ref"`
CreatedAt time.Time `json:"created_at"`
}
// ChildQuotaPurchaseListResponse 下属额度购买记录列表
type ChildQuotaPurchaseListResponse struct {
Total int64 `json:"total"`
Items []ChildQuotaPurchaseItem `json:"items"`
}
// ChildQuotaAccountItem 下属产品额度账户
type ChildQuotaAccountItem struct {
ProductID string `json:"product_id"`
TotalQuota int64 `json:"total_quota"`
UsedQuota int64 `json:"used_quota"`
AvailableQuota int64 `json:"available_quota"`
}

View File

@@ -17,4 +17,8 @@ type SubordinateApplicationService interface {
AssignChildSubscription(ctx context.Context, cmd *commands.AssignChildSubscriptionCommand) error
ListChildSubscriptions(ctx context.Context, cmd *commands.ListChildSubscriptionsCommand) ([]responses.ChildSubscriptionItem, error)
RemoveChildSubscription(ctx context.Context, cmd *commands.RemoveChildSubscriptionCommand) error
PurchaseChildQuota(ctx context.Context, cmd *commands.PurchaseChildQuotaCommand) error
ListChildQuotaPurchases(ctx context.Context, cmd *commands.ListChildQuotaPurchasesCommand) (*responses.ChildQuotaPurchaseListResponse, error)
ListChildQuotaAccounts(ctx context.Context, cmd *commands.ListChildQuotaAccountsCommand) ([]responses.ChildQuotaAccountItem, error)
ListMyQuotaAccounts(ctx context.Context, userID string) ([]responses.ChildQuotaAccountItem, error)
}

View File

@@ -391,3 +391,218 @@ func (s *SubordinateApplicationServiceImpl) RemoveChildSubscription(ctx context.
}
return s.productSub.CancelSubscription(ctx, cmd.SubscriptionID)
}
// PurchaseChildQuota 主账号为子账号购买调用额度(按子账号订阅价结算)
func (s *SubordinateApplicationServiceImpl) PurchaseChildQuota(ctx context.Context, cmd *commands.PurchaseChildQuotaCommand) error {
if cmd.CallCount <= 0 {
return fmt.Errorf("购买次数必须大于0")
}
parentUser, err := s.userRepo.GetByID(ctx, cmd.ParentUserID)
if err != nil {
return fmt.Errorf("主账号信息获取失败")
}
if err := s.smsService.VerifyCode(ctx, parentUser.Phone, strings.TrimSpace(cmd.VerifyCode), user_entities.SMSSceneLogin); err != nil {
return fmt.Errorf("验证码错误或已过期")
}
lnk, err := s.subRepo.FindLinkByParentAndChild(ctx, cmd.ParentUserID, cmd.ChildUserID)
if err != nil {
return err
}
if lnk == nil || lnk.Status != subentities.LinkStatusActive {
return fmt.Errorf("该用户不是您的有效下属")
}
parentSub, err := s.productSub.GetUserSubscribedProduct(ctx, cmd.ParentUserID, cmd.ProductID)
if err != nil {
return err
}
if parentSub == nil {
return fmt.Errorf("主账号未订阅该产品,无法购买额度")
}
if !parentSub.Price.GreaterThan(decimal.Zero) {
return fmt.Errorf("主账号订阅价格异常,无法购买额度")
}
callCountDec := decimal.NewFromInt(cmd.CallCount)
totalAmount := parentSub.Price.Mul(callCountDec)
if !totalAmount.GreaterThan(decimal.Zero) {
return fmt.Errorf("购买金额必须大于0")
}
bizRef := uuid.New().String()
return s.txm.ExecuteInTx(ctx, func(txCtx context.Context) error {
// 购买额度前自动确保子账号存在该产品订阅,并统一为主账号订阅价
childSub, err := s.productSub.GetUserSubscribedProduct(txCtx, cmd.ChildUserID, cmd.ProductID)
if err != nil {
return err
}
if childSub == nil {
newSub := &productentities.Subscription{
UserID: cmd.ChildUserID,
ProductID: cmd.ProductID,
Price: parentSub.Price,
UIComponentPrice: parentSub.UIComponentPrice,
}
if err := s.productSub.SaveSubscription(txCtx, newSub); err != nil {
return fmt.Errorf("为下属创建订阅失败: %w", err)
}
} else {
childSub.Price = parentSub.Price
childSub.UIComponentPrice = parentSub.UIComponentPrice
if err := s.productSub.SaveSubscription(txCtx, childSub); err != nil {
return fmt.Errorf("更新下属订阅失败: %w", err)
}
}
ok, err := s.walletRepo.UpdateBalanceByUserID(txCtx, cmd.ParentUserID, totalAmount, "subtract")
if err != nil {
return err
}
if !ok {
return fmt.Errorf("主账号扣款失败,请重试")
}
account, err := s.subRepo.FindQuotaAccount(txCtx, cmd.ChildUserID, cmd.ProductID)
if err != nil {
return err
}
var beforeAvailable int64
if account == nil {
account = &subentities.UserProductQuotaAccount{
UserID: cmd.ChildUserID,
ProductID: cmd.ProductID,
TotalQuota: cmd.CallCount,
UsedQuota: 0,
AvailableQuota: cmd.CallCount,
}
beforeAvailable = 0
if err := s.subRepo.CreateQuotaAccount(txCtx, account); err != nil {
return err
}
} else {
beforeAvailable = account.AvailableQuota
account.TotalQuota += cmd.CallCount
account.AvailableQuota += cmd.CallCount
if err := s.subRepo.UpdateQuotaAccount(txCtx, account); err != nil {
return err
}
}
purchase := &subentities.SubordinateQuotaPurchase{
ParentUserID: cmd.ParentUserID,
ChildUserID: cmd.ChildUserID,
ProductID: cmd.ProductID,
CallCount: cmd.CallCount,
UnitPrice: parentSub.Price,
TotalAmount: totalAmount,
BusinessRef: bizRef,
OperatorUserID: cmd.ParentUserID,
}
if err := s.subRepo.CreateQuotaPurchase(txCtx, purchase); err != nil {
return err
}
ledger := &subentities.UserProductQuotaLedger{
UserID: cmd.ChildUserID,
ProductID: cmd.ProductID,
ChangeType: subentities.QuotaLedgerChangeTypePurchaseForSub,
DeltaQuota: cmd.CallCount,
BeforeQuota: beforeAvailable,
AfterQuota: beforeAvailable + cmd.CallCount,
SourceID: purchase.ID,
OperatorID: cmd.ParentUserID,
Remark: "主账号为子账号购买额度",
}
return s.subRepo.CreateQuotaLedger(txCtx, ledger)
})
}
// ListChildQuotaPurchases 下属额度购买记录
func (s *SubordinateApplicationServiceImpl) ListChildQuotaPurchases(ctx context.Context, cmd *commands.ListChildQuotaPurchasesCommand) (*responses.ChildQuotaPurchaseListResponse, error) {
lnk, err := s.subRepo.FindLinkByParentAndChild(ctx, cmd.ParentUserID, cmd.ChildUserID)
if err != nil {
return nil, err
}
if lnk == nil || lnk.Status != subentities.LinkStatusActive {
return nil, fmt.Errorf("该用户不是您的有效下属")
}
page := cmd.Page
pageSize := cmd.PageSize
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
offset := (page - 1) * pageSize
rows, total, err := s.subRepo.ListQuotaPurchasesByParentAndChild(ctx, cmd.ParentUserID, cmd.ChildUserID, pageSize, offset)
if err != nil {
return nil, err
}
items := make([]responses.ChildQuotaPurchaseItem, 0, len(rows))
for _, row := range rows {
items = append(items, responses.ChildQuotaPurchaseItem{
ID: row.ID,
ProductID: row.ProductID,
CallCount: row.CallCount,
UnitPrice: row.UnitPrice.StringFixed(2),
TotalAmount: row.TotalAmount.StringFixed(2),
BusinessRef: row.BusinessRef,
CreatedAt: row.CreatedAt,
})
}
return &responses.ChildQuotaPurchaseListResponse{
Total: total,
Items: items,
}, nil
}
// ListChildQuotaAccounts 下属额度账户
func (s *SubordinateApplicationServiceImpl) ListChildQuotaAccounts(ctx context.Context, cmd *commands.ListChildQuotaAccountsCommand) ([]responses.ChildQuotaAccountItem, error) {
lnk, err := s.subRepo.FindLinkByParentAndChild(ctx, cmd.ParentUserID, cmd.ChildUserID)
if err != nil {
return nil, err
}
if lnk == nil || lnk.Status != subentities.LinkStatusActive {
return nil, fmt.Errorf("该用户不是您的有效下属")
}
accounts, err := s.subRepo.ListQuotaAccountsByUser(ctx, cmd.ChildUserID)
if err != nil {
return nil, err
}
items := make([]responses.ChildQuotaAccountItem, 0, len(accounts))
for _, account := range accounts {
items = append(items, responses.ChildQuotaAccountItem{
ProductID: account.ProductID,
TotalQuota: account.TotalQuota,
UsedQuota: account.UsedQuota,
AvailableQuota: account.AvailableQuota,
})
}
return items, nil
}
// ListMyQuotaAccounts 查询当前用户额度账户(通用能力,适配所有用户)
func (s *SubordinateApplicationServiceImpl) ListMyQuotaAccounts(ctx context.Context, userID string) ([]responses.ChildQuotaAccountItem, error) {
accounts, err := s.subRepo.ListQuotaAccountsByUser(ctx, userID)
if err != nil {
return nil, err
}
items := make([]responses.ChildQuotaAccountItem, 0, len(accounts))
for _, account := range accounts {
items = append(items, responses.ChildQuotaAccountItem{
ProductID: account.ProductID,
TotalQuota: account.TotalQuota,
UsedQuota: account.UsedQuota,
AvailableQuota: account.AvailableQuota,
})
}
return items, nil
}