package service import ( "errors" "fmt" "regexp" "strings" "affiliate_dash/internal/model" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) var merchantCodePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{2,63}$`) type MerchantService struct { db *gorm.DB codec *SecretCodec tenant *TenantService } func NewMerchantService(db *gorm.DB, codec *SecretCodec, tenant *TenantService) *MerchantService { return &MerchantService{db: db, codec: codec, tenant: tenant} } type CreateMerchantInput struct { Code string Name string ContactName string ContactInfo string OwnerUserID uint OwnerUsername string OwnerPassword string OwnerNickname string Features string FeeType string FeeRateBP int64 FeeFixedAmount int64 } func (s *MerchantService) CreateMerchant(in CreateMerchantInput, actorUserID uint) (*model.Merchant, error) { in.Code = strings.ToLower(strings.TrimSpace(in.Code)) in.Name = strings.TrimSpace(in.Name) in.Features = NormalizeMerchantFeatures(in.Features) if !merchantCodePattern.MatchString(in.Code) { return nil, errors.New("商户编码需为 3-64 位小写字母、数字或连字符") } if in.Name == "" { return nil, errors.New("商户名称不能为空") } if in.OwnerUserID == 0 && strings.TrimSpace(in.OwnerUsername) == "" { return nil, errors.New("商户负责人不能为空") } if in.FeeRateBP < 0 || in.FeeRateBP > 10000 { return nil, errors.New("手续费比例需在 0-10000 BP 之间") } if in.FeeFixedAmount < 0 { return nil, errors.New("固定手续费不能小于零") } feeType := in.FeeType if feeType == "" { feeType = model.FeeTypeRate } if feeType != model.FeeTypeRate && feeType != model.FeeTypeFixed { return nil, errors.New("手续费类型仅支持 rate 或 fixed") } merchant := &model.Merchant{ Code: in.Code, Name: in.Name, Status: model.MerchantStatusActive, ContactName: in.ContactName, ContactInfo: in.ContactInfo, Features: in.Features, FeeType: feeType, FeeRateBP: in.FeeRateBP, FeeFixedAmount: in.FeeFixedAmount, } err := s.db.Transaction(func(tx *gorm.DB) error { owner, err := s.resolveOrCreateOwner(tx, in) if err != nil { return err } if owner.Status != 1 { return errors.New("商户负责人已禁用") } if err := tx.Create(merchant).Error; err != nil { return err } if err := tx.Create(&model.WalletAccount{MerchantID: merchant.ID, Currency: "POINT"}).Error; err != nil { return err } if err := tx.Create(&model.MerchantMember{ MerchantID: merchant.ID, UserID: owner.ID, Role: model.MemberRoleOwner, Status: 1, IsDefault: true, }).Error; err != nil { return err } if err := copyDefaultProducts(tx, merchant.ID); err != nil { return err } return writeAudit(tx, &merchant.ID, &actorUserID, nil, "merchant.create", "merchant", fmt.Sprint(merchant.ID), map[string]string{"code": merchant.Code}) }) if err != nil { return nil, err } return merchant, nil } func (s *MerchantService) resolveOrCreateOwner(tx *gorm.DB, in CreateMerchantInput) (*model.User, error) { if in.OwnerUserID != 0 { var owner model.User if err := tx.First(&owner, in.OwnerUserID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("商户负责人不存在") } return nil, err } return &owner, nil } username := strings.TrimSpace(in.OwnerUsername) password := strings.TrimSpace(in.OwnerPassword) nickname := strings.TrimSpace(in.OwnerNickname) if username == "" || len(username) < 3 { return nil, errors.New("负责人用户名至少 3 位") } if len(password) < 6 { return nil, errors.New("负责人密码至少 6 位") } if nickname == "" { nickname = username } var count int64 if err := tx.Model(&model.User{}).Where("username = ?", username).Count(&count).Error; err != nil { return nil, err } if count > 0 { return nil, errors.New("负责人用户名已存在") } hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return nil, err } owner := &model.User{ Username: username, PasswordHash: string(hash), Nickname: nickname, Role: model.RoleMerchant, Status: 1, } if err := tx.Create(owner).Error; err != nil { return nil, err } return owner, nil } type UpdateMerchantSettingsInput struct { Name *string Status *string ContactName *string ContactInfo *string Features *string FeeType *string FeeRateBP *int64 FeeFixedAmount *int64 } func (s *MerchantService) UpdateMerchantSettings(merchantID uint, in UpdateMerchantSettingsInput, actorUserID uint) error { updates := map[string]interface{}{} if in.Name != nil { name := strings.TrimSpace(*in.Name) if name == "" { return errors.New("商户名称不能为空") } updates["name"] = name } if in.Status != nil { if *in.Status != model.MerchantStatusActive && *in.Status != model.MerchantStatusDisabled { return errors.New("无效的商户状态") } updates["status"] = *in.Status } if in.ContactName != nil { updates["contact_name"] = strings.TrimSpace(*in.ContactName) } if in.ContactInfo != nil { updates["contact_info"] = strings.TrimSpace(*in.ContactInfo) } if in.Features != nil { updates["features"] = NormalizeMerchantFeatures(*in.Features) } if in.FeeRateBP != nil { if *in.FeeRateBP < 0 || *in.FeeRateBP > 10000 { return errors.New("手续费比例需在 0-10000 BP 之间") } updates["fee_rate_bp"] = *in.FeeRateBP } if in.FeeFixedAmount != nil { if *in.FeeFixedAmount < 0 { return errors.New("固定手续费不能小于零") } updates["fee_fixed_amount"] = *in.FeeFixedAmount } if in.FeeType != nil { if *in.FeeType != model.FeeTypeRate && *in.FeeType != model.FeeTypeFixed { return errors.New("手续费类型仅支持 rate 或 fixed") } updates["fee_type"] = *in.FeeType } if len(updates) == 0 { return errors.New("没有可更新字段") } return s.db.Transaction(func(tx *gorm.DB) error { result := tx.Model(&model.Merchant{}).Where("id = ?", merchantID).Updates(updates) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return errors.New("商户不存在") } return writeAudit(tx, &merchantID, &actorUserID, nil, "merchant.settings.update", "merchant", fmt.Sprint(merchantID), updates) }) } func (s *MerchantService) ListMerchants(page, size int) ([]model.Merchant, int64, error) { page, size = normalizePage(page, size) tx := s.db.Model(&model.Merchant{}) var total int64 if err := tx.Count(&total).Error; err != nil { return nil, 0, err } var merchants []model.Merchant err := tx.Order("id DESC").Offset((page - 1) * size).Limit(size).Find(&merchants).Error return merchants, total, err } func (s *MerchantService) GetMerchant(merchantID uint) (*model.Merchant, error) { var merchant model.Merchant if err := s.db.First(&merchant, merchantID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("商户不存在") } return nil, err } return &merchant, nil }