- 修复登录参数 username→account - 修复前后端字段名不匹配 (ticketno/contactname/createtime等) - 修复AI分析GLM返回markdown包裹和priority类型问题 - 添加AutoMigrate自动建表 - 统一API路由为 /api/auth/ 前缀 - 添加config.example.yaml,.gitignore排除config.yaml
13 KiB
审查报告: ticket-workbench (Go + Vue)
语言: Go 1.24 / Vue 3 + Arco Design | 后端 19 文件 | 前端 14 文件
🔴 必须修复 (10)
① [detail.vue:7,17,22,117] 前端字段名与后端 API 返回不匹配 — 页面无法正常渲染
改什么: 后端返回 ticketid/contactname/contactphone/createtime (snake_case),前端用 ticket.id/ticket.contactName/ticket.contactPhone/ticket.createdAt (camelCase)。同样 note 用 note.id/note.createdAt 而后端返回 noteid/createtime。
怎么改:
- <a-descriptions-item label="工单编号">{{ ticket.id }}</a-descriptions-item>
+ <a-descriptions-item label="工单编号">{{ ticket.ticketid }}</a-descriptions-item>
- <a-descriptions-item label="联系人">{{ ticket.contactName }}</a-descriptions-item>
+ <a-descriptions-item label="联系人">{{ ticket.contactname }}</a-descriptions-item>
- <a-descriptions-item label="联系电话">{{ ticket.contactPhone || '-' }}</a-descriptions-item>
+ <a-descriptions-item label="联系电话">{{ ticket.contactphone || '-' }}</a-descriptions-item>
- <a-descriptions-item label="创建时间">{{ ticket.createdAt }}</a-descriptions-item>
+ <a-descriptions-item label="创建时间">{{ ticket.createtime }}</a-descriptions-item>
- <a-timeline-item v-for="note in notes" :key="note.id">
+ <a-timeline-item v-for="note in notes" :key="note.noteid">
- <div class="note-time">{{ note.createdAt }}</div>
+ <div class="note-time">{{ note.createtime }}</div>
② [ticket_service.go:23,29] 工单列表默认过滤 bug — 不传 status/priority 时只查 status=0 且 priority=0 的记录
改什么: int16 零值是 0,if status >= 0 对零值成立。用户不传筛选参数时,SQL 自动追加 WHERE status=0 AND priority=0,查不到其他数据。
怎么改: DTO 改为指针类型,service 判 nil:
// dto/ticket.go
type TicketListQuery struct {
- Status int16 `form:"status"`
+ Status *int16 `form:"status"`
Category string `form:"category"`
- Priority int16 `form:"priority"`
+ Priority *int16 `form:"priority"`
Keyword string `form:"keyword"`
Page int `form:"page"`
PageSize int `form:"pageSize"`
}
// ticket_service.go
- if status >= 0 {
- query = query.Where("status = ?", status)
+ if status != nil {
+ query = query.Where("status = ?", *status)
}
- if priority >= 0 {
- query = query.Where("priority = ?", priority)
+ if priority != nil {
+ query = query.Where("priority = ?", *priority)
}
handler 传参也要同步调整:
// ticket_handler.go
- result, err := service.ListTickets(db, query.Status, query.Category, query.Priority, query.Keyword, query.Page, query.PageSize)
+ result, err := service.ListTickets(db, query.Status, query.Category, query.Priority, query.Keyword, query.Page, query.PageSize)
③ [auth_service.go:13] session map 无并发保护 — 并发登录/请求会 panic
改什么: sessions 是裸 map,gin 并发读写会触发 fatal error: concurrent map writes。
怎么改: 加 sync.RWMutex:
+ import "sync"
- var sessions = make(map[string]*model.TicketUser)
+ var (
+ sessions = make(map[string]*model.TicketUser)
+ sessionMu sync.RWMutex
+ )
// Login
+ sessionMu.Lock()
sessions[sessionID] = &user
+ sessionMu.Unlock()
// Logout
+ sessionMu.Lock()
delete(sessions, sessionID)
+ sessionMu.Unlock()
// GetUserBySession
+ sessionMu.RLock()
+ defer sessionMu.RUnlock()
return sessions[sessionID]
④ [config.yaml:4-11] 数据库密码和 AI API Key 硬编码在配置文件中
改什么: Lake@2019 和 GLM API Key 明文写在版本控制的 config.yaml 中。
怎么改: config.yaml 加入 .gitignore,创建 config.example.yaml 作为模板。程序支持环境变量覆盖:
// config.go Load 函数中加入
v.AutomaticEnv()
v.SetEnvPrefix("TKT")
⑤ [detail.vue:238-241] 状态更新调错 API — 用了 updateTicket 而非 updateTicketStatus
改什么: handleUpdateStatus 调用 updateTicket(id, { status }),对应 PUT /tickets/:id,但该接口不处理 status 字段变更。
怎么改:
async function handleUpdateStatus(status: number) {
try {
- await updateTicket(ticketId, { status })
+ await updateTicketStatus(ticketId, status)
Message.success('状态更新成功')
fetchDetail()
⑥ [detail.vue:214-236] AI 确认调错 API — 用 updateTicket 替代了 confirmAnalysis
改什么: handleConfirmAnalysis 调用 updateTicket 传 aiAnalysis 和 suggestedRole 字段,但后端 UpdateTicket 不处理这些字段。
怎么改:
async function handleConfirmAnalysis() {
confirming.value = true
try {
- const aiAnalysis = JSON.stringify({
- category: analysisForm.category,
- priority: analysisForm.priority,
- summary: analysisForm.summary,
- suggestedRole: analysisForm.suggestedRole
- })
- await updateTicket(ticketId, {
- category: analysisForm.category,
- priority: analysisForm.priority,
- aiAnalysis,
- suggestedRole: analysisForm.suggestedRole
- })
+ await confirmAnalysis(ticketId, {
+ category: analysisForm.category,
+ priority: analysisForm.priority,
+ summary: analysisForm.summary
+ })
Message.success('确认成功')
fetchDetail()
同时需要在 import 中引入 confirmAnalysis:
import {
getTicketDetail,
- updateTicket,
analyzeTicket,
+ confirmAnalysis,
getTicketNotes,
createNote
} from '@/api/ticket'
⑦ [auth_handler.go:52] /auth/me 返回空 account — middleware 未设置 account 到 context
改什么: c.GetString("account") 取不到值,Auth 中间件只设置了 userid/username/role/team,没有 account。
怎么改:
// middleware/auth.go
c.Set("userid", user.Userid)
c.Set("username", user.Username)
+ c.Set("account", user.Account)
c.Set("role", user.Role)
c.Set("team", user.Team)
⑧ [detail.vue:178,180] ticketData.assignee 和 ticketData.aiAnalysis 字段不存在
改什么: Ticket model 没有 assignee 和 aiAnalysis 字段,访问 undefined 对象导致运行时问题。AI 分析需通过 getAnalysis(ticketId) 单独获取。
怎么改:
async function fetchDetail() {
loading.value = true
try {
- const [ticketData, notesData] = await Promise.all([
+ const [ticketData, notesData, analysisData] = await Promise.all([
getTicketDetail(ticketId),
- getTicketNotes(ticketId)
+ getTicketNotes(ticketId),
+ getAnalysis(ticketId).catch(() => [])
])
ticket.value = ticketData
notes.value = notesData
- assignee.value = ticketData.assignee || ''
- if (ticketData.aiAnalysis) {
- try {
- const analysis = JSON.parse(ticketData.aiAnalysis)
+ if (analysisData && analysisData.length > 0) {
+ const latest = analysisData[0]
analysisForm.category = latest.category || ''
analysisForm.priority = latest.priority || 0
analysisForm.summary = latest.summary || ''
- analysisForm.suggestedRole = analysis.suggested_role || analysis.suggestedRole || ''
+ analysisForm.suggestedRole = latest.suggestrole || ''
- } catch {
- analysisForm.summary = ticketData.aiAnalysis
- }
}
同时需要在 import 中引入 getAnalysis:
import {
getTicketDetail,
updateTicketStatus,
analyzeTicket,
+ getAnalysis,
+ confirmAnalysis,
getTicketNotes,
createNote
} from '@/api/ticket'
⑨ [analysis_service.go:93] AI 返回 JSON 解析过于脆弱 — 没处理 markdown 代码块包裹
改什么: AI 模型经常返回 ```json {...} ``` 格式,直接 json.Unmarshal 会失败。
怎么改:
+ import "strings"
content := glmResp.Choices[0].Message.Content
+ // 去除 markdown 代码块包裹
+ content = strings.TrimSpace(content)
+ if strings.HasPrefix(content, "```") {
+ if idx := strings.Index(content, "\n"); idx >= 0 {
+ content = content[idx+1:]
+ }
+ if idx := strings.LastIndex(content, "```"); idx >= 0 {
+ content = content[:idx]
+ }
+ content = strings.TrimSpace(content)
+ }
var result AnalysisResult
- if err := json.Unmarshal([]byte(glmResp.Choices[0].Message.Content), &result); err != nil {
+ if err := json.Unmarshal([]byte(content), &result); err != nil {
⑩ [main.go] 缺少 AutoMigrate — 首次启动表不存在会报错
改什么: 没有 db.AutoMigrate(...) 调用,数据库表需手动创建。
怎么改:
sqlDB.SetMaxOpenConns(100)
+ if err := db.AutoMigrate(
+ &model.TicketUser{},
+ &model.TicketInfo{},
+ &model.TicketAiAnalysis{},
+ &model.TicketNote{},
+ &model.TicketOperationLog{},
+ ); err != nil {
+ log.Fatalf("Failed to auto migrate: %v", err)
+ }
gin.SetMode(gin.ReleaseMode)
🟡 建议改进 (7)
⑪ [auth_service.go:15-18] MD5 哈希密码不安全
说明: MD5 可快速碰撞/彩虹表。生产环境应使用 bcrypt。
⑫ [analysis_service.go:41-54] AI prompt 注入风险
说明: 用户提交的工单内容直接拼入 prompt,恶意用户可构造内容操控 AI 输出格式/内容。应对用户输入做转义或用 structured prompt 模式。
⑬ [ticket_service.go:111] UpdateTicket 不检查记录是否存在
说明: 传入不存在的 ticketid 时 Updates 不报错(0 rows affected),返回成功。应检查 RowsAffected 或先用 First 确认存在。
⑭ [analysis_service.go:64,81] json.Marshal 和 io.ReadAll 错误被忽略
说明: jsonData, _ := json.Marshal(reqBody) 和 body, _ := io.ReadAll(resp.Body) 都丢弃了 error。
⑮ [detail.vue:89-99] 处理人下拉硬编码 admin/user1/user2
说明: 应从后端获取用户列表,或至少与数据库用户数据一致。当前硬编码值 admin/user1/user2 与后端不对应。
⑯ [create.vue:66] form 提交 submitterid: 1 硬编码
说明: 后端已从 Auth 中间件获取真实 userid,此字段多余且误导。应移除。
⑰ [store/user.ts:21] login 存 username 用的是 account 而非实际 username
说明: setUsername(account) 存的是登录账号,不是用户真实姓名。应使用 res.user.username。
async function login(account: string, password: string) {
const res = await loginApi({ account, password })
setToken(res.token)
- setUsername(account)
+ setUsername(res.user.username)
}
⚪ 可选优化 (3)
⑱ [ticket_service.go:104] if priority >= 0 导致 priority=0 无法区分"未传"和"P0紧急"
说明: 与问题②同类,UpdateTicket 的 priority 参数也应改为指针类型。
⑲ [ticket_service.go:58] ticketno 用 UUID 前 8 位有碰撞风险
说明: uuid.New().String()[:8] 理论上可能重复。可加时间戳或用更长前缀。
⑳ [analysis_service.go:30] AnalyzeTicket 参数过多
说明: 8 个参数的函数签名过长,建议传入结构体。
✅ 亮点
- 后端分层清晰 (handler/service/model/dto)
- 前端类型定义完整,API 层封装规范
- 操作日志设计完善,关键动作都有记录
- CORS 中间件处理周全
📊 摘要
| # | 等级 | 文件:行 | 修改内容 |
|---|---|---|---|
| ① | 🔴 | detail.vue:7,17,22 | 前端字段名与后端不匹配,页面无法渲染 |
| ② | 🔴 | ticket_service.go:23 | 列表默认过滤 bug,不传参只查 status=0 |
| ③ | 🔴 | auth_service.go:13 | session map 无锁,并发 panic |
| ④ | 🔴 | config.yaml:4-11 | 密码和 API Key 硬编码 |
| ⑤ | 🔴 | detail.vue:238 | 状态更新调错 API |
| ⑥ | 🔴 | detail.vue:214 | AI 确认调错 API |
| ⑦ | 🔴 | auth_handler.go:52 | /auth/me 返回空 account |
| ⑧ | 🔴 | detail.vue:178 | 访问不存在的 ticket.assignee/aiAnalysis 字段 |
| ⑨ | 🔴 | analysis_service.go:93 | AI JSON 解析未处理 markdown 包裹 |
| ⑩ | 🔴 | main.go | 缺少 AutoMigrate |
| ⑪ | 🟡 | auth_service.go:15 | MD5 密码哈希不安全 |
| ⑫ | 🟡 | analysis_service.go:41 | AI prompt 注入风险 |
| ⑬ | 🟡 | ticket_service.go:111 | 更新不检查记录是否存在 |
| ⑭ | 🟡 | analysis_service.go:64,81 | 错误被忽略 |
| ⑮ | 🟡 | detail.vue:89 | 处理人下拉硬编码 |
| ⑯ | 🟡 | create.vue:66 | submitterid 硬编码 |
| ⑰ | 🟡 | store/user.ts:21 | username 存为 account |
| ⑱ | ⚪ | ticket_service.go:104 | priority 零值歧义 |
| ⑲ | ⚪ | ticket_service.go:58 | ticketno 碰撞风险 |
| ⑳ | ⚪ | analysis_service.go:30 | 函数参数过多 |
总计: 🔴10 🟡7 ⚪3
总体评价: 前端 detail.vue 与后端严重脱节,核心流程跑不通;后端有并发安全隐患和过滤逻辑 bug
质量评级: 需重构