2026-05-29 19:56:14 +08:00

407 lines
12 KiB
Go

package teamsalarypolicy
import (
"errors"
"fmt"
"strconv"
"strings"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/model"
"hyapp-admin-server/internal/repository"
)
const (
policyTypeBD = "bd"
policyTypeAdmin = "admin"
policyStatusActive = "active"
policyStatusDisabled = "disabled"
settlementTriggerAutomatic = "automatic"
settlementTriggerManual = "manual"
)
type Service struct {
store *repository.Store
}
func NewService(store *repository.Store) *Service {
return &Service{store: store}
}
func (s *Service) List(appCode string, options repository.TeamSalaryPolicyListOptions) ([]policyDTO, int64, error) {
options.AppCode = appctx.Normalize(appCode)
options.PolicyType = normalizePolicyType(options.PolicyType)
if options.PolicyType == "" {
return nil, 0, errors.New("政策类型不正确")
}
options.Status = normalizeStatusFilter(options.Status)
options.SettlementTriggerMode = normalizeSettlementTriggerModeFilter(options.SettlementTriggerMode)
items, total, err := s.store.ListTeamSalaryPolicies(options)
if err != nil {
return nil, 0, err
}
out := make([]policyDTO, 0, len(items))
for _, item := range items {
out = append(out, policyFromModel(item))
}
return out, total, nil
}
func (s *Service) Create(appCode string, actorID uint, req policyRequest) (policyDTO, error) {
item, err := policyModelFromRequest(appctx.Normalize(appCode), actorID, req)
if err != nil {
return policyDTO{}, err
}
if err := s.ensureNoActiveOverlap(item, 0); err != nil {
return policyDTO{}, err
}
if err := s.store.CreateTeamSalaryPolicy(&item); err != nil {
return policyDTO{}, err
}
return policyFromModel(item), nil
}
func (s *Service) Update(appCode string, actorID uint, id uint, req policyRequest) (policyDTO, error) {
policyType := normalizePolicyType(req.PolicyType)
if policyType == "" {
return policyDTO{}, errors.New("政策类型不正确")
}
item, err := s.store.GetTeamSalaryPolicy(appctx.Normalize(appCode), policyType, id)
if err != nil {
return policyDTO{}, err
}
updated, err := policyModelFromRequest(item.AppCode, actorID, req)
if err != nil {
return policyDTO{}, err
}
item.Name = updated.Name
item.RegionID = updated.RegionID
item.Status = updated.Status
item.SettlementTriggerMode = updated.SettlementTriggerMode
item.EffectiveFromMS = updated.EffectiveFromMS
item.EffectiveToMS = updated.EffectiveToMS
item.Description = updated.Description
item.UpdatedByAdminID = actorID
item.Levels = updated.Levels
if err := s.ensureNoActiveOverlap(item, id); err != nil {
return policyDTO{}, err
}
if err := s.store.UpdateTeamSalaryPolicy(&item); err != nil {
return policyDTO{}, err
}
return policyFromModel(item), nil
}
func (s *Service) Delete(appCode string, policyType string, id uint) error {
policyType = normalizePolicyType(policyType)
if policyType == "" {
return errors.New("政策类型不正确")
}
if _, err := s.store.GetTeamSalaryPolicy(appctx.Normalize(appCode), policyType, id); err != nil {
return err
}
return s.store.DeleteTeamSalaryPolicy(appctx.Normalize(appCode), policyType, id)
}
func (s *Service) ensureNoActiveOverlap(item model.TeamSalaryPolicy, excludeID uint) error {
if item.Status != policyStatusActive {
return nil
}
overlap, err := s.store.HasOverlappingActiveTeamSalaryPolicy(item.AppCode, item.PolicyType, item.RegionID, item.EffectiveFromMS, item.EffectiveToMS, excludeID)
if err != nil {
return err
}
if overlap {
return errors.New("同一区域同一时间只能存在一个启用政策")
}
return nil
}
func policyModelFromRequest(appCode string, actorID uint, req policyRequest) (model.TeamSalaryPolicy, error) {
policyType := normalizePolicyType(req.PolicyType)
if policyType == "" {
return model.TeamSalaryPolicy{}, errors.New("政策类型不正确")
}
name := strings.TrimSpace(req.Name)
if name == "" || len([]rune(name)) > 120 {
return model.TeamSalaryPolicy{}, errors.New("政策名称不正确")
}
if req.RegionID <= 0 {
return model.TeamSalaryPolicy{}, errors.New("请选择适用区域")
}
status := normalizeStatus(req.Status)
if status == "" {
// BD/Admin 政策默认先保存为停用,运营确认等级表无误后再启用,避免自动结算读到半成品。
status = policyStatusDisabled
}
if !validStatus(status) {
return model.TeamSalaryPolicy{}, errors.New("政策状态不正确")
}
triggerMode := normalizeSettlementTriggerMode(req.SettlementTriggerMode)
if triggerMode == "" {
triggerMode = settlementTriggerAutomatic
}
if !validSettlementTriggerMode(triggerMode) {
return model.TeamSalaryPolicy{}, errors.New("结算触发方式不正确")
}
if req.EffectiveFromMS < 0 || req.EffectiveToMS < 0 || (req.EffectiveToMS > 0 && req.EffectiveToMS <= req.EffectiveFromMS) {
return model.TeamSalaryPolicy{}, errors.New("政策生效时间不正确")
}
description := strings.TrimSpace(req.Description)
if len([]rune(description)) > 255 {
return model.TeamSalaryPolicy{}, errors.New("备注不能超过 255 个字符")
}
levels, err := levelModelsFromRequest(policyType, req.Levels)
if err != nil {
return model.TeamSalaryPolicy{}, err
}
return model.TeamSalaryPolicy{
AppCode: appCode,
PolicyType: policyType,
Name: name,
RegionID: req.RegionID,
Status: status,
SettlementTriggerMode: triggerMode,
EffectiveFromMS: req.EffectiveFromMS,
EffectiveToMS: req.EffectiveToMS,
Description: description,
CreatedByAdminID: actorID,
UpdatedByAdminID: actorID,
Levels: levels,
}, nil
}
func levelModelsFromRequest(policyType string, requests []levelRequest) ([]model.TeamSalaryLevel, error) {
if len(requests) == 0 {
return nil, errors.New("至少需要配置一个等级")
}
levels := append([]levelRequest(nil), requests...)
sortLevelRequests(levels)
seen := map[int32]struct{}{}
out := make([]model.TeamSalaryLevel, 0, len(levels))
var previousThresholdCents int64
var previousSalaryCents int64
for index, req := range levels {
if req.Level <= 0 {
return nil, errors.New("等级必须大于 0")
}
if _, ok := seen[req.Level]; ok {
return nil, fmt.Errorf("等级 %d 重复", req.Level)
}
seen[req.Level] = struct{}{}
threshold, thresholdCents, err := parseFixedDecimal(req.ThresholdUSD, 2, false, thresholdFieldName(policyType))
if err != nil {
return nil, fmt.Errorf("等级 %d %w", req.Level, err)
}
if index > 0 && thresholdCents <= previousThresholdCents {
return nil, fmt.Errorf("等级 %d 收入门槛必须大于上一等级", req.Level)
}
// rate_percent 保存百分比文本,例如 8 表示 8%;允许 4 位小数应对后续更细比例。
rate, _, err := parseFixedDecimal(req.RatePercent, 4, false, "提成比例")
if err != nil {
return nil, fmt.Errorf("等级 %d %w", req.Level, err)
}
salary, salaryCents, err := parseFixedDecimal(req.SalaryUSD, 2, true, salaryFieldName(policyType))
if err != nil {
return nil, fmt.Errorf("等级 %d %w", req.Level, err)
}
if index > 0 && salaryCents < previousSalaryCents {
return nil, fmt.Errorf("等级 %d 累计工资不能小于上一等级", req.Level)
}
status := normalizeStatus(req.Status)
if status == "" {
status = policyStatusActive
}
if !validStatus(status) {
return nil, fmt.Errorf("等级 %d 状态不正确", req.Level)
}
sortOrder := req.SortOrder
if sortOrder == 0 {
sortOrder = req.Level
}
out = append(out, model.TeamSalaryLevel{
LevelNo: req.Level,
ThresholdUSD: threshold,
RatePercent: rate,
SalaryUSD: salary,
Status: status,
SortOrder: sortOrder,
})
previousThresholdCents = thresholdCents
previousSalaryCents = salaryCents
}
return out, nil
}
func thresholdFieldName(policyType string) string {
if policyType == policyTypeAdmin {
return "BD工资门槛"
}
return "下属主播工资门槛"
}
func salaryFieldName(policyType string) string {
if policyType == policyTypeAdmin {
return "Admin工资"
}
return "BD工资"
}
func sortLevelRequests(items []levelRequest) {
for i := 1; i < len(items); i++ {
item := items[i]
j := i - 1
for j >= 0 && items[j].Level > item.Level {
items[j+1] = items[j]
j--
}
items[j+1] = item
}
}
func parseFixedDecimal(raw string, scale int, allowZero bool, field string) (string, int64, error) {
value := strings.TrimSpace(raw)
if strings.HasPrefix(value, "+") {
value = strings.TrimPrefix(value, "+")
}
if value == "" || strings.HasPrefix(value, "-") {
return "", 0, fmt.Errorf("%s不正确", field)
}
parts := strings.Split(value, ".")
if len(parts) > 2 || parts[0] == "" || !allDigits(parts[0]) {
return "", 0, fmt.Errorf("%s不正确", field)
}
if len(parts) == 2 && (parts[1] == "" || len(parts[1]) > scale || !allDigits(parts[1])) {
return "", 0, fmt.Errorf("%s最多支持 %d 位小数", field, scale)
}
whole, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return "", 0, fmt.Errorf("%s过大", field)
}
multiplier := pow10(scale)
if whole > (1<<62)/multiplier {
return "", 0, fmt.Errorf("%s过大", field)
}
var fraction int64
if len(parts) == 2 {
fractionText := parts[1] + strings.Repeat("0", scale-len(parts[1]))
fraction, err = strconv.ParseInt(fractionText, 10, 64)
if err != nil {
return "", 0, fmt.Errorf("%s不正确", field)
}
}
scaled := whole*multiplier + fraction
if !allowZero && scaled <= 0 {
return "", 0, fmt.Errorf("%s必须大于 0", field)
}
return strconv.FormatInt(whole, 10) + "." + fixedDigits(fraction, scale), scaled, nil
}
func fixedDigits(value int64, scale int) string {
text := strconv.FormatInt(value, 10)
if len(text) >= scale {
return text
}
return strings.Repeat("0", scale-len(text)) + text
}
func trimDecimalZeros(value string) string {
value = strings.TrimSpace(value)
if !strings.Contains(value, ".") {
return value
}
value = strings.TrimRight(value, "0")
return strings.TrimRight(value, ".")
}
func pow10(scale int) int64 {
var out int64 = 1
for i := 0; i < scale; i++ {
out *= 10
}
return out
}
func allDigits(value string) bool {
for _, r := range value {
if r < '0' || r > '9' {
return false
}
}
return true
}
func firstNonBlank(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return value
}
}
return ""
}
func normalizePolicyType(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case policyTypeBD:
return policyTypeBD
case policyTypeAdmin, "super_admin", "super-admin":
return policyTypeAdmin
default:
return ""
}
}
func normalizeStatus(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "", policyStatusActive, "enabled":
if strings.TrimSpace(value) == "" {
return ""
}
return policyStatusActive
case policyStatusDisabled, "inactive":
return policyStatusDisabled
default:
return strings.ToLower(strings.TrimSpace(value))
}
}
func normalizeStatusFilter(value string) string {
status := normalizeStatus(value)
if !validStatus(status) {
return ""
}
return status
}
func validStatus(value string) bool {
return value == policyStatusActive || value == policyStatusDisabled
}
func normalizeSettlementTriggerMode(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "", settlementTriggerAutomatic, "auto":
if strings.TrimSpace(value) == "" {
return ""
}
return settlementTriggerAutomatic
case settlementTriggerManual, "manual_settlement":
return settlementTriggerManual
default:
return strings.ToLower(strings.TrimSpace(value))
}
}
func normalizeSettlementTriggerModeFilter(value string) string {
mode := normalizeSettlementTriggerMode(value)
if !validSettlementTriggerMode(mode) {
return ""
}
return mode
}
func validSettlementTriggerMode(value string) bool {
return value == settlementTriggerAutomatic || value == settlementTriggerManual
}