feat(套餐真流量预警): AUG26-004 真流量预警规则、达量扫描通知与导出
新增 000228 迁移:规则表 tb_package_traffic_alert_rule(每套餐商品至多一条,无软删除,package_id 非部分唯一约束)、达量预警快照表 tb_package_traffic_alert(以主套餐使用记录 + 阈值快照为唯一键, 触发时冻结用量、额度、比例、阈值、到期时间、归属与资产快照),并为 tb_package_usage 新增扫描 范围部分索引 idx_package_usage_alert_scope;down 在预警表存在数据时阻断回滚。 新增规则维护接口 GET/POST/PUT /api/admin/package-traffic-alert-rules(仅超级管理员与平台账号): 创建校验套餐存在且真流量额度大于零,阈值为 1%~100% 的两位小数;修改只影响后续扫描,不回填也 不改写既有预警快照;全部写操作记录操作者、前后值与时间。 新增每日 06:00(Asia/Shanghai)扫描任务 package:traffic:alert:scan,与套餐临期扫描共用 data_cleanup 队列:按资产汇总当前有效套餐的真流量,分子取使用记录真已用量、分母取使用记录真总量快照,命中 主套餐规则阈值时在同一事务创建预警与可靠通知事件;重复执行以唯一冲突视为已处理,不重复投递, 不建停机锁、不调用运营商。 新增预警列表、详情与异步导出 GET /api/admin/package-traffic-alerts、GET /api/admin/package-traffic-alerts/:id、 POST /api/admin/package-traffic-alerts/export,列表与详情一律读冻结快照;新增通知类型 package.traffic.alert 与受控目标 package_traffic_alert_detail,目标解析仅对超级管理员与平台账号 返回可跳转,越权与不存在统一按资源不可见处理。 同步 OpenAPI(cmd/gendocs、cmd/api/docs.go、pkg/openapi/handlers.go)、审计动作与资源注册、上下文 健康检查证据;归档变更并同步 package-traffic-alert 主 Spec。
This commit is contained in:
183
internal/handler/admin/package_traffic_alert.go
Normal file
183
internal/handler/admin/package_traffic_alert.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
packagetrafficalertapp "github.com/break/junhong_cmp_fiber/internal/application/packagetrafficalert"
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/validation"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
packagetrafficalertquery "github.com/break/junhong_cmp_fiber/internal/query/packagetrafficalert"
|
||||
exportTaskService "github.com/break/junhong_cmp_fiber/internal/service/export_task"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
)
|
||||
|
||||
// PackageTrafficAlertHandler 套餐真流量预警 Handler。
|
||||
// 规则维护与预警读取/导出只对超级管理员与平台账号开放,路由组已有角色门禁,
|
||||
// Handler 仍不做任何跳过业务校验的分支。
|
||||
type PackageTrafficAlertHandler struct {
|
||||
ruleService *packagetrafficalertapp.RuleService
|
||||
query *packagetrafficalertquery.Query
|
||||
exportService *exportTaskService.Service
|
||||
validator *validator.Validate
|
||||
}
|
||||
|
||||
// NewPackageTrafficAlertHandler 创建套餐真流量预警 Handler。
|
||||
func NewPackageTrafficAlertHandler(ruleService *packagetrafficalertapp.RuleService,
|
||||
query *packagetrafficalertquery.Query, exportService *exportTaskService.Service,
|
||||
validator *validator.Validate) *PackageTrafficAlertHandler {
|
||||
return &PackageTrafficAlertHandler{ruleService: ruleService, query: query, exportService: exportService, validator: validator}
|
||||
}
|
||||
|
||||
// CreateRule 创建套餐真流量预警规则。
|
||||
// POST /api/admin/package-traffic-alert-rules
|
||||
func (h *PackageTrafficAlertHandler) CreateRule(c *fiber.Ctx) error {
|
||||
var req dto.CreatePackageTrafficAlertRuleRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数格式不正确")
|
||||
}
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, validation.Message("创建套餐真流量预警规则参数不合法", &req, err))
|
||||
}
|
||||
result, err := h.ruleService.Create(c.UserContext(), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, result)
|
||||
}
|
||||
|
||||
// UpdateRule 修改套餐真流量预警规则的阈值、启停与备注。
|
||||
// PUT /api/admin/package-traffic-alert-rules/:id
|
||||
func (h *PackageTrafficAlertHandler) UpdateRule(c *fiber.Ctx) error {
|
||||
ruleID, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil || ruleID == 0 {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的预警规则 ID")
|
||||
}
|
||||
var req dto.UpdatePackageTrafficAlertRuleRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数格式不正确")
|
||||
}
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, validation.Message("修改套餐真流量预警规则参数不合法", &req, err))
|
||||
}
|
||||
result, err := h.ruleService.Update(c.UserContext(), uint(ruleID), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, result)
|
||||
}
|
||||
|
||||
// ListRules 查询套餐真流量预警规则列表。
|
||||
// GET /api/admin/package-traffic-alert-rules
|
||||
func (h *PackageTrafficAlertHandler) ListRules(c *fiber.Ctx) error {
|
||||
var req dto.ListPackageTrafficAlertRuleRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数格式不正确")
|
||||
}
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, validation.Message("查询套餐真流量预警规则参数不合法", &req, err))
|
||||
}
|
||||
result, err := h.query.ListRules(c.UserContext(), req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.SuccessWithPagination(c, result.Items, result.Total, result.Page, result.Size)
|
||||
}
|
||||
|
||||
// ListAlerts 查询套餐真流量达量预警列表。
|
||||
// GET /api/admin/package-traffic-alerts
|
||||
func (h *PackageTrafficAlertHandler) ListAlerts(c *fiber.Ctx) error {
|
||||
var req dto.ListPackageTrafficAlertRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数格式不正确")
|
||||
}
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, validation.Message("查询套餐真流量达量预警参数不合法", &req, err))
|
||||
}
|
||||
if req.NotificationStatus != nil && !constants.IsValidPackageTrafficAlertNotifyStatus(*req.NotificationStatus) {
|
||||
return errors.New(errors.CodeInvalidParam, "通知投递结果必须为 1/2/3/4/5 之一")
|
||||
}
|
||||
result, err := h.query.ListAlerts(c.UserContext(), req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.SuccessWithPagination(c, result.Items, result.Total, result.Page, result.Size)
|
||||
}
|
||||
|
||||
// GetAlert 查询套餐真流量达量预警详情。
|
||||
// GET /api/admin/package-traffic-alerts/:id
|
||||
func (h *PackageTrafficAlertHandler) GetAlert(c *fiber.Ctx) error {
|
||||
alertID, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil || alertID == 0 {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的预警 ID")
|
||||
}
|
||||
result, err := h.query.GetAlert(c.UserContext(), uint(alertID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, result)
|
||||
}
|
||||
|
||||
// ExportAlerts 创建套餐真流量达量预警异步导出任务。
|
||||
// POST /api/admin/package-traffic-alerts/export
|
||||
// 只暴露受控入口;导出任务创建时冻结操作者、筛选、时间范围与可见资产范围。
|
||||
func (h *PackageTrafficAlertHandler) ExportAlerts(c *fiber.Ctx) error {
|
||||
var req dto.ExportPackageTrafficAlertRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数格式不正确")
|
||||
}
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, validation.Message("导出套餐真流量达量预警参数不合法", &req, err))
|
||||
}
|
||||
if req.NotificationStatus != nil && !constants.IsValidPackageTrafficAlertNotifyStatus(*req.NotificationStatus) {
|
||||
return errors.New(errors.CodeInvalidParam, "通知投递结果必须为 1/2/3/4/5 之一")
|
||||
}
|
||||
createRequest := dto.CreateExportTaskRequest{
|
||||
Scene: constants.ExportTaskScenePackageTrafficAlert,
|
||||
Format: req.Format,
|
||||
Query: map[string]interface{}{"filters": exportFilters(req)},
|
||||
}
|
||||
result, err := h.exportService.CreateTask(c.UserContext(), &createRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, result)
|
||||
}
|
||||
|
||||
// exportFilters 把导出请求转换为导出任务的筛选快照。
|
||||
// 时间范围在创建时冻结为 RFC3339 字符串,执行期按触发时间解析为闭区间。
|
||||
func exportFilters(req dto.ExportPackageTrafficAlertRequest) map[string]interface{} {
|
||||
filters := make(map[string]interface{})
|
||||
if req.PackageID != nil {
|
||||
filters["package_id"] = *req.PackageID
|
||||
}
|
||||
if req.ShopID != nil {
|
||||
filters["shop_id"] = *req.ShopID
|
||||
}
|
||||
if req.BusinessOwnerAccountID != nil {
|
||||
filters["business_owner_account_id"] = *req.BusinessOwnerAccountID
|
||||
}
|
||||
if req.AssetType != "" {
|
||||
filters["asset_type"] = req.AssetType
|
||||
}
|
||||
if req.AssetIdentifier != "" {
|
||||
filters["asset_identifier"] = req.AssetIdentifier
|
||||
}
|
||||
if req.ThresholdPercent != nil {
|
||||
filters["threshold_percent"] = *req.ThresholdPercent
|
||||
}
|
||||
if req.StartTime != nil {
|
||||
filters["start_time"] = req.StartTime.UTC().Format("2006-01-02T15:04:05Z07:00")
|
||||
}
|
||||
if req.EndTime != nil {
|
||||
filters["end_time"] = req.EndTime.UTC().Format("2006-01-02T15:04:05Z07:00")
|
||||
}
|
||||
if req.NotificationStatus != nil {
|
||||
filters["notification_status"] = *req.NotificationStatus
|
||||
}
|
||||
return filters
|
||||
}
|
||||
Reference in New Issue
Block a user