feat: 新增语音转文字(ASR)功能

- 后端:新增 /api/audio/transcribe 接口,调用 Ollama whisper 进行语音识别
- 前端:新增 AudioTranscribePage.vue 页面,支持 MP3/WAV/M4A/OGG/FLAC 等格式
- 注册路由、工具卡片、智能助手欢迎语更新

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
eaiadmin
2026-09-14 00:53:36 +08:00
co-authored by Claude Code
parent 752c7837ef
commit 0455f064ac
299 changed files with 1588 additions and 432 deletions
@@ -0,0 +1,90 @@
package api
import (
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"eai_agentplatform/backend/internal/middleware"
"eai_agentplatform/backend/internal/model"
"eai_agentplatform/backend/internal/store"
"eai_agentplatform/backend/internal/web"
)
// —— 积分规则(游戏化成长值,集中一处便于调整,见 docs/changelog.md 待确认项)——
const (
ptFirstCompany = 10 // 首次浏览公司介绍
ptFirstProduct = 2 // 首次浏览单个产品
ptFirstCourse = 5 // 首次浏览单门课程
ptSelfTestSubmit = 5 // 自测提交一次
ptFormalPass = 50 // 正式考试通过
ptMistakeResolved = 3 // 错题标记已掌握
)
// awardPoints 记录积分流水并累加到 user.learning_points(points<=0 直接忽略,避免负分刷分)。
func awardPoints(userID uint, eventType string, points int, refType string, refID uint) {
if userID == 0 || points <= 0 {
return
}
store.DB.Create(&model.PointEvent{
UserID: userID,
EventType: eventType,
Points: points,
RefType: refType,
RefID: refID,
})
store.DB.Model(&model.User{}).Where("id = ?", userID).
UpdateColumn("learning_points", gorm.Expr("learning_points + ?", points))
}
// awardFirstView 首次浏览公司/产品/课程加分(由 RecordLearningProgress 幂等触发)。
func awardFirstView(userID uint, itemType string, itemID uint) {
switch itemType {
case "company":
awardPoints(userID, "first_company", ptFirstCompany, itemType, itemID)
case "product":
awardPoints(userID, "first_product", ptFirstProduct, itemType, itemID)
case "course":
awardPoints(userID, "first_course", ptFirstCourse, itemType, itemID)
}
}
// MyPoints GET /api/points/me —— 我的积分 + 最近积分流水
func MyPoints(c *gin.Context) {
u := middleware.CurrentUser(c)
if u == nil {
web.Fail(c, web.NewAuthError("未登录"))
return
}
var total int
store.DB.Raw(`SELECT COALESCE(SUM(points), 0) FROM point_event WHERE user_id = ?`, u.ID).Scan(&total)
var events []model.PointEvent
store.DB.Where("user_id = ?", u.ID).Order("created_at DESC, id DESC").Limit(50).Find(&events)
web.OK(c, gin.H{"total": total, "events": events})
}
// Leaderboard GET /api/points/leaderboard —— 学习积分排行榜(全员,含我的名次)
func Leaderboard(c *gin.Context) {
u := middleware.CurrentUser(c)
var users []model.User
store.DB.Where("role = ? AND status = ?", "employee", "active").
Order("learning_points DESC, id ASC").Find(&users)
out := make([]gin.H, 0, len(users))
myRank := 0
for i, usr := range users {
out = append(out, gin.H{
"rank": i + 1,
"user_id": usr.ID,
"full_name": usr.FullName,
"department": usr.Department,
"learning_points": usr.LearningPoints,
"is_me": u != nil && usr.ID == u.ID,
})
if u != nil && usr.ID == u.ID {
myRank = i + 1
}
}
web.OK(c, gin.H{"items": out, "my_rank": myRank, "total": len(out), "updated_at": time.Now()})
}