feat: 微信公众号技能包重命名(weixin_public_account)并增强功能
- 将 wechat_official_account 重命名为 weixin_public_account,符合中文命名规范 - 新增 DOCX 文档生成技能、聊天历史、请求 ID 中间件 - 增强工作流、热点服务、文章服务等模块功能 - 前端同步重命名组件和 API - 新增架构文档 AR13/AR14、专员文档更新 - 补充测试用例(seed_specialists_test, db_migration_test) Co-Authored-AI: yes
This commit is contained in:
@@ -2,36 +2,117 @@ package web
|
||||
|
||||
import "net/http"
|
||||
|
||||
// AppError 业务错误(对齐 Python core/errors.py 的错误码与状态码)
|
||||
// 错误分类(category)—— 表达展示策略、跳转策略、重试策略、监控聚类,使用小写 snake_case。
|
||||
const (
|
||||
CatValidation = "validation" // 参数不合法 / 表单缺失 / 格式错误
|
||||
CatAuth = "auth" // 认证失败:未登录、会话过期、令牌无效
|
||||
CatPermission = "permission" // 权限不足
|
||||
CatNotFound = "not_found" // 资源不存在
|
||||
CatConflict = "conflict" // 状态冲突:重复创建、并发更新
|
||||
CatRateLimit = "rate_limit" // 限流:请求过快
|
||||
CatDependency = "dependency" // 外部依赖错误:第三方 API、数据库、LLM
|
||||
CatProviderQuota = "provider_quota" // 第三方额度不足:AI 积分耗尽
|
||||
CatServer = "server" // 系统内部异常:未捕获、代码 bug
|
||||
)
|
||||
|
||||
// 稳定业务错误码 —— 分段:1000 通用 / 2000 认证 / 3000 权限 / 6000 AI / 9000 系统。
|
||||
// 同一语义稳定复用,不允许同一错误码被不同语义复用。
|
||||
const (
|
||||
CodeOK = 0 // 成功(response.go OK 信封使用)
|
||||
CodeValidation = 1000 // 通用参数错误
|
||||
CodeNotFound = 1001 // 资源不存在
|
||||
CodeConflict = 1002 // 状态冲突 / 重复创建
|
||||
CodeRateLimit = 1003 // 限流
|
||||
CodeUnauthorized = 2000 // 未认证 / 会话过期 / 令牌无效
|
||||
CodeForbidden = 3000 // 权限不足
|
||||
CodeLLMNotConfigured = 6001 // LLM 未配置
|
||||
CodeLLMRequestFailed = 6002 // LLM 请求失败
|
||||
CodePointsExhausted = 6003 // AI 积分耗尽
|
||||
CodeInternal = 9000 // 系统内部错误(未捕获异常兜底)
|
||||
)
|
||||
|
||||
// 各分类默认的下一步操作建议(hint):message 回答"发生了什么",hint 回答"下一步怎么做"。
|
||||
const (
|
||||
DefaultHintValidation = "请检查输入内容、必填项和字段格式后重试。"
|
||||
DefaultHintAuth = "请重新登录后再试。"
|
||||
DefaultHintPermission = "请确认当前账号是否拥有相应权限,或联系管理员授权。"
|
||||
DefaultHintNotFound = "请确认资源标识是否正确,或刷新列表后再试。"
|
||||
DefaultHintConflict = "请刷新页面确认当前状态后,再重试操作。"
|
||||
DefaultHintRateLimit = "操作过于频繁,请稍后重试。"
|
||||
DefaultHintLLMNotConfig = "请先在系统配置中启用并配置 AI 模型路由后重试。"
|
||||
DefaultHintLLMRequest = "请稍后重试;如持续失败,请检查 AI 服务商状态与网络链路。"
|
||||
DefaultHintPointsExhaust = "请检查账户积分余额,或联系管理员补充后重试。"
|
||||
DefaultHintInternal = "请稍后重试;如持续失败,请将请求编号反馈给开发排查。"
|
||||
)
|
||||
|
||||
// AppError 统一业务错误模型:HTTP 状态码 + 稳定业务错误码 + 分类 + 用户提示。
|
||||
// 对齐项目错误治理规范,为"协议语义(状态码) / 稳定编号(code) / 行为分流(category)
|
||||
// / 用户可理解(message+hint) / 开发可定位(request_id+details)"四层模型。
|
||||
type AppError struct {
|
||||
StatusCode int
|
||||
Code string
|
||||
Code int
|
||||
Message string
|
||||
Category string
|
||||
Hint string
|
||||
Details any
|
||||
RequestID string
|
||||
}
|
||||
|
||||
func (e *AppError) Error() string { return e.Message }
|
||||
|
||||
// NewBadRequest 参数错误(保持旧签名,向后兼容,内部自动携带稳定 code/category/hint)。
|
||||
func NewBadRequest(msg string) *AppError {
|
||||
return &AppError{http.StatusBadRequest, "bad_request", msg}
|
||||
return &AppError{http.StatusBadRequest, CodeValidation, msg, CatValidation, DefaultHintValidation, nil, ""}
|
||||
}
|
||||
|
||||
// NewAuthError 认证失败(未登录 / 会话过期 / 令牌无效)。
|
||||
func NewAuthError(msg string) *AppError {
|
||||
return &AppError{http.StatusUnauthorized, "unauthorized", msg}
|
||||
return &AppError{http.StatusUnauthorized, CodeUnauthorized, msg, CatAuth, DefaultHintAuth, nil, ""}
|
||||
}
|
||||
|
||||
// NewForbiddenError 权限不足。
|
||||
func NewForbiddenError(msg string) *AppError {
|
||||
return &AppError{http.StatusForbidden, "forbidden", msg}
|
||||
return &AppError{http.StatusForbidden, CodeForbidden, msg, CatPermission, DefaultHintPermission, nil, ""}
|
||||
}
|
||||
|
||||
// NewNotFoundError 资源不存在。
|
||||
func NewNotFoundError(msg string) *AppError {
|
||||
return &AppError{http.StatusNotFound, "not_found", msg}
|
||||
return &AppError{http.StatusNotFound, CodeNotFound, msg, CatNotFound, DefaultHintNotFound, nil, ""}
|
||||
}
|
||||
|
||||
// NewConflictError 状态冲突 / 重复创建。
|
||||
func NewConflictError(msg string) *AppError {
|
||||
return &AppError{http.StatusConflict, "conflict", msg}
|
||||
return &AppError{http.StatusConflict, CodeConflict, msg, CatConflict, DefaultHintConflict, nil, ""}
|
||||
}
|
||||
|
||||
// NewLLMNotConfigured LLM 路由未配置。
|
||||
func NewLLMNotConfigured(msg string) *AppError {
|
||||
return &AppError{http.StatusNotImplemented, "llm_not_configured", msg}
|
||||
return &AppError{http.StatusNotImplemented, CodeLLMNotConfigured, msg, CatDependency, DefaultHintLLMNotConfig, nil, ""}
|
||||
}
|
||||
|
||||
// NewLLMError LLM 请求失败(外部依赖)。
|
||||
func NewLLMError(msg string) *AppError {
|
||||
return &AppError{http.StatusBadGateway, "llm_request_failed", msg}
|
||||
return &AppError{http.StatusBadGateway, CodeLLMRequestFailed, msg, CatDependency, DefaultHintLLMRequest, nil, ""}
|
||||
}
|
||||
|
||||
// NewPointsExhausted AI 积分耗尽。
|
||||
func NewPointsExhausted(msg string) *AppError {
|
||||
return &AppError{http.StatusPaymentRequired, "ai_points_exhausted", msg}
|
||||
return &AppError{http.StatusPaymentRequired, CodePointsExhausted, msg, CatProviderQuota, DefaultHintPointsExhaust, nil, ""}
|
||||
}
|
||||
|
||||
// NewServerError 系统内部错误(未捕获异常兜底)。
|
||||
func NewServerError(msg string, details any) *AppError {
|
||||
return &AppError{http.StatusInternalServerError, CodeInternal, msg, CatServer, DefaultHintInternal, details, ""}
|
||||
}
|
||||
|
||||
// WithHint 覆写提示语(链式),用于需要给出更具体下一步建议的场景。
|
||||
func (e *AppError) WithHint(hint string) *AppError {
|
||||
e.Hint = hint
|
||||
return e
|
||||
}
|
||||
|
||||
// WithDetails 附带结构化开发上下文(链式),禁止塞大段日志/堆栈。
|
||||
func (e *AppError) WithDetails(details any) *AppError {
|
||||
e.Details = details
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -1,17 +1,78 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// OK 成功响应信封 {"data": ..., "error": null, "message": "success"}
|
||||
func OK(c *gin.Context, data any) {
|
||||
c.JSON(http.StatusOK, gin.H{"data": data, "error": nil, "message": "success"})
|
||||
// CtxRequestID Gin Context 中存放 request_id 的键(由 middleware.RequestID 写入)。
|
||||
const CtxRequestID = "req_id"
|
||||
|
||||
// RequestIDOf 从 Gin Context 读取本次请求的 request_id(无则返回空串)。
|
||||
func RequestIDOf(c *gin.Context) string {
|
||||
if v, ok := c.Get(CtxRequestID); ok {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Fail 错误响应信封 {"data": null, "error": ..., "message": ...}
|
||||
// OK 成功响应信封 {"code":0,"message":"success","data":...,"error":null}
|
||||
func OK(c *gin.Context, data any) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": CodeOK,
|
||||
"message": "success",
|
||||
"data": data,
|
||||
"error": nil,
|
||||
})
|
||||
}
|
||||
|
||||
// Fail 错误响应信封,统一输出四层模型:
|
||||
//
|
||||
// {"code": 稳定业务错误码, "message": 用户主提示, "data": null,
|
||||
// "error": {"category":分类, "status":HTTP状态码, "hint":下一步建议, "request_id":追踪编号, "details":开发上下文}}
|
||||
//
|
||||
// 并回写 X-Request-ID 响应头,便于前后端与日志联查。
|
||||
func Fail(c *gin.Context, e *AppError) {
|
||||
c.JSON(e.StatusCode, gin.H{"data": nil, "error": e.Code, "message": e.Message})
|
||||
requestID := RequestIDOf(c)
|
||||
e.RequestID = requestID
|
||||
c.Header("X-Request-ID", requestID)
|
||||
|
||||
errorObj := gin.H{
|
||||
"category": e.Category,
|
||||
"status": e.StatusCode,
|
||||
"hint": e.Hint,
|
||||
"request_id": requestID,
|
||||
}
|
||||
if e.Details != nil {
|
||||
errorObj["details"] = e.Details
|
||||
}
|
||||
|
||||
logFail(c, e)
|
||||
|
||||
c.JSON(e.StatusCode, gin.H{
|
||||
"code": e.Code,
|
||||
"message": e.Message,
|
||||
"data": nil,
|
||||
"error": errorObj,
|
||||
})
|
||||
}
|
||||
|
||||
// logFail 记录结构化错误日志,满足可定位要求:request_id / method / path / status / code / category。
|
||||
func logFail(c *gin.Context, e *AppError) {
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
req := c.Request
|
||||
method := ""
|
||||
path := ""
|
||||
if req != nil {
|
||||
method = req.Method
|
||||
path = req.URL.Path
|
||||
}
|
||||
log.Printf("HTTP ERROR [%s] status=%d code=%d %s %s -> %s request_id=%s",
|
||||
e.Category, e.StatusCode, e.Code, method, path, e.Message, e.RequestID)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user