2dbc89b902
- 后端: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>
117 lines
2.8 KiB
Go
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,
|
|
})
|
|
}
|