1. 新增Audit HTTP Handler (AUD-05, AUD-06完成) - POST /api/v1/audit/events - 创建审计事件(支持幂等) - GET /api/v1/audit/events - 查询事件列表(支持分页和过滤) 2. 提升IAM Middleware测试覆盖率 - 从63.8%提升至83.5% - 新增SetRouteScopePolicy测试 - 新增RequireRole/RequireMinLevel中间件测试 - 新增hasAnyScope测试 TDD完成:33/33任务 (100%)
184 lines
5.3 KiB
Go
184 lines
5.3 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"lijiaoqiao/supply-api/internal/audit/model"
|
|
"lijiaoqiao/supply-api/internal/audit/service"
|
|
)
|
|
|
|
// AuditHandler HTTP处理器
|
|
type AuditHandler struct {
|
|
svc *service.AuditService
|
|
}
|
|
|
|
// NewAuditHandler 创建审计处理器
|
|
func NewAuditHandler(svc *service.AuditService) *AuditHandler {
|
|
return &AuditHandler{svc: svc}
|
|
}
|
|
|
|
// CreateEventRequest 创建事件请求
|
|
type CreateEventRequest struct {
|
|
EventName string `json:"event_name"`
|
|
EventCategory string `json:"event_category"`
|
|
EventSubCategory string `json:"event_sub_category"`
|
|
OperatorID int64 `json:"operator_id"`
|
|
TenantID int64 `json:"tenant_id"`
|
|
ObjectType string `json:"object_type"`
|
|
ObjectID int64 `json:"object_id"`
|
|
Action string `json:"action"`
|
|
IdempotencyKey string `json:"idempotency_key,omitempty"`
|
|
SourceIP string `json:"source_ip,omitempty"`
|
|
Success bool `json:"success"`
|
|
ResultCode string `json:"result_code,omitempty"`
|
|
}
|
|
|
|
// ErrorResponse 错误响应
|
|
type ErrorResponse struct {
|
|
Error string `json:"error"`
|
|
Code string `json:"code,omitempty"`
|
|
Details string `json:"details,omitempty"`
|
|
}
|
|
|
|
// ListEventsResponse 事件列表响应
|
|
type ListEventsResponse struct {
|
|
Events []*model.AuditEvent `json:"events"`
|
|
Total int64 `json:"total"`
|
|
Offset int `json:"offset"`
|
|
Limit int `json:"limit"`
|
|
}
|
|
|
|
// CreateEvent 处理POST /api/v1/audit/events
|
|
// @Summary 创建审计事件
|
|
// @Description 创建新的审计事件,支持幂等
|
|
// @Tags audit
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param event body CreateEventRequest true "事件信息"
|
|
// @Success 201 {object} service.CreateEventResult
|
|
// @Success 200 {object} service.CreateEventResult "幂等重复"
|
|
// @Success 409 {object} service.CreateEventResult "幂等冲突"
|
|
// @Failure 400 {object} ErrorResponse
|
|
// @Failure 500 {object} ErrorResponse
|
|
// @Router /api/v1/audit/events [post]
|
|
func (h *AuditHandler) CreateEvent(w http.ResponseWriter, r *http.Request) {
|
|
var req CreateEventRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "INVALID_REQUEST", "invalid request body: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// 验证必填字段
|
|
if req.EventName == "" {
|
|
writeError(w, http.StatusBadRequest, "MISSING_FIELD", "event_name is required")
|
|
return
|
|
}
|
|
if req.EventCategory == "" {
|
|
writeError(w, http.StatusBadRequest, "MISSING_FIELD", "event_category is required")
|
|
return
|
|
}
|
|
|
|
event := &model.AuditEvent{
|
|
EventName: req.EventName,
|
|
EventCategory: req.EventCategory,
|
|
EventSubCategory: req.EventSubCategory,
|
|
OperatorID: req.OperatorID,
|
|
TenantID: req.TenantID,
|
|
ObjectType: req.ObjectType,
|
|
ObjectID: req.ObjectID,
|
|
Action: req.Action,
|
|
IdempotencyKey: req.IdempotencyKey,
|
|
SourceIP: req.SourceIP,
|
|
Success: req.Success,
|
|
ResultCode: req.ResultCode,
|
|
}
|
|
|
|
result, err := h.svc.CreateEvent(r.Context(), event)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "CREATE_FAILED", err.Error())
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(result.StatusCode)
|
|
json.NewEncoder(w).Encode(result)
|
|
}
|
|
|
|
// ListEvents 处理GET /api/v1/audit/events
|
|
// @Summary 查询审计事件
|
|
// @Description 查询审计事件列表,支持分页和过滤
|
|
// @Tags audit
|
|
// @Produce json
|
|
// @Param tenant_id query int false "租户ID"
|
|
// @Param category query string false "事件类别"
|
|
// @Param event_name query string false "事件名称"
|
|
// @Param offset query int false "偏移量" default(0)
|
|
// @Param limit query int false "限制数量" default(100)
|
|
// @Success 200 {object} ListEventsResponse
|
|
// @Failure 500 {object} ErrorResponse
|
|
// @Router /api/v1/audit/events [get]
|
|
func (h *AuditHandler) ListEvents(w http.ResponseWriter, r *http.Request) {
|
|
filter := &service.EventFilter{}
|
|
|
|
// 解析查询参数
|
|
if tenantIDStr := r.URL.Query().Get("tenant_id"); tenantIDStr != "" {
|
|
tenantID, err := strconv.ParseInt(tenantIDStr, 10, 64)
|
|
if err == nil {
|
|
filter.TenantID = tenantID
|
|
}
|
|
}
|
|
|
|
if category := r.URL.Query().Get("category"); category != "" {
|
|
filter.Category = category
|
|
}
|
|
|
|
if eventName := r.URL.Query().Get("event_name"); eventName != "" {
|
|
filter.EventName = eventName
|
|
}
|
|
|
|
if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" {
|
|
offset, err := strconv.Atoi(offsetStr)
|
|
if err == nil {
|
|
filter.Offset = offset
|
|
}
|
|
}
|
|
|
|
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
|
|
limit, err := strconv.Atoi(limitStr)
|
|
if err == nil && limit > 0 && limit <= 1000 {
|
|
filter.Limit = limit
|
|
}
|
|
}
|
|
|
|
if filter.Limit == 0 {
|
|
filter.Limit = 100
|
|
}
|
|
|
|
events, total, err := h.svc.ListEventsWithFilter(r.Context(), filter)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "QUERY_FAILED", err.Error())
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(ListEventsResponse{
|
|
Events: events,
|
|
Total: total,
|
|
Offset: filter.Offset,
|
|
Limit: filter.Limit,
|
|
})
|
|
}
|
|
|
|
// writeError 写入错误响应
|
|
func writeError(w http.ResponseWriter, status int, code, message string) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
json.NewEncoder(w).Encode(ErrorResponse{
|
|
Error: message,
|
|
Code: code,
|
|
Details: "",
|
|
})
|
|
}
|