# 审查报告: 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
**质量评级: 需重构**