重构: 手续费改为百分比/固定二选一,清理旧 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:
yml2213
2026-07-30 14:12:27 +08:00
parent 83429456a6
commit bafe41a3ea
36 changed files with 1247 additions and 2446 deletions
+154 -16
View File
@@ -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{})