This commit is contained in:
2026-04-25 11:59:10 +08:00
parent e246271a24
commit ba463ae38d
33 changed files with 1600 additions and 112 deletions

View File

@@ -0,0 +1,201 @@
package subordinate
import (
"context"
"errors"
"fmt"
"time"
"go.uber.org/zap"
"gorm.io/gorm"
"tyapi-server/internal/domains/subordinate/entities"
"tyapi-server/internal/domains/subordinate/repositories"
shared_database "tyapi-server/internal/shared/database"
)
// GormSubordinateRepository 下属模块 GORM 实现
type GormSubordinateRepository struct {
db *gorm.DB
logger *zap.Logger
}
var _ repositories.SubordinateRepository = (*GormSubordinateRepository)(nil)
// NewGormSubordinateRepository 构造
func NewGormSubordinateRepository(db *gorm.DB, logger *zap.Logger) *GormSubordinateRepository {
return &GormSubordinateRepository{db: db, logger: logger}
}
func (r *GormSubordinateRepository) withCtx(ctx context.Context) *gorm.DB {
if tx, ok := shared_database.GetTx(ctx); ok {
return tx.WithContext(ctx)
}
return r.db.WithContext(ctx)
}
// CreateInvitation 创建邀请
func (r *GormSubordinateRepository) CreateInvitation(ctx context.Context, inv *entities.SubordinateInvitation) error {
return r.withCtx(ctx).Create(inv).Error
}
// FindInvitationByTokenHash 按 token 哈希查询
func (r *GormSubordinateRepository) FindInvitationByTokenHash(ctx context.Context, tokenHash string) (*entities.SubordinateInvitation, error) {
var inv entities.SubordinateInvitation
err := r.withCtx(ctx).Where("token_hash = ?", tokenHash).First(&inv).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &inv, nil
}
// FindInvitationByID 按ID
func (r *GormSubordinateRepository) FindInvitationByID(ctx context.Context, id string) (*entities.SubordinateInvitation, error) {
var inv entities.SubordinateInvitation
err := r.withCtx(ctx).Where("id = ?", id).First(&inv).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &inv, nil
}
// UpdateInvitation 更新
func (r *GormSubordinateRepository) UpdateInvitation(ctx context.Context, inv *entities.SubordinateInvitation) error {
return r.withCtx(ctx).Save(inv).Error
}
// ConsumeInvitation 原子核销邀请(仅 pending 可核销)
func (r *GormSubordinateRepository) ConsumeInvitation(ctx context.Context, invitationID, childUserID string, consumedAt time.Time) (bool, error) {
uid := childUserID
res := r.withCtx(ctx).
Model(&entities.SubordinateInvitation{}).
Where("id = ? AND status = ?", invitationID, entities.InvitationStatusPending).
Updates(map[string]interface{}{
"status": entities.InvitationStatusConsumed,
"consumed_by_user_id": &uid,
"consumed_at": &consumedAt,
})
if res.Error != nil {
return false, res.Error
}
return res.RowsAffected > 0, nil
}
// ListInvitationsByParent 主账号邀请列表
func (r *GormSubordinateRepository) ListInvitationsByParent(ctx context.Context, parentUserID string, limit, offset int) ([]*entities.SubordinateInvitation, int64, error) {
var list []entities.SubordinateInvitation
var total int64
q := r.withCtx(ctx).Model(&entities.SubordinateInvitation{}).Where("parent_user_id = ?", parentUserID)
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
if err := q.Order("created_at DESC").Limit(limit).Offset(offset).Find(&list).Error; err != nil {
return nil, 0, err
}
out := make([]*entities.SubordinateInvitation, len(list))
for i := range list {
out[i] = &list[i]
}
return out, total, nil
}
// CreateLink 创建主从
func (r *GormSubordinateRepository) CreateLink(ctx context.Context, link *entities.UserSubordinateLink) error {
return r.withCtx(ctx).Create(link).Error
}
// FindLinkByChildUserID 按子查
func (r *GormSubordinateRepository) FindLinkByChildUserID(ctx context.Context, childUserID string) (*entities.UserSubordinateLink, error) {
var l entities.UserSubordinateLink
err := r.withCtx(ctx).Where("child_user_id = ? AND status = ?", childUserID, entities.LinkStatusActive).First(&l).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &l, nil
}
// FindLinkByParentAndChild 精确查
func (r *GormSubordinateRepository) FindLinkByParentAndChild(ctx context.Context, parentUserID, childUserID string) (*entities.UserSubordinateLink, error) {
var l entities.UserSubordinateLink
err := r.withCtx(ctx).Where("parent_user_id = ? AND child_user_id = ?", parentUserID, childUserID).First(&l).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &l, nil
}
// ListChildrenByParent 列出下属
func (r *GormSubordinateRepository) ListChildrenByParent(ctx context.Context, parentUserID string, limit, offset int) ([]*entities.UserSubordinateLink, int64, error) {
var list []entities.UserSubordinateLink
var total int64
q := r.withCtx(ctx).Model(&entities.UserSubordinateLink{}).Where("parent_user_id = ? AND status = ?", parentUserID, entities.LinkStatusActive)
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
if err := q.Order("created_at DESC").Limit(limit).Offset(offset).Find(&list).Error; err != nil {
return nil, 0, err
}
out := make([]*entities.UserSubordinateLink, len(list))
for i := range list {
out[i] = &list[i]
}
return out, total, nil
}
// UpdateLink 更新
func (r *GormSubordinateRepository) UpdateLink(ctx context.Context, link *entities.UserSubordinateLink) error {
return r.withCtx(ctx).Save(link).Error
}
// IsUserSubordinate 是否为主账号的下属(存在 active 的 child 记录)
func (r *GormSubordinateRepository) IsUserSubordinate(ctx context.Context, userID string) (bool, error) {
var n int64
err := r.withCtx(ctx).Model(&entities.UserSubordinateLink{}).Where("child_user_id = ? AND status = ?", userID, entities.LinkStatusActive).Count(&n).Error
if err != nil {
return false, err
}
return n > 0, nil
}
// CreateWalletAllocation 记划拨
func (r *GormSubordinateRepository) CreateWalletAllocation(ctx context.Context, a *entities.SubordinateWalletAllocation) error {
// 幂等:同 business_ref 不重复
var cnt int64
if err := r.withCtx(ctx).Model(&entities.SubordinateWalletAllocation{}).Where("business_ref = ?", a.BusinessRef).Count(&cnt).Error; err != nil {
return err
}
if cnt > 0 {
return fmt.Errorf("划拨记录已存在")
}
return r.withCtx(ctx).Create(a).Error
}
// ListWalletAllocationsByParentAndChild 查询主对子划拨记录
func (r *GormSubordinateRepository) ListWalletAllocationsByParentAndChild(ctx context.Context, parentUserID, childUserID string, limit, offset int) ([]*entities.SubordinateWalletAllocation, int64, error) {
var list []entities.SubordinateWalletAllocation
var total int64
q := r.withCtx(ctx).Model(&entities.SubordinateWalletAllocation{}).Where("from_user_id = ? AND to_user_id = ?", parentUserID, childUserID)
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
if err := q.Order("created_at DESC").Limit(limit).Offset(offset).Find(&list).Error; err != nil {
return nil, 0, err
}
out := make([]*entities.SubordinateWalletAllocation, len(list))
for i := range list {
out[i] = &list[i]
}
return out, total, nil
}

View File

@@ -11,6 +11,7 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"tyapi-server/internal/domains/user/entities"
"tyapi-server/internal/domains/user/repositories"
@@ -107,7 +108,48 @@ func (r *GormUserRepository) ExistsByUnifiedSocialCode(ctx context.Context, unif
}
func (r *GormUserRepository) Update(ctx context.Context, user entities.User) error {
return r.UpdateEntity(ctx, &user)
db := r.GetDB(ctx)
return db.Transaction(func(tx *gorm.DB) error {
// 避免 GORM 自动保存关联触发 ON CONFLICT受历史库索引差异影响
if err := tx.WithContext(ctx).Omit(clause.Associations).Save(&user).Error; err != nil {
return err
}
// 企业信息单独按 user_id 做更新或创建,避免关联 upsert 依赖冲突约束
if user.EnterpriseInfo == nil {
return nil
}
enterpriseInfo := *user.EnterpriseInfo
enterpriseInfo.UserID = user.ID
enterpriseInfo.User = nil
var count int64
if err := tx.WithContext(ctx).
Model(&entities.EnterpriseInfo{}).
Where("user_id = ?", user.ID).
Count(&count).Error; err != nil {
return err
}
if count > 0 {
updates := map[string]interface{}{
"company_name": enterpriseInfo.CompanyName,
"unified_social_code": enterpriseInfo.UnifiedSocialCode,
"legal_person_name": enterpriseInfo.LegalPersonName,
"legal_person_id": enterpriseInfo.LegalPersonID,
"legal_person_phone": enterpriseInfo.LegalPersonPhone,
"enterprise_address": enterpriseInfo.EnterpriseAddress,
}
return tx.WithContext(ctx).
Model(&entities.EnterpriseInfo{}).
Where("user_id = ?", user.ID).
Updates(updates).Error
}
return tx.WithContext(ctx).Create(&enterpriseInfo).Error
})
}
func (r *GormUserRepository) CreateBatch(ctx context.Context, users []entities.User) error {