From 5f4f739f1606619d0b564cbd2ecc9d0180652ffe Mon Sep 17 00:00:00 2001 From: Blizzard Date: Thu, 26 Mar 2026 11:26:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=9F=B3=E8=89=B2=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/v1/radio/enter.go | 2 + api/v1/radio/voice.go | 214 +++++++++++++++++++++++++++++++++ initialize/gorm.go | 1 + initialize/router.go | 1 + model/radio/radio_voice.go | 26 ++++ model/radio/request/voice.go | 37 ++++++ model/radio/response/voice.go | 37 ++++++ router/radio/enter.go | 2 + router/radio/voice_router.go | 21 ++++ service/radio/enter.go | 1 + service/radio/voice_service.go | 200 ++++++++++++++++++++++++++++++ 11 files changed, 542 insertions(+) create mode 100644 api/v1/radio/voice.go create mode 100644 model/radio/radio_voice.go create mode 100644 model/radio/request/voice.go create mode 100644 model/radio/response/voice.go create mode 100644 router/radio/voice_router.go create mode 100644 service/radio/voice_service.go diff --git a/api/v1/radio/enter.go b/api/v1/radio/enter.go index 9f09646..a5e8d1d 100644 --- a/api/v1/radio/enter.go +++ b/api/v1/radio/enter.go @@ -12,6 +12,7 @@ type ApiGroup struct { VipApi AnalyticsApi UserApi + VoiceApi } var ApiGroupApp = new(ApiGroup) @@ -26,4 +27,5 @@ var ( vipService = service.GroupApp.RadioServiceGroup.VipService analyticsService = service.GroupApp.RadioServiceGroup.AnalyticsService userService = service.GroupApp.RadioServiceGroup.UserService + voiceService = service.GroupApp.RadioServiceGroup.VoiceService ) diff --git a/api/v1/radio/voice.go b/api/v1/radio/voice.go new file mode 100644 index 0000000..1f04239 --- /dev/null +++ b/api/v1/radio/voice.go @@ -0,0 +1,214 @@ +package radio + +import ( + "sundynix-go/global" + common "sundynix-go/model/commom/request" + "sundynix-go/model/commom/response" + "sundynix-go/model/radio/request" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type VoiceApi struct{} + +// GetVoiceList 获取音色列表 +// @Tags 音色管理 +// @Summary 获取音色列表 +// @Produce application/json +// @Param data body request.GetVoiceList true "分页查询" +// @Success 200 {object} response.Response +// @Router /radio/voice/list [post] +func (a *VoiceApi) GetVoiceList(c *gin.Context) { + var req request.GetVoiceList + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMsg("参数错误: "+err.Error(), c) + return + } + + list, total, err := voiceService.GetVoiceList(req) + if err != nil { + global.Logger.Error("获取音色列表失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + + response.OkWithData(response.PageResult{ + List: list, + Total: total, + Page: req.Current, + PageSize: req.PageSize, + }, c) +} + +// GetVoiceDetail 获取音色详情 +// @Tags 音色管理 +// @Summary 获取音色详情 +// @Produce application/json +// @Param id query string true "音色ID" +// @Success 200 {object} response.Response +// @Router /radio/voice/detail [get] +func (a *VoiceApi) GetVoiceDetail(c *gin.Context) { + id := c.Query("id") + if id == "" { + response.FailWithMsg("参数错误: id不能为空", c) + return + } + + voice, err := voiceService.GetVoiceById(id) + if err != nil { + global.Logger.Error("获取音色详情失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + + response.OkWithData(voice, c) +} + +// GetVoiceOptions 获取音色选项列表(前端下拉选择用) +// @Tags 音色管理 +// @Summary 获取音色选项列表 +// @Produce application/json +// @Success 200 {object} response.Response +// @Router /radio/voice/options [get] +func (a *VoiceApi) GetVoiceOptions(c *gin.Context) { + list, err := voiceService.GetAllEnabledVoice() + if err != nil { + global.Logger.Error("获取音色列表失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + + response.OkWithData(list, c) +} + +// SaveVoice 保存音色 +// @Tags 音色管理 +// @Summary 保存音色 +// @Accept application/json +// @Produce application/json +// @Param data body request.SaveVoice true "音色信息" +// @Success 200 {object} response.Response +// @Router /radio/voice/save [post] +func (a *VoiceApi) SaveVoice(c *gin.Context) { + var req request.SaveVoice + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMsg("参数错误: "+err.Error(), c) + return + } + + // 检查speakerId是否已存在 + existing, _ := voiceService.GetVoiceBySpeakerId(req.SpeakerId) + if existing != nil { + response.FailWithMsg("该音色ID已存在", c) + return + } + + if err := voiceService.SaveVoice(req); err != nil { + global.Logger.Error("保存音色失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + + response.OkWithMsg("保存成功", c) +} + +// UpdateVoice 更新音色 +// @Tags 音色管理 +// @Summary 更新音色 +// @Accept application/json +// @Produce application/json +// @Param data body request.UpdateVoice true "音色信息" +// @Success 200 {object} response.Response +// @Router /radio/voice/update [post] +func (a *VoiceApi) UpdateVoice(c *gin.Context) { + var req request.UpdateVoice + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMsg("参数错误: "+err.Error(), c) + return + } + + // 如果修改了speakerId,检查是否与其他音色冲突 + if req.SpeakerId != "" { + existing, _ := voiceService.GetVoiceBySpeakerId(req.SpeakerId) + if existing != nil && existing.Id != req.Id { + response.FailWithMsg("该音色ID已被其他音色使用", c) + return + } + } + + if err := voiceService.UpdateVoice(req); err != nil { + global.Logger.Error("更新音色失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + + response.OkWithMsg("更新成功", c) +} + +// DeleteVoice 删除音色 +// @Tags 音色管理 +// @Summary 删除音色 +// @Produce json +// @Param data body common.IdsReq true "音色ID列表" +// @Success 200 {object} response.Response +// @Router /radio/voice/delete [post] +func (a *VoiceApi) DeleteVoice(c *gin.Context) { + var req common.IdsReq + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMsg("参数错误: id不能为空", c) + return + } + + if err := voiceService.DeleteVoice(req.Ids); err != nil { + global.Logger.Error("删除音色失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + + response.OkWithMsg("删除成功", c) +} + +// GetDefaultVoice 获取默认音色 +// @Tags 音色管理 +// @Summary 获取默认音色 +// @Produce application/json +// @Success 200 {object} response.Response +// @Router /radio/voice/default [get] +func (a *VoiceApi) GetDefaultVoice(c *gin.Context) { + voice, err := voiceService.GetDefaultVoice() + if err != nil { + global.Logger.Error("获取默认音色失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + + response.OkWithData(voice, c) +} + +// SetDefaultVoice 设置默认音色 +// @Tags 音色管理 +// @Summary 设置默认音色 +// @Produce json +// @Param id query string true "音色ID" +// @Success 200 {object} response.Response +// @Router /radio/voice/set-default [post] +func (a *VoiceApi) SetDefaultVoice(c *gin.Context) { + id := c.Query("id") + if id == "" { + response.FailWithMsg("参数错误: id不能为空", c) + return + } + + if err := voiceService.SetDefaultVoice(id); err != nil { + global.Logger.Error("设置默认音色失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + + response.OkWithMsg("设置成功", c) +} diff --git a/initialize/gorm.go b/initialize/gorm.go index 4dcff26..4e09f0d 100644 --- a/initialize/gorm.go +++ b/initialize/gorm.go @@ -39,6 +39,7 @@ func MigrateTable() { system.SysOperationRecord{}, system.Oss{}, + radio.RadioVoice{}, radio.Vip{}, radio.RadioCategory{}, radio.RadioChannel{}, diff --git a/initialize/router.go b/initialize/router.go index 0313346..269919f 100644 --- a/initialize/router.go +++ b/initialize/router.go @@ -57,6 +57,7 @@ func Routers() { radioRouter.InitInteractionRouter(NeedAuthGroup) //用户互动相关 radioRouter.InitAnalyticsRouter(NeedAuthGroup) //数据分析相关 radioRouter.InitUserRouter(NeedAuthGroup) //用户管理相关 + radioRouter.InitVoiceRouter(NeedAuthGroup) //音色管理相关 } address := fmt.Sprintf(":%d", global.Config.System.Addr) diff --git a/model/radio/radio_voice.go b/model/radio/radio_voice.go new file mode 100644 index 0000000..db97aba --- /dev/null +++ b/model/radio/radio_voice.go @@ -0,0 +1,26 @@ +package radio + +import ( + "sundynix-go/global" + "sundynix-go/model/system" +) + +// RadioVoice 音色管理表 +type RadioVoice struct { + global.BaseModel + SpeakerId string `gorm:"size:50;uniqueIndex" json:"speakerId"` // 音色ID (火山引擎的speaker值) + Name string `gorm:"size:50" json:"name"` // 音色名称 + Description string `gorm:"size:255" json:"description"` // 音色描述 + Gender string `gorm:"size:10" json:"gender"` // 性别: male/female/neutral + Icon string `gorm:"size:255" json:"icon"` // 音色图标URL + AudioId string `gorm:"size:50" json:"audioId"` // 试听音频OSS ID + Audio *system.Oss `gorm:"foreignKey:AudioId" json:"audio"` // 试听音频OSS + Sort int `gorm:"default:0" json:"sort"` // 排序 + Status int `gorm:"default:1" json:"status"` // 状态 0:禁用 1:启用 + IsDefault int `gorm:"default:0" json:"isDefault"` // 是否默认音色 0:否 1:是 + UseCount int `gorm:"default:0" json:"useCount"` // 使用次数 +} + +func (RadioVoice) TableName() string { + return "sundynix_radio_voice" +} diff --git a/model/radio/request/voice.go b/model/radio/request/voice.go new file mode 100644 index 0000000..f041a48 --- /dev/null +++ b/model/radio/request/voice.go @@ -0,0 +1,37 @@ +package request + +import common "sundynix-go/model/commom/request" + +// GetVoiceList 获取音色列表请求 +type GetVoiceList struct { + common.PageInfo + Name string `json:"name" form:"name"` // 音色名称 + Status int `json:"status" form:"status"` // 状态 +} + +// SaveVoice 保存音色请求 +type SaveVoice struct { + SpeakerId string `json:"speakerId" binding:"required"` // 音色ID + Name string `json:"name" binding:"required"` // 音色名称 + Description string `json:"description"` // 音色描述 + Gender string `json:"gender"` // 性别: male/female/neutral + Icon string `json:"icon"` // 音色图标URL + AudioId string `json:"audioId"` // 试听音频OSS ID + Sort int `json:"sort"` // 排序 + Status int `json:"status"` // 状态 + IsDefault int `json:"isDefault"` // 是否默认音色 +} + +// UpdateVoice 更新音色请求 +type UpdateVoice struct { + Id string `json:"id" binding:"required"` // 音色ID + SpeakerId string `json:"speakerId"` // 音色ID + Name string `json:"name"` // 音色名称 + Description string `json:"description"` // 音色描述 + Gender string `json:"gender"` // 性别 + Icon string `json:"icon"` // 音色图标URL + AudioId string `json:"audioId"` // 试听音频OSS ID + Sort int `json:"sort"` // 排序 + Status int `json:"status"` // 状态 + IsDefault int `json:"isDefault"` // 是否默认音色 +} diff --git a/model/radio/response/voice.go b/model/radio/response/voice.go new file mode 100644 index 0000000..24f211a --- /dev/null +++ b/model/radio/response/voice.go @@ -0,0 +1,37 @@ +package response + +import "sundynix-go/model/system" + +// VoiceResponse 音色响应 +type VoiceResponse struct { + Id string `json:"id"` + SpeakerId string `json:"speakerId"` + Name string `json:"name"` + Description string `json:"description"` + Gender string `json:"gender"` + Icon string `json:"icon"` + AudioId string `json:"audioId"` + Audio *system.Oss `json:"audio"` // 试听音频OSS + Sort int `json:"sort"` + Status int `json:"status"` + IsDefault int `json:"isDefault"` + UseCount int `json:"useCount"` +} + +// VoiceDetailResponse 音色详情响应 +type VoiceDetailResponse struct { + Id string `json:"id"` + SpeakerId string `json:"speakerId"` + Name string `json:"name"` + Description string `json:"description"` + Gender string `json:"gender"` + Icon string `json:"icon"` + AudioId string `json:"audioId"` + Audio *system.Oss `json:"audio"` // 试听音频OSS + Sort int `json:"sort"` + Status int `json:"status"` + IsDefault int `json:"isDefault"` + UseCount int `json:"useCount"` + CreateTime string `json:"createTime"` + UpdateTime string `json:"updateTime"` +} diff --git a/router/radio/enter.go b/router/radio/enter.go index 25b7351..696b3e3 100644 --- a/router/radio/enter.go +++ b/router/radio/enter.go @@ -12,6 +12,7 @@ type RadioRouterGroup struct { VipRouter AnalyticsRouter UserRouter + VoiceRouter } var GroupApp = new(RadioRouterGroup) @@ -26,4 +27,5 @@ var ( vipApi = v1.ApiGroupApp.RadioApiGroup.VipApi analyticsApi = v1.ApiGroupApp.RadioApiGroup.AnalyticsApi userApi = v1.ApiGroupApp.RadioApiGroup.UserApi + voiceApi = v1.ApiGroupApp.RadioApiGroup.VoiceApi ) diff --git a/router/radio/voice_router.go b/router/radio/voice_router.go new file mode 100644 index 0000000..081e880 --- /dev/null +++ b/router/radio/voice_router.go @@ -0,0 +1,21 @@ +package radio + +import ( + "github.com/gin-gonic/gin" +) + +type VoiceRouter struct{} + +func (r *VoiceRouter) InitVoiceRouter(Router *gin.RouterGroup) { + voiceRouter := Router.Group("/radio/voice") + { + voiceRouter.POST("list", voiceApi.GetVoiceList) + voiceRouter.GET("detail", voiceApi.GetVoiceDetail) + voiceRouter.GET("options", voiceApi.GetVoiceOptions) + voiceRouter.POST("save", voiceApi.SaveVoice) + voiceRouter.POST("update", voiceApi.UpdateVoice) + voiceRouter.POST("delete", voiceApi.DeleteVoice) + voiceRouter.GET("default", voiceApi.GetDefaultVoice) + voiceRouter.POST("set-default", voiceApi.SetDefaultVoice) + } +} diff --git a/service/radio/enter.go b/service/radio/enter.go index 9930223..572ce1c 100644 --- a/service/radio/enter.go +++ b/service/radio/enter.go @@ -12,6 +12,7 @@ type ServiceGroup struct { TTSService AnalyticsService UserService + VoiceService } var GroupApp = new(ServiceGroup) diff --git a/service/radio/voice_service.go b/service/radio/voice_service.go new file mode 100644 index 0000000..b5ee161 --- /dev/null +++ b/service/radio/voice_service.go @@ -0,0 +1,200 @@ +package radio + +import ( + "sundynix-go/global" + "sundynix-go/model/radio" + radioReq "sundynix-go/model/radio/request" + radioRes "sundynix-go/model/radio/response" + + "gorm.io/gorm" +) + +type VoiceService struct{} + +// GetVoiceList 获取音色列表 +func (s *VoiceService) GetVoiceList(req radioReq.GetVoiceList) ([]radioRes.VoiceResponse, int64, error) { + db := global.DB.Model(&radio.RadioVoice{}) + var list []radio.RadioVoice + var total int64 + + if req.Name != "" { + db = db.Where("name LIKE ?", "%"+req.Name+"%") + } + if req.Status > 0 { + db = db.Where("status = ?", req.Status) + } + + err := db.Count(&total).Error + if err != nil { + return nil, 0, err + } + + offset := (req.Current - 1) * req.PageSize + err = db.Offset(offset).Limit(req.PageSize).Order("sort ASC").Find(&list).Error + if err != nil { + return nil, 0, err + } + + // 转换为响应结构 + var resp []radioRes.VoiceResponse + for _, v := range list { + resp = append(resp, radioRes.VoiceResponse{ + Id: v.Id, + SpeakerId: v.SpeakerId, + Name: v.Name, + Description: v.Description, + Gender: v.Gender, + Icon: v.Icon, + AudioId: v.AudioId, + Sort: v.Sort, + Status: v.Status, + IsDefault: v.IsDefault, + UseCount: v.UseCount, + }) + } + return resp, total, nil +} + +// GetVoiceById 获取音色详情 +func (s *VoiceService) GetVoiceById(id string) (*radioRes.VoiceDetailResponse, error) { + var voice radio.RadioVoice + err := global.DB.Preload("Audio").Where("id = ?", id).First(&voice).Error + if err != nil { + return nil, err + } + + return &radioRes.VoiceDetailResponse{ + Id: voice.Id, + SpeakerId: voice.SpeakerId, + Name: voice.Name, + Description: voice.Description, + Gender: voice.Gender, + Icon: voice.Icon, + AudioId: voice.AudioId, + Audio: voice.Audio, + Sort: voice.Sort, + Status: voice.Status, + IsDefault: voice.IsDefault, + UseCount: voice.UseCount, + CreateTime: voice.CreatedAt.Format("2006-01-02 15:04:05"), + UpdateTime: voice.UpdatedAt.Format("2006-01-02 15:04:05"), + }, nil +} + +// GetVoiceBySpeakerId 根据SpeakerId获取音色 +func (s *VoiceService) GetVoiceBySpeakerId(speakerId string) (*radio.RadioVoice, error) { + var voice radio.RadioVoice + err := global.DB.Where("speaker_id = ? AND status = ?", speakerId, 1).First(&voice).Error + return &voice, err +} + +// GetDefaultVoice 获取默认音色 +func (s *VoiceService) GetDefaultVoice() (*radio.RadioVoice, error) { + var voice radio.RadioVoice + err := global.DB.Where("is_default = ? AND status = ?", 1, 1).First(&voice).Error + if err != nil { + // 如果没有默认音色,返回第一个启用的音色 + err = global.DB.Where("status = ?", 1).Order("sort ASC").First(&voice).Error + } + return &voice, err +} + +// GetAllEnabledVoice 获取所有启用的音色(前端选择用) +func (s *VoiceService) GetAllEnabledVoice() ([]radioRes.VoiceResponse, error) { + var list []radio.RadioVoice + err := global.DB.Where("status = ?", 1).Order("sort ASC").Find(&list).Error + if err != nil { + return nil, err + } + + var resp []radioRes.VoiceResponse + for _, v := range list { + resp = append(resp, radioRes.VoiceResponse{ + Id: v.Id, + SpeakerId: v.SpeakerId, + Name: v.Name, + Gender: v.Gender, + Icon: v.Icon, + IsDefault: v.IsDefault, + }) + } + return resp, nil +} + +// SaveVoice 保存音色 +func (s *VoiceService) SaveVoice(req radioReq.SaveVoice) error { + return global.DB.Transaction(func(tx *gorm.DB) error { + // 如果设置为默认音色,先取消其他默认 + if req.IsDefault == 1 { + if err := tx.Model(&radio.RadioVoice{}).Where("is_default = ?", 1).Update("is_default", 0).Error; err != nil { + return err + } + } + + voice := radio.RadioVoice{ + SpeakerId: req.SpeakerId, + Name: req.Name, + Description: req.Description, + Gender: req.Gender, + Icon: req.Icon, + AudioId: req.AudioId, + Sort: req.Sort, + Status: req.Status, + IsDefault: req.IsDefault, + } + return tx.Create(&voice).Error + }) +} + +// UpdateVoice 更新音色 +func (s *VoiceService) UpdateVoice(req radioReq.UpdateVoice) error { + return global.DB.Transaction(func(tx *gorm.DB) error { + updates := map[string]interface{}{ + "speaker_id": req.SpeakerId, + "name": req.Name, + "description": req.Description, + "gender": req.Gender, + "icon": req.Icon, + "audio_id": req.AudioId, + "sort": req.Sort, + "status": req.Status, + } + + // 如果设置为默认音色,先取消其他默认 + if req.IsDefault == 1 { + if err := tx.Model(&radio.RadioVoice{}).Where("is_default = ? AND id != ?", 1, req.Id).Update("is_default", 0).Error; err != nil { + return err + } + updates["is_default"] = 1 + } + + return tx.Model(&radio.RadioVoice{}).Where("id = ?", req.Id).Updates(updates).Error + }) +} + +// DeleteVoice 删除音色 +func (s *VoiceService) DeleteVoice(ids []string) error { + return global.DB.Where("id IN ?", ids).Delete(&radio.RadioVoice{}).Error +} + +// IncrementUseCount 增加使用次数 +func (s *VoiceService) IncrementUseCount(speakerId string) error { + return global.DB.Model(&radio.RadioVoice{}).Where("speaker_id = ?", speakerId).UpdateColumn("use_count", gorm.Expr("use_count + 1")).Error +} + +// SetDefaultVoice 设置默认音色 +func (s *VoiceService) SetDefaultVoice(id string) error { + return global.DB.Transaction(func(tx *gorm.DB) error { + // 先清除其他默认 + if err := tx.Model(&radio.RadioVoice{}).Where("is_default = ?", 1).Update("is_default", 0).Error; err != nil { + return err + } + // 设置当前为默认 + if err := tx.Model(&radio.RadioVoice{}).Where("id = ?", id).Update("is_default", 1).Error; err != nil { + return err + } + return nil + }) +} + +var VoiceServiceApp = new(VoiceService)