# 审查报告: 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`。 **怎么改:** ```diff - {{ ticket.id }} + {{ ticket.ticketid }} - {{ ticket.contactName }} + {{ ticket.contactname }} - {{ ticket.contactPhone || '-' }} + {{ ticket.contactphone || '-' }} - {{ ticket.createdAt }} + {{ ticket.createtime }} - + -
{{ note.createdAt }}
+
{{ note.createtime }}
``` --- ### ② [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: ```diff // 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 传参也要同步调整: ```diff // 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: ```diff + 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` 作为模板。程序支持环境变量覆盖: ```go // config.go Load 函数中加入 v.AutomaticEnv() v.SetEnvPrefix("TKT") ``` --- ### ⑤ [detail.vue:238-241] 状态更新调错 API — 用了 updateTicket 而非 updateTicketStatus **改什么:** `handleUpdateStatus` 调用 `updateTicket(id, { status })`,对应 `PUT /tickets/:id`,但该接口不处理 status 字段变更。 **怎么改:** ```diff 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` 不处理这些字段。 **怎么改:** ```diff 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`: ```diff 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`。 **怎么改:** ```diff // 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)` 单独获取。 **怎么改:** ```diff 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`: ```diff import { getTicketDetail, updateTicketStatus, analyzeTicket, + getAnalysis, + confirmAnalysis, getTicketNotes, createNote } from '@/api/ticket' ``` --- ### ⑨ [analysis_service.go:93] AI 返回 JSON 解析过于脆弱 — 没处理 markdown 代码块包裹 **改什么:** AI 模型经常返回 `` ```json {...} ``` `` 格式,直接 `json.Unmarshal` 会失败。 **怎么改:** ```diff + 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(...)` 调用,数据库表需手动创建。 **怎么改:** ```diff 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`。 ```diff 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 **质量评级: 需重构**