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 }