f
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
package commands
|
||||
|
||||
// SubPortalRegisterCommand 子站注册(邀请码必填)
|
||||
type SubPortalRegisterCommand struct {
|
||||
Phone string `json:"phone" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
ConfirmPassword string `json:"confirm_password" binding:"required"`
|
||||
Code string `json:"code" binding:"required"`
|
||||
InviteToken string `json:"invite_token" binding:"required"`
|
||||
}
|
||||
|
||||
// CreateInvitationCommand 主账号创建邀请
|
||||
type CreateInvitationCommand struct {
|
||||
ParentUserID string
|
||||
// ExpiresInHours 可选,0 用默认 168 小时
|
||||
ExpiresInHours int `json:"expires_in_hours"`
|
||||
}
|
||||
|
||||
// AllocateToChildCommand 主账号向下属划余额
|
||||
type AllocateToChildCommand struct {
|
||||
ParentUserID string
|
||||
ChildUserID string `json:"child_user_id" binding:"required"`
|
||||
Amount string `json:"amount" binding:"required"`
|
||||
VerifyCode string `json:"verify_code" binding:"required,len=6"`
|
||||
}
|
||||
|
||||
// AssignChildSubscriptionCommand 为下属代配订阅
|
||||
type AssignChildSubscriptionCommand struct {
|
||||
ParentUserID string
|
||||
ChildUserID string `json:"child_user_id" binding:"required"`
|
||||
ProductID string `json:"product_id" binding:"required"`
|
||||
Price string `json:"price" binding:"required"`
|
||||
UIComponentPrice string `json:"ui_component_price"`
|
||||
}
|
||||
|
||||
// ListChildAllocationsCommand 下属划拨记录查询
|
||||
type ListChildAllocationsCommand 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"`
|
||||
}
|
||||
|
||||
// ListChildSubscriptionsCommand 下属订阅列表查询
|
||||
type ListChildSubscriptionsCommand struct {
|
||||
ParentUserID string
|
||||
ChildUserID string `json:"child_user_id" form:"child_user_id" binding:"required"`
|
||||
}
|
||||
|
||||
// RemoveChildSubscriptionCommand 删除下属订阅
|
||||
type RemoveChildSubscriptionCommand struct {
|
||||
ParentUserID string
|
||||
ChildUserID string `json:"child_user_id" binding:"required"`
|
||||
SubscriptionID string `json:"subscription_id" binding:"required"`
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package responses
|
||||
|
||||
import "time"
|
||||
|
||||
// CreateInvitationResponse 创建邀请
|
||||
type CreateInvitationResponse struct {
|
||||
InviteToken string `json:"invite_token" description:"仅返回一次,请转达被邀请人"`
|
||||
InviteURL string `json:"invite_url" description:"子站注册完整链接"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
InvitationID string `json:"invitation_id"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// SubordinateListResponse 列表
|
||||
type SubordinateListResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
Items []SubordinateListItem `json:"items"`
|
||||
}
|
||||
|
||||
// SubPortalRegisterResponse 子站注册
|
||||
type SubPortalRegisterResponse struct {
|
||||
ID string `json:"id"`
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
|
||||
// ChildAllocationItem 下属划拨记录
|
||||
type ChildAllocationItem struct {
|
||||
ID string `json:"id"`
|
||||
Amount string `json:"amount"`
|
||||
BusinessRef string `json:"business_ref"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ChildAllocationListResponse 下属划拨记录列表
|
||||
type ChildAllocationListResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
Items []ChildAllocationItem `json:"items"`
|
||||
}
|
||||
|
||||
// ChildSubscriptionItem 下属订阅项
|
||||
type ChildSubscriptionItem struct {
|
||||
ID string `json:"id"`
|
||||
ProductID string `json:"product_id"`
|
||||
Price string `json:"price"`
|
||||
UIComponentPrice string `json:"ui_component_price"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
35
internal/application/subordinate/invite_token.go
Normal file
35
internal/application/subordinate/invite_token.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package subordinate
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
const (
|
||||
// 邀请码固定 6 位,字符集为大写字母+数字
|
||||
inviteTokenLength = 6
|
||||
inviteTokenCharset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
)
|
||||
|
||||
// HashInviteToken 邀请码 SHA256 十六进制
|
||||
func HashInviteToken(raw string) string {
|
||||
sum := sha256.Sum256([]byte(raw))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// GenerateInviteToken 生成随机邀请明文与存储用哈希
|
||||
func GenerateInviteToken() (raw string, hash string, err error) {
|
||||
token := make([]byte, inviteTokenLength)
|
||||
charsetSize := big.NewInt(int64(len(inviteTokenCharset)))
|
||||
for i := range token {
|
||||
n, e := rand.Int(rand.Reader, charsetSize)
|
||||
if e != nil {
|
||||
return "", "", e
|
||||
}
|
||||
token[i] = inviteTokenCharset[n.Int64()]
|
||||
}
|
||||
raw = string(token)
|
||||
return raw, HashInviteToken(raw), nil
|
||||
}
|
||||
26
internal/application/subordinate/invite_token_test.go
Normal file
26
internal/application/subordinate/invite_token_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package subordinate
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGenerateInviteTokenFormat(t *testing.T) {
|
||||
raw, hash, err := GenerateInviteToken()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateInviteToken error: %v", err)
|
||||
}
|
||||
|
||||
if len(raw) != inviteTokenLength {
|
||||
t.Fatalf("unexpected token length: got %d, want %d", len(raw), inviteTokenLength)
|
||||
}
|
||||
|
||||
for _, ch := range raw {
|
||||
isUpper := ch >= 'A' && ch <= 'Z'
|
||||
isDigit := ch >= '0' && ch <= '9'
|
||||
if !isUpper && !isDigit {
|
||||
t.Fatalf("token contains invalid char: %q", ch)
|
||||
}
|
||||
}
|
||||
|
||||
if hash != HashInviteToken(raw) {
|
||||
t.Fatalf("hash mismatch for token")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package subordinate
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tyapi-server/internal/application/product"
|
||||
"tyapi-server/internal/domains/subordinate/repositories"
|
||||
)
|
||||
|
||||
// BlockSelfSubscribeForSubordinate 子账号禁止自助订
|
||||
type BlockSelfSubscribeForSubordinate struct {
|
||||
repo repositories.SubordinateRepository
|
||||
}
|
||||
|
||||
// NewBlockSelfSubscribeForSubordinate 构造
|
||||
func NewBlockSelfSubscribeForSubordinate(repo repositories.SubordinateRepository) product.SelfSubscribePolicy {
|
||||
return &BlockSelfSubscribeForSubordinate{repo: repo}
|
||||
}
|
||||
|
||||
// Allow 若为主账号的下属则拒绝
|
||||
func (p *BlockSelfSubscribeForSubordinate) Allow(ctx context.Context, userID string) (bool, string, error) {
|
||||
ok, err := p.repo.IsUserSubordinate(ctx, userID)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
if ok {
|
||||
return false, "子账号需由主账号配置订阅", nil
|
||||
}
|
||||
return true, "", nil
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package subordinate
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tyapi-server/internal/application/subordinate/dto/commands"
|
||||
"tyapi-server/internal/application/subordinate/dto/responses"
|
||||
)
|
||||
|
||||
// SubordinateApplicationService 下属账号:邀请/注册/划款/代配
|
||||
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)
|
||||
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
|
||||
ListChildSubscriptions(ctx context.Context, cmd *commands.ListChildSubscriptionsCommand) ([]responses.ChildSubscriptionItem, error)
|
||||
RemoveChildSubscription(ctx context.Context, cmd *commands.RemoveChildSubscriptionCommand) error
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
package subordinate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"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"
|
||||
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"
|
||||
)
|
||||
|
||||
// SubordinateApplicationServiceImpl 实现
|
||||
type SubordinateApplicationServiceImpl struct {
|
||||
subRepo subrepositories.SubordinateRepository
|
||||
userAgg domain_user_services.UserAggregateService
|
||||
smsService *domain_user_services.SMSCodeService
|
||||
productSub *product_service.ProductSubscriptionService
|
||||
cfg *config.Config
|
||||
txm *database.TransactionManager
|
||||
walletRepo repositories.WalletRepository
|
||||
userRepo user_repositories.UserRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewSubordinateApplicationService 构造
|
||||
func NewSubordinateApplicationService(
|
||||
subRepo subrepositories.SubordinateRepository,
|
||||
userAgg domain_user_services.UserAggregateService,
|
||||
smsService *domain_user_services.SMSCodeService,
|
||||
productSub *product_service.ProductSubscriptionService,
|
||||
cfg *config.Config,
|
||||
txm *database.TransactionManager,
|
||||
walletRepo repositories.WalletRepository,
|
||||
userRepo user_repositories.UserRepository,
|
||||
logger *zap.Logger,
|
||||
) SubordinateApplicationService {
|
||||
return &SubordinateApplicationServiceImpl{
|
||||
subRepo: subRepo,
|
||||
userAgg: userAgg,
|
||||
smsService: smsService,
|
||||
productSub: productSub,
|
||||
cfg: cfg,
|
||||
txm: txm,
|
||||
walletRepo: walletRepo,
|
||||
userRepo: userRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterSubPortal 子站注册
|
||||
func (s *SubordinateApplicationServiceImpl) RegisterSubPortal(ctx context.Context, cmd *commands.SubPortalRegisterCommand) (*responses.SubPortalRegisterResponse, error) {
|
||||
if cmd.Password != cmd.ConfirmPassword {
|
||||
return nil, fmt.Errorf("两次输入的密码不一致")
|
||||
}
|
||||
if err := s.smsService.VerifyCode(ctx, cmd.Phone, cmd.Code, user_entities.SMSSceneRegister); err != nil {
|
||||
return nil, fmt.Errorf("验证码错误或已过期")
|
||||
}
|
||||
|
||||
var resp *responses.SubPortalRegisterResponse
|
||||
err := s.txm.ExecuteInTx(ctx, func(txCtx context.Context) error {
|
||||
inv, err := s.subRepo.FindInvitationByTokenHash(txCtx, HashInviteToken(strings.TrimSpace(cmd.InviteToken)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if inv == nil {
|
||||
return fmt.Errorf("邀请码无效")
|
||||
}
|
||||
if inv.Status != subentities.InvitationStatusPending {
|
||||
return fmt.Errorf("邀请码已使用或已失效")
|
||||
}
|
||||
now := time.Now()
|
||||
if now.After(inv.ExpiresAt) {
|
||||
return fmt.Errorf("邀请码已过期")
|
||||
}
|
||||
|
||||
u, createErr := s.userAgg.CreateUser(txCtx, cmd.Phone, cmd.Password)
|
||||
if createErr != nil {
|
||||
return createErr
|
||||
}
|
||||
|
||||
link := &subentities.UserSubordinateLink{
|
||||
ParentUserID: inv.ParentUserID,
|
||||
ChildUserID: u.ID,
|
||||
InvitationID: &inv.ID,
|
||||
Status: subentities.LinkStatusActive,
|
||||
}
|
||||
if linkErr := s.subRepo.CreateLink(txCtx, link); linkErr != nil {
|
||||
s.logger.Error("创建主从关系失败", zap.Error(linkErr), zap.String("user_id", u.ID))
|
||||
return fmt.Errorf("注册失败,请重试或联系主账号")
|
||||
}
|
||||
|
||||
consumed, consumeErr := s.subRepo.ConsumeInvitation(txCtx, inv.ID, u.ID, now)
|
||||
if consumeErr != nil {
|
||||
s.logger.Error("核销邀请失败", zap.Error(consumeErr), zap.String("user_id", u.ID))
|
||||
return fmt.Errorf("注册失败,请重试或联系主账号")
|
||||
}
|
||||
if !consumed {
|
||||
return fmt.Errorf("邀请码已使用或已失效")
|
||||
}
|
||||
|
||||
resp = &responses.SubPortalRegisterResponse{ID: u.ID, Phone: u.Phone}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// CreateInvitation 主账号发邀请
|
||||
func (s *SubordinateApplicationServiceImpl) CreateInvitation(ctx context.Context, cmd *commands.CreateInvitationCommand) (*responses.CreateInvitationResponse, error) {
|
||||
hours := cmd.ExpiresInHours
|
||||
if hours <= 0 {
|
||||
hours = 24 * 7
|
||||
}
|
||||
raw, hash, err := GenerateInviteToken()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("生成邀请失败")
|
||||
}
|
||||
inv := &subentities.SubordinateInvitation{
|
||||
ParentUserID: cmd.ParentUserID,
|
||||
TokenHash: hash,
|
||||
ExpiresAt: time.Now().Add(time.Duration(hours) * time.Hour),
|
||||
Status: subentities.InvitationStatusPending,
|
||||
}
|
||||
if err := s.subRepo.CreateInvitation(ctx, inv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
base := strings.TrimSpace(os.Getenv("SUB_PORTAL_BASE_URL"))
|
||||
if base == "" {
|
||||
base = s.cfg.App.SubPortalBaseURL
|
||||
}
|
||||
base = strings.TrimRight(base, "/")
|
||||
if base == "" {
|
||||
return nil, fmt.Errorf("子账号域名未配置,请设置 app.sub_portal_base_url 或环境变量 SUB_PORTAL_BASE_URL")
|
||||
}
|
||||
// 与前端同仓路由一致:/sub/auth/register 为子账号专用注册页
|
||||
inviteURL := base + "/sub/auth/register?invite=" + raw
|
||||
return &responses.CreateInvitationResponse{
|
||||
InviteToken: raw,
|
||||
InviteURL: inviteURL,
|
||||
ExpiresAt: inv.ExpiresAt,
|
||||
InvitationID: inv.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListMySubordinates 主账号的下属
|
||||
func (s *SubordinateApplicationServiceImpl) ListMySubordinates(ctx context.Context, parentUserID string, page, pageSize int) (*responses.SubordinateListResponse, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
links, total, err := s.subRepo.ListChildrenByParent(ctx, parentUserID, pageSize, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]responses.SubordinateListItem, 0, len(links))
|
||||
for _, ln := range links {
|
||||
phone := ""
|
||||
companyName := "未认证"
|
||||
isCertified := false
|
||||
registeredAt := ln.CreatedAt
|
||||
balance := "0.00"
|
||||
|
||||
if u, e := s.userRepo.GetByIDWithEnterpriseInfo(ctx, ln.ChildUserID); e == nil {
|
||||
phone = u.Phone
|
||||
isCertified = u.IsCertified
|
||||
registeredAt = u.CreatedAt
|
||||
if u.EnterpriseInfo != nil && strings.TrimSpace(u.EnterpriseInfo.CompanyName) != "" {
|
||||
companyName = strings.TrimSpace(u.EnterpriseInfo.CompanyName)
|
||||
}
|
||||
} else {
|
||||
s.logger.Warn("获取下属用户失败", zap.String("child_id", ln.ChildUserID), zap.Error(e))
|
||||
}
|
||||
if w, e := s.walletRepo.GetByUserID(ctx, ln.ChildUserID); e == nil && w != nil {
|
||||
balance = w.Balance.StringFixed(2)
|
||||
}
|
||||
items = append(items, responses.SubordinateListItem{
|
||||
ChildUserID: ln.ChildUserID,
|
||||
Phone: phone,
|
||||
LinkID: ln.ID,
|
||||
RegisteredAt: registeredAt,
|
||||
CompanyName: companyName,
|
||||
IsCertified: isCertified,
|
||||
Balance: balance,
|
||||
})
|
||||
}
|
||||
return &responses.SubordinateListResponse{Total: total, Items: items}, nil
|
||||
}
|
||||
|
||||
// AllocateToChild 划款
|
||||
func (s *SubordinateApplicationServiceImpl) AllocateToChild(ctx context.Context, cmd *commands.AllocateToChildCommand) error {
|
||||
amount, err := decimal.NewFromString(strings.TrimSpace(cmd.Amount))
|
||||
if err != nil || !amount.GreaterThan(decimal.Zero) {
|
||||
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("该用户不是您的有效下属")
|
||||
}
|
||||
bizRef := uuid.New().String()
|
||||
return s.txm.ExecuteInTx(ctx, func(txCtx context.Context) error {
|
||||
ok, err := s.walletRepo.UpdateBalanceByUserID(txCtx, cmd.ParentUserID, amount, "subtract")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("主账号扣款失败,请重试")
|
||||
}
|
||||
ok2, err := s.walletRepo.UpdateBalanceByUserID(txCtx, cmd.ChildUserID, amount, "add")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok2 {
|
||||
return fmt.Errorf("向下属入账失败,请重试")
|
||||
}
|
||||
alloc := &subentities.SubordinateWalletAllocation{
|
||||
FromUserID: cmd.ParentUserID,
|
||||
ToUserID: cmd.ChildUserID,
|
||||
Amount: amount,
|
||||
BusinessRef: bizRef,
|
||||
OperatorUserID: cmd.ParentUserID,
|
||||
}
|
||||
return s.subRepo.CreateWalletAllocation(txCtx, alloc)
|
||||
})
|
||||
}
|
||||
|
||||
// ListChildAllocations 下属划拨记录
|
||||
func (s *SubordinateApplicationServiceImpl) ListChildAllocations(ctx context.Context, cmd *commands.ListChildAllocationsCommand) (*responses.ChildAllocationListResponse, 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.ListWalletAllocationsByParentAndChild(ctx, cmd.ParentUserID, cmd.ChildUserID, pageSize, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]responses.ChildAllocationItem, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, responses.ChildAllocationItem{
|
||||
ID: row.ID,
|
||||
Amount: row.Amount.StringFixed(2),
|
||||
BusinessRef: row.BusinessRef,
|
||||
CreatedAt: row.CreatedAt,
|
||||
})
|
||||
}
|
||||
return &responses.ChildAllocationListResponse{
|
||||
Total: total,
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AssignChildSubscription 代配订阅
|
||||
func (s *SubordinateApplicationServiceImpl) AssignChildSubscription(ctx context.Context, cmd *commands.AssignChildSubscriptionCommand) 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("该用户不是您的有效下属")
|
||||
}
|
||||
|
||||
price, err := decimal.NewFromString(strings.TrimSpace(cmd.Price))
|
||||
if err != nil {
|
||||
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 price.LessThan(parentSub.Price) {
|
||||
return fmt.Errorf("下属订阅价不能低于主账号对该产品的订阅价")
|
||||
}
|
||||
|
||||
uip := parentSub.UIComponentPrice
|
||||
if strings.TrimSpace(cmd.UIComponentPrice) != "" {
|
||||
p, err2 := decimal.NewFromString(strings.TrimSpace(cmd.UIComponentPrice))
|
||||
if err2 != nil {
|
||||
return fmt.Errorf("UI组件价格格式无效")
|
||||
}
|
||||
if p.LessThan(parentSub.UIComponentPrice) {
|
||||
return fmt.Errorf("下属 UI 组合价不能低于主账号的 UI 组合价")
|
||||
}
|
||||
uip = p
|
||||
}
|
||||
|
||||
existing, err := s.productSub.GetUserSubscribedProduct(ctx, cmd.ChildUserID, cmd.ProductID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing == nil {
|
||||
newSub := &productentities.Subscription{
|
||||
UserID: cmd.ChildUserID,
|
||||
ProductID: cmd.ProductID,
|
||||
Price: price,
|
||||
UIComponentPrice: uip,
|
||||
}
|
||||
return s.productSub.SaveSubscription(ctx, newSub)
|
||||
}
|
||||
existing.Price = price
|
||||
existing.UIComponentPrice = uip
|
||||
return s.productSub.SaveSubscription(ctx, existing)
|
||||
}
|
||||
|
||||
// ListChildSubscriptions 下属订阅列表
|
||||
func (s *SubordinateApplicationServiceImpl) ListChildSubscriptions(ctx context.Context, cmd *commands.ListChildSubscriptionsCommand) ([]responses.ChildSubscriptionItem, 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("该用户不是您的有效下属")
|
||||
}
|
||||
subs, err := s.productSub.GetUserSubscriptions(ctx, cmd.ChildUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]responses.ChildSubscriptionItem, 0, len(subs))
|
||||
for _, sub := range subs {
|
||||
items = append(items, responses.ChildSubscriptionItem{
|
||||
ID: sub.ID,
|
||||
ProductID: sub.ProductID,
|
||||
Price: sub.Price.StringFixed(2),
|
||||
UIComponentPrice: sub.UIComponentPrice.StringFixed(2),
|
||||
CreatedAt: sub.CreatedAt,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// RemoveChildSubscription 删除下属订阅
|
||||
func (s *SubordinateApplicationServiceImpl) RemoveChildSubscription(ctx context.Context, cmd *commands.RemoveChildSubscriptionCommand) 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("该用户不是您的有效下属")
|
||||
}
|
||||
sub, err := s.productSub.GetSubscriptionByID(ctx, cmd.SubscriptionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("订阅不存在")
|
||||
}
|
||||
if sub.UserID != cmd.ChildUserID {
|
||||
return fmt.Errorf("订阅不属于该下属")
|
||||
}
|
||||
return s.productSub.CancelSubscription(ctx, cmd.SubscriptionID)
|
||||
}
|
||||
Reference in New Issue
Block a user