package repository import ( "errors" "regexp" "strconv" "strings" "hyapp-admin-server/internal/appctx" "hyapp-admin-server/internal/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) var databiPeriodMonthPattern = regexp.MustCompile(`^\d{4}-(0[1-9]|1[0-2])$`) // ListMoneyScopeOperators 返回所有配置过财务/数据范围的后台用户及其范围; // 社交 BI 的"运营人员"就是这批人,不再依赖固定 team_id 约定。 func (s *Store) ListMoneyScopeOperators() ([]model.User, map[uint][]model.UserMoneyScope, error) { var scopes []model.UserMoneyScope if err := s.db.Order("user_id ASC, app_code ASC, region_id ASC").Find(&scopes).Error; err != nil { return nil, nil, err } scopesByUser := map[uint][]model.UserMoneyScope{} userIDs := make([]uint, 0, len(scopes)) for _, scope := range scopes { if _, ok := scopesByUser[scope.UserID]; !ok { userIDs = append(userIDs, scope.UserID) } scopesByUser[scope.UserID] = append(scopesByUser[scope.UserID], scope) } if len(userIDs) == 0 { return []model.User{}, scopesByUser, nil } var users []model.User if err := s.db.Where("id IN ?", userIDs).Order("id ASC").Find(&users).Error; err != nil { return nil, nil, err } return users, scopesByUser, nil } func (s *Store) ListDatabiKpiTargets(periodMonths []string, userIDs []uint) ([]model.DatabiKpiTarget, error) { query := s.db.Model(&model.DatabiKpiTarget{}) months := normalizeDatabiPeriodMonths(periodMonths) if len(months) > 0 { query = query.Where("period_month IN ?", months) } if len(userIDs) > 0 { query = query.Where("user_id IN ?", userIDs) } var targets []model.DatabiKpiTarget err := query.Order("user_id ASC, app_code ASC, region_id ASC, period_month ASC").Find(&targets).Error return targets, err } // UpsertDatabiKpiTargets 逐条 upsert 目标;月目标和日目标同时为 0 视为清除该条目标。 func (s *Store) UpsertDatabiKpiTargets(targets []model.DatabiKpiTarget) error { normalized, removals, err := normalizeDatabiKpiTargets(targets) if err != nil { return err } return s.db.Transaction(func(tx *gorm.DB) error { for _, target := range removals { if err := tx.Where( "user_id = ? AND app_code = ? AND region_id = ? AND period_month = ?", target.UserID, target.AppCode, target.RegionID, target.PeriodMonth, ).Delete(&model.DatabiKpiTarget{}).Error; err != nil { return err } } if len(normalized) == 0 { return nil } return tx.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "user_id"}, {Name: "app_code"}, {Name: "region_id"}, {Name: "period_month"}}, DoUpdates: clause.AssignmentColumns([]string{ "target_usd_minor", "daily_target_usd_minor", "updated_at_ms", }), }).Create(&normalized).Error }) } func (s *Store) ListDatabiAppKpiTargets(periodMonths []string) ([]model.DatabiAppKpiTarget, error) { query := s.db.Model(&model.DatabiAppKpiTarget{}) months := normalizeDatabiPeriodMonths(periodMonths) if len(months) > 0 { query = query.Where("period_month IN ?", months) } var targets []model.DatabiAppKpiTarget err := query.Order("app_code ASC, period_month ASC").Find(&targets).Error return targets, err } // UpsertDatabiAppKpiTargets 逐条 upsert App 级目标;月目标和日目标同时为 0 视为清除。 func (s *Store) UpsertDatabiAppKpiTargets(targets []model.DatabiAppKpiTarget) error { seen := map[string]struct{}{} upserts := make([]model.DatabiAppKpiTarget, 0, len(targets)) removals := make([]model.DatabiAppKpiTarget, 0) for _, target := range targets { target.AppCode = appctx.Normalize(target.AppCode) target.PeriodMonth = strings.TrimSpace(target.PeriodMonth) if target.AppCode == "" { return errors.New("app kpi target app code is required") } if !databiPeriodMonthPattern.MatchString(target.PeriodMonth) { return errors.New("app kpi target period month must be formatted as YYYY-MM") } if target.TargetUSDMinor < 0 || target.DailyTargetUSDMinor < 0 { return errors.New("app kpi target amounts must not be negative") } key := target.AppCode + ":" + target.PeriodMonth if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} if target.TargetUSDMinor == 0 && target.DailyTargetUSDMinor == 0 { removals = append(removals, target) continue } target.ID = 0 upserts = append(upserts, target) } return s.db.Transaction(func(tx *gorm.DB) error { for _, target := range removals { if err := tx.Where("app_code = ? AND period_month = ?", target.AppCode, target.PeriodMonth). Delete(&model.DatabiAppKpiTarget{}).Error; err != nil { return err } } if len(upserts) == 0 { return nil } return tx.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "app_code"}, {Name: "period_month"}}, DoUpdates: clause.AssignmentColumns([]string{ "target_usd_minor", "daily_target_usd_minor", "updated_at_ms", }), }).Create(&upserts).Error }) } func normalizeDatabiKpiTargets(targets []model.DatabiKpiTarget) ([]model.DatabiKpiTarget, []model.DatabiKpiTarget, error) { seen := map[string]struct{}{} upserts := make([]model.DatabiKpiTarget, 0, len(targets)) removals := make([]model.DatabiKpiTarget, 0) for _, target := range targets { target.AppCode = appctx.Normalize(target.AppCode) target.PeriodMonth = strings.TrimSpace(target.PeriodMonth) if target.UserID == 0 || target.AppCode == "" || target.RegionID < 0 { return nil, nil, errors.New("kpi target user, app code and region id are required") } if !databiPeriodMonthPattern.MatchString(target.PeriodMonth) { return nil, nil, errors.New("kpi target period month must be formatted as YYYY-MM") } if target.TargetUSDMinor < 0 || target.DailyTargetUSDMinor < 0 { return nil, nil, errors.New("kpi target amounts must not be negative") } key := strconv.FormatUint(uint64(target.UserID), 10) + ":" + target.AppCode + ":" + strconv.FormatInt(target.RegionID, 10) + ":" + target.PeriodMonth if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} if target.TargetUSDMinor == 0 && target.DailyTargetUSDMinor == 0 { removals = append(removals, target) continue } target.ID = 0 upserts = append(upserts, target) } return upserts, removals, nil } func normalizeDatabiPeriodMonths(periodMonths []string) []string { out := make([]string, 0, len(periodMonths)) seen := map[string]struct{}{} for _, month := range periodMonths { month = strings.TrimSpace(month) if month == "" || !databiPeriodMonthPattern.MatchString(month) { continue } if _, ok := seen[month]; ok { continue } seen[month] = struct{}{} out = append(out, month) } return out }