package stopresumerule import ( "context" "sort" "strconv" "strings" "github.com/break/junhong_cmp_fiber/internal/domain/stopresume" "github.com/break/junhong_cmp_fiber/internal/model/dto" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/middleware" ) // 明细分页默认值与上限。 const ( defaultPageSize = 20 maxPageSize = 500 ) // EnsureAllowed 校验演练与导出权限:仅超级管理员与平台账号。 func EnsureAllowed(userType int) error { if userType == constants.UserTypeSuperAdmin || userType == constants.UserTypePlatform { return nil } return errors.New(errors.CodeForbidden, constants.PlatformManagementForbiddenMessage) } // BuildFilter 把请求参数转换为演练筛选;假设开启与关闭的账户不得重叠。 func BuildFilter(carrierID *uint, stopReason, assumeOn, assumeOff string) (Filter, error) { on, err := parseIDs(assumeOn, "assume_control_on") if err != nil { return Filter{}, err } off, err := parseIDs(assumeOff, "assume_control_off") if err != nil { return Filter{}, err } control := make(map[uint]bool, len(on)+len(off)) for _, id := range on { control[id] = true } for _, id := range off { if control[id] { return Filter{}, errors.New(errors.CodeInvalidParam, "同一运营商账户不能同时假设开启和关闭实名管控") } control[id] = false } return Filter{CarrierID: carrierID, StopReason: strings.TrimSpace(stopReason), RealnameControl: control}, nil } func parseIDs(raw, field string) ([]uint, error) { raw = strings.TrimSpace(raw) if raw == "" { return nil, nil } parts := strings.Split(raw, ",") ids := make([]uint, 0, len(parts)) for _, part := range parts { part = strings.TrimSpace(part) if part == "" { continue } id, err := strconv.ParseUint(part, 10, 64) if err != nil || id == 0 { return nil, errors.New(errors.CodeInvalidParam, field+" 只能是逗号分隔的运营商账户ID") } ids = append(ids, uint(id)) } return ids, nil } type summaryKey struct { carrierID uint stopReason string result string reason string } // DryRun 执行演练:全量分批判断,返回汇总与按 result 筛选后的分页明细。只读。 func (q *Query) DryRun(ctx context.Context, req *dto.StopResumeRuleDryRunRequest) (*dto.StopResumeRuleDryRunResponse, error) { if err := EnsureAllowed(middleware.GetUserTypeFromContext(ctx)); err != nil { return nil, err } filter, err := BuildFilter(req.CarrierID, req.StopReason, req.AssumeControlOn, req.AssumeControlOff) if err != nil { return nil, err } page, size := req.Page, req.PageSize if page < 1 { page = 1 } if size < 1 { size = defaultPageSize } if size > maxPageSize { size = maxPageSize } offset := (page - 1) * size resp := &dto.StopResumeRuleDryRunResponse{Page: page, PageSize: size, Items: []dto.StopResumeRuleDryRunItem{}} counts := map[summaryKey]int64{} names := map[uint]string{} var matched int64 err = q.Scan(ctx, filter, func(rows []Row) error { for index := range rows { row := &rows[index] resp.Totals.Total++ switch row.Result { case stopresume.ResultWillResume: resp.Totals.WillResume++ case stopresume.ResultWillStop: resp.Totals.WillStop++ default: resp.Totals.NoAction++ } counts[summaryKey{row.CarrierID, row.StopReason, row.Result, row.Reason}]++ names[row.CarrierID] = row.CarrierName if req.Result != "" && row.Result != req.Result { continue } if matched >= int64(offset) && len(resp.Items) < size { resp.Items = append(resp.Items, ToItem(row)) } matched++ } return nil }) if err != nil { return nil, err } resp.Total = matched resp.Summary = buildSummary(counts, names) return resp, nil } func buildSummary(counts map[summaryKey]int64, names map[uint]string) []dto.StopResumeRuleDryRunSummaryItem { items := make([]dto.StopResumeRuleDryRunSummaryItem, 0, len(counts)) for key, count := range counts { items = append(items, dto.StopResumeRuleDryRunSummaryItem{ CarrierID: key.carrierID, CarrierName: names[key.carrierID], StopReason: key.stopReason, StopReasonName: stopReasonName(key.stopReason), Result: key.result, ResultName: stopresume.ResultName(key.result), Reason: key.reason, ReasonName: stopresume.ReasonName(key.reason), Count: count, }) } sort.Slice(items, func(i, j int) bool { a, b := items[i], items[j] if a.CarrierID != b.CarrierID { return a.CarrierID < b.CarrierID } if a.StopReason != b.StopReason { return a.StopReason < b.StopReason } if a.Result != b.Result { return a.Result < b.Result } return a.Reason < b.Reason }) return items } // ToItem 把演练行转换为接口明细。 func ToItem(row *Row) dto.StopResumeRuleDryRunItem { return dto.StopResumeRuleDryRunItem{ CardID: row.CardID, ICCID: row.ICCID, CarrierID: row.CarrierID, CarrierName: row.CarrierName, DeviceID: row.DeviceID, NetworkStatus: row.NetworkStatus, StopReason: row.StopReason, StopReasonName: stopReasonName(row.StopReason), CardCategory: row.CardCategory, RealnameOK: row.RealnameOK, Result: row.Result, ResultName: stopresume.ResultName(row.Result), Reason: row.Reason, ReasonName: stopresume.ReasonName(row.Reason), } } // stopReasonName 返回当前停机原因名称;空停因显示为"无"。 func stopReasonName(reason string) string { if strings.TrimSpace(reason) == "" { return "无" } return constants.AgentOpenAPIStopReasonName(reason) }