refactor: 将客服后端重构为宿主可嵌入模块
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。 - 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。 - 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。 - 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Public and persisted customer-service payload maps are part of the frontend
|
||||
// contract. Keep their literal keys in snake_case. Raw enterprise WeChat
|
||||
// inbound payloads retain the provider's original field names.
|
||||
func TestPublicPayloadMapKeysDoNotUseCamelCase(t *testing.T) {
|
||||
dirs := []string{
|
||||
".",
|
||||
"../builders",
|
||||
"../events",
|
||||
"../handlers/api",
|
||||
"../handlers/dashboard",
|
||||
"../pkg/httpx",
|
||||
"../pkg/utils",
|
||||
}
|
||||
for _, dir := range dirs {
|
||||
fset := token.NewFileSet()
|
||||
packages, err := parser.ParseDir(fset, dir, func(info fs.FileInfo) bool {
|
||||
return !strings.HasSuffix(info.Name(), "_test.go")
|
||||
}, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", dir, err)
|
||||
}
|
||||
for _, pkg := range packages {
|
||||
for filename, file := range pkg.Files {
|
||||
if filepath.Base(filename) == "wxwork_kf_inbound_service.go" {
|
||||
continue
|
||||
}
|
||||
ast.Inspect(file, func(node ast.Node) bool {
|
||||
literal, ok := node.(*ast.CompositeLit)
|
||||
if !ok || !isStringKeyedMap(literal.Type) {
|
||||
return true
|
||||
}
|
||||
for _, element := range literal.Elts {
|
||||
pair, ok := element.(*ast.KeyValueExpr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
key, ok := pair.Key.(*ast.BasicLit)
|
||||
if !ok || key.Kind != token.STRING {
|
||||
continue
|
||||
}
|
||||
name, err := strconv.Unquote(key.Value)
|
||||
if err != nil {
|
||||
t.Errorf("%s: invalid map key %s: %v", filename, key.Value, err)
|
||||
continue
|
||||
}
|
||||
// Dotted keys are internal i18n lookup identifiers, not JSON
|
||||
// property names exposed to clients.
|
||||
if !strings.Contains(name, ".") && containsUppercase(name) {
|
||||
t.Errorf("%s: public payload map key %q must use snake_case", filename, name)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isStringKeyedMap(expression ast.Expr) bool {
|
||||
mapType, ok := expression.(*ast.MapType)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
identifier, ok := mapType.Key.(*ast.Ident)
|
||||
return ok && identifier.Name == "string"
|
||||
}
|
||||
|
||||
func containsUppercase(value string) bool {
|
||||
for _, r := range value {
|
||||
if unicode.IsUpper(r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user