init: radio init commit

This commit is contained in:
Blizzard
2026-02-28 15:56:26 +08:00
parent fc585fa4df
commit d79beb4663
63 changed files with 2540 additions and 6399 deletions
+10
View File
@@ -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/
+11
View File
@@ -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>
+8
View File
@@ -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>
+9
View File
@@ -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
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
-173
View File
@@ -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 生成内容"标识
+2
View File
@@ -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
}
+159
View File
@@ -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)
}
+146
View File
@@ -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)
}
+21
View File
@@ -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
)
+253
View File
@@ -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)
}
+140
View File
@@ -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)
}
+134
View File
@@ -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)
}
+5 -5
View File
@@ -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详情"
+7 -7
View File
@@ -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]
+4 -4
View File
@@ -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 "授权菜单给角色"
+8 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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"` //微信支付
}
-22
View File
@@ -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 // 钻石
)
-6
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
-2332
View File
File diff suppressed because it is too large Load Diff
-1482
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -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))
+9
View File
@@ -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)
-4
View File
@@ -1,4 +0,0 @@
[sundynix-radio-server]2026-02-25 17:24:01 error /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 error /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 error /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 error /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/utils/auth/claims.go:125 获取用户信息失败,请检查请求头是否存在x-token且claims是否为规定结构
-12
View File
@@ -1,12 +0,0 @@
[sundynix-radio-server]2026-02-25 16:53:23 info /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 info /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 info /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 info /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 info /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 info /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 info /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 info /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 info /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 info /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/service/radio/crawl.go:79 没有启用的抓取源,跳过本次抓取
[sundynix-radio-server]2026-02-25 17:30:50 info /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 info /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/service/radio/crawl.go:158 GitHub 数据抓取完成 {"totalResults": 46, "fetched": 10}
+9
View File
@@ -0,0 +1,9 @@
[sundynix-radio-server]2026-02-28 15:39:45 error /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 error /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 error /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/internal/gorm_logger_writer.go:29 [/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]
[error] failed to parse value %!v(MISSING), got error %!v(MISSING)
[sundynix-radio-server]2026-02-28 15:39:51 error /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/internal/gorm_logger_writer.go:29 [/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]
[error] failed to parse value %!v(MISSING), got error %!v(MISSING)
[sundynix-radio-server]2026-02-28 15:39:51 error /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 error /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 error /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"}
+3
View File
@@ -0,0 +1,3 @@
[sundynix-radio-server]2026-02-28 15:39:45 info /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 info /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 info /Users/blizzard/sourceCode/GolandProjects/src/morning-radio/morning-radio-backend/initialize/gorm.go:52 Migrate table success
+1 -1
View File
@@ -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 /
+3
View File
@@ -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"`
}
+23
View File
@@ -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"
}
+26
View File
@@ -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"
}
+19
View File
@@ -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"
}
+16
View File
@@ -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"
}
+18
View File
@@ -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"
}
+16
View File
@@ -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"
}
+28
View File
@@ -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"
}
+17
View File
@@ -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"
}
+29
View File
@@ -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"
}
+32
View File
@@ -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"` // 状态
}
+37
View File
@@ -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"` // 状态
}
+68
View File
@@ -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
}
+39
View File
@@ -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"` // 状态
}
+43
View File
@@ -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
}
+29
View File
@@ -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"`
}
+350
View File
@@ -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()
+2
View File
@@ -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
}
+19
View File
@@ -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)
}
}
+18
View File
@@ -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)
}
}
+21
View File
@@ -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
)
+38
View File
@@ -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)
}
}
+18
View File
@@ -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)
}
}
+17
View File
@@ -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)
}
}
+2
View File
@@ -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
}
+87
View File
@@ -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
})
}
+127
View File
@@ -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
})
}
+11
View File
@@ -0,0 +1,11 @@
package radio
type ServiceGroup struct {
CategoryService
ChannelService
ProgramService
SubscriptionService
InteractionService
}
var GroupApp = new(ServiceGroup)
+176
View File
@@ -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
}
+98
View File
@@ -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
}
+144
View File
@@ -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
View File
@@ -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