Files
affiliate_dash/backend/internal/service/tenant.go
T
yml2213 bafe41a3ea 重构: 手续费改为百分比/固定二选一,清理旧 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 旧表
2026-07-30 14:12:27 +08:00

164 lines
4.3 KiB
Go

package service
import (
"errors"
"strconv"
"strings"
"affiliate_dash/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// TenantService 负责商户成员关系与请求租户解析。
type TenantService struct {
db *gorm.DB
}
func NewTenantService(db *gorm.DB) *TenantService {
return &TenantService{db: db}
}
func (s *TenantService) ResolveMember(userID uint, merchantRef string) (*model.MerchantMember, error) {
if userID == 0 {
return nil, errors.New("无效的用户身份")
}
tx := s.db.Preload("Merchant").
Where("merchant_members.user_id = ? AND merchant_members.status = ?", userID, 1).
Joins("JOIN merchants ON merchants.id = merchant_members.merchant_id AND merchants.status = ?", model.MerchantStatusActive)
if merchantRef != "" {
if id, err := strconv.ParseUint(merchantRef, 10, 64); err == nil {
tx = tx.Where("merchant_members.merchant_id = ?", uint(id))
} else {
tx = tx.Where("merchants.code = ?", merchantRef)
}
}
var member model.MerchantMember
err := tx.Order("merchant_members.is_default DESC, merchant_members.id ASC").First(&member).Error
if err == nil {
return &member, nil
}
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("当前账号无权访问该商户")
}
return nil, err
}
func (s *TenantService) EnsureSelfMember(userID uint, role string, status int) error {
var merchant model.Merchant
if err := s.db.Where("code = ?", "self-operated").First(&merchant).Error; err != nil {
return err
}
return s.EnsureMember(merchant.ID, userID, role, status, role == model.MemberRoleOwner)
}
func (s *TenantService) EnsureMember(merchantID, userID uint, role string, status int, isDefault bool) error {
if merchantID == 0 || userID == 0 {
return errors.New("商户和用户不能为空")
}
if !isValidMemberRole(role) {
return errors.New("无效的商户成员角色")
}
member := model.MerchantMember{
MerchantID: merchantID,
UserID: userID,
Role: role,
Status: status,
IsDefault: isDefault,
}
return s.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "merchant_id"}, {Name: "user_id"}},
DoUpdates: clause.Assignments(map[string]interface{}{
"role": role,
"status": status,
"is_default": isDefault,
}),
}).Create(&member).Error
}
func isValidMemberRole(role string) bool {
switch role {
case model.MemberRoleOwner, model.MemberRoleOperator, model.MemberRoleFinance, model.MemberRoleViewer:
return true
default:
return false
}
}
func MemberCanManage(memberRole string) bool {
return memberRole == model.MemberRoleOwner || memberRole == model.MemberRoleOperator
}
func MemberCanManageFinance(memberRole string) bool {
return memberRole == model.MemberRoleOwner || memberRole == model.MemberRoleFinance
}
func ParseScopes(scopes string) map[string]struct{} {
out := make(map[string]struct{})
for _, scope := range strings.FieldsFunc(scopes, func(r rune) bool {
return r == ',' || r == ' ' || r == '\n' || r == '\t'
}) {
if scope != "" {
out[scope] = struct{}{}
}
}
return out
}
func HasAnyScope(scopes string, wanted ...string) bool {
set := ParseScopes(scopes)
if _, ok := set["*"]; ok {
return true
}
for _, scope := range wanted {
if _, ok := set[scope]; ok {
return true
}
}
return false
}
func NormalizeMerchantFeatures(features string) string {
set := ParseScopes(features)
if len(set) == 0 {
set = ParseScopes(model.DefaultMerchantFeatures)
}
valid := map[string]struct{}{
model.MerchantFeatureProducts: {},
model.MerchantFeatureOrders: {},
model.MerchantFeatureWallet: {},
model.MerchantFeatureAPI: {},
model.MerchantFeatureCallbacks: {},
}
ordered := []string{
model.MerchantFeatureProducts,
model.MerchantFeatureOrders,
model.MerchantFeatureWallet,
model.MerchantFeatureAPI,
model.MerchantFeatureCallbacks,
}
out := make([]string, 0, len(ordered))
for _, feature := range ordered {
if _, ok := set[feature]; ok {
if _, allowed := valid[feature]; allowed {
out = append(out, feature)
}
}
}
if len(out) == 0 {
return model.DefaultMerchantFeatures
}
return strings.Join(out, ",")
}
func MerchantHasFeature(features string, wanted ...string) bool {
set := ParseScopes(features)
for _, feature := range wanted {
if _, ok := set[feature]; ok {
return true
}
}
return false
}