init: radio init commit
This commit is contained in:
Generated
+10
@@ -0,0 +1,10 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
Generated
+11
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GoImports">
|
||||
<option name="excludedPackages">
|
||||
<array>
|
||||
<option value="github.com/pkg/errors" />
|
||||
<option value="golang.org/x/net/context" />
|
||||
</array>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/morning-radio-backend.iml" filepath="$PROJECT_DIR$/.idea/morning-radio-backend.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+9
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
-173
@@ -1,173 +0,0 @@
|
||||
# 项目改造 Walkthrough:从"植趣"到"早安电台"
|
||||
|
||||
## 一、项目概述
|
||||
|
||||
本次改造将后端项目从**植物养护小程序(植趣)**全面转型为**早安电台微信小程序**后端。保留了底层的系统模块(用户认证、OSS 文件管理等),删除了全部植物相关的业务模块,新建了早安电台核心业务模块。
|
||||
|
||||
---
|
||||
|
||||
## 二、技术栈(保持不变)
|
||||
|
||||
| 技术 | 版本/框架 | 用途 |
|
||||
|------|-----------|------|
|
||||
| Go | 1.24 | 后端语言 |
|
||||
| Gin | v1.10 | Web 框架 |
|
||||
| GORM | v1.26 | ORM 框架 |
|
||||
| MySQL | - | 主数据库 |
|
||||
| Redis | - | 缓存与分布式锁 |
|
||||
| MinIO | - | 对象存储(音频文件) |
|
||||
| Viper | v1.20 | 配置管理 |
|
||||
| Zap | v1.27 | 结构化日志 |
|
||||
| robfig/cron | v3 | 定时任务调度 |
|
||||
| JWT | v5 | 接口鉴权 |
|
||||
|
||||
---
|
||||
|
||||
## 三、已删除的模块(植物相关)
|
||||
|
||||
### 3.1 删除的目录
|
||||
|
||||
| 目录 | 文件数 | 说明 |
|
||||
|------|--------|------|
|
||||
| `api/v1/plant/` | 12 个文件 | 植物 API 处理器 |
|
||||
| `router/plant/` | 12 个文件 | 植物路由定义 |
|
||||
| `service/plant/` | 13 个文件 | 植物业务逻辑 |
|
||||
| `model/plant/` | 22+ 个文件 | 植物数据模型(含 request/response 子目录) |
|
||||
| `task/` | 2 个文件 | 植物养护定时任务 |
|
||||
| `config/baidu_img_classify.go` | 1 个文件 | 百度植物识别配置 |
|
||||
|
||||
### 3.2 删除的植物功能清单
|
||||
|
||||
- 我的植物管理(CRUD、养护计划、养护任务、成长记录)
|
||||
- 社区帖子(发帖、评论、点赞、话题)
|
||||
- 植物百科(分类、百科条目)
|
||||
- 植物 OCR 识别
|
||||
- 等级配置系统
|
||||
- 徽章成就系统(植物养护相关)
|
||||
- 用户资料管理
|
||||
- 兑换中心(阳光值兑换商品)
|
||||
- 微信内容安全回调
|
||||
- 植物养护提醒定时任务
|
||||
|
||||
---
|
||||
|
||||
## 四、新建的模块(早安电台)
|
||||
|
||||
### 4.1 数据模型层 `model/radio/`
|
||||
|
||||
| 文件 | 表名 | 说明 |
|
||||
|------|------|------|
|
||||
| `audio_content.go` | `sundynix_audio_content` | 音频内容表:标题、音频URL、封面、专辑名、场景、时长、脚本文本、来源类型、状态 |
|
||||
| `crawl_source.go` | `sundynix_crawl_source` | 抓取源配置表:名称、类型(github/producthunt/rss)、API地址、查询参数、场景、抓取间隔 |
|
||||
| `crawl_record.go` | `sundynix_crawl_record` | 抓取记录表:原始数据、清洗文本、AI摘要、处理状态、关联音频ID |
|
||||
| `play_record.go` | `sundynix_play_record` | 播放记录表:用户ID、音频ID、播放进度(秒)、是否完成、完成时间 |
|
||||
| `user_medal.go` | `sundynix_user_medal` | 用户勋章表:用户ID、勋章类型、获得时间 |
|
||||
| `user_subscription.go` | `sundynix_user_subscription` | 用户订阅表:用户ID、场景、是否启用、推送时间 |
|
||||
| `request/audio.go` | - | 请求参数:音频列表请求、播放状态更新请求、订阅更新请求 |
|
||||
| `response/audio.go` | - | 响应参数:音频列表响应、播放状态响应、勋章列表响应、订阅响应 |
|
||||
|
||||
### 4.2 服务层 `service/radio/`
|
||||
|
||||
| 文件 | 核心功能 |
|
||||
|------|----------|
|
||||
| `audio_content.go` | 音频列表分页查询(支持场景筛选)、音频详情、最新音频获取 |
|
||||
| `crawl.go` | 获取启用的抓取源、创建/更新抓取记录、获取待处理记录 |
|
||||
| `play_record.go` | 更新播放进度、断点续传支持、**完成收听时自动触发勋章解锁** |
|
||||
| `user_medal.go` | 查询用户勋章列表、检查勋章拥有状态 |
|
||||
| `user_subscription.go` | 查询/创建/更新用户订阅偏好 |
|
||||
| `enter.go` | ServiceGroup 聚合入口 |
|
||||
|
||||
### 4.3 API 处理器层 `api/v1/radio/`
|
||||
|
||||
| 文件 | 接口 | 说明 |
|
||||
|------|------|------|
|
||||
| `audio_content.go` | `GET /audio/list` | 分页获取音频列表,支持 `scene` 筛选 |
|
||||
| | `GET /audio/detail` | 根据 ID 获取音频详情 |
|
||||
| | `GET /audio/latest` | 获取指定场景的最新音频 |
|
||||
| `play_record.go` | `POST /audio/play-status` | 更新播放状态,完成时返回新解锁的勋章 |
|
||||
| | `GET /audio/play-record` | 获取播放进度(用于 `startTime` 断点续传) |
|
||||
| `user_medal.go` | `GET /medal/list` | 获取当前用户的所有勋章 |
|
||||
| `user_subscription.go` | `GET /subscription/info` | 获取用户的场景订阅列表 |
|
||||
| | `POST /subscription/update` | 更新/创建订阅设置 |
|
||||
|
||||
### 4.4 路由层 `router/radio/`
|
||||
|
||||
| 文件 | 路由组前缀 | 注册的 API |
|
||||
|------|-----------|-----------|
|
||||
| `audio_router.go` | `/audio` | list, detail, latest |
|
||||
| `play_record_router.go` | `/audio` | play-status, play-record |
|
||||
| `medal_router.go` | `/medal` | list |
|
||||
| `subscription_router.go` | `/subscription` | info, update |
|
||||
|
||||
---
|
||||
|
||||
## 五、修改的全局文件
|
||||
|
||||
### 5.1 聚合入口文件(enter.go)
|
||||
|
||||
- `api/v1/enter.go` — `PlantApiGroup` → `RadioApiGroup`
|
||||
- `router/enter.go` — `Plant plant.RouterGroup` → `Radio radio.RouterGroup`
|
||||
- `service/enter.go` — `PlantServiceGroup` → `RadioServiceGroup`
|
||||
|
||||
### 5.2 初始化文件(initialize/)
|
||||
|
||||
- `router.go` — 全部 `plantGroup.InitXxxRouter()` → `radioGroup.InitXxxRouter()`,项目名称更新为 `sundynix-morning-radio`
|
||||
- `gorm.go` — 全部 `plant.Xxx{}` 模型迁移 → `radio.Xxx{}` 模型迁移
|
||||
- `timer.go` — 植物养护提醒 → 早安电台数据抓取定时任务(每2小时) + 音频生成任务(每天凌晨5点)
|
||||
|
||||
### 5.3 全局常量(global/enums.go)
|
||||
|
||||
移除植物养护相关常量,新增:
|
||||
|
||||
- **勋章类型**:`EARLY_BIRD_7/30/100`, `EXPLORER`, `ALL_ROUNDER`, `FIRST_LISTEN`
|
||||
- **场景定义**:`morning_career`, `morning_health`, `morning_life`, `morning_study`
|
||||
- **抓取源类型**:`github`, `producthunt`, `rss`, `huggingface`, `webscrape`
|
||||
- **音频状态**:草稿(0)、已发布(1)、已下线(2)
|
||||
- **抓取状态**:pending、processed、failed
|
||||
|
||||
### 5.4 配置文件
|
||||
|
||||
- `config/config.go` — 移除 `BaiduImgClassify` 字段
|
||||
- `config-dev.yaml` — 移除百度植物识别配置,更新 MinIO bucket 为 `sundynix-audios`,更新 RocketMQ topic,更新 Zap 日志前缀
|
||||
|
||||
---
|
||||
|
||||
## 六、核心业务流程
|
||||
|
||||
### 6.1 数据抓取与音频生成流水线
|
||||
|
||||
```
|
||||
定时任务(每2小时) → 抓取数据源 → 清洗HTML(Goquery) → AI摘要改写 → TTS语音合成 → MinIO存储 → CDN分发
|
||||
```
|
||||
|
||||
### 6.2 用户收听与勋章系统
|
||||
|
||||
```
|
||||
获取音频列表 → 播放音频 → 实时上报进度(断点续传) → 完成收听 → 触发勋章检查 → 解锁成就
|
||||
```
|
||||
|
||||
### 6.3 场景化订阅
|
||||
|
||||
```
|
||||
用户选择场景(硬核职场/效率健康/极简生活/知识胶囊) → 设置推送时间 → 按需接收个性化早安电台
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、编译验证
|
||||
|
||||
```bash
|
||||
$ go build ./...
|
||||
# 编译通过,无错误
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、后续开发建议(TODO)
|
||||
|
||||
1. **抓取服务实现**:接入 GitHub API、Product Hunt GraphQL API、RSS 订阅源
|
||||
2. **AI 摘要服务**:集成 DeepSeek/OpenAI API,实现抓取数据 → 口播脚本的自动改写
|
||||
3. **TTS 语音合成**:接入腾讯云 TTS 或其他 TTS 服务,生成 AAC 格式音频
|
||||
4. **CDN 配置**:为 MinIO 配置 Nginx HTTPS 反向代理 + CDN 边缘缓存
|
||||
5. **小程序前端**:集成 `BackgroundAudioManager` 实现后台播放、锁屏控件、断点续传
|
||||
6. **AIGC 合规标识**:在前端 UI 添加"AI 生成内容"标识
|
||||
@@ -1,6 +1,7 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"sundynix-go/api/v1/radio"
|
||||
"sundynix-go/api/v1/system"
|
||||
)
|
||||
|
||||
@@ -9,4 +10,5 @@ var ApiGroupApp = new(ApiGroup)
|
||||
// ApiGroup 路由组
|
||||
type ApiGroup struct {
|
||||
SystemApiGroup system.ApiGroup
|
||||
RadioApiGroup radio.ApiGroup
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
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 CategoryApi struct{}
|
||||
|
||||
// GetCategoryPage 获取分类列表
|
||||
// @Tags 分类管理
|
||||
// @Summary 获取分类列表
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.GetCategoryList true "分页查询"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/category/page [post]
|
||||
func (a *CategoryApi) GetCategoryPage(c *gin.Context) {
|
||||
var req request.GetCategoryList
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
list, total, err := categoryService.GetCategoryList(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)
|
||||
}
|
||||
|
||||
// GetCategoryList 获取分类列表
|
||||
// @Tags 分类管理
|
||||
// @Summary 获取分类列表
|
||||
// @Produce application/json
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/category/list [get]
|
||||
func (a *CategoryApi) GetCategoryList(c *gin.Context) {
|
||||
list, err := categoryService.GetAllCategory()
|
||||
if err != nil {
|
||||
global.Logger.Error("获取分类列表失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(response.ListResult{
|
||||
List: list,
|
||||
}, c)
|
||||
}
|
||||
|
||||
// GetCategoryDetail 获取分类详情
|
||||
// @Tags 分类管理
|
||||
// @Summary 获取分类详情
|
||||
// @Produce application/json
|
||||
// @Param id query string true "分类ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/category/detail [get]
|
||||
func (a *CategoryApi) GetCategoryDetail(c *gin.Context) {
|
||||
id := c.Query("id")
|
||||
if id == "" {
|
||||
response.FailWithMsg("参数错误: id不能为空", c)
|
||||
return
|
||||
}
|
||||
|
||||
category, err := categoryService.GetCategoryById(id)
|
||||
if err != nil {
|
||||
global.Logger.Error("获取分类详情失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithData(category, c)
|
||||
}
|
||||
|
||||
// SaveCategory 保存分类
|
||||
// @Tags 分类管理
|
||||
// @Summary 保存分类
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.SaveCategory true "分类信息"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/category/save [post]
|
||||
func (a *CategoryApi) SaveCategory(c *gin.Context) {
|
||||
var req request.SaveCategory
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := categoryService.SaveCategory(req); err != nil {
|
||||
global.Logger.Error("保存分类失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMsg("保存成功", c)
|
||||
}
|
||||
|
||||
// UpdateCategory 更新分类
|
||||
// @Tags 分类管理
|
||||
// @Summary 更新分类
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.UpdateCategory true "Success 200 {分类信息"
|
||||
// @object} response.Response
|
||||
// @Router /radio/category/update [post]
|
||||
func (a *CategoryApi) UpdateCategory(c *gin.Context) {
|
||||
var req request.UpdateCategory
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := categoryService.UpdateCategory(req); err != nil {
|
||||
global.Logger.Error("更新分类失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMsg("更新成功", c)
|
||||
}
|
||||
|
||||
// DeleteCategory 删除分类
|
||||
// @Tags 分类管理
|
||||
// @Summary 删除分类
|
||||
// @Produce application/json
|
||||
// @Param data body common.GetById true "分类ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/category/delete [post]
|
||||
func (a *CategoryApi) DeleteCategory(c *gin.Context) {
|
||||
var req common.IdReq
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil || req.Id == "" {
|
||||
response.FailWithMsg("参数错误: id不能为空", c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := categoryService.DeleteCategory(req.Id); err != nil {
|
||||
global.Logger.Error("删除分类失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMsg("删除成功", c)
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
common "sundynix-go/model/commom/request"
|
||||
|
||||
"sundynix-go/model/commom/response"
|
||||
"sundynix-go/model/radio/request"
|
||||
"sundynix-go/utils/auth"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ChannelApi struct{}
|
||||
|
||||
// GetChannelList 获取频道列表
|
||||
// @Tags 频道管理
|
||||
// @Summary 获取频道列表
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.GetChannelList true "分页查询"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/channel/list [post]
|
||||
func (a *ChannelApi) GetChannelList(c *gin.Context) {
|
||||
userId := auth.GetUserId(c)
|
||||
var req request.GetChannelList
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
list, total, err := channelService.GetChannelList(userId, 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)
|
||||
}
|
||||
|
||||
// GetChannelDetail 获取频道详情
|
||||
// @Tags 频道管理
|
||||
// @Summary 获取频道详情
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param id query string true "频道ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/channel/detail [get]
|
||||
func (a *ChannelApi) GetChannelDetail(c *gin.Context) {
|
||||
userId := auth.GetUserId(c)
|
||||
id := c.Query("id")
|
||||
if id == "" {
|
||||
response.FailWithMsg("参数错误: id不能为空", c)
|
||||
return
|
||||
}
|
||||
|
||||
channel, err := channelService.GetChannelById(userId, id)
|
||||
if err != nil {
|
||||
global.Logger.Error("获取频道详情失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(channel, c)
|
||||
}
|
||||
|
||||
// SaveChannel 保存频道
|
||||
// @Tags 频道管理
|
||||
// @Summary 保存频道
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.SaveChannel true "频道信息"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/channel/save [post]
|
||||
func (a *ChannelApi) SaveChannel(c *gin.Context) {
|
||||
var req request.SaveChannel
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := channelService.SaveChannel(req); err != nil {
|
||||
global.Logger.Error("保存频道失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMsg("保存成功", c)
|
||||
}
|
||||
|
||||
// UpdateChannel 更新频道
|
||||
// @Tags 频道管理
|
||||
// @Summary 更新频道
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.UpdateChannel true "频道信息"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/channel/update [post]
|
||||
func (a *ChannelApi) UpdateChannel(c *gin.Context) {
|
||||
var req request.UpdateChannel
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := channelService.UpdateChannel(req); err != nil {
|
||||
global.Logger.Error("更新频道失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMsg("更新成功", c)
|
||||
}
|
||||
|
||||
// DeleteChannel 删除频道
|
||||
// @Tags 频道管理
|
||||
// @Summary 删除频道
|
||||
// @Produce json
|
||||
// @Param data body common.GetById true "频道ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/channel/delete [post]
|
||||
func (a *ChannelApi) DeleteChannel(c *gin.Context) {
|
||||
var req common.IdReq
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil || req.Id == "" {
|
||||
response.FailWithMsg("参数错误: id不能为空", c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := channelService.DeleteChannel(req.Id); err != nil {
|
||||
global.Logger.Error("删除频道失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMsg("删除成功", c)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package radio
|
||||
|
||||
import "sundynix-go/service"
|
||||
|
||||
type ApiGroup struct {
|
||||
CategoryApi
|
||||
ChannelApi
|
||||
ProgramApi
|
||||
SubscriptionApi
|
||||
InteractionApi
|
||||
}
|
||||
|
||||
var ApiGroupApp = new(ApiGroup)
|
||||
|
||||
var (
|
||||
categoryService = service.GroupApp.RadioServiceGroup.CategoryService
|
||||
channelService = service.GroupApp.RadioServiceGroup.ChannelService
|
||||
programService = service.GroupApp.RadioServiceGroup.ProgramService
|
||||
subscriptionService = service.GroupApp.RadioServiceGroup.SubscriptionService
|
||||
interactionService = service.GroupApp.RadioServiceGroup.InteractionService
|
||||
)
|
||||
@@ -0,0 +1,253 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/commom/response"
|
||||
"sundynix-go/model/radio/request"
|
||||
"sundynix-go/utils/auth"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type InteractionApi struct{}
|
||||
|
||||
// AddHistory 添加收听历史
|
||||
// @Tags 用户互动
|
||||
// @Summary 添加收听历史
|
||||
// @Produce json
|
||||
// @Param data body request.AddHistory true "收听信息"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/history/add [post]
|
||||
func (a *InteractionApi) AddHistory(c *gin.Context) {
|
||||
userId := auth.GetUserId(c)
|
||||
var req request.AddHistory
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := interactionService.AddHistory(userId, req); err != nil {
|
||||
global.Logger.Error("添加收听历史失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMsg("添加成功", c)
|
||||
}
|
||||
|
||||
// GetHistoryList 获取收听历史列表
|
||||
// @Tags 用户互动
|
||||
// @Summary 获取收听历史列表
|
||||
// @Produce json
|
||||
// @Param data body request.GetHistoryList true "分页查询"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/history/list [post]
|
||||
func (a *InteractionApi) GetHistoryList(c *gin.Context) {
|
||||
userId := auth.GetUserId(c)
|
||||
var req request.GetHistoryList
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
list, total, err := interactionService.GetHistoryList(userId, 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,
|
||||
}, c)
|
||||
}
|
||||
|
||||
// ToggleLike 切换点赞状态
|
||||
// @Tags 用户互动
|
||||
// @Summary 切换点赞状态
|
||||
// @Produce json
|
||||
// @Param data body request.ToggleLike true "节目ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/like/toggle [post]
|
||||
func (a *InteractionApi) ToggleLike(c *gin.Context) {
|
||||
userId := auth.GetUserId(c)
|
||||
var req request.ToggleLike
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
isLiked, err := interactionService.ToggleLike(userId, req.ProgramId)
|
||||
if err != nil {
|
||||
global.Logger.Error("操作失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithData(isLiked, c)
|
||||
}
|
||||
|
||||
// GetFavoriteList 获取收藏列表
|
||||
// @Tags 用户互动
|
||||
// @Summary 获取收藏列表
|
||||
// @Produce json
|
||||
// @Param data body request.GetFavoriteList true "分页查询"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/favorite/list [post]
|
||||
func (a *InteractionApi) GetFavoriteList(c *gin.Context) {
|
||||
userId := auth.GetUserId(c)
|
||||
var req request.GetFavoriteList
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
list, total, err := interactionService.GetFavoriteList(userId, 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,
|
||||
}, c)
|
||||
}
|
||||
|
||||
// AddFavorite 添加收藏
|
||||
// @Tags 用户互动
|
||||
// @Summary 添加收藏
|
||||
// @Produce json
|
||||
// @Param data body request.AddFavorite true "节目ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/favorite/add [post]
|
||||
func (a *InteractionApi) AddFavorite(c *gin.Context) {
|
||||
userId := auth.GetUserId(c)
|
||||
var req request.AddFavorite
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := interactionService.AddFavorite(userId, req.ProgramId); err != nil {
|
||||
global.Logger.Error("添加收藏失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMsg("收藏成功", c)
|
||||
}
|
||||
|
||||
// RemoveFavorite 取消收藏
|
||||
// @Tags 用户互动
|
||||
// @Summary 取消收藏
|
||||
// @Produce json
|
||||
// @Param data body request.RemoveFavorite true "节目ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/favorite/remove [post]
|
||||
func (a *InteractionApi) RemoveFavorite(c *gin.Context) {
|
||||
userId := auth.GetUserId(c)
|
||||
var req request.RemoveFavorite
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := interactionService.RemoveFavorite(userId, req.ProgramId); err != nil {
|
||||
global.Logger.Error("取消收藏失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMsg("取消收藏成功", c)
|
||||
}
|
||||
|
||||
// GetCommentList 获取评论列表
|
||||
// @Tags 用户互动
|
||||
// @Summary 获取评论列表
|
||||
// @Produce json
|
||||
// @Param data body request.GetCommentList true "分页查询"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/comment/list [post]
|
||||
func (a *InteractionApi) GetCommentList(c *gin.Context) {
|
||||
var req request.GetCommentList
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
list, total, err := interactionService.GetCommentList(req.ProgramId, 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,
|
||||
}, c)
|
||||
}
|
||||
|
||||
// AddComment 添加评论
|
||||
// @Tags 用户互动
|
||||
// @Summary 添加评论
|
||||
// @Produce json
|
||||
// @Param data body request.AddComment true "评论信息"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/comment/add [post]
|
||||
func (a *InteractionApi) AddComment(c *gin.Context) {
|
||||
userId := auth.GetUserId(c)
|
||||
var req request.AddComment
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := interactionService.AddComment(userId, req); err != nil {
|
||||
global.Logger.Error("添加评论失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMsg("评论成功", c)
|
||||
}
|
||||
|
||||
// DeleteComment 删除评论
|
||||
// @Tags 用户互动
|
||||
// @Summary 删除评论
|
||||
// @Produce json
|
||||
// @Param data body request.DeleteComment true "评论ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/comment/delete [post]
|
||||
func (a *InteractionApi) DeleteComment(c *gin.Context) {
|
||||
userId := auth.GetUserId(c)
|
||||
var req request.DeleteComment
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := interactionService.DeleteComment(userId, req.CommentId); err != nil {
|
||||
global.Logger.Error("删除评论失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMsg("删除成功", c)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
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 ProgramApi struct{}
|
||||
|
||||
// GetProgramList 获取节目列表
|
||||
// @Tags 节目管理
|
||||
// @Summary 获取节目列表
|
||||
// @Produce application/json
|
||||
// @Param data body request.GetProgramList true "分页查询"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/program/list [post]
|
||||
func (a *ProgramApi) GetProgramList(c *gin.Context) {
|
||||
var req request.GetProgramList
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
list, total, err := programService.GetProgramList(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)
|
||||
}
|
||||
|
||||
// GetProgramDetail 获取节目详情
|
||||
// @Tags 节目管理
|
||||
// @Summary 获取节目详情
|
||||
// @Produce application/json
|
||||
// @Param id query string true "节目ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/program/detail [get]
|
||||
func (a *ProgramApi) GetProgramDetail(c *gin.Context) {
|
||||
id := c.Query("id")
|
||||
if id == "" {
|
||||
response.FailWithMsg("参数错误: id不能为空", c)
|
||||
return
|
||||
}
|
||||
program, err := programService.GetProgramById(id)
|
||||
if err != nil {
|
||||
global.Logger.Error("获取节目详情失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithData(program, c)
|
||||
}
|
||||
|
||||
// SaveProgram 保存节目
|
||||
// @Tags 节目管理
|
||||
// @Summary 保存节目
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.SaveProgram true "节目信息"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/program/save [post]
|
||||
func (a *ProgramApi) SaveProgram(c *gin.Context) {
|
||||
var req request.SaveProgram
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := programService.SaveProgram(req); err != nil {
|
||||
global.Logger.Error("保存节目失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMsg("保存成功", c)
|
||||
}
|
||||
|
||||
// UpdateProgram 更新节目
|
||||
// @Tags 节目管理
|
||||
// @Summary 更新节目
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.UpdateProgram true "节目信息"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/program/update [post]
|
||||
func (a *ProgramApi) UpdateProgram(c *gin.Context) {
|
||||
var req request.UpdateProgram
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := programService.UpdateProgram(req); err != nil {
|
||||
global.Logger.Error("更新节目失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMsg("更新成功", c)
|
||||
}
|
||||
|
||||
// DeleteProgram 删除节目
|
||||
// @Tags 节目管理
|
||||
// @Summary 删除节目
|
||||
// @Produce json
|
||||
// @Param data body common.GetById true "节目ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/program/delete [post]
|
||||
func (a *ProgramApi) DeleteProgram(c *gin.Context) {
|
||||
var req common.IdsReq
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: id不能为空", c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := programService.DeleteProgram(req.Ids); err != nil {
|
||||
global.Logger.Error("删除节目失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMsg("删除成功", c)
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
common "sundynix-go/model/commom/request"
|
||||
"sundynix-go/model/commom/response"
|
||||
"sundynix-go/model/radio/request"
|
||||
"sundynix-go/utils/auth"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type SubscriptionApi struct{}
|
||||
|
||||
// GetSubscriptionList 获取订阅列表
|
||||
// @Tags 订阅管理
|
||||
// @Summary 获取我的订阅列表
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.GetSubscriptionList true "分页查询"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/subscription/list [post]
|
||||
func (a *SubscriptionApi) GetSubscriptionList(c *gin.Context) {
|
||||
userId := auth.GetUserId(c)
|
||||
var req common.PageInfo
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
list, total, err := subscriptionService.GetUserSubscription(userId, 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,
|
||||
}, c)
|
||||
}
|
||||
|
||||
// CanSubscribe 检查是否可以订阅
|
||||
// @Tags 订阅管理
|
||||
// @Summary 检查是否可以订阅
|
||||
// @Produce application/json
|
||||
// @Param data body request.SubscribeChannel true "频道ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/subscription/can-subscribe [post]
|
||||
func (a *SubscriptionApi) CanSubscribe(c *gin.Context) {
|
||||
userId := auth.GetUserId(c)
|
||||
var req request.SubscribeChannel
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
can, _, err := subscriptionService.CanSubscribe(userId, req.ChannelId)
|
||||
if err != nil {
|
||||
global.Logger.Error("检查订阅权限失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithData(can, c)
|
||||
}
|
||||
|
||||
// Subscribe 订阅频道
|
||||
// @Tags 订阅管理
|
||||
// @Summary 订阅频道
|
||||
// @Produce application/json
|
||||
// @Param data body request.SubscribeChannel true "频道ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/subscription/subscribe [post]
|
||||
func (a *SubscriptionApi) Subscribe(c *gin.Context) {
|
||||
userId := auth.GetUserId(c)
|
||||
var req request.SubscribeChannel
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
can, reason, err := subscriptionService.CanSubscribe(userId, req.ChannelId)
|
||||
if err != nil {
|
||||
global.Logger.Error("订阅失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
if !can {
|
||||
response.FailWithMsg(reason, c)
|
||||
return
|
||||
}
|
||||
|
||||
// 订阅类型 1:免费
|
||||
if err := subscriptionService.Subscribe(userId, req.ChannelId, 1); err != nil {
|
||||
global.Logger.Error("订阅失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMsg("订阅成功", c)
|
||||
}
|
||||
|
||||
// Unsubscribe 退订频道
|
||||
// @Tags 订阅管理
|
||||
// @Summary 退订频道
|
||||
// @Produce application/json
|
||||
// @Param data body request.UnsubscribeChannel true "频道ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /radio/subscription/unsubscribe [post]
|
||||
func (a *SubscriptionApi) Unsubscribe(c *gin.Context) {
|
||||
userId := auth.GetUserId(c)
|
||||
var req request.UnsubscribeChannel
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMsg("参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := subscriptionService.Unsubscribe(userId, req.ChannelId); err != nil {
|
||||
global.Logger.Error("退订失败!", zap.Error(err))
|
||||
response.FailWithMsg(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMsg("退订成功", c)
|
||||
}
|
||||
@@ -17,7 +17,7 @@ type ClientApi struct {
|
||||
// SaveClient
|
||||
// @Tags 客户端管理
|
||||
// @Summary 创建client
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body system.Client true "client"
|
||||
@@ -42,7 +42,7 @@ func (s *ClientApi) SaveClient(c *gin.Context) {
|
||||
// UpdateClient
|
||||
// @Tags 客户端管理
|
||||
// @Summary 更新client
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body system.Client true "client"
|
||||
@@ -67,7 +67,7 @@ func (s *ClientApi) UpdateClient(c *gin.Context) {
|
||||
// GetClientList
|
||||
// @Tags 客户端管理
|
||||
// @Summary 获取client列表
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body systemReq.GetClientList true "client"
|
||||
@@ -97,7 +97,7 @@ func (s *ClientApi) GetClientList(c *gin.Context) {
|
||||
// Delete
|
||||
// @Tags 客户端管理
|
||||
// @Summary 删除client
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.IdsReq true "ids"
|
||||
@@ -123,7 +123,7 @@ func (s *ClientApi) Delete(c *gin.Context) {
|
||||
// @Tags 客户端管理
|
||||
// @Summary 获取client详情
|
||||
// @Description id获取详情
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @Produce application/json
|
||||
// @Param id query string true "id"
|
||||
// @Success 200 {object} response.Response{data=system.Client,msg=string} "获取client详情"
|
||||
|
||||
@@ -17,7 +17,7 @@ type MenuApi struct {
|
||||
// SaveMenu
|
||||
// @Tags 菜单管理
|
||||
// @Summary 新增菜单
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body system.Menu false "menu"
|
||||
@@ -42,7 +42,7 @@ func (m *MenuApi) SaveMenu(c *gin.Context) {
|
||||
// UpdateMenu
|
||||
// @Tags 菜单管理
|
||||
// @Summary 更新菜单
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body system.Menu false "menu"
|
||||
@@ -68,7 +68,7 @@ func (m *MenuApi) UpdateMenu(c *gin.Context) {
|
||||
// @Tags 菜单管理
|
||||
// @Summary 删除menu
|
||||
// @Description 删除menu
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @Produce application/json
|
||||
// @Param id query string true "id"
|
||||
// @Success 200 {object} response.Response{msg=string} "详情"
|
||||
@@ -88,7 +88,7 @@ func (m *MenuApi) DeleteMenu(c *gin.Context) {
|
||||
// @Tags 菜单管理
|
||||
// @Summary 获取menu详情
|
||||
// @Description id获取详情
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @Produce application/json
|
||||
// @Param id query string true "id"
|
||||
// @Success 200 {object} response.Response{data=system.Menu,msg=string} "详情"
|
||||
@@ -107,7 +107,7 @@ func (m *MenuApi) Detail(c *gin.Context) {
|
||||
// GetAllMenuTree
|
||||
// @Tags 菜单管理
|
||||
// @Summary 获取所有菜单树
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body systemReq.GetMenuTree true "菜单信息"
|
||||
@@ -132,7 +132,7 @@ func (m *MenuApi) GetAllMenuTree(c *gin.Context) {
|
||||
// GetUserMenuTree
|
||||
// @Tags 菜单管理
|
||||
// @Summary 用户菜单数据
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.Response{data=[]system.Menu,msg=string} "用户菜单数据"
|
||||
// @Router /menu/getUserMenuTree [get]
|
||||
@@ -150,7 +150,7 @@ func (m *MenuApi) GetUserMenuTree(c *gin.Context) {
|
||||
// Route
|
||||
// @Tags 菜单管理
|
||||
// @Summary 用户路由
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.Response{data=[]system.Menu,msg=string} "用户route"
|
||||
// @Router /menu/route [get]
|
||||
|
||||
@@ -17,7 +17,7 @@ type RoleApi struct {
|
||||
// SaveRole
|
||||
// @tags 角色管理
|
||||
// @Summary 创建角色
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @accept json
|
||||
// @Produce json
|
||||
// @Param data body system.Role true "角色信息"
|
||||
@@ -42,7 +42,7 @@ func (a *RoleApi) SaveRole(context *gin.Context) {
|
||||
// UpdateRole
|
||||
// @tags 角色管理
|
||||
// @Summary 修改角色
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body system.Role true "角色ID"
|
||||
@@ -121,7 +121,7 @@ func (a *RoleApi) Delete(context *gin.Context) {
|
||||
// Detail
|
||||
// @Tags 角色管理
|
||||
// @Summary 角色详情
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @Produce application/json
|
||||
// @Param id query string true "id"
|
||||
// @Success 200 {object} response.Response{data=system.Role} "角色详情"
|
||||
@@ -140,7 +140,7 @@ func (a *RoleApi) Detail(context *gin.Context) {
|
||||
// GrantMenu
|
||||
// @tags 角色管理
|
||||
// @Summary 授权菜单给角色
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body systemreq.GrantMenu true "授权菜单给角色"
|
||||
|
||||
@@ -18,7 +18,7 @@ type UserApi struct {
|
||||
// CurrentUser
|
||||
// @tags 用户管理
|
||||
// @Summary 当前登录用户
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.Response "{"code": 200, "data": {}, "msg": "添加成功"}"
|
||||
// @Router /user/info [get]
|
||||
@@ -36,7 +36,7 @@ func (u *UserApi) CurrentUser(c *gin.Context) {
|
||||
// SaveUser
|
||||
// @tags 用户管理
|
||||
// @Summary 新增用户
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @accept json
|
||||
// @Produce json
|
||||
// @Param data body system.User true "用户信息"
|
||||
@@ -60,7 +60,7 @@ func (u *UserApi) SaveUser(c *gin.Context) {
|
||||
// UpdateUser
|
||||
// @tags 用户管理
|
||||
// @Summary 更新用户
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body system.User true "用户ID,用户信息"
|
||||
@@ -84,7 +84,7 @@ func (u *UserApi) UpdateUser(c *gin.Context) {
|
||||
// GetUserList
|
||||
// @tags 用户管理
|
||||
// @Summary 获取用户列表
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body systemReq.GetUserList true "页码, 每页大小, 搜索条件"
|
||||
@@ -114,7 +114,7 @@ func (u *UserApi) GetUserList(c *gin.Context) {
|
||||
// Delete
|
||||
// @Tags 用户管理
|
||||
// @Summary 删除用户
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.IdsReq true "批量删除用户"
|
||||
@@ -139,7 +139,7 @@ func (u *UserApi) Delete(c *gin.Context) {
|
||||
// Detail
|
||||
// @Tags 用户管理
|
||||
// @Summary 获取用户详情
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @Produce application/json
|
||||
// @Param id query string true "id"
|
||||
// @Success 200 {object} response.Response{data=system.User} "获取用户详情成功"
|
||||
@@ -158,7 +158,7 @@ func (u *UserApi) Detail(c *gin.Context) {
|
||||
// ChangePassword
|
||||
// @Tags 用户管理
|
||||
// @Summary 修改密码
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @Description 修改密码
|
||||
// @accept json
|
||||
// @Produce application/json
|
||||
@@ -184,7 +184,7 @@ func (u *UserApi) ChangePassword(c *gin.Context) {
|
||||
// GrantRole
|
||||
// @Tags 用户管理
|
||||
// @Summary 给用户分配角色
|
||||
// @Security ApiKeyAuth
|
||||
// @Security BasicAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body systemReq.GrantRole true "用户ID, 角色ID"
|
||||
|
||||
+3
-8
@@ -17,11 +17,6 @@ mini-program:
|
||||
app-id: wxb463820bf36dd5d6
|
||||
app-secret: 731784a74c76c6d31fa00bb847af2c7d
|
||||
|
||||
# 服务号
|
||||
service-account:
|
||||
app-id: wxc236cddde8e7f863
|
||||
app-secret: 26c1fcecfc98a748d8916355623c975c
|
||||
|
||||
# 微信支付
|
||||
wechat-pay:
|
||||
mch-id: 1735188493 # 商户号
|
||||
@@ -37,8 +32,8 @@ minio:
|
||||
access-key-id: qP5QXP3g6Axw1hkwX21Y
|
||||
access-key-secret: sddT6J3S6yDn9m1wfth0pzelPg9KWmbHjMAUF5S9
|
||||
base-path: ""
|
||||
bucket-name: sundynix-audios
|
||||
bucket-url: https://res.sundynix.cn/sundynix-audios
|
||||
bucket-name: sundynix-radio
|
||||
bucket-url: https://res.sundynix.cn/sundynix-radio
|
||||
endpoint: 129.28.103.17:3407
|
||||
use-ssl: false
|
||||
|
||||
@@ -46,7 +41,7 @@ rocket-mq:
|
||||
name-space: 192.168.100.140:9876
|
||||
endpoint: 192.168.100.140:8081 # 5.x版本使用了proxy 默认proxy地址是8081
|
||||
consumer-group: sundynix-radio
|
||||
topic: sundynix-radio-audio-generate
|
||||
topic: sundynix-radio-generate
|
||||
access-key: ""
|
||||
secret-key: ""
|
||||
enable-ssl: false
|
||||
|
||||
+2
-3
@@ -13,7 +13,6 @@ type Config struct {
|
||||
RocketMQConfig RocketMQConfig `mapstructure:"rocket-mq" json:"rocket-mq" yaml:"rocket-mq"`
|
||||
TencentCOS TencentCOS `mapstructure:"tencent-cos" json:"tencent-cos" yaml:"tencent-cos"`
|
||||
|
||||
MiniProgram MiniProgram `mapstructure:"mini-program" json:"mini-program" yaml:"mini-program"` //小程序
|
||||
ServiceAccount ServiceAccount `mapstructure:"service-account" json:"service-account" yaml:"service-account"` //服务号
|
||||
WechatPay WechatPay `mapstructure:"wechat-pay" json:"wechat-pay" yaml:"wechat-pay"` //微信支付
|
||||
MiniProgram MiniProgram `mapstructure:"mini-program" json:"mini-program" yaml:"mini-program"` //小程序
|
||||
WechatPay WechatPay `mapstructure:"wechat-pay" json:"wechat-pay" yaml:"wechat-pay"` //微信支付
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package config
|
||||
|
||||
// ActionType 定义养护行为类型
|
||||
type ActionType string
|
||||
|
||||
const (
|
||||
ActionWater ActionType = "WATER" // 浇水
|
||||
ActionFertilize ActionType = "FERTILIZE" // 施肥
|
||||
ActionPrune ActionType = "PRUNE" // 修剪
|
||||
ActionPhoto ActionType = "PHOTO" // 拍照
|
||||
ActionLogin ActionType = "LOGIN" // 每日签到
|
||||
)
|
||||
|
||||
// BadgeTier 定义徽章的稀有度
|
||||
type BadgeTier int
|
||||
|
||||
const (
|
||||
TierBronze BadgeTier = 1 // 铜
|
||||
TierSilver BadgeTier = 2 // 银
|
||||
TierGold BadgeTier = 3 // 金
|
||||
TierDiamond BadgeTier = 4 // 钻石
|
||||
)
|
||||
@@ -1,6 +0,0 @@
|
||||
package config
|
||||
|
||||
type ServiceAccount struct {
|
||||
AppId string `mapstructure:"app-id" json:"app-id" yaml:"app-id"`
|
||||
AppSecret string `mapstructure:"app-secret" json:"app-secret" yaml:"app-secret"`
|
||||
}
|
||||
-2332
File diff suppressed because it is too large
Load Diff
-2332
File diff suppressed because it is too large
Load Diff
-1482
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ package initialize
|
||||
import (
|
||||
"os"
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/radio"
|
||||
"sundynix-go/model/system"
|
||||
|
||||
"go.uber.org/zap"
|
||||
@@ -37,6 +38,12 @@ func MigrateTable() {
|
||||
system.Menu{},
|
||||
system.SysOperationRecord{},
|
||||
system.Oss{},
|
||||
|
||||
radio.RadioCategory{},
|
||||
radio.RadioChannel{},
|
||||
radio.RadioProgram{},
|
||||
radio.RadioSubscription{},
|
||||
radio.RadioUser{},
|
||||
)
|
||||
if err != nil {
|
||||
global.Logger.Error("Migrate table failed,err:", zap.Error(err))
|
||||
|
||||
@@ -26,6 +26,8 @@ func Routers() {
|
||||
|
||||
// 系统组路由
|
||||
systemRouter := router.GroupApp.System
|
||||
// Radio组路由
|
||||
radioRouter := router.GroupApp.Radio
|
||||
|
||||
NeedAuthGroup := Router.Group(global.Config.System.RouterPrefix)
|
||||
PublicGroup := Router.Group(global.Config.System.RouterPrefix)
|
||||
@@ -44,6 +46,13 @@ func Routers() {
|
||||
systemRouter.InitRoleRouter(NeedAuthGroup) //角色相关
|
||||
systemRouter.InitMenuRouter(NeedAuthGroup) //菜单相关
|
||||
systemRouter.InitOssRouter(NeedAuthGroup) //OSS相关
|
||||
|
||||
// Radio模块路由
|
||||
radioRouter.InitCategoryRouter(NeedAuthGroup) //分类相关
|
||||
radioRouter.InitChannelRouter(NeedAuthGroup) //频道相关
|
||||
radioRouter.InitProgramRouter(NeedAuthGroup) //节目相关
|
||||
radioRouter.InitSubscriptionRouter(NeedAuthGroup) //订阅相关
|
||||
radioRouter.InitInteractionRouter(NeedAuthGroup) //用户互动相关
|
||||
}
|
||||
|
||||
address := fmt.Sprintf(":%d", global.Config.System.Addr)
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[sundynix-radio-server]2026-02-25 17:24:01 [31merror[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/utils/auth/claims.go:125 获取用户信息失败,请检查请求头是否存在x-token且claims是否为规定结构
|
||||
[sundynix-radio-server]2026-02-25 17:24:44 [31merror[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/utils/auth/claims.go:125 获取用户信息失败,请检查请求头是否存在x-token且claims是否为规定结构
|
||||
[sundynix-radio-server]2026-02-25 17:25:21 [31merror[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/utils/auth/claims.go:125 获取用户信息失败,请检查请求头是否存在x-token且claims是否为规定结构
|
||||
[sundynix-radio-server]2026-02-25 17:26:46 [31merror[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/utils/auth/claims.go:125 获取用户信息失败,请检查请求头是否存在x-token且claims是否为规定结构
|
||||
@@ -1,12 +0,0 @@
|
||||
[sundynix-radio-server]2026-02-25 16:53:23 [34minfo[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/gorm_mysql.go:40 Mysql connect success
|
||||
[sundynix-radio-server]2026-02-25 16:53:23 [34minfo[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/redis.go:46 Redis connect ping response: {"name": "", "pong": "PONG"}
|
||||
[sundynix-radio-server]2026-02-25 16:53:28 [34minfo[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/gorm.go:54 Migrate table success
|
||||
[sundynix-radio-server]2026-02-25 17:01:39 [34minfo[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/gorm_mysql.go:40 Mysql connect success
|
||||
[sundynix-radio-server]2026-02-25 17:01:39 [34minfo[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/redis.go:46 Redis connect ping response: {"name": "", "pong": "PONG"}
|
||||
[sundynix-radio-server]2026-02-25 17:01:52 [34minfo[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/gorm.go:54 Migrate table success
|
||||
[sundynix-radio-server]2026-02-25 17:12:22 [34minfo[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/gorm_mysql.go:40 Mysql connect success
|
||||
[sundynix-radio-server]2026-02-25 17:12:22 [34minfo[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/redis.go:46 Redis connect ping response: {"name": "", "pong": "PONG"}
|
||||
[sundynix-radio-server]2026-02-25 17:12:37 [34minfo[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/gorm.go:54 Migrate table success
|
||||
[sundynix-radio-server]2026-02-25 17:27:33 [34minfo[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/service/radio/crawl.go:79 没有启用的抓取源,跳过本次抓取
|
||||
[sundynix-radio-server]2026-02-25 17:30:50 [34minfo[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/service/radio/crawl.go:95 开始抓取 GitHub 数据 {"source": "GitHub AI 热榜"}
|
||||
[sundynix-radio-server]2026-02-25 17:30:51 [34minfo[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/service/radio/crawl.go:158 GitHub 数据抓取完成 {"totalResults": 46, "fetched": 10}
|
||||
@@ -0,0 +1,9 @@
|
||||
[sundynix-radio-server]2026-02-28 15:39:45 [31merror[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/redis.go:43 Redis connect ping failed,err: {"name": "", "error": "dial tcp 127.0.0.1:6379: connect: connection refused"}
|
||||
[sundynix-radio-server]2026-02-28 15:39:45 [31merror[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/redis.go:17 Redis connect failed,err: {"error": "dial tcp 127.0.0.1:6379: connect: connection refused"}
|
||||
[sundynix-radio-server]2026-02-28 15:39:45 [31merror[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/internal/gorm_logger_writer.go:29 [35m[/Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/gorm.go:34 {{ 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC {0001-01-01 00:00:00 +0000 UTC %!s(bool=false)} } %!s(*system.Oss=<nil>) %!s(*system.Oss=<nil>) %!s(int=0) %!s(int=0)} invalid field found for struct sundynix-go/model/radio.RadioCategory's field Cover: define a valid foreign key for relations or implement the Valuer/Scanner interface]
|
||||
[0m[31m[error] [0mfailed to parse value %!v(MISSING), got error %!v(MISSING)
|
||||
[sundynix-radio-server]2026-02-28 15:39:51 [31merror[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/internal/gorm_logger_writer.go:29 [35m[/Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/gorm.go:34 {{ 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC {0001-01-01 00:00:00 +0000 UTC %!s(bool=false)} } %!s(*system.Oss=<nil>) %!s(*system.Oss=<nil>) %!s(int=0) %!s(int=0)} invalid field found for struct sundynix-go/model/radio.RadioCategory's field Cover: define a valid foreign key for relations or implement the Valuer/Scanner interface]
|
||||
[0m[31m[error] [0mfailed to parse value %!v(MISSING), got error %!v(MISSING)
|
||||
[sundynix-radio-server]2026-02-28 15:39:51 [31merror[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/gorm.go:49 Migrate table failed,err: {"error": "invalid field found for struct sundynix-go/model/radio.RadioCategory's field Cover: define a valid foreign key for relations or implement the Valuer/Scanner interface"}
|
||||
[sundynix-radio-server]2026-02-28 15:40:52 [31merror[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/redis.go:43 Redis connect ping failed,err: {"name": "", "error": "dial tcp 127.0.0.1:6379: connect: connection refused"}
|
||||
[sundynix-radio-server]2026-02-28 15:40:52 [31merror[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/redis.go:17 Redis connect failed,err: {"error": "dial tcp 127.0.0.1:6379: connect: connection refused"}
|
||||
@@ -0,0 +1,3 @@
|
||||
[sundynix-radio-server]2026-02-28 15:39:45 [34minfo[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/gorm_mysql.go:40 Mysql connect success
|
||||
[sundynix-radio-server]2026-02-28 15:40:52 [34minfo[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/gorm_mysql.go:40 Mysql connect success
|
||||
[sundynix-radio-server]2026-02-28 15:41:00 [34minfo[0m /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/gorm.go:52 Migrate table success
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
// @title Swagger API接口文档
|
||||
// @version v1.0.0
|
||||
// @description 使用gin + gorm进行极速开发的全栈开发基础平台
|
||||
// @securityDefinitions.apikey BearerAuth
|
||||
// @securityDefinitions.basic BearerAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @BasePath /
|
||||
|
||||
@@ -36,6 +36,9 @@ func (r *GetById) Uint() string {
|
||||
return string(r.ID)
|
||||
}
|
||||
|
||||
type IdReq struct {
|
||||
Id string `json:"id" form:"id"`
|
||||
}
|
||||
type IdsReq struct {
|
||||
Ids []string `json:"ids" form:"ids"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/system"
|
||||
)
|
||||
|
||||
// RadioCategory 电台分类表
|
||||
type RadioCategory struct {
|
||||
global.BaseModel
|
||||
Name string `gorm:"size:50" json:"name"` // 分类名称
|
||||
Description string `gorm:"size:255" json:"description"` // 分类描述
|
||||
IconId string `gorm:"size:50" json:"iconId"` // 图标OSS ID
|
||||
Icon *system.Oss `gorm:"foreignKey:IconId" json:"icon"` // 图标OSS
|
||||
CoverId string `gorm:"size:50" json:"coverId"` // 封面图OSS ID
|
||||
Cover *system.Oss `gorm:"foreignKey:CoverId" json:"cover"` // 封面图OSS
|
||||
Sort int `gorm:"default:0" json:"sort"` // 排序
|
||||
Status int `gorm:"default:1" json:"status"` // 状态 0:禁用 1:启用
|
||||
}
|
||||
|
||||
func (RadioCategory) TableName() string {
|
||||
return "sundynix_radio_category"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/system"
|
||||
)
|
||||
|
||||
// RadioChannel 电台频道表
|
||||
type RadioChannel struct {
|
||||
global.BaseModel
|
||||
CategoryId string `gorm:"size:50;index" json:"categoryId"` // 分类ID
|
||||
Name string `gorm:"size:50" json:"name"` // 频道名称
|
||||
Description string `gorm:"size:500" json:"description"` // 频道描述
|
||||
Price int `gorm:"default:0;comment:价格,单位,分 " json:"price"` //价格 单位:分
|
||||
CoverId string `gorm:"size:50" json:"coverId"` // 封面图OSS ID
|
||||
Cover *system.Oss `gorm:"foreignKey:CoverId" json:"cover"` // 封面图OSS
|
||||
Tags string `gorm:"size:255" json:"tags"` // 标签,逗号分隔
|
||||
IsVipOnly int `gorm:"default:0" json:"isVipOnly"` // 是否VIP专享 0:否 1:是
|
||||
Sort int `gorm:"default:0" json:"sort"` // 排序
|
||||
Status int `gorm:"default:1" json:"status"` // 状态 0:禁用 1:启用
|
||||
HasSubscribed int `gorm:"-" json:"hasSubscribed"` // 状态 0:未订阅 1:已订阅
|
||||
}
|
||||
|
||||
func (RadioChannel) TableName() string {
|
||||
return "sundynix_radio_channel"
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
)
|
||||
|
||||
// RadioComment 用户评论表
|
||||
type RadioComment struct {
|
||||
global.BaseModel
|
||||
ProgramId string `gorm:"size:50;index" json:"programId"` // 节目ID
|
||||
UserId string `gorm:"size:50;index" json:"userId"` // 用户ID
|
||||
ParentId string `gorm:"size:50" json:"parentId"` // 父评论ID
|
||||
Content string `gorm:"type:text" json:"content"` // 评论内容
|
||||
LikeCount int `gorm:"default:0" json:"likeCount"` // 点赞数
|
||||
}
|
||||
|
||||
func (RadioComment) TableName() string {
|
||||
return "sundynix_radio_comment"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
)
|
||||
|
||||
// RadioFavorite 用户收藏表
|
||||
type RadioFavorite struct {
|
||||
global.BaseModel
|
||||
UserId string `gorm:"size:50;index" json:"userId"` // 用户ID
|
||||
ProgramId string `gorm:"size:50;index" json:"programId"` // 节目ID
|
||||
}
|
||||
|
||||
func (RadioFavorite) TableName() string {
|
||||
return "sundynix_radio_favorite"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
)
|
||||
|
||||
// RadioHistory 用户收听历史表
|
||||
type RadioHistory struct {
|
||||
global.BaseModel
|
||||
UserId string `gorm:"size:50;index" json:"userId"` // 用户ID
|
||||
ProgramId string `gorm:"size:50;index" json:"programId"` // 节目ID
|
||||
Progress int `gorm:"default:0" json:"progress"` // 播放进度(秒)
|
||||
Duration int `gorm:"default:0" json:"duration"` // 节目总时长(秒)
|
||||
}
|
||||
|
||||
func (RadioHistory) TableName() string {
|
||||
return "sundynix_radio_history"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
)
|
||||
|
||||
// RadioLike 用户点赞表
|
||||
type RadioLike struct {
|
||||
global.BaseModel
|
||||
UserId string `gorm:"size:50;index" json:"userId"` // 用户ID
|
||||
ProgramId string `gorm:"size:50;index" json:"programId"` // 节目ID
|
||||
}
|
||||
|
||||
func (RadioLike) TableName() string {
|
||||
return "sundynix_radio_like"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/system"
|
||||
)
|
||||
|
||||
// RadioProgram 电台节目表
|
||||
type RadioProgram struct {
|
||||
global.BaseModel
|
||||
ChannelId string `gorm:"size:50;index" json:"channelId"` // 频道ID
|
||||
Title string `gorm:"size:100" json:"title"` // 节目标题
|
||||
Description string `gorm:"size:500" json:"description"` // 节目描述
|
||||
Content string `gorm:"type:text" json:"content"`
|
||||
CoverId string `gorm:"size:50" json:"coverId"` // 封面图OSS ID
|
||||
Cover *system.Oss `gorm:"foreignKey:CoverId" json:"coverUrl"` // 封面图OSS
|
||||
AudioId string `gorm:"size:50" json:"audioId"` // 音频OSS ID
|
||||
Audio *system.Oss `gorm:"foreignKey:AudioId" json:"audio"` // 音频OSS
|
||||
Duration int `gorm:"default:0" json:"duration"` // 时长(秒)
|
||||
Tags string `gorm:"size:255" json:"tags"` // 标签,逗号分隔
|
||||
PlayCount int `gorm:"default:0" json:"playCount"` // 播放次数
|
||||
LikeCount int `gorm:"default:0" json:"likeCount"` // 点赞次数
|
||||
Status int `gorm:"default:1" json:"status"` // 状态 0:下架 1:上架
|
||||
}
|
||||
|
||||
func (RadioProgram) TableName() string {
|
||||
return "sundynix_radio_program"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
)
|
||||
|
||||
// RadioSubscription 用户订阅表
|
||||
type RadioSubscription struct {
|
||||
global.BaseModel
|
||||
UserId string `gorm:"size:50;index;not null;uniqueIndex:idx_user_channel" json:"userId"` // 用户ID
|
||||
ChannelId string `gorm:"size:50;index;uniqueIndex:idx_user_channel" json:"channelId"` // 频道ID
|
||||
Status int `gorm:"type:tinyint;default:1"` //1-订阅中,2-已取消
|
||||
}
|
||||
|
||||
func (RadioSubscription) TableName() string {
|
||||
return "sundynix_radio_subscription"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/system"
|
||||
)
|
||||
|
||||
// RadioUser 小程序用户信息表
|
||||
type RadioUser struct {
|
||||
global.BaseModel
|
||||
UserId string `gorm:"size:50;uniqueIndex" json:"userId"` // 关联system用户ID
|
||||
OpenId string `gorm:"size:80;uniqueIndex" json:"openId"` // 微信openid
|
||||
UnionId string `gorm:"size:80" json:"unionId"` // 微信unionid
|
||||
SessionKey string `gorm:"size:200" json:"sessionKey"` // 会话密钥
|
||||
NickName string `gorm:"size:50" json:"nickName"` // 昵称
|
||||
AvatarId string `gorm:"size:50" json:"avatarId"` // 头像OSS ID
|
||||
Avatar *system.Oss `gorm:"foreignKey:AvatarId" json:"avatar"` // 头像OSS
|
||||
Gender int `gorm:"default:0" json:"gender"` // 性别 0:未知 1:男 2:女
|
||||
Country string `gorm:"size:50" json:"country"` // 国家
|
||||
Province string `gorm:"size:50" json:"province"` // 省份
|
||||
City string `gorm:"size:50" json:"city"` // 城市
|
||||
Language string `gorm:"size:20" json:"language"` // 语言
|
||||
IsVip int `gorm:"default:0" json:"isVip"` // 是否VIP 0:否 1:是
|
||||
VipExpireAt *int64 `gorm:"type:bigint" json:"vipExpireAt"` // VIP过期时间
|
||||
}
|
||||
|
||||
func (RadioUser) TableName() string {
|
||||
return "sundynix_radio_user"
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package request
|
||||
|
||||
import common "sundynix-go/model/commom/request"
|
||||
|
||||
// GetCategoryList 获取分类列表请求
|
||||
type GetCategoryList struct {
|
||||
common.PageInfo
|
||||
Name string `json:"name" form:"name"` // 分类名称
|
||||
Status int `json:"status" form:"status"` // 状态
|
||||
}
|
||||
|
||||
// SaveCategory 保存分类请求
|
||||
type SaveCategory struct {
|
||||
Id string `json:"id" form:"id"` // 分类ID(更新时使用)
|
||||
Name string `json:"name" binding:"required"` // 分类名称
|
||||
Description string `json:"description"` // 分类描述
|
||||
IconId string `json:"iconId"` // 图标URL
|
||||
CoverId string `json:"coverId"` // 封面图URL
|
||||
Sort int `json:"sort"` // 排序
|
||||
Status int `json:"status"` // 状态
|
||||
}
|
||||
|
||||
// UpdateCategory 更新分类请求
|
||||
type UpdateCategory struct {
|
||||
Id string `json:"id" binding:"required"` // 分类ID
|
||||
Name string `json:"name"` // 分类名称
|
||||
Description string `json:"description"` // 分类描述
|
||||
IconId string `json:"iconId"` // 图标URL
|
||||
CoverId string `json:"coverId"` // 封面图URL
|
||||
Sort int `json:"sort"` // 排序
|
||||
Status int `json:"status"` // 状态
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package request
|
||||
|
||||
import common "sundynix-go/model/commom/request"
|
||||
|
||||
// GetChannelList 获取频道列表请求
|
||||
type GetChannelList struct {
|
||||
common.PageInfo
|
||||
CategoryId string `json:"categoryId" form:"categoryId"` // 分类ID
|
||||
Name string `json:"name" form:"name"` // 频道名称
|
||||
Status int `json:"status" form:"status"` // 状态
|
||||
}
|
||||
|
||||
// SaveChannel 保存频道请求
|
||||
type SaveChannel struct {
|
||||
Id string `json:"id" form:"id"` // 频道ID(更新时使用)
|
||||
CategoryId string `json:"categoryId" binding:"required"` // 分类ID
|
||||
Name string `json:"name" binding:"required"` // 频道名称
|
||||
Description string `json:"description"` // 频道描述
|
||||
CoverId string `json:"coverId"` // 封面图URL
|
||||
Tags string `json:"tags"` // 标签
|
||||
IsVipOnly int `json:"isVipOnly"` // 是否VIP专享
|
||||
Sort int `json:"sort"` // 排序
|
||||
Status int `json:"status"` // 状态
|
||||
}
|
||||
|
||||
// UpdateChannel 更新频道请求
|
||||
type UpdateChannel struct {
|
||||
Id string `json:"id" binding:"required"` // 频道ID
|
||||
CategoryId string `json:"categoryId"` // 分类ID
|
||||
Name string `json:"name"` // 频道名称
|
||||
Description string `json:"description"` // 频道描述
|
||||
CoverId string `json:"coverId"` // 封面图URL
|
||||
Tags string `json:"tags"` // 标签
|
||||
IsVipOnly int `json:"isVipOnly"` // 是否VIP专享
|
||||
Sort int `json:"sort"` // 排序
|
||||
Status int `json:"status"` // 状态
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package request
|
||||
|
||||
import common "sundynix-go/model/commom/request"
|
||||
|
||||
// SubscribeChannel 订阅频道请求
|
||||
type SubscribeChannel struct {
|
||||
ChannelId string `json:"channelId" binding:"required"` // 频道ID
|
||||
}
|
||||
|
||||
// UnsubscribeChannel 退订频道请求
|
||||
type UnsubscribeChannel struct {
|
||||
ChannelId string `json:"channelId" binding:"required"` // 频道ID
|
||||
}
|
||||
|
||||
// AddHistory 添加收听历史请求
|
||||
type AddHistory struct {
|
||||
ProgramId string `json:"programId" binding:"required"` // 节目ID
|
||||
Progress int `json:"progress"` // 播放进度(秒)
|
||||
Duration int `json:"duration"` // 节目总时长(秒)
|
||||
}
|
||||
|
||||
// ToggleLike 切换点赞请求
|
||||
type ToggleLike struct {
|
||||
ProgramId string `json:"programId" binding:"required"` // 节目ID
|
||||
}
|
||||
|
||||
// AddFavorite 添加收藏请求
|
||||
type AddFavorite struct {
|
||||
ProgramId string `json:"programId" binding:"required"` // 节目ID
|
||||
}
|
||||
|
||||
// RemoveFavorite 移除收藏请求
|
||||
type RemoveFavorite struct {
|
||||
ProgramId string `json:"programId" binding:"required"` // 节目ID
|
||||
}
|
||||
|
||||
// AddComment 添加评论请求
|
||||
type AddComment struct {
|
||||
ProgramId string `json:"programId" binding:"required"` // 节目ID
|
||||
ParentId string `json:"parentId"` // 父评论ID
|
||||
Content string `json:"content" binding:"required"` // 评论内容
|
||||
}
|
||||
|
||||
// DeleteComment 删除评论请求
|
||||
type DeleteComment struct {
|
||||
CommentId string `json:"commentId" binding:"required"` // 评论ID
|
||||
}
|
||||
|
||||
// GetHistoryList 获取收听历史列表请求
|
||||
type GetHistoryList struct {
|
||||
common.PageInfo
|
||||
}
|
||||
|
||||
// GetFavoriteList 获取收藏列表请求
|
||||
type GetFavoriteList struct {
|
||||
common.PageInfo
|
||||
}
|
||||
|
||||
// GetCommentList 获取评论列表请求
|
||||
type GetCommentList struct {
|
||||
common.PageInfo
|
||||
ProgramId string `json:"programId" form:"programId"` // 节目ID
|
||||
}
|
||||
|
||||
// GetSubscriptionList 获取订阅列表请求
|
||||
type GetSubscriptionList struct {
|
||||
common.PageInfo
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package request
|
||||
|
||||
import common "sundynix-go/model/commom/request"
|
||||
|
||||
// GetProgramList 获取节目列表请求
|
||||
type GetProgramList struct {
|
||||
common.PageInfo
|
||||
ChannelId string `json:"channelId" form:"channelId"` // 频道ID
|
||||
Title string `json:"title" form:"title"` // 节目标题
|
||||
Status int `json:"status" form:"status"` // 状态
|
||||
}
|
||||
|
||||
// SaveProgram 保存节目请求
|
||||
type SaveProgram struct {
|
||||
Id string `json:"id" form:"id"` // 节目ID(更新时使用)
|
||||
ChannelId string `json:"channelId" binding:"required"` // 频道ID
|
||||
Title string `json:"title" binding:"required"` // 节目标题
|
||||
Description string `json:"description"` // 节目描述
|
||||
Content string `json:"content"`
|
||||
CoverId string `json:"coverId"` // 封面图URL
|
||||
AudioId string `json:"audioId"` // 音频URL
|
||||
Duration int `json:"duration"` // 时长(秒)
|
||||
Tags string `json:"tags"` // 标签
|
||||
Status int `json:"status"` // 状态
|
||||
}
|
||||
|
||||
// UpdateProgram 更新节目请求
|
||||
type UpdateProgram struct {
|
||||
Id string `json:"id" binding:"required"` // 节目ID
|
||||
ChannelId string `json:"channelId"` // 频道ID
|
||||
Title string `json:"title"` // 节目标题
|
||||
Description string `json:"description"` // 节目描述
|
||||
Content string `json:"content"`
|
||||
CoverId string `json:"coverId"` // 封面图URL
|
||||
AudioId string `json:"audioId"` // 音频URL
|
||||
Duration int `json:"duration"` // 时长(秒)
|
||||
Tags string `json:"tags"` // 标签
|
||||
Status int `json:"status"` // 状态
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package response
|
||||
|
||||
import "sundynix-go/model/radio"
|
||||
|
||||
// CategoryResponse 分类响应
|
||||
type CategoryResponse struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Icon string `json:"icon"`
|
||||
CoverUrl string `json:"coverUrl"`
|
||||
Sort int `json:"sort"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
// CategoryDetailResponse 分类详情响应
|
||||
type CategoryDetailResponse struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Icon string `json:"icon"`
|
||||
CoverUrl string `json:"coverUrl"`
|
||||
Sort int `json:"sort"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
// ToCategoryResponse 转换为分类响应
|
||||
func ToCategoryResponse(category *radio.RadioCategory) CategoryResponse {
|
||||
resp := CategoryResponse{
|
||||
Id: category.Id,
|
||||
Name: category.Name,
|
||||
Description: category.Description,
|
||||
Sort: category.Sort,
|
||||
Status: category.Status,
|
||||
}
|
||||
if category.Icon != nil {
|
||||
resp.Icon = category.Icon.Url
|
||||
}
|
||||
if category.Cover != nil {
|
||||
resp.CoverUrl = category.Cover.Url
|
||||
}
|
||||
return resp
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package response
|
||||
|
||||
// ChannelResponse 频道响应
|
||||
type ChannelResponse struct {
|
||||
Id string `json:"id"`
|
||||
CategoryId string `json:"categoryId"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
CoverUrl string `json:"coverUrl"`
|
||||
StreamUrl string `json:"streamUrl"`
|
||||
Tags string `json:"tags"`
|
||||
IsVipOnly int `json:"isVipOnly"`
|
||||
Sort int `json:"sort"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
// ChannelDetailResponse 频道详情响应
|
||||
type ChannelDetailResponse struct {
|
||||
Id string `json:"id"`
|
||||
CategoryId string `json:"categoryId"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
CoverUrl string `json:"coverUrl"`
|
||||
StreamUrl string `json:"streamUrl"`
|
||||
Tags string `json:"tags"`
|
||||
IsVipOnly int `json:"isVipOnly"`
|
||||
Sort int `json:"sort"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
# Radio 模块开发计划 (v2.0)
|
||||
|
||||
## 1. 项目架构分析
|
||||
|
||||
### 1.1 现有项目结构
|
||||
本项目是基于 Gin + Gorm 的 Go 后端服务,采用典型的分层架构:
|
||||
|
||||
```
|
||||
morning-radio-backend/
|
||||
├── main.go # 程序入口
|
||||
├── api/v1/ # API 路由处理层
|
||||
├── router/ # 路由定义层
|
||||
├── service/ # 业务逻辑层
|
||||
├── model/ # 数据模型层
|
||||
├── initialize/ # 初始化模块
|
||||
├── global/ # 全局变量
|
||||
├── middleware/ # 中间件
|
||||
├── config/ # 配置管理
|
||||
└── utils/ # 工具类
|
||||
```
|
||||
|
||||
## 2. Radio 模块设计 (v2.0)
|
||||
|
||||
### 2.1 功能模块层级
|
||||
|
||||
```
|
||||
分类 (Category)
|
||||
└── 频道 (Channel)
|
||||
└── 节目 (Program)
|
||||
```
|
||||
|
||||
### 2.2 用户权限体系
|
||||
|
||||
| 权限类型 | 说明 |
|
||||
|----------|------|
|
||||
| 免费用户 | 可订阅2个频道,解锁对应节目 |
|
||||
| 订阅用户 | 支付订阅特定频道,解锁该频道所有节目 |
|
||||
| VIP用户 | 解锁全部频道的所有节目 |
|
||||
|
||||
### 2.3 数据库表设计
|
||||
|
||||
#### 2.3.1 分类表 (radio_category)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | string | 主键ID (继承BaseModel) |
|
||||
| name | string | 分类名称 |
|
||||
| description | string | 分类描述 |
|
||||
| icon | string | 图标URL |
|
||||
| sort | int | 排序 |
|
||||
| status | int | 状态(0:禁用 1:启用) |
|
||||
|
||||
#### 2.3.2 频道表 (radio_channel)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | string | 主键ID (继承BaseModel) |
|
||||
| category_id | string | 分类ID |
|
||||
| name | string | 频道名称 |
|
||||
| description | string | 频道描述 |
|
||||
| cover_url | string | 封面图URL |
|
||||
| stream_url | string | 音频流地址 |
|
||||
| tags | string | 标签 |
|
||||
| is_vip_only | int | 是否VIP专享(0:否 1:是) |
|
||||
| sort | int | 排序 |
|
||||
| status | int | 状态(0:禁用 1:启用) |
|
||||
|
||||
#### 2.3.3 节目表 (radio_program)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | string | 主键ID (继承BaseModel) |
|
||||
| channel_id | string | 频道ID |
|
||||
| title | string | 节目标题 |
|
||||
| description | string | 节目描述 |
|
||||
| cover_url | string | 封面图URL |
|
||||
| audio_url | string | 音频URL |
|
||||
| duration | int | 时长(秒) |
|
||||
| tags | string | 标签 |
|
||||
| play_count | int | 播放次数 |
|
||||
| like_count | int | 点赞次数 |
|
||||
| status | int | 状态(0:下架 1:上架) |
|
||||
|
||||
#### 2.3.4 用户订阅表 (radio_subscription)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | string | 主键ID (继承BaseModel) |
|
||||
| user_id | string | 用户ID |
|
||||
| channel_id | string | 频道ID |
|
||||
| subscription_type | int | 订阅类型(1:免费 2:付费 3:VIP) |
|
||||
| expire_at | time | 过期时间 |
|
||||
| created_at | time | 订阅时间 |
|
||||
|
||||
#### 2.3.5 收听历史表 (radio_history)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | string | 主键ID (继承BaseModel) |
|
||||
| user_id | string | 用户ID |
|
||||
| program_id | string | 节目ID |
|
||||
| progress | int | 播放进度(秒) |
|
||||
| duration | int | 节目总时长 |
|
||||
| created_at | time | 收听时间 |
|
||||
|
||||
#### 2.3.6 点赞表 (radio_like)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | string | 主键ID (继承BaseModel) |
|
||||
| user_id | string | 用户ID |
|
||||
| program_id | string | 节目ID |
|
||||
| created_at | time | 点赞时间 |
|
||||
|
||||
#### 2.3.7 收藏表 (radio_favorite)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | string | 主键ID (继承BaseModel) |
|
||||
| user_id | string | 用户ID |
|
||||
| program_id | string | 节目ID |
|
||||
| created_at | time | 收藏时间 |
|
||||
|
||||
#### 2.3.8 评论表 (radio_comment)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | string | 主键ID (继承BaseModel) |
|
||||
| program_id | string | 节目ID |
|
||||
| user_id | string | 用户ID |
|
||||
| parent_id | string | 父评论ID |
|
||||
| content | string | 评论内容 |
|
||||
| like_count | int | 点赞数 |
|
||||
| created_at | time | 评论时间 |
|
||||
|
||||
### 2.4 Radio 模块目录结构
|
||||
|
||||
```
|
||||
model/radio/
|
||||
├── radio_category.go # 分类模型
|
||||
├── radio_channel.go # 频道模型
|
||||
├── radio_program.go # 节目模型
|
||||
├── radio_subscription.go # 订阅模型
|
||||
├── radio_favorite.go # 收藏模型
|
||||
├── radio_history.go # 收听历史模型
|
||||
├── radio_like.go # 点赞模型
|
||||
├── radio_comment.go # 评论模型
|
||||
├── request/
|
||||
│ ├── category.go # 分类请求结构
|
||||
│ ├── channel.go # 频道请求结构
|
||||
│ ├── program.go # 节目请求结构
|
||||
│ └── interaction.go # 互动请求结构
|
||||
└── response/
|
||||
├── category.go # 分类响应结构
|
||||
├── channel.go # 频道响应结构
|
||||
├── program.go # 节目响应结构
|
||||
└── interaction.go # 互动响应结构
|
||||
|
||||
api/v1/radio/
|
||||
├── enter.go # Radio API组定义
|
||||
├── category.go # 分类API
|
||||
├── channel.go # 频道API
|
||||
├── program.go # 节目API
|
||||
├── subscription.go # 订阅API
|
||||
└── interaction.go # 互动API
|
||||
|
||||
router/radio/
|
||||
├── enter.go # Radio 路由组定义
|
||||
├── category_router.go # 分类路由
|
||||
├── channel_router.go # 频道路由
|
||||
├── program_router.go # 节目路由
|
||||
├── subscription_router.go # 订阅路由
|
||||
└── interaction_router.go # 互动路由
|
||||
|
||||
service/radio/
|
||||
├── enter.go # Radio 服务组定义
|
||||
├── category_service.go # 分类业务逻辑
|
||||
├── channel_service.go # 频道业务逻辑
|
||||
├── program_service.go # 节目业务逻辑
|
||||
├── subscription_service.go # 订阅业务逻辑
|
||||
└── interaction_service.go # 互动业务逻辑
|
||||
```
|
||||
|
||||
## 3. 开发步骤
|
||||
|
||||
### 步骤1: 创建模块目录结构和文件骨架
|
||||
|
||||
- [ ] 创建 model/radio/ 目录及子目录
|
||||
- [ ] 创建 api/v1/radio/ 目录
|
||||
- [ ] 创建 router/radio/ 目录
|
||||
- [ ] 创建 service/radio/ 目录
|
||||
- [ ] 创建各层级的 enter.go 入口文件
|
||||
|
||||
### 步骤2: 实现数据模型层
|
||||
|
||||
- [ ] 实现 RadioCategory 分类模型
|
||||
- [ ] 实现 RadioChannel 频道模型
|
||||
- [ ] 实现 RadioProgram 节目模型
|
||||
- [ ] 实现 RadioSubscription 订阅模型
|
||||
- [ ] 实现 RadioHistory 收听历史模型
|
||||
- [ ] 实现 RadioLike 点赞模型
|
||||
- [ ] 实现 RadioFavorite 收藏模型
|
||||
- [ ] 实现 RadioComment 评论模型
|
||||
|
||||
### 步骤3: 实现请求/响应数据结构
|
||||
|
||||
- [ ] 实现分类请求/响应结构
|
||||
- [ ] 实现频道请求/响应结构
|
||||
- [ ] 实现节目请求/响应结构
|
||||
- [ ] 实现订阅请求/响应结构
|
||||
- [ ] 实现互动请求/响应结构
|
||||
|
||||
### 步骤4: 实现业务逻辑层
|
||||
|
||||
- [ ] 实现分类Service (CRUD)
|
||||
- [ ] 实现频道Service (CRUD, 获取用户权限下的节目列表)
|
||||
- [ ] 实现节目Service (CRUD, 根据用户权限过滤 实现订阅Service ()
|
||||
- [ ]订阅/退订/查询/检查权限)
|
||||
- [ ] 实现收听历史Service (记录/列表)
|
||||
- [ ] 实现点赞Service (点赞/取消/状态)
|
||||
- [ ] 实现收藏Service (添加/删除/列表)
|
||||
- [ ] 实现评论Service (发表/删除/列表)
|
||||
|
||||
### 步骤5: 实现API路由处理层
|
||||
|
||||
- [ ] 实现分类API
|
||||
- [ ] 实现频道API
|
||||
- [ ] 实现节目API
|
||||
- [ ] 实现订阅API
|
||||
- [ ] 实现互动API
|
||||
|
||||
### 步骤6: 实现路由定义层
|
||||
|
||||
- [ ] 实现分类路由初始化
|
||||
- [ ] 实现频道路由初始化
|
||||
- [ ] 实现节目路由初始化
|
||||
- [ ] 实现订阅路由初始化
|
||||
- [ ] 实现互动路由初始化
|
||||
|
||||
### 步骤7: 注册模块到全局
|
||||
|
||||
- [ ] 修改 api/v1/enter.go 添加 RadioApiGroup
|
||||
- [ ] 修改 router/enter.go 添加 RadioRouterGroup
|
||||
- [ ] 修改 service/enter.go 添加 RadioServiceGroup
|
||||
- [ ] 修改 initialize/router.go 注册Radio路由
|
||||
|
||||
## 4. API 接口设计
|
||||
|
||||
### 4.1 分类管理 API
|
||||
|
||||
| 方法 | 路径 | 说明 | 权限 |
|
||||
|------|------|------|------|
|
||||
| GET | /radio/category/list | 获取分类列表 | 公开 |
|
||||
| GET | /radio/category/detail | 获取分类详情 | 公开 |
|
||||
| POST | /radio/category/save | 新增分类 | 需鉴权 |
|
||||
| POST | /radio/category/update | 更新分类 | 需鉴权 |
|
||||
| POST | /radio/category/delete | 删除分类 | 需鉴权 |
|
||||
|
||||
### 4.2 频道管理 API
|
||||
|
||||
| 方法 | 路径 | 说明 | 权限 |
|
||||
|------|------|------|------|
|
||||
| GET | /radio/channel/list | 获取频道列表(按分类) | 公开 |
|
||||
| GET | /radio/channel/detail | 获取频道详情 | 公开 |
|
||||
| GET | /radio/channel/programs | 获取频道节目(权限过滤) | 需鉴权 |
|
||||
| POST | /radio/channel/save | 新增频道 | 需鉴权 |
|
||||
| POST | /radio/channel/update | 更新频道 | 需鉴权 |
|
||||
| POST | /radio/channel/delete | 删除频道 | 需鉴权 |
|
||||
|
||||
### 4.3 节目管理 API
|
||||
|
||||
| 方法 | 路径 | 说明 | 权限 |
|
||||
|------|------|------|------|
|
||||
| GET | /radio/program/list | 获取节目列表 | 公开 |
|
||||
| GET | /radio/program/detail | 获取节目详情(权限验证) | 需鉴权 |
|
||||
| POST | /radio/program/save | 新增节目 | 需鉴权 |
|
||||
| POST | /radio/program/update | 更新节目 | 需鉴权 |
|
||||
| POST | /radio/program/delete | 删除节目 | 需鉴权 |
|
||||
|
||||
### 4.4 订阅管理 API
|
||||
|
||||
| 方法 | 路径 | 说明 | 权限 |
|
||||
|------|------|------|------|
|
||||
| GET | /radio/subscription/list | 获取我的订阅列表 | 需鉴权 |
|
||||
| GET | /radio/subscription/can-subscribe | 检查是否可以订阅 | 需鉴权 |
|
||||
| POST | /radio/subscription/subscribe | 订阅频道 | 需鉴权 |
|
||||
| POST | /radio/subscription/unsubscribe | 退订频道 | 需鉴权 |
|
||||
| GET | /radio/subscription/my-vip-status | 获取VIP状态 | 需鉴权 |
|
||||
|
||||
### 4.5 用户互动 API
|
||||
|
||||
| 方法 | 路径 | 说明 | 权限 |
|
||||
|------|------|------|------|
|
||||
| GET | /radio/history/list | 获取收听历史 | 需鉴权 |
|
||||
| POST | /radio/history/add | 添加收听历史 | 需鉴权 |
|
||||
| POST | /radio/like/toggle | 切换点赞状态 | 需鉴权 |
|
||||
| GET | /radio/favorite/list | 获取收藏列表 | 需鉴权 |
|
||||
| POST | /radio/favorite/add | 添加收藏 | 需鉴权 |
|
||||
| POST | /radio/favorite/remove | 取消收藏 | 需鉴权 |
|
||||
| GET | /radio/comment/list | 获取评论列表 | 公开 |
|
||||
| POST | /radio/comment/add | 添加评论 | 需鉴权 |
|
||||
| POST | /radio/comment/delete | 删除评论 | 需鉴权 |
|
||||
|
||||
## 5. 权限检查逻辑
|
||||
|
||||
### 5.1 节目访问权限检查
|
||||
|
||||
```go
|
||||
func CanAccessProgram(userId, programId string) bool {
|
||||
// 1. 获取节目信息
|
||||
program := GetProgram(programId)
|
||||
channel := GetChannel(program.ChannelId)
|
||||
|
||||
// 2. 检查频道是否为VIP专享
|
||||
if channel.IsVipOnly == 1 {
|
||||
return user.HasVip(userId)
|
||||
}
|
||||
|
||||
// 3. 检查用户是否订阅了该频道
|
||||
return user.HasSubscription(userId, channel.Id)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 订阅数量限制检查
|
||||
|
||||
```go
|
||||
func CanSubscribe(userId string) (bool, string) {
|
||||
// 1. 检查是否为VIP
|
||||
if user.IsVip(userId) {
|
||||
return true, ""
|
||||
}
|
||||
|
||||
// 2. 检查免费订阅数量(上限2个)
|
||||
count := GetFreeSubscriptionCount(userId)
|
||||
if count >= 2 {
|
||||
return false, "免费订阅数量已达上限(2个),请开通VIP或订阅付费频道"
|
||||
}
|
||||
|
||||
return true, ""
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 注意事项
|
||||
|
||||
1. **继承BaseModel**: 所有模型必须继承 global.BaseModel
|
||||
2. **分层架构**: 严格按照 api -> service -> model 层级开发
|
||||
3. **鉴权中间件**: 新增/修改/删除操作需要使用 NeedAuthGroup
|
||||
4. **权限验证**: 节目详情API需要验证用户权限
|
||||
5. **分页查询**: 使用 common.PageInfo
|
||||
6. **响应格式**: 使用 response.OkWithData() 和 response.FailWithMsg()
|
||||
@@ -1,6 +1,7 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"sundynix-go/router/radio"
|
||||
"sundynix-go/router/system"
|
||||
)
|
||||
|
||||
@@ -9,4 +10,5 @@ var GroupApp = new(Group)
|
||||
// Group 路由组
|
||||
type Group struct {
|
||||
System system.SysRouterGroup
|
||||
Radio radio.RadioRouterGroup
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type CategoryRouter struct{}
|
||||
|
||||
func (r *CategoryRouter) InitCategoryRouter(Router *gin.RouterGroup) {
|
||||
categoryRouter := Router.Group("/radio/category")
|
||||
{
|
||||
categoryRouter.POST("page", categoryApi.GetCategoryPage)
|
||||
categoryRouter.POST("list", categoryApi.GetCategoryList)
|
||||
categoryRouter.GET("detail", categoryApi.GetCategoryDetail)
|
||||
categoryRouter.POST("save", categoryApi.SaveCategory)
|
||||
categoryRouter.POST("update", categoryApi.UpdateCategory)
|
||||
categoryRouter.POST("delete", categoryApi.DeleteCategory)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ChannelRouter struct{}
|
||||
|
||||
func (r *ChannelRouter) InitChannelRouter(Router *gin.RouterGroup) {
|
||||
channelRouter := Router.Group("/radio/channel")
|
||||
{
|
||||
channelRouter.POST("list", channelApi.GetChannelList)
|
||||
channelRouter.GET("detail", channelApi.GetChannelDetail)
|
||||
channelRouter.POST("save", channelApi.SaveChannel)
|
||||
channelRouter.POST("update", channelApi.UpdateChannel)
|
||||
channelRouter.POST("delete", channelApi.DeleteChannel)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package radio
|
||||
|
||||
import v1 "sundynix-go/api/v1"
|
||||
|
||||
type RadioRouterGroup struct {
|
||||
CategoryRouter
|
||||
ChannelRouter
|
||||
ProgramRouter
|
||||
SubscriptionRouter
|
||||
InteractionRouter
|
||||
}
|
||||
|
||||
var GroupApp = new(RadioRouterGroup)
|
||||
|
||||
var (
|
||||
categoryApi = v1.ApiGroupApp.RadioApiGroup.CategoryApi
|
||||
channelApi = v1.ApiGroupApp.RadioApiGroup.ChannelApi
|
||||
programApi = v1.ApiGroupApp.RadioApiGroup.ProgramApi
|
||||
subscriptionApi = v1.ApiGroupApp.RadioApiGroup.SubscriptionApi
|
||||
interactionApi = v1.ApiGroupApp.RadioApiGroup.InteractionApi
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type InteractionRouter struct{}
|
||||
|
||||
func (r *InteractionRouter) InitInteractionRouter(Router *gin.RouterGroup) {
|
||||
// 收听历史
|
||||
historyRouter := Router.Group("history")
|
||||
{
|
||||
historyRouter.POST("list", interactionApi.GetHistoryList)
|
||||
historyRouter.POST("add", interactionApi.AddHistory)
|
||||
}
|
||||
|
||||
// 点赞
|
||||
likeRouter := Router.Group("like")
|
||||
{
|
||||
likeRouter.POST("toggle", interactionApi.ToggleLike)
|
||||
}
|
||||
|
||||
// 收藏
|
||||
favoriteRouter := Router.Group("favorite")
|
||||
{
|
||||
favoriteRouter.POST("list", interactionApi.GetFavoriteList)
|
||||
favoriteRouter.POST("add", interactionApi.AddFavorite)
|
||||
favoriteRouter.POST("remove", interactionApi.RemoveFavorite)
|
||||
}
|
||||
|
||||
// 评论
|
||||
commentRouter := Router.Group("comment")
|
||||
{
|
||||
commentRouter.POST("list", interactionApi.GetCommentList)
|
||||
commentRouter.POST("add", interactionApi.AddComment)
|
||||
commentRouter.POST("delete", interactionApi.DeleteComment)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ProgramRouter struct{}
|
||||
|
||||
func (r *ProgramRouter) InitProgramRouter(Router *gin.RouterGroup) {
|
||||
programRouter := Router.Group("/radio/program")
|
||||
{
|
||||
programRouter.POST("list", programApi.GetProgramList)
|
||||
programRouter.GET("detail", programApi.GetProgramDetail)
|
||||
programRouter.POST("save", programApi.SaveProgram)
|
||||
programRouter.POST("update", programApi.UpdateProgram)
|
||||
programRouter.POST("delete", programApi.DeleteProgram)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SubscriptionRouter struct{}
|
||||
|
||||
func (r *SubscriptionRouter) InitSubscriptionRouter(Router *gin.RouterGroup) {
|
||||
subscriptionRouter := Router.Group("radio/subscription")
|
||||
{
|
||||
subscriptionRouter.POST("list", subscriptionApi.GetSubscriptionList)
|
||||
subscriptionRouter.POST("can-subscribe", subscriptionApi.CanSubscribe)
|
||||
subscriptionRouter.POST("subscribe", subscriptionApi.Subscribe)
|
||||
subscriptionRouter.POST("unsubscribe", subscriptionApi.Unsubscribe)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"sundynix-go/service/radio"
|
||||
"sundynix-go/service/system"
|
||||
)
|
||||
|
||||
@@ -8,4 +9,5 @@ var GroupApp = new(Group)
|
||||
|
||||
type Group struct {
|
||||
SystemServiceGroup system.ServiceGroup
|
||||
RadioServiceGroup radio.ServiceGroup
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/radio"
|
||||
radioReq "sundynix-go/model/radio/request"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CategoryService struct{}
|
||||
|
||||
// GetCategoryList 获取分类列表
|
||||
func (s *CategoryService) GetCategoryList(info radioReq.GetCategoryList) ([]radio.RadioCategory, int64, error) {
|
||||
db := global.DB.Model(&radio.RadioCategory{}).Preload("Icon").Preload("Cover")
|
||||
var list []radio.RadioCategory
|
||||
var total int64
|
||||
|
||||
if info.Name != "" {
|
||||
db = db.Where("name LIKE ?", "%"+info.Name+"%")
|
||||
}
|
||||
if info.Status > 0 {
|
||||
db = db.Where("status = ?", info.Status)
|
||||
}
|
||||
|
||||
err := db.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (info.Current - 1) * info.PageSize
|
||||
err = db.Offset(offset).Limit(info.PageSize).Order("sort ASC").Find(&list).Error
|
||||
return list, total, err
|
||||
}
|
||||
|
||||
func (s *CategoryService) GetAllCategory() ([]radio.RadioCategory, error) {
|
||||
var res []radio.RadioCategory
|
||||
err := global.DB.Find(&res).Preload(":Icon").Preload("Cover").Error
|
||||
return res, err
|
||||
}
|
||||
|
||||
// GetCategoryById 获取分类详情
|
||||
func (s *CategoryService) GetCategoryById(id string) (*radio.RadioCategory, error) {
|
||||
var category radio.RadioCategory
|
||||
err := global.DB.Where("id = ?", id).Preload("Icon").Preload("Cover").First(&category).Error
|
||||
return &category, err
|
||||
}
|
||||
|
||||
// SaveCategory 保存分类
|
||||
func (s *CategoryService) SaveCategory(req radioReq.SaveCategory) error {
|
||||
category := radio.RadioCategory{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
IconId: req.IconId,
|
||||
CoverId: req.CoverId,
|
||||
Sort: req.Sort,
|
||||
Status: req.Status,
|
||||
}
|
||||
return global.DB.Create(&category).Error
|
||||
}
|
||||
|
||||
// UpdateCategory 更新分类
|
||||
func (s *CategoryService) UpdateCategory(req radioReq.UpdateCategory) error {
|
||||
updates := map[string]interface{}{
|
||||
"name": req.Name,
|
||||
"description": req.Description,
|
||||
"icon_id": req.IconId,
|
||||
"cover_id": req.CoverId,
|
||||
"sort": req.Sort,
|
||||
"status": req.Status,
|
||||
}
|
||||
return global.DB.Model(&radio.RadioCategory{}).Where("id = ?", req.Id).Updates(updates).Error
|
||||
}
|
||||
|
||||
// DeleteCategory 删除分类
|
||||
func (s *CategoryService) DeleteCategory(id string) error {
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// 检查是否有频道使用此分类
|
||||
var count int64
|
||||
tx.Model(&radio.RadioChannel{}).Where("category_id = ?", id).Count(&count)
|
||||
if count > 0 {
|
||||
return errors.New("该分类下存在频道,无法删除")
|
||||
}
|
||||
return tx.Where("id = ?", id).Delete(&radio.RadioCategory{}).Error
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/radio"
|
||||
radioReq "sundynix-go/model/radio/request"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ChannelService struct{}
|
||||
|
||||
// GetChannelList 获取频道列表
|
||||
func (s *ChannelService) GetChannelList(userId string, info radioReq.GetChannelList) ([]radio.RadioChannel, int64, error) {
|
||||
db := global.DB.Model(&radio.RadioChannel{})
|
||||
var list []radio.RadioChannel
|
||||
var total int64
|
||||
|
||||
if info.CategoryId != "" {
|
||||
db = db.Where("category_id = ?", info.CategoryId)
|
||||
}
|
||||
if info.Name != "" {
|
||||
db = db.Where("name LIKE ?", "%"+info.Name+"%")
|
||||
}
|
||||
if info.Status > 0 {
|
||||
db = db.Where("status = ?", info.Status)
|
||||
}
|
||||
|
||||
err := db.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (info.Current - 1) * info.PageSize
|
||||
err = db.Offset(offset).Limit(info.PageSize).Order("sort ASC").Find(&list).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 批量查询用户订阅的频道,避免N+1问题
|
||||
if userId != "" {
|
||||
subService := &SubscriptionService{}
|
||||
subscribedChannelIds, _ := subService.GetUserSubscriptionHistory(userId)
|
||||
// 转换为map以便快速查找
|
||||
subscribedMap := make(map[string]bool)
|
||||
for _, cid := range subscribedChannelIds {
|
||||
subscribedMap[cid] = true
|
||||
}
|
||||
// 填充HasSubscribed字段
|
||||
for i := range list {
|
||||
if subscribedMap[list[i].Id] {
|
||||
list[i].HasSubscribed = 1
|
||||
} else {
|
||||
list[i].HasSubscribed = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
// GetChannelById 获取频道详情
|
||||
func (s *ChannelService) GetChannelById(userId, id string) (radio.RadioChannel, error) {
|
||||
var channel radio.RadioChannel
|
||||
err := global.DB.Where("id = ?", id).Preload("Cover").First(&channel).Error
|
||||
if err != nil {
|
||||
return channel, err
|
||||
}
|
||||
|
||||
// 填充HasSubscribed字段
|
||||
if userId != "" {
|
||||
subService := &SubscriptionService{}
|
||||
hasSub, _ := subService.HasSubscription(userId, channel.Id)
|
||||
if hasSub {
|
||||
channel.HasSubscribed = 1
|
||||
} else {
|
||||
channel.HasSubscribed = 0
|
||||
}
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
// SaveChannel 保存频道
|
||||
func (s *ChannelService) SaveChannel(req radioReq.SaveChannel) error {
|
||||
channel := radio.RadioChannel{
|
||||
CategoryId: req.CategoryId,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
CoverId: req.CoverId,
|
||||
Tags: req.Tags,
|
||||
IsVipOnly: req.IsVipOnly,
|
||||
Sort: req.Sort,
|
||||
Status: req.Status,
|
||||
}
|
||||
return global.DB.Create(&channel).Error
|
||||
}
|
||||
|
||||
// UpdateChannel 更新频道
|
||||
func (s *ChannelService) UpdateChannel(req radioReq.UpdateChannel) error {
|
||||
updates := map[string]interface{}{
|
||||
"category_id": req.CategoryId,
|
||||
"name": req.Name,
|
||||
"description": req.Description,
|
||||
"cover_id": req.CoverId,
|
||||
"tags": req.Tags,
|
||||
"is_vip_only": req.IsVipOnly,
|
||||
"sort": req.Sort,
|
||||
"status": req.Status,
|
||||
}
|
||||
return global.DB.Model(&radio.RadioChannel{}).Where("id = ?", req.Id).Updates(updates).Error
|
||||
}
|
||||
|
||||
// DeleteChannel 删除频道
|
||||
func (s *ChannelService) DeleteChannel(id string) error {
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// 检查是否有节目使用此频道
|
||||
var count int64
|
||||
tx.Model(&radio.RadioProgram{}).Where("channel_id = ?", id).Count(&count)
|
||||
if count > 0 {
|
||||
return errors.New("该频道下存在节目,无法删除")
|
||||
}
|
||||
return tx.Where("id = ?", id).Delete(&radio.RadioChannel{}).Error
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package radio
|
||||
|
||||
type ServiceGroup struct {
|
||||
CategoryService
|
||||
ChannelService
|
||||
ProgramService
|
||||
SubscriptionService
|
||||
InteractionService
|
||||
}
|
||||
|
||||
var GroupApp = new(ServiceGroup)
|
||||
@@ -0,0 +1,176 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/radio"
|
||||
radioReq "sundynix-go/model/radio/request"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type InteractionService struct{}
|
||||
|
||||
// AddHistory 添加收听历史
|
||||
func (s *InteractionService) AddHistory(userId string, req radioReq.AddHistory) error {
|
||||
// 先查找是否已存在记录
|
||||
var history radio.RadioHistory
|
||||
err := global.DB.Where("user_id = ? AND program_id = ?", userId, req.ProgramId).First(&history).Error
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// 不存在,创建新记录
|
||||
history = radio.RadioHistory{
|
||||
UserId: userId,
|
||||
ProgramId: req.ProgramId,
|
||||
Progress: req.Progress,
|
||||
Duration: req.Duration,
|
||||
}
|
||||
return global.DB.Create(&history).Error
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 存在,更新进度
|
||||
return global.DB.Model(&history).Updates(map[string]interface{}{
|
||||
"progress": req.Progress,
|
||||
"duration": req.Duration,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// GetHistoryList 获取收听历史列表
|
||||
func (s *InteractionService) GetHistoryList(userId string, info radioReq.GetHistoryList) ([]radio.RadioHistory, int64, error) {
|
||||
db := global.DB.Model(&radio.RadioHistory{}).Where("user_id = ?", userId)
|
||||
var list []radio.RadioHistory
|
||||
var total int64
|
||||
|
||||
err := db.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (info.Current - 1) * info.PageSize
|
||||
err = db.Offset(offset).Limit(info.PageSize).Order("created_at DESC").Find(&list).Error
|
||||
return list, total, err
|
||||
}
|
||||
|
||||
// ToggleLike 切换点赞状态
|
||||
func (s *InteractionService) ToggleLike(userId, programId string) (bool, error) {
|
||||
var like radio.RadioLike
|
||||
err := global.DB.Where("user_id = ? AND program_id = ?", userId, programId).First(&like).Error
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// 未点赞,添加点赞
|
||||
like = radio.RadioLike{
|
||||
UserId: userId,
|
||||
ProgramId: programId,
|
||||
}
|
||||
if err := global.DB.Create(&like).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
// 增加节目点赞数
|
||||
global.DB.Model(&radio.RadioProgram{}).Where("id = ?", programId).
|
||||
UpdateColumn("like_count", gorm.Expr("like_count + ?", 1))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 已点赞,取消点赞
|
||||
if err := global.DB.Delete(&like).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
// 减少节目点赞数
|
||||
global.DB.Model(&radio.RadioProgram{}).Where("id = ?", programId).
|
||||
UpdateColumn("like_count", gorm.Expr("like_count - ?", 1))
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// IsLiked 检查是否已点赞
|
||||
func (s *InteractionService) IsLiked(userId, programId string) (bool, error) {
|
||||
var count int64
|
||||
err := global.DB.Model(&radio.RadioLike{}).Where("user_id = ? AND program_id = ?", userId, programId).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// AddFavorite 添加收藏
|
||||
func (s *InteractionService) AddFavorite(userId, programId string) error {
|
||||
// 检查是否已收藏
|
||||
var existing radio.RadioFavorite
|
||||
err := global.DB.Where("user_id = ? AND program_id = ?", userId, programId).First(&existing).Error
|
||||
if err == nil {
|
||||
return errors.New("已经收藏过该节目")
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
favorite := radio.RadioFavorite{
|
||||
UserId: userId,
|
||||
ProgramId: programId,
|
||||
}
|
||||
return global.DB.Create(&favorite).Error
|
||||
}
|
||||
|
||||
// RemoveFavorite 取消收藏
|
||||
func (s *InteractionService) RemoveFavorite(userId, programId string) error {
|
||||
return global.DB.Where("user_id = ? AND program_id = ?", userId, programId).Delete(&radio.RadioFavorite{}).Error
|
||||
}
|
||||
|
||||
// GetFavoriteList 获取收藏列表
|
||||
func (s *InteractionService) GetFavoriteList(userId string, info radioReq.GetFavoriteList) ([]radio.RadioFavorite, int64, error) {
|
||||
db := global.DB.Model(&radio.RadioFavorite{}).Where("user_id = ?", userId)
|
||||
var list []radio.RadioFavorite
|
||||
var total int64
|
||||
|
||||
err := db.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (info.Current - 1) * info.PageSize
|
||||
err = db.Offset(offset).Limit(info.PageSize).Order("created_at DESC").Find(&list).Error
|
||||
return list, total, err
|
||||
}
|
||||
|
||||
// IsFavorited 检查是否已收藏
|
||||
func (s *InteractionService) IsFavorited(userId, programId string) (bool, error) {
|
||||
var count int64
|
||||
err := global.DB.Model(&radio.RadioFavorite{}).Where("user_id = ? AND program_id = ?", userId, programId).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// AddComment 添加评论
|
||||
func (s *InteractionService) AddComment(userId string, req radioReq.AddComment) error {
|
||||
comment := radio.RadioComment{
|
||||
ProgramId: req.ProgramId,
|
||||
UserId: userId,
|
||||
ParentId: req.ParentId,
|
||||
Content: req.Content,
|
||||
}
|
||||
return global.DB.Create(&comment).Error
|
||||
}
|
||||
|
||||
// DeleteComment 删除评论
|
||||
func (s *InteractionService) DeleteComment(userId, commentId string) error {
|
||||
return global.DB.Where("id = ? AND user_id = ?", commentId, userId).Delete(&radio.RadioComment{}).Error
|
||||
}
|
||||
|
||||
// GetCommentList 获取评论列表
|
||||
func (s *InteractionService) GetCommentList(programId string, info radioReq.GetCommentList) ([]radio.RadioComment, int64, error) {
|
||||
db := global.DB.Model(&radio.RadioComment{}).Where("program_id = ?", programId)
|
||||
var list []radio.RadioComment
|
||||
var total int64
|
||||
|
||||
err := db.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (info.Current - 1) * info.PageSize
|
||||
err = db.Offset(offset).Limit(info.PageSize).Order("created_at DESC").Find(&list).Error
|
||||
return list, total, err
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/radio"
|
||||
radioReq "sundynix-go/model/radio/request"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProgramService struct{}
|
||||
|
||||
// GetProgramList 获取节目列表
|
||||
func (s *ProgramService) GetProgramList(info radioReq.GetProgramList) ([]radio.RadioProgram, int64, error) {
|
||||
db := global.DB.Model(&radio.RadioProgram{}).Preload("Cover").Preload("Audio")
|
||||
var list []radio.RadioProgram
|
||||
var total int64
|
||||
|
||||
if info.ChannelId != "" {
|
||||
db = db.Where("channel_id = ?", info.ChannelId)
|
||||
}
|
||||
if info.Title != "" {
|
||||
db = db.Where("title LIKE ?", "%"+info.Title+"%")
|
||||
}
|
||||
if info.Status > 0 {
|
||||
db = db.Where("status = ?", info.Status)
|
||||
}
|
||||
|
||||
err := db.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (info.Current - 1) * info.PageSize
|
||||
err = db.Offset(offset).Limit(info.PageSize).Order("created_at DESC").Find(&list).Error
|
||||
return list, total, err
|
||||
}
|
||||
|
||||
// GetProgramById 获取节目详情
|
||||
func (s *ProgramService) GetProgramById(id string) (*radio.RadioProgram, error) {
|
||||
var program radio.RadioProgram
|
||||
err := global.DB.Where("id = ?", id).Preload("Cover").Preload("Audio").First(&program).Error
|
||||
return &program, err
|
||||
}
|
||||
|
||||
// SaveProgram 保存节目
|
||||
func (s *ProgramService) SaveProgram(req radioReq.SaveProgram) error {
|
||||
program := radio.RadioProgram{
|
||||
ChannelId: req.ChannelId,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
Content: req.Content,
|
||||
CoverId: req.CoverId,
|
||||
AudioId: req.AudioId,
|
||||
Duration: req.Duration,
|
||||
Tags: req.Tags,
|
||||
Status: req.Status,
|
||||
}
|
||||
return global.DB.Create(&program).Error
|
||||
}
|
||||
|
||||
// UpdateProgram 更新节目
|
||||
func (s *ProgramService) UpdateProgram(req radioReq.UpdateProgram) error {
|
||||
updates := map[string]interface{}{
|
||||
"channel_id": req.ChannelId,
|
||||
"title": req.Title,
|
||||
"description": req.Description,
|
||||
"content": req.Content,
|
||||
"cover_id": req.CoverId,
|
||||
"audio_id": req.AudioId,
|
||||
"duration": req.Duration,
|
||||
"tags": req.Tags,
|
||||
"status": req.Status,
|
||||
}
|
||||
return global.DB.Model(&radio.RadioProgram{}).Where("id = ?", req.Id).Updates(updates).Error
|
||||
}
|
||||
|
||||
// DeleteProgram 删除节目
|
||||
func (s *ProgramService) DeleteProgram(ids []string) error {
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// 删除相关的收藏记录
|
||||
tx.Where("program_id in ?", ids).Delete(&radio.RadioFavorite{})
|
||||
// 删除相关的点赞记录
|
||||
tx.Where("program_id in ?", ids).Delete(&radio.RadioLike{})
|
||||
// 删除相关的历史记录
|
||||
tx.Where("program_id in ?", ids).Delete(&radio.RadioHistory{})
|
||||
// 删除相关的评论
|
||||
tx.Where("program_id in ?", ids).Delete(&radio.RadioComment{})
|
||||
// 删除节目
|
||||
return tx.Where("id in ?", ids).Delete(&radio.RadioProgram{}).Error
|
||||
})
|
||||
}
|
||||
|
||||
// IncrementPlayCount 增加播放次数
|
||||
func (s *ProgramService) IncrementPlayCount(id string) error {
|
||||
return global.DB.Model(&radio.RadioProgram{}).Where("id = ?", id).
|
||||
UpdateColumn("play_count", gorm.Expr("play_count + ?", 1)).Error
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package radio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sundynix-go/global"
|
||||
common "sundynix-go/model/commom/request"
|
||||
"sundynix-go/model/radio"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SubscriptionService struct{}
|
||||
|
||||
const MaxFreeSubscription = 2
|
||||
|
||||
// GetUserSubscription 获取用户订阅列表
|
||||
func (s *SubscriptionService) GetUserSubscription(userId string, info common.PageInfo) ([]radio.RadioSubscription, int64, error) {
|
||||
db := global.DB.Model(&radio.RadioSubscription{}).Where("user_id = ?", userId)
|
||||
var list []radio.RadioSubscription
|
||||
var total int64
|
||||
|
||||
err := db.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (info.Current - 1) * info.PageSize
|
||||
err = db.Offset(offset).Limit(info.PageSize).Order("created_at DESC").Find(&list).Error
|
||||
return list, total, err
|
||||
}
|
||||
|
||||
// GetUserSubscriptionHistory 获取用户历史订阅过的频道ID列表
|
||||
func (s *SubscriptionService) GetUserSubscriptionHistory(userId string) ([]string, error) {
|
||||
var channelIds []string
|
||||
err := global.DB.Model(&radio.RadioSubscription{}).
|
||||
Where("user_id = ?", userId).
|
||||
Pluck("channel_id", &channelIds).Error
|
||||
return channelIds, err
|
||||
}
|
||||
|
||||
// HasEverSubscribed 检查用户是否曾经订阅过该频道(包括已取消的)
|
||||
func (s *SubscriptionService) HasEverSubscribed(userId, channelId string) (bool, error) {
|
||||
var count int64
|
||||
// 使用Unscoped查询包括已软删除的记录
|
||||
err := global.DB.Unscoped().Model(&radio.RadioSubscription{}).
|
||||
Where("user_id = ? AND channel_id = ?", userId, channelId).
|
||||
Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// HasSubscription 检查用户当前是否订阅该频道(未取消的)
|
||||
func (s *SubscriptionService) HasSubscription(userId, channelId string) (bool, error) {
|
||||
var count int64
|
||||
err := global.DB.Model(&radio.RadioSubscription{}).
|
||||
Where("user_id = ? AND channel_id = ?", userId, channelId).
|
||||
Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// CanSubscribe 检查是否可以订阅
|
||||
// 规则:
|
||||
// 1. 如果用户是VIP且未过期,可以订阅任意频道
|
||||
// 2. 如果用户曾经订阅过该频道(取消后再订阅),可以免费订阅
|
||||
// 3. 否则检查当前有效订阅数量是否达到上限(2个)
|
||||
func (s *SubscriptionService) CanSubscribe(userId, channelId string) (bool, string, error) {
|
||||
// 检查是否已经是订阅用户(未取消的订阅)
|
||||
var existing radio.RadioSubscription
|
||||
err := global.DB.Where("user_id = ? AND channel_id = ?", userId, channelId).First(&existing).Error
|
||||
if err == nil {
|
||||
return false, "您已订阅该频道", nil
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
// 获取radio_user检查VIP状态
|
||||
var radioUser radio.RadioUser
|
||||
err = global.DB.Where("user_id = ?", userId).First(&radioUser).Error
|
||||
if err == nil && radioUser.IsVip == 1 {
|
||||
// 检查VIP是否过期
|
||||
if radioUser.VipExpireAt != nil && *radioUser.VipExpireAt > time.Now().Unix() {
|
||||
return true, "", nil
|
||||
}
|
||||
}
|
||||
|
||||
// 检查用户是否曾经订阅过该频道(取消后又订阅的情况)
|
||||
hasEverSubscribed, err := s.HasEverSubscribed(userId, channelId)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
if hasEverSubscribed {
|
||||
// 曾今订阅过,可以免费再次订阅
|
||||
return true, "", nil
|
||||
}
|
||||
|
||||
// 非VIP用户,检查当前有效订阅数量(排除已取消的)
|
||||
var count int64
|
||||
err = global.DB.Model(&radio.RadioSubscription{}).Where("user_id = ?", userId).Count(&count).Error
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
if count >= MaxFreeSubscription {
|
||||
return false, "免费订阅数量已达上限(2个),请开通VIP或订阅付费频道", nil
|
||||
}
|
||||
|
||||
return true, "", nil
|
||||
}
|
||||
|
||||
// Subscribe 订阅频道
|
||||
func (s *SubscriptionService) Subscribe(userId, channelId string, subType int) error {
|
||||
subscription := radio.RadioSubscription{
|
||||
UserId: userId,
|
||||
ChannelId: channelId,
|
||||
}
|
||||
return global.DB.Create(&subscription).Error
|
||||
}
|
||||
|
||||
// Unsubscribe 退订频道(逻辑删除,更新删除时间表示已取消)
|
||||
func (s *SubscriptionService) Unsubscribe(userId, channelId string) error {
|
||||
// 软删除:将DeletedAt设置为当前时间,表示已取消订阅
|
||||
// 这样用户可以再次免费订阅该频道
|
||||
return global.DB.Model(&radio.RadioSubscription{}).
|
||||
Where("user_id = ? AND channel_id = ?", userId, channelId).
|
||||
Update("deleted_at", time.Now()).Error
|
||||
}
|
||||
|
||||
// GetVipStatus 获取VIP状态
|
||||
func (s *SubscriptionService) GetVipStatus(userId string) (bool, int64, error) {
|
||||
var radioUser radio.RadioUser
|
||||
err := global.DB.Where("user_id = ?", userId).First(&radioUser).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, 0, nil
|
||||
}
|
||||
return false, 0, err
|
||||
}
|
||||
|
||||
if radioUser.IsVip == 1 && radioUser.VipExpireAt != nil && *radioUser.VipExpireAt > time.Now().Unix() {
|
||||
return true, *radioUser.VipExpireAt, nil
|
||||
}
|
||||
|
||||
return false, 0, nil
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strconv"
|
||||
"sundynix-go/global"
|
||||
common "sundynix-go/model/commom/request"
|
||||
"sundynix-go/model/radio"
|
||||
"sundynix-go/model/system"
|
||||
systemReq "sundynix-go/model/system/request"
|
||||
systemResp "sundynix-go/model/system/response"
|
||||
@@ -157,6 +158,17 @@ func (userService *UserService) MiniLogin(code string) (result *system.User, err
|
||||
if err := tx.Create(&newUser).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 创建小程序用户
|
||||
mpUser := radio.RadioUser{
|
||||
UserId: newUser.Id,
|
||||
OpenId: wxResp.Openid,
|
||||
UnionId: wxResp.Unionid,
|
||||
SessionKey: wxResp.SessionKey,
|
||||
}
|
||||
if err := tx.Create(&mpUser).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 赋值给外部变量以便返回
|
||||
user = newUser
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user