feat: 迁移预警处理审核与备注附件流程

This commit is contained in:
Yvan
2026-11-03 20:40:16 +08:00
parent 7203c34409
commit 838c2c496f
36 changed files with 2312 additions and 13 deletions
+133
View File
@@ -0,0 +1,133 @@
package business
import (
"context"
"encoding/json"
"strconv"
"strings"
"kra/internal/biz/business"
"kra/internal/service/dto"
)
type AlertRecordUsecase interface {
List(context.Context, business.AlertRecordFilter, ...business.ListOption) (*business.AlertRecordPage, error)
Detail(context.Context, uint64) (*business.AlertRecordDetail, error)
Process(context.Context, uint64, business.AlertAction, string) error
AddNote(context.Context, uint64, string, []string) error
}
type AlertRecordService struct{ uc AlertRecordUsecase }
func NewAlertRecordService(uc *business.AlertRecordUsecase) *AlertRecordService {
return &AlertRecordService{uc: uc}
}
func alertRecordResponse(r *business.AlertRecord) *dto.AlertRecordResponse {
metricLabel := ""
if r.MonitorMetric != nil {
metricLabel = business.AlertMetricLabel(*r.MonitorMetric)
}
return &dto.AlertRecordResponse{ID: r.ID, RuleID: r.RuleID, PlatformCode: r.PlatformCode, PlatformName: r.PlatformName, RuleName: r.RuleName, RuleType: r.RuleType, RuleTypeLabel: business.AlertRuleTypeLabel(r.RuleType), MonitorMetric: r.MonitorMetric, MonitorMetricLabel: metricLabel, TargetPromoteID: r.TargetPromoteID, TargetPromoteName: r.TargetPromoteName, BusinessID: r.BusinessID, BusinessName: r.BusinessName, AlertContent: r.AlertContent, Status: r.Status, StatusLabel: business.AlertStatusLabel(r.Status), CreatedAt: localTime(&r.CreatedAt), UpdatedAt: localTime(&r.UpdatedAt)}
}
func (s *AlertRecordService) List(ctx context.Context, req *dto.AlertRecordQuery) (*dto.AlertRecordPage, error) {
if req == nil {
return nil, business.ErrAlertRecordInput
}
if req.MonitorMetric != nil && (*req.MonitorMetric < 1 || *req.MonitorMetric > 3) {
return nil, business.ErrAlertRecordInput
}
if req.Status != nil && (*req.Status < 1 || *req.Status > 5) {
return nil, business.ErrAlertRecordInput
}
page, size := req.Page, req.PageSize
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
if size > 500 {
size = 500
}
if page > int(^uint(0)>>1)/size {
return nil, business.ErrAlertRecordInput
}
filter := business.AlertRecordFilter{TargetPromoteID: req.TargetPromoteID, BusinessID: req.BusinessID, TargetPromoteName: strings.TrimSpace(req.TargetPromoteName), RuleName: strings.TrimSpace(req.RuleName), RuleType: strings.TrimSpace(req.RuleType), BusinessName: strings.TrimSpace(req.BusinessName), MonitorMetric: req.MonitorMetric, Status: req.Status}
seen := map[string]bool{}
for _, code := range strings.Split(req.PlatformCodes, ",") {
code = strings.TrimSpace(code)
if code != "" && !seen[code] {
filter.PlatformCodes = append(filter.PlatformCodes, code)
seen[code] = true
}
}
result, err := s.uc.List(ctx, filter, business.ListOffset((page-1)*size), business.ListLimit(size))
if err != nil {
return nil, err
}
out := &dto.AlertRecordPage{PageData: dto.AlertRecordRows{Rows: []*dto.AlertRecordResponse{}, Total: result.Total}, StatusCount: result.StatusCount, Available: result.Available}
for _, row := range result.Rows {
out.PageData.Rows = append(out.PageData.Rows, alertRecordResponse(row))
}
return out, nil
}
func (s *AlertRecordService) Detail(ctx context.Context, id uint64) (*dto.AlertRecordDetail, error) {
if id == 0 {
return nil, business.ErrAlertRecordInput
}
detail, err := s.uc.Detail(ctx, id)
if err != nil {
return nil, err
}
out := &dto.AlertRecordDetail{AlertRecord: alertRecordResponse(detail.Record), OperationLogs: []*dto.AlertLogResponse{}, CanAudit: detail.CanAudit}
for _, l := range detail.Logs {
out.OperationLogs = append(out.OperationLogs, &dto.AlertLogResponse{ID: l.ID, AlertID: l.AlertID, OperatorID: l.OperatorID, OperatorName: l.OperatorName, LogType: l.LogType, LogTypeLabel: business.AlertLogTypeLabel(l.LogType), ActionDetail: l.ActionDetail, NoteContent: l.NoteContent, Attachments: l.Attachments, CreatedAt: localTime(&l.CreatedAt)})
}
return out, nil
}
func (s *AlertRecordService) Process(ctx context.Context, req *dto.AlertProcessRequest, action business.AlertAction) error {
if req == nil || len(req.Note) > 65535 {
return business.ErrAlertRecordInput
}
id, err := strconv.ParseUint(req.AlertID.String(), 10, 64)
if err != nil || id == 0 {
return business.ErrAlertRecordInput
}
return s.uc.Process(ctx, id, action, req.Note)
}
func (s *AlertRecordService) AddNote(ctx context.Context, req *dto.AlertNoteRequest) error {
if req == nil || len(req.NoteContent) > 65535 {
return business.ErrAlertRecordInput
}
id, err := strconv.ParseUint(req.AlertID.String(), 10, 64)
if err != nil || id == 0 {
return business.ErrAlertRecordInput
}
if strings.TrimSpace(req.NoteContent) == "" {
return business.ErrAlertNoteEmpty
}
values := make([]string, 0, len(req.AttachmentIDs))
for _, raw := range req.AttachmentIDs {
var value string
if json.Unmarshal(raw, &value) != nil {
var number json.Number
if json.Unmarshal(raw, &number) != nil {
return business.ErrAlertRecordInput
}
if _, err := number.Int64(); err != nil {
return business.ErrAlertRecordInput
}
value = number.String()
}
if value == "" {
return business.ErrAlertRecordInput
}
values = append(values, value)
}
return s.uc.AddNote(ctx, id, req.NoteContent, values)
}
@@ -0,0 +1,79 @@
package business
import (
"context"
"encoding/json"
"reflect"
"testing"
"kra/internal/biz/business"
"kra/internal/service/dto"
)
type alertRecordUsecaseFake struct {
AlertRecordUsecase
id uint64
filter business.AlertRecordFilter
attachments []string
}
func (f *alertRecordUsecaseFake) Process(_ context.Context, id uint64, _ business.AlertAction, _ string) error {
f.id = id
return nil
}
func (f *alertRecordUsecaseFake) AddNote(_ context.Context, id uint64, _ string, attachments []string) error {
f.id = id
f.attachments = attachments
return nil
}
func (f *alertRecordUsecaseFake) List(_ context.Context, filter business.AlertRecordFilter, _ ...business.ListOption) (*business.AlertRecordPage, error) {
f.filter = filter
return &business.AlertRecordPage{Rows: []*business.AlertRecord{}, StatusCount: map[int]int64{}, Available: true}, nil
}
func TestAlertRecordServiceAcceptsExactIDsAndAttachmentFormats(t *testing.T) {
f := &alertRecordUsecaseFake{}
s := &AlertRecordService{uc: f}
for _, raw := range []string{`{"alertId":"9007199254740993"}`, `{"alertId":9007199254740993}`} {
var req dto.AlertProcessRequest
if err := json.Unmarshal([]byte(raw), &req); err != nil {
t.Fatal(err)
}
if err := s.Process(context.Background(), &req, business.AlertStart); err != nil || f.id != 9007199254740993 {
t.Fatal("预警 ID 在 DTO 边界失真")
}
}
var req dto.AlertNoteRequest
if err := json.Unmarshal([]byte(`{"alertId":"10","noteContent":"备注","attachmentIds":[9007199254740993,"2","/files/a.png"]}`), &req); err != nil {
t.Fatal(err)
}
if err := s.AddNote(context.Background(), &req); err != nil || !reflect.DeepEqual(f.attachments, []string{"9007199254740993", "2", "/files/a.png"}) {
t.Fatalf("附件解析错误: %+v %v", f.attachments, err)
}
for _, raw := range []string{`null`, `1.5`, `true`, `{"url":"bad"}`} {
req.AttachmentIDs = []json.RawMessage{json.RawMessage(raw)}
if err := s.AddNote(context.Background(), &req); err == nil {
t.Fatalf("错误附件值被接受: %s", raw)
}
}
}
func TestAlertRecordServiceQueryNormalizationAndIDResponse(t *testing.T) {
f := &alertRecordUsecaseFake{}
s := &AlertRecordService{uc: f}
_, err := s.List(context.Background(), &dto.AlertRecordQuery{PlatformCodes: " lx, hw,lx, ", RuleName: " 注册 "})
if err != nil || !reflect.DeepEqual(f.filter.PlatformCodes, []string{"lx", "hw"}) || f.filter.RuleName != "注册" {
t.Fatal("查询参数未归一化")
}
raw, err := json.Marshal(alertRecordResponse(&business.AlertRecord{ID: 9007199254740993, RuleID: 9007199254740995, Status: 3}))
if err != nil {
t.Fatal(err)
}
var value map[string]any
if err = json.Unmarshal(raw, &value); err != nil {
t.Fatal(err)
}
if value["id"] != "9007199254740993" || value["ruleId"] != "9007199254740995" || value["statusLabel"] != "待审核" || value["warningColor"] != nil {
t.Fatalf("响应丢失精度或改变快照语义: %s", raw)
}
}
+81
View File
@@ -0,0 +1,81 @@
package dto
import "encoding/json"
type AlertRecordQuery struct {
Page int `form:"pageNum"`
PageSize int `form:"pageSize"`
PlatformCodes string `form:"platformCodes"`
TargetPromoteID *uint64 `form:"targetPromoteId"`
BusinessID *uint `form:"businessId"`
TargetPromoteName string `form:"targetPromoteName"`
RuleName string `form:"ruleName"`
RuleType string `form:"ruleType"`
BusinessName string `form:"businessName"`
MonitorMetric *int `form:"monitorMetric"`
Status *int `form:"status"`
}
type AlertProcessRequest struct {
AlertID json.Number `json:"alertId"`
Note string `json:"note"`
}
type AlertNoteRequest struct {
AlertID json.Number `json:"alertId"`
NoteContent string `json:"noteContent"`
AttachmentIDs []json.RawMessage `json:"attachmentIds"`
}
type AlertRecordResponse struct {
ID uint64 `json:"id,string"`
PlatformCode string `json:"platformCode"`
PlatformName string `json:"platformName"`
RuleID uint64 `json:"ruleId,string"`
RuleName string `json:"ruleName"`
RuleType string `json:"ruleType"`
RuleTypeLabel string `json:"ruleTypeLabel"`
MonitorMetric *int `json:"monitorMetric"`
MonitorMetricLabel string `json:"monitorMetricLabel"`
// 旧视图声明了颜色,但记录没有对应快照字段,保留空值。
WarningColor *string `json:"warningColor"`
TargetPromoteID uint64 `json:"targetPromoteId"`
TargetPromoteName string `json:"targetPromoteName"`
BusinessID uint `json:"businessId"`
BusinessName string `json:"businessName"`
AlertContent string `json:"alertContent"`
Status int `json:"status"`
StatusLabel string `json:"statusLabel"`
CreatedAt *string `json:"createdAt"`
UpdatedAt *string `json:"updatedAt"`
}
type AlertLogResponse struct {
ID uint64 `json:"id,string"`
AlertID uint64 `json:"alertId,string"`
OperatorID uint `json:"operatorId"`
OperatorName string `json:"operatorName"`
LogType int `json:"logType"`
LogTypeLabel string `json:"logTypeLabel"`
ActionDetail string `json:"actionDetail"`
NoteContent string `json:"noteContent"`
Attachments []string `json:"attachments"`
CreatedAt *string `json:"createdAt"`
}
type AlertRecordRows struct {
Rows []*AlertRecordResponse `json:"rows"`
Total int64 `json:"total"`
}
type AlertRecordPage struct {
PageData AlertRecordRows `json:"pageData"`
StatusCount map[int]int64 `json:"statusCount"`
Available bool `json:"available"`
}
type AlertRecordDetail struct {
AlertRecord *AlertRecordResponse `json:"alertRecord"`
OperationLogs []*AlertLogResponse `json:"operationLogs"`
CanAudit bool `json:"canAudit"`
}
+1
View File
@@ -18,6 +18,7 @@ var ProviderSet = wire.NewSet(
businessservice.NewPasswordResetService,
businessservice.NewBusinessPromoteService,
businessservice.NewAlertRuleService,
businessservice.NewAlertRecordService,
systemservice.ProviderSet,
paymentservice.ProviderSet,
integrationservice.ProviderSet,