All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m26s
统一停复机规则转正的第一次发布:总开关仍关闭,现有停复机行为不变。 - 新增停复机规则演练:GET /stop-resume-rule/dry-run 按新规则对开机与停机卡只判断不执行,返回运营商账户 × 停机原因 × 判断结果汇总与分页明细,可假设通道实名管控配置;POST /stop-resume-rule/dry-run/export 导出明细(场景 stop_resume_rule_dry_run);仅超级管理员与平台账号 - 风险停机、已销户关闭轮询不再限于独立卡,只关该卡自身轮询,不级联设备 - 错误分类规则增加 auto_disable_polling,仅终态类别可勾选;停复机(含设备停复机)命中该规则时不重试、关闭该卡轮询、回写网关扩展状态并写审计 iot_card.terminal_disable_polling - 迁移 000250 加列与约束,并为"已销户""仅支持风险复机"置位
182 lines
5.4 KiB
Go
182 lines
5.4 KiB
Go
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)
|
|
}
|