Files
jiu/backend/internal/handler/error_report.go
T
wangjia 2dbc89b902 feat: 错误自动上报机制
- 后端:error_reports 表 + POST /api/v1/public/errors + GET /api/v1/admin/errors
- Flutter:ErrorReporter 单例,60s 去重,fire-and-forget,不影响用户
- main.dart:FlutterError 和 Zone 错误自动上报
- 打印异常:catch 块加 reportError,Windows 打印问题可自动采集堆栈

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:08:03 +08:00

117 lines
2.8 KiB
Go

package handler
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/wangjia/jiu/backend/internal/model"
)
type ErrorReportHandler struct{ db *gorm.DB }
func NewErrorReportHandler(db *gorm.DB) *ErrorReportHandler {
return &ErrorReportHandler{db: db}
}
type submitErrorRequest struct {
ErrorType string `json:"error_type" binding:"required"`
AppVersion string `json:"app_version"`
Platform string `json:"platform"`
Username string `json:"username"`
ShopID uint64 `json:"shop_id"`
ShopNo string `json:"shop_no"`
Role string `json:"role"`
ErrorMsg string `json:"error_msg" binding:"required"`
StackTrace string `json:"stack_trace"`
OccurredAt int64 `json:"occurred_at"` // Unix 毫秒
}
// Submit POST /api/v1/public/errors
func (h *ErrorReportHandler) Submit(c *gin.Context) {
var req submitErrorRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
occurredAt := time.Now()
if req.OccurredAt > 0 {
occurredAt = time.UnixMilli(req.OccurredAt)
}
stack := req.StackTrace
const maxStack = 4000
if len(stack) > maxStack {
stack = stack[:maxStack]
}
report := model.ErrorReport{
ErrorType: req.ErrorType,
AppVersion: req.AppVersion,
Platform: req.Platform,
Username: req.Username,
ShopID: req.ShopID,
ShopNo: req.ShopNo,
Role: req.Role,
ClientIP: c.ClientIP(),
ErrorMsg: req.ErrorMsg,
StackTrace: stack,
OccurredAt: occurredAt,
}
if err := h.db.Create(&report).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "submit failed"})
return
}
c.JSON(http.StatusCreated, gin.H{"id": report.ID})
}
// List GET /api/v1/admin/errors (superadmin only)
func (h *ErrorReportHandler) List(c *gin.Context) {
var q struct {
ErrorType string `form:"error_type"`
Username string `form:"username"`
Platform string `form:"platform"`
Page int `form:"page,default=1"`
PageSize int `form:"page_size,default=50"`
}
if err := c.ShouldBindQuery(&q); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if q.PageSize <= 0 || q.PageSize > 200 {
q.PageSize = 50
}
if q.Page <= 0 {
q.Page = 1
}
db := h.db.Model(&model.ErrorReport{})
if q.ErrorType != "" {
db = db.Where("error_type = ?", q.ErrorType)
}
if q.Username != "" {
db = db.Where("username LIKE ?", "%"+q.Username+"%")
}
if q.Platform != "" {
db = db.Where("platform = ?", q.Platform)
}
var total int64
db.Count(&total)
var reports []model.ErrorReport
db.Order("id DESC").Offset((q.Page - 1) * q.PageSize).Limit(q.PageSize).Find(&reports)
c.JSON(http.StatusOK, gin.H{
"data": reports,
"total": total,
"page": q.Page,
"page_size": q.PageSize,
})
}