refactor: 将客服后端重构为宿主可嵌入模块
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。 - 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。 - 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。 - 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHandlerRequestParametersUseSnakeCase(t *testing.T) {
|
||||
for _, dir := range []string{".", "../api", "../../middleware", "../../pkg/httpx"} {
|
||||
fset := token.NewFileSet()
|
||||
packages, err := parser.ParseDir(fset, dir, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("parse handler directory %s: %v", dir, err)
|
||||
}
|
||||
|
||||
for _, pkg := range packages {
|
||||
for filename, file := range pkg.Files {
|
||||
ast.Inspect(file, func(node ast.Node) bool {
|
||||
switch value := node.(type) {
|
||||
case *ast.CallExpr:
|
||||
checkHandlerParameterCall(t, filename, value)
|
||||
case *ast.CompositeLit:
|
||||
checkQueryFilterParameter(t, filename, value)
|
||||
case *ast.Field:
|
||||
checkHandlerRequestTag(t, filename, value)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkHandlerParameterCall(t *testing.T, filename string, call *ast.CallExpr) {
|
||||
if identifier, ok := call.Fun.(*ast.Ident); ok {
|
||||
if identifier.Name == "requestExternalValue" && len(call.Args) > 2 {
|
||||
checkSnakeCaseStringLiteral(t, filename, call.Args[2])
|
||||
}
|
||||
return
|
||||
}
|
||||
selector, ok := call.Fun.(*ast.SelectorExpr)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
identifier, ok := selector.X.(*ast.Ident)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
argIndex := -1
|
||||
if identifier.Name == "params" {
|
||||
switch selector.Sel.Name {
|
||||
case "Get", "GetInt", "GetInt64", "GetBool", "GetTime", "GetInt64Arr", "FormValue":
|
||||
argIndex = 1
|
||||
}
|
||||
} else if identifier.Name == "ctx" {
|
||||
switch selector.Sel.Name {
|
||||
case "Query", "DefaultQuery", "GetQuery", "QueryArray", "PostForm", "PostFormArray":
|
||||
argIndex = 0
|
||||
}
|
||||
}
|
||||
if argIndex < 0 || len(call.Args) <= argIndex {
|
||||
return
|
||||
}
|
||||
checkSnakeCaseStringLiteral(t, filename, call.Args[argIndex])
|
||||
}
|
||||
|
||||
func checkQueryFilterParameter(t *testing.T, filename string, literal *ast.CompositeLit) {
|
||||
selector, ok := literal.Type.(*ast.SelectorExpr)
|
||||
if !ok || selector.Sel.Name != "QueryFilter" {
|
||||
return
|
||||
}
|
||||
identifier, ok := selector.X.(*ast.Ident)
|
||||
if !ok || identifier.Name != "params" {
|
||||
return
|
||||
}
|
||||
for _, element := range literal.Elts {
|
||||
pair, ok := element.(*ast.KeyValueExpr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
key, ok := pair.Key.(*ast.Ident)
|
||||
if ok && key.Name == "ParamName" {
|
||||
checkSnakeCaseStringLiteral(t, filename, pair.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkHandlerRequestTag(t *testing.T, filename string, field *ast.Field) {
|
||||
if field.Tag == nil {
|
||||
return
|
||||
}
|
||||
rawTag, err := strconv.Unquote(field.Tag.Value)
|
||||
if err != nil {
|
||||
t.Errorf("%s: invalid struct tag %s: %v", filename, field.Tag.Value, err)
|
||||
return
|
||||
}
|
||||
for _, key := range []string{"json", "form", "query", "uri"} {
|
||||
name := strings.Split(reflect.StructTag(rawTag).Get(key), ",")[0]
|
||||
if name != "" && name != "-" && !isSnakeCaseHandlerParameter(name) {
|
||||
t.Errorf("%s: %s tag %q must use snake_case", filename, key, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkSnakeCaseStringLiteral(t *testing.T, filename string, expression ast.Expr) {
|
||||
literal, ok := expression.(*ast.BasicLit)
|
||||
if !ok || literal.Kind != token.STRING {
|
||||
return
|
||||
}
|
||||
name, err := strconv.Unquote(literal.Value)
|
||||
if err != nil {
|
||||
t.Errorf("%s: invalid request parameter %s: %v", filename, literal.Value, err)
|
||||
return
|
||||
}
|
||||
if !isSnakeCaseHandlerParameter(name) {
|
||||
t.Errorf("%s: request parameter %q must use snake_case", filename, name)
|
||||
}
|
||||
}
|
||||
|
||||
func isSnakeCaseHandlerParameter(name string) bool {
|
||||
for _, r := range name {
|
||||
if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '_' {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return name != ""
|
||||
}
|
||||
Reference in New Issue
Block a user