- 后端 internal/service 按职责拆分: fulfillment.go(1397→527)拆出 wallet/timeout/data/order/query/dashboard/shipnotify delivery.go(1124→801)拆出 upstream/link/state/helpers merchant.go(855→251)拆出 member/product/api_client/catalog/helpers - 前端 MerchantCenter.tsx(1327→606)拆出 merchantCenterTabs/merchantCenterUtils - docker-compose backend 透传 CORS_ALLOWED_ORIGINS - CORS 白名单实现(config/router/README/.env.example 配套) - 修复 gofmt 与文件尾部多余空行
250 lines
7.1 KiB
Go
250 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 := 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
|
|
}
|