This commit is contained in:
2026-06-06 14:45:22 +08:00
parent e4eb41ce10
commit 59c09d6a33
10 changed files with 315 additions and 24 deletions

View File

@@ -9,6 +9,36 @@ type SubPortalRegisterCommand struct {
InviteToken string `json:"invite_token" binding:"required"`
}
// ListChildApiCallsCommand 下属 API 调用记录查询
type ListChildApiCallsCommand struct {
ParentUserID string
ChildUserID string `form:"child_user_id" binding:"required"`
Page int `form:"page"`
PageSize int `form:"page_size"`
TransactionID string `form:"transaction_id"`
ProductName string `form:"product_name"`
Status string `form:"status"`
StartTime string `form:"start_time"`
EndTime string `form:"end_time"`
}
// ListSubordinatesCommand 下属列表查询
type ListSubordinatesCommand struct {
ParentUserID string
Page int `form:"page"`
PageSize int `form:"page_size"`
Remark string `form:"remark"`
Phone string `form:"phone"`
CompanyName string `form:"company_name"`
}
// UpdateSubordinateRemarkCommand 更新下属备注
type UpdateSubordinateRemarkCommand struct {
ParentUserID string
ChildUserID string `json:"child_user_id" binding:"required"`
Remark string `json:"remark"`
}
// CreateInvitationCommand 主账号创建邀请
type CreateInvitationCommand struct {
ParentUserID string

View File

@@ -10,15 +10,24 @@ type CreateInvitationResponse struct {
InvitationID string `json:"invitation_id"`
}
// SubordinateProductQuotaItem 下属产品额度
type SubordinateProductQuotaItem struct {
ProductID string `json:"product_id"`
ProductName string `json:"product_name"`
AvailableQuota int64 `json:"available_quota"`
}
// SubordinateListItem 下属一条
type SubordinateListItem struct {
ChildUserID string `json:"child_user_id"`
Phone string `json:"phone,omitempty"`
LinkID string `json:"link_id"`
RegisteredAt time.Time `json:"registered_at"`
CompanyName string `json:"company_name"`
IsCertified bool `json:"is_certified"`
Balance string `json:"balance"`
ChildUserID string `json:"child_user_id"`
Phone string `json:"phone,omitempty"`
LinkID string `json:"link_id"`
Remark string `json:"remark"`
RegisteredAt time.Time `json:"registered_at"`
CompanyName string `json:"company_name"`
IsCertified bool `json:"is_certified"`
Balance string `json:"balance"`
ProductQuotas []SubordinateProductQuotaItem `json:"product_quotas"`
}
// SubordinateListResponse 列表

View File

@@ -3,6 +3,7 @@ package subordinate
import (
"context"
api_dto "tyapi-server/internal/application/api/dto"
"tyapi-server/internal/application/subordinate/dto/commands"
"tyapi-server/internal/application/subordinate/dto/responses"
)
@@ -11,7 +12,8 @@ import (
type SubordinateApplicationService interface {
RegisterSubPortal(ctx context.Context, cmd *commands.SubPortalRegisterCommand) (*responses.SubPortalRegisterResponse, error)
CreateInvitation(ctx context.Context, cmd *commands.CreateInvitationCommand) (*responses.CreateInvitationResponse, error)
ListMySubordinates(ctx context.Context, parentUserID string, page, pageSize int) (*responses.SubordinateListResponse, error)
ListMySubordinates(ctx context.Context, cmd *commands.ListSubordinatesCommand) (*responses.SubordinateListResponse, error)
UpdateSubordinateRemark(ctx context.Context, cmd *commands.UpdateSubordinateRemarkCommand) error
AllocateToChild(ctx context.Context, cmd *commands.AllocateToChildCommand) error
ListChildAllocations(ctx context.Context, cmd *commands.ListChildAllocationsCommand) (*responses.ChildAllocationListResponse, error)
AssignChildSubscription(ctx context.Context, cmd *commands.AssignChildSubscriptionCommand) error
@@ -20,5 +22,6 @@ type SubordinateApplicationService interface {
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)
ListChildApiCalls(ctx context.Context, cmd *commands.ListChildApiCallsCommand) (*api_dto.ApiCallListResponse, error)
ListMyQuotaAccounts(ctx context.Context, userID string) ([]responses.ChildQuotaAccountItem, error)
}

View File

@@ -11,18 +11,22 @@ import (
"github.com/shopspring/decimal"
"go.uber.org/zap"
api_app "tyapi-server/internal/application/api"
api_dto "tyapi-server/internal/application/api/dto"
"tyapi-server/internal/application/subordinate/dto/commands"
"tyapi-server/internal/application/subordinate/dto/responses"
"tyapi-server/internal/config"
"tyapi-server/internal/domains/finance/repositories"
productentities "tyapi-server/internal/domains/product/entities"
product_service "tyapi-server/internal/domains/product/services"
product_repositories "tyapi-server/internal/domains/product/repositories"
subentities "tyapi-server/internal/domains/subordinate/entities"
subrepositories "tyapi-server/internal/domains/subordinate/repositories"
user_entities "tyapi-server/internal/domains/user/entities"
user_repositories "tyapi-server/internal/domains/user/repositories"
domain_user_services "tyapi-server/internal/domains/user/services"
"tyapi-server/internal/shared/database"
shared_interfaces "tyapi-server/internal/shared/interfaces"
)
// SubordinateApplicationServiceImpl 实现
@@ -30,11 +34,13 @@ type SubordinateApplicationServiceImpl struct {
subRepo subrepositories.SubordinateRepository
userAgg domain_user_services.UserAggregateService
smsService *domain_user_services.SMSCodeService
productSub *product_service.ProductSubscriptionService
productSub *product_service.ProductSubscriptionService
productRepo product_repositories.ProductRepository
cfg *config.Config
txm *database.TransactionManager
walletRepo repositories.WalletRepository
userRepo user_repositories.UserRepository
apiApp api_app.ApiApplicationService
logger *zap.Logger
}
@@ -44,10 +50,12 @@ func NewSubordinateApplicationService(
userAgg domain_user_services.UserAggregateService,
smsService *domain_user_services.SMSCodeService,
productSub *product_service.ProductSubscriptionService,
productRepo product_repositories.ProductRepository,
cfg *config.Config,
txm *database.TransactionManager,
walletRepo repositories.WalletRepository,
userRepo user_repositories.UserRepository,
apiApp api_app.ApiApplicationService,
logger *zap.Logger,
) SubordinateApplicationService {
return &SubordinateApplicationServiceImpl{
@@ -55,10 +63,12 @@ func NewSubordinateApplicationService(
userAgg: userAgg,
smsService: smsService,
productSub: productSub,
productRepo: productRepo,
cfg: cfg,
txm: txm,
walletRepo: walletRepo,
userRepo: userRepo,
apiApp: apiApp,
logger: logger,
}
}
@@ -176,7 +186,9 @@ func (s *SubordinateApplicationServiceImpl) buildInvitationResponse(inv *subenti
}
// ListMySubordinates 主账号的下属
func (s *SubordinateApplicationServiceImpl) ListMySubordinates(ctx context.Context, parentUserID string, page, pageSize int) (*responses.SubordinateListResponse, error) {
func (s *SubordinateApplicationServiceImpl) ListMySubordinates(ctx context.Context, cmd *commands.ListSubordinatesCommand) (*responses.SubordinateListResponse, error) {
page := cmd.Page
pageSize := cmd.PageSize
if page < 1 {
page = 1
}
@@ -184,10 +196,58 @@ func (s *SubordinateApplicationServiceImpl) ListMySubordinates(ctx context.Conte
pageSize = 20
}
offset := (page - 1) * pageSize
links, total, err := s.subRepo.ListChildrenByParent(ctx, parentUserID, pageSize, offset)
filter := subrepositories.SubordinateListFilter{
Remark: strings.TrimSpace(cmd.Remark),
Phone: strings.TrimSpace(cmd.Phone),
CompanyName: strings.TrimSpace(cmd.CompanyName),
}
links, total, err := s.subRepo.ListChildrenByParent(ctx, cmd.ParentUserID, filter, pageSize, offset)
if err != nil {
return nil, err
}
childIDs := make([]string, 0, len(links))
for _, ln := range links {
childIDs = append(childIDs, ln.ChildUserID)
}
quotaByUser := make(map[string][]*subentities.UserProductQuotaAccount)
if len(childIDs) > 0 {
accounts, quotaErr := s.subRepo.ListQuotaAccountsByUserIDs(ctx, childIDs)
if quotaErr != nil {
return nil, quotaErr
}
for _, account := range accounts {
quotaByUser[account.UserID] = append(quotaByUser[account.UserID], account)
}
}
productNameMap := make(map[string]string)
if s.productRepo != nil {
productIDSet := make(map[string]struct{})
for _, accounts := range quotaByUser {
for _, account := range accounts {
productIDSet[account.ProductID] = struct{}{}
}
}
if len(productIDSet) > 0 {
productIDs := make([]string, 0, len(productIDSet))
for id := range productIDSet {
productIDs = append(productIDs, id)
}
products, productErr := s.productRepo.FindProductsByIDs(ctx, productIDs)
if productErr != nil {
s.logger.Warn("批量获取产品名称失败", zap.Error(productErr))
} else {
for _, product := range products {
if product != nil {
productNameMap[product.ID] = product.Name
}
}
}
}
}
items := make([]responses.SubordinateListItem, 0, len(links))
for _, ln := range links {
phone := ""
@@ -209,19 +269,52 @@ func (s *SubordinateApplicationServiceImpl) ListMySubordinates(ctx context.Conte
if w, e := s.walletRepo.GetByUserID(ctx, ln.ChildUserID); e == nil && w != nil {
balance = w.Balance.StringFixed(2)
}
productQuotas := make([]responses.SubordinateProductQuotaItem, 0)
for _, account := range quotaByUser[ln.ChildUserID] {
name := productNameMap[account.ProductID]
if name == "" {
name = account.ProductID
}
productQuotas = append(productQuotas, responses.SubordinateProductQuotaItem{
ProductID: account.ProductID,
ProductName: name,
AvailableQuota: account.AvailableQuota,
})
}
items = append(items, responses.SubordinateListItem{
ChildUserID: ln.ChildUserID,
Phone: phone,
LinkID: ln.ID,
RegisteredAt: registeredAt,
CompanyName: companyName,
IsCertified: isCertified,
Balance: balance,
ChildUserID: ln.ChildUserID,
Phone: phone,
LinkID: ln.ID,
Remark: ln.Remark,
RegisteredAt: registeredAt,
CompanyName: companyName,
IsCertified: isCertified,
Balance: balance,
ProductQuotas: productQuotas,
})
}
return &responses.SubordinateListResponse{Total: total, Items: items}, nil
}
// UpdateSubordinateRemark 更新下属备注
func (s *SubordinateApplicationServiceImpl) UpdateSubordinateRemark(ctx context.Context, cmd *commands.UpdateSubordinateRemarkCommand) error {
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("该用户不是您的有效下属")
}
remark := strings.TrimSpace(cmd.Remark)
if len([]rune(remark)) > 255 {
return fmt.Errorf("备注不能超过255个字符")
}
lnk.Remark = remark
return s.subRepo.UpdateLink(ctx, lnk)
}
// AllocateToChild 划款
func (s *SubordinateApplicationServiceImpl) AllocateToChild(ctx context.Context, cmd *commands.AllocateToChildCommand) error {
amount, err := decimal.NewFromString(strings.TrimSpace(cmd.Amount))
@@ -604,6 +697,57 @@ func (s *SubordinateApplicationServiceImpl) ListChildQuotaAccounts(ctx context.C
return items, nil
}
// ListChildApiCalls 主账号查看下属 API 调用记录
func (s *SubordinateApplicationServiceImpl) ListChildApiCalls(ctx context.Context, cmd *commands.ListChildApiCallsCommand) (*api_dto.ApiCallListResponse, error) {
if s.apiApp == nil {
return nil, fmt.Errorf("API 服务未初始化")
}
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 = 10
}
filters := make(map[string]interface{})
if strings.TrimSpace(cmd.TransactionID) != "" {
filters["transaction_id"] = strings.TrimSpace(cmd.TransactionID)
}
if strings.TrimSpace(cmd.ProductName) != "" {
filters["product_name"] = strings.TrimSpace(cmd.ProductName)
}
if strings.TrimSpace(cmd.Status) != "" {
filters["status"] = strings.TrimSpace(cmd.Status)
}
if strings.TrimSpace(cmd.StartTime) != "" {
if t, parseErr := time.Parse("2006-01-02 15:04:05", strings.TrimSpace(cmd.StartTime)); parseErr == nil {
filters["start_time"] = t
}
}
if strings.TrimSpace(cmd.EndTime) != "" {
if t, parseErr := time.Parse("2006-01-02 15:04:05", strings.TrimSpace(cmd.EndTime)); parseErr == nil {
filters["end_time"] = t
}
}
return s.apiApp.GetUserApiCalls(ctx, cmd.ChildUserID, filters, shared_interfaces.ListOptions{
Page: page,
PageSize: pageSize,
Sort: "created_at",
Order: "desc",
})
}
// ListMyQuotaAccounts 查询当前用户额度账户(通用能力,适配所有用户)
func (s *SubordinateApplicationServiceImpl) ListMyQuotaAccounts(ctx context.Context, userID string) ([]responses.ChildQuotaAccountItem, error) {
accounts, err := s.subRepo.ListQuotaAccountsByUser(ctx, userID)