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:
@@ -0,0 +1,169 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"eai_agentplatform/backend/internal/model"
|
||||
"eai_agentplatform/backend/internal/store"
|
||||
"eai_agentplatform/backend/internal/web"
|
||||
)
|
||||
|
||||
// courseView 课程详情视图(含绑定产品)
|
||||
func courseView(c *gin.Context, co model.Course) {
|
||||
out := gin.H{
|
||||
"id": co.ID,
|
||||
"code": co.Code,
|
||||
"name": co.Name,
|
||||
"category": co.Category,
|
||||
"target_customers": co.TargetCustomers,
|
||||
"forbidden_customers": co.ForbiddenCustomers,
|
||||
"scripts": co.Scripts,
|
||||
"sales_process": co.SalesProcess,
|
||||
"objection_handling": co.ObjectionHandling,
|
||||
"delivery_pitfalls": co.DeliveryPitfalls,
|
||||
"report_rules": co.ReportRules,
|
||||
"related_product_id": co.RelatedProductID,
|
||||
"status": co.Status,
|
||||
"created_at": co.CreatedAt,
|
||||
"updated_at": co.UpdatedAt,
|
||||
}
|
||||
if co.RelatedProductID != nil {
|
||||
var p model.Product
|
||||
if store.DB.Where("id = ? AND status != ?", *co.RelatedProductID, "inactive").First(&p).Error == nil {
|
||||
out["product"] = gin.H{"id": p.ID, "code": p.Code, "name": p.Name, "category": p.Category}
|
||||
}
|
||||
}
|
||||
|
||||
var medias []model.MediaFile
|
||||
store.DB.Where("bind_type = ? AND bind_id = ? AND status = ?", "course", co.ID, "approved").
|
||||
Order("id ASC").Find(&medias)
|
||||
if len(medias) > 0 {
|
||||
items := make([]gin.H, 0, len(medias))
|
||||
for _, m := range medias {
|
||||
items = append(items, gin.H{
|
||||
"id": m.ID,
|
||||
"filename": m.Filename,
|
||||
"file_ext": m.FileExt,
|
||||
"preview_url": "/api/media/preview/" + strconv.FormatUint(uint64(m.ID), 10),
|
||||
})
|
||||
}
|
||||
out["medias"] = items
|
||||
}
|
||||
web.OK(c, out)
|
||||
}
|
||||
|
||||
// ListCourses GET /api/courses?category=&status=
|
||||
func ListCourses(c *gin.Context) {
|
||||
q := store.DB.Model(&model.Course{})
|
||||
if cat := c.Query("category"); cat != "" {
|
||||
q = q.Where("category = ?", cat)
|
||||
}
|
||||
switch st := c.Query("status"); st {
|
||||
case "": // 默认仅 active(员工浏览)
|
||||
q = q.Where("status = ?", "active")
|
||||
case "all": // 管理员维护全量
|
||||
default:
|
||||
q = q.Where("status = ?", st)
|
||||
}
|
||||
var items []model.Course
|
||||
if err := q.Order("id ASC").Find(&items).Error; err != nil {
|
||||
web.Fail(c, web.NewBadRequest("查询课程失败"))
|
||||
return
|
||||
}
|
||||
web.OK(c, items)
|
||||
}
|
||||
|
||||
// GetCourse GET /api/courses/{id}
|
||||
func GetCourse(c *gin.Context) {
|
||||
id, ok := parseID(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var co model.Course
|
||||
if err := store.DB.First(&co, id).Error; err != nil {
|
||||
web.Fail(c, web.NewNotFoundError("课程不存在"))
|
||||
return
|
||||
}
|
||||
courseView(c, co)
|
||||
}
|
||||
|
||||
// CreateCourse POST /api/courses (admin)
|
||||
func CreateCourse(c *gin.Context) {
|
||||
var co model.Course
|
||||
if err := c.ShouldBindJSON(&co); err != nil {
|
||||
web.Fail(c, web.NewBadRequest("请求参数错误"))
|
||||
return
|
||||
}
|
||||
if co.Code == "" || co.Name == "" || co.Category == "" {
|
||||
web.Fail(c, web.NewBadRequest("编号、名称、分类为必填"))
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
store.DB.Model(&model.Course{}).Where("code = ?", co.Code).Count(&count)
|
||||
if count > 0 {
|
||||
web.Fail(c, web.NewConflictError("课程编号已存在"))
|
||||
return
|
||||
}
|
||||
if co.Status == "" {
|
||||
co.Status = "active"
|
||||
}
|
||||
if err := store.DB.Create(&co).Error; err != nil {
|
||||
web.Fail(c, web.NewBadRequest("创建课程失败"))
|
||||
return
|
||||
}
|
||||
web.OK(c, co)
|
||||
}
|
||||
|
||||
// UpdateCourse PUT /api/courses/{id} (admin) —— 支持绑定/解绑产品
|
||||
func UpdateCourse(c *gin.Context) {
|
||||
id, ok := parseID(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var co model.Course
|
||||
if err := store.DB.First(&co, id).Error; err != nil {
|
||||
web.Fail(c, web.NewNotFoundError("课程不存在"))
|
||||
return
|
||||
}
|
||||
var req model.Course
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
web.Fail(c, web.NewBadRequest("请求参数错误"))
|
||||
return
|
||||
}
|
||||
if req.Name == "" || req.Category == "" {
|
||||
web.Fail(c, web.NewBadRequest("名称、分类为必填"))
|
||||
return
|
||||
}
|
||||
if req.Status == "" {
|
||||
req.Status = "active"
|
||||
}
|
||||
|
||||
if req.Code != "" && req.Code != co.Code {
|
||||
var count int64
|
||||
store.DB.Model(&model.Course{}).Where("code = ? AND id <> ?", req.Code, id).Count(&count)
|
||||
if count > 0 {
|
||||
web.Fail(c, web.NewConflictError("课程编号已存在"))
|
||||
return
|
||||
}
|
||||
co.Code = req.Code
|
||||
}
|
||||
co.Name = req.Name
|
||||
co.Category = req.Category
|
||||
co.TargetCustomers = req.TargetCustomers
|
||||
co.ForbiddenCustomers = req.ForbiddenCustomers
|
||||
co.Scripts = req.Scripts
|
||||
co.SalesProcess = req.SalesProcess
|
||||
co.ObjectionHandling = req.ObjectionHandling
|
||||
co.DeliveryPitfalls = req.DeliveryPitfalls
|
||||
co.ReportRules = req.ReportRules
|
||||
co.RelatedProductID = req.RelatedProductID
|
||||
co.Status = req.Status
|
||||
|
||||
if err := store.DB.Save(&co).Error; err != nil {
|
||||
web.Fail(c, web.NewBadRequest("更新课程失败"))
|
||||
return
|
||||
}
|
||||
courseView(c, co)
|
||||
}
|
||||
Reference in New Issue
Block a user