Files
user-system/internal/api/middleware/response_wrapper.go
long-agent 5ca3633be4 feat: 系统全面优化 - 设备管理/登录日志导出/性能监控/设置页面
后端:
- 新增全局设备管理 API(DeviceHandler.GetAllDevices)
- 新增登录日志导出功能(LogHandler.ExportLoginLogs, CSV/XLSX)
- 新增设置服务(SettingsService)和设置页面 API
- 设备管理支持多条件筛选(状态/信任状态/关键词)
- 登录日志支持流式导出防 OOM
- 操作日志支持按方法/时间范围搜索
- 主题配置服务(ThemeService)
- 增强监控健康检查(Prometheus metrics + SLO)
- 移除旧 ratelimit.go(已迁移至 robustness)
- 修复 SocialAccount NULL 扫描问题
- 新增 API 契约测试、Handler 测试、Settings 测试

前端:
- 新增管理员设备管理页面(DevicesPage)
- 新增管理员登录日志导出功能
- 新增系统设置页面(SettingsPage)
- 设备管理支持筛选和分页
- 增强 HTTP 响应类型

测试:
- 业务逻辑测试 68 个(含并发 CONC_001~003)
- 规模测试 16 个(P99 百分位统计)
- E2E 测试、集成测试、契约测试
- 性能基准测试、鲁棒性测试

全面测试通过(38 个测试包)
2026-04-07 12:08:16 +08:00

136 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package middleware
import (
"bytes"
"encoding/json"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
// responseWrapper 捕获 handler 输出的中间件
// 将所有裸 JSON 响应自动包装为 {code: 0, message: "success", data: ...} 格式
type responseWrapper struct {
gin.ResponseWriter
body *bytes.Buffer
statusCode int
}
func (w *responseWrapper) Write(b []byte) (int, error) {
w.body.Write(b)
// 不再同时写到原始 writer让 body 完全缓冲
return len(b), nil
}
func (w *responseWrapper) WriteString(s string) (int, error) {
w.body.WriteString(s)
return len(s), nil
}
func (w *responseWrapper) WriteHeader(code int) {
w.statusCode = code
// 不实际写入,让 gin 的最终写入处理
}
// ResponseWrapper 返回包装响应格式的中间件
func ResponseWrapper() gin.HandlerFunc {
return func(c *gin.Context) {
// 跳过非 JSON 响应(如文件下载、流式响应)
contentType := c.GetHeader("Content-Type")
if strings.Contains(contentType, "text/event-stream") ||
contentType == "application/octet-stream" ||
strings.HasPrefix(c.Request.URL.Path, "/swagger/") {
c.Next()
return
}
// 包装 response writer 以捕获输出
wrapper := &responseWrapper{
ResponseWriter: c.Writer,
body: bytes.NewBuffer(nil),
statusCode: http.StatusOK,
}
c.Writer = wrapper
c.Next()
// 检查是否已标记为已包装
if _, exists := c.Get("response_wrapped"); exists {
// 直接把捕获的内容写回到底层 writer
wrapper.ResponseWriter.WriteHeader(wrapper.statusCode)
wrapper.ResponseWriter.Write(wrapper.body.Bytes())
return
}
// 只处理成功响应2xx
if wrapper.statusCode < 200 || wrapper.statusCode >= 300 {
// 非成功状态,直接把捕获的内容写回
wrapper.ResponseWriter.WriteHeader(wrapper.statusCode)
wrapper.ResponseWriter.Write(wrapper.body.Bytes())
return
}
// 解析捕获的 body
if wrapper.body.Len() == 0 {
wrapper.ResponseWriter.WriteHeader(wrapper.statusCode)
return
}
bodyBytes := wrapper.body.Bytes()
// 尝试解析为 JSON 对象
var raw json.RawMessage
if err := json.Unmarshal(bodyBytes, &raw); err != nil {
// 不是有效 JSON不包装
wrapper.ResponseWriter.WriteHeader(wrapper.statusCode)
wrapper.ResponseWriter.Write(bodyBytes)
return
}
// 检查是否已经是标准格式(有 code 字段)
var checkMap map[string]interface{}
if err := json.Unmarshal(bodyBytes, &checkMap); err == nil {
if _, hasCode := checkMap["code"]; hasCode {
// 已经是标准格式,不重复包装
wrapper.ResponseWriter.WriteHeader(wrapper.statusCode)
wrapper.ResponseWriter.Write(bodyBytes)
return
}
}
// 包装为标准格式
wrapped := map[string]interface{}{
"code": 0,
"message": "success",
"data": raw,
}
wrappedBytes, err := json.Marshal(wrapped)
if err != nil {
wrapper.ResponseWriter.WriteHeader(wrapper.statusCode)
wrapper.ResponseWriter.Write(bodyBytes)
return
}
// 设置响应头并写入包装后的内容
wrapper.ResponseWriter.Header().Set("Content-Type", "application/json")
wrapper.ResponseWriter.WriteHeader(wrapper.statusCode)
wrapper.ResponseWriter.Write(wrappedBytes)
}
}
// WrapResponse 标记响应为已包装,防止重复包装
// handler 中使用 response.Success() 等方法后调用此函数
func WrapResponse(c *gin.Context) {
c.Set("response_wrapped", true)
}
// NoWrapper 跳过包装的中间件处理器
func NoWrapper() gin.HandlerFunc {
return func(c *gin.Context) {
WrapResponse(c)
c.Next()
}
}