Files
affiliate_dash/backend/internal/service/merchant.go
T

253 lines
7.1 KiB
Go

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 := createDefaultMerchantRoles(tx, merchant.ID); 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
}