重构: 手续费改为百分比/固定二选一,清理旧 Skin/Order/ShipLog 演示链路
手续费: - 新增 fee_type 字段(rate/fixed),百分比与固定金额二选一,不再叠加 - calculateServiceFee 按 fee_type 分支计算 - 商户创建/更新校验 fee_type,订单落库快照 fee_type - 前端表单改为下拉选择手续费类型,动态显示对应输入框 - 测试拆为 TestFeeRate + TestFeeFixed 清理旧链路: - 删除旧 Skin/Order/ShipLog 模型及 OrderService/SkinService - Dashboard 迁移到 FulfillmentService - 上游 /api/open/v1 改为基于 FulfillmentOrder 实现,接口契约不变 - 推送留痕改用 AuditLog,不再建 ShipLog 表 - 删除前端 Skins/Orders/ShipLogs/Distributors 页面及路由、菜单、API、类型 - 新增迁移 003: 添加 fee_type 列并 DROP 旧表
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"affiliate_dash/internal/model"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
@@ -28,38 +29,60 @@ func NewMerchantService(db *gorm.DB, codec *SecretCodec, tenant *TenantService)
|
||||
}
|
||||
|
||||
type CreateMerchantInput struct {
|
||||
Code string
|
||||
Name string
|
||||
ContactName string
|
||||
ContactInfo string
|
||||
OwnerUserID uint
|
||||
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 {
|
||||
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,
|
||||
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 {
|
||||
var owner model.User
|
||||
if err := tx.First(&owner, in.OwnerUserID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("商户负责人不存在")
|
||||
}
|
||||
owner, err := s.resolveOrCreateOwner(tx, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if owner.Status != 1 {
|
||||
@@ -88,6 +111,121 @@ func (s *MerchantService) CreateMerchant(in CreateMerchantInput, actorUserID uin
|
||||
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{})
|
||||
|
||||
Reference in New Issue
Block a user