feat: 音色管理

This commit is contained in:
Blizzard
2026-04-28 10:16:23 +08:00
parent 5f4f739f16
commit 141424878c
11 changed files with 1148 additions and 387 deletions
-97
View File
@@ -1,97 +0,0 @@
《“早安电台”项目完整架构设计与开发计划书》
一、 产品愿景与垂直领域选择
1. 垂直领域对比与 MVP 推荐
文档中提出了四个垂直方向:【硬核职场】、【效率健康】、【极简生活】与【知识胶囊】 2-4。MVP 阶段(两周内)最推荐方向:【硬核职场】AI 商业机会与搞钱情报站 1。
推荐理由:
自动化实现成本最低:该方向的数据源(如 GitHub API、Product Hunt API)高度结构化,极易通过 Go 后端定时任务抓取并交给 LLM 处理 1, 5。
目标用户契合度极高:数据显示,63.3%的中文长音频听众是21-35岁的年轻精英群体,且72.1%的用户订阅是因为内容的“专业性”或“干货多” 6。AI 商业情报完美契合这批高价值用户的痛点。
2. 目标用户画像及其核心痛点
用户画像:21-35岁的一线/新一线城市开发者、产品经理、渴望通过 AI 变现的职场精英 2, 6。
核心痛点:
信息差焦虑:AI工具爆发,缺乏时间筛选信息,需要在早餐、通勤等“脱手”场景下实现“零点击”的信息获取 2, 7。
情绪过渡与陪伴:早安电台处于从睡眠向职业场景过渡的交汇点,需提供缓解早起焦虑的“情绪价值”与专业陪伴感 8。
二、 数据抓取与 AI 场景感知流水线 (Data Pipeline)
整个后台流转依托“专家灵魂 + AI 躯干”的自动化生成模式 9,核心链路如下:
3. 自动化数据抓取 (Cron Job)
设定 Go 后端定时任务,每 2 小时运行一次 10。
GitHub API 调用 (补充架构细节):调用 GET /search/repositories。后端构建查询参数 q=topic:ai+created:>YYYY-MM-DD&sort=stars&order=desc,筛选近24小时星标增长最快的AI项目 5,提取前 3 个项目描述。
Product Hunt API 调用 (补充架构细节):通过 POST 请求调用其 GraphQL 接口,获取 posts(first: 10, order: RANKING),提取每日 Upvote 前10的产品的 description 5。
4. 数据清洗与 AI 提炼
Goquery 清洗:抓取到的 README 或网页文本包含大量无效 HTML 标签,必须使用 Goquery 库进行剥离,仅保留核心文本,大幅降低 LLM Token 消耗 10。
大模型介入:将清洗后的 10 个产品和 3 个开源项目发送给 DeepSeek/OpenAI 模型,通过 Prompt 强制要求:“提取出今日可落地的 3 个变现点或核心业务逻辑,并改写为适合口播的连贯短句” 2, 7。
5. TTS 语音合成与 MinIO 私有化存储
利用 TTS 技术,每日更新的音频边际成本几乎为零 9。
腾讯云 TTS (建议配置):选择带有“睿智专业”风格的虚拟主播音色 9,指定输出为 aac 格式,采样率 16000Hz,以匹配微信最优音频规范 8。
MinIO 落库 (补充架构细节)
后端接收到 TTS 音频流后,通过 minio-go SDK 直接上传至自建 MinIO 的 sundynix-audios Bucket 中。
文件命名:文件使用生成的唯一 UUID 命名(如 123e4567-xxx.aac),防止内容重复 10。
网络合规与分发 (核心风险应对,补充细节):因微信小程序强制要求 HTTPS 且需白名单拦截,MinIO 前端必须部署 Nginx 反向代理并配置 SSL 证书。为应对早高峰并发,Nginx 域名上必须套用公有云 CDN 服务进行边缘缓存加速 8, 11。
三、 核心架构设计与技术规范
6. 数据建模 (核心表结构)
表名必须以 sundynix_开头,主键统一为 UUID 字符串 10, 12。(Go 结构体转 JSON 时统一使用 camelCase)。
表名 (Table),字段 (Column),类型,说明
sundynix_audio_content,id (PK),String(UUID),唯一音频ID 10
,title,String,音频标题,用于锁屏界面展示 8
,audio_url,String,绑定 CDN 的 MinIO AAC 音频链接
,epname,String,专辑名 (如:早安电台 - 硬核职场) 8
sundynix_play_record,id (PK),String(UUID),播放记录ID (外部扩展)
,audio_id,String(UUID),关联音频
,progress_sec,Integer,播放进度(秒),用于 startTime 恢复 8
sundynix_user_medal,id (PK),String(UUID),勋章记录ID (外部扩展)
,medal_type,String,勋章标识 (如: EARLY_BIRD_7) 12
7. 核心后端 API 接口设计 (camelCase 响应)
API 1: 获取音频列表
Route: GET /api/v1/audios?scene=morning&limit=5
Response:
{
"code": 200,
"message": "success",
"data": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"title": "今日 GitHub 热榜:3个 Agent 变现线索",
"audioUrl": "https://cdn.sundynix.com/sundynix-audios/123e4567-e89b-12d3-a456-426614174000.aac",
"epname": "早安电台 - 硬核职场",
"duration": 180
}
]
}
API 2: 更新播放状态 (含勋章触发逻辑) (扩展功能 13)
Route: POST /api/v1/play-status
Body: {"audioId": "123e...", "progressSec": 180, "isCompleted": true}
Response:
{
"code": 200,
"data": { "saved": true, "newMedalUnlocked": "EARLY_BIRD_7" }
}
API 3: 获取用户勋章 (扩展功能 13)
Route: GET /api/v1/users/{userId}/medals
Response: 返回解锁的勋章数组及图标。
8. 逾期逻辑处理 (NextRunTime 计算) (外部扩展设计 13)
如果用户错过了“清晨任务”,在下午完成了收听,系统在计算 NextRunTime 时,绝对不能采用 当前时间 + 24小时。必须采用基于“自然日锚点”的对齐策略,将下一次触发重置为“第二天的清晨 05:00”,从而死守产品的“晨间唤醒与规律伴随”核心心智 6。
四、 微信小程序端深度整合
BackgroundAudioManager 是小程序的底层核心,必须妥善设计以保障弱网及锁屏体验 8:
参数合规:获取 API 1 的响应后,必须将 audioUrl 赋值给 src。同时,必须配置 title、epname 和 coverImgUrl,这三者直接决定了系统锁屏播放控件的展示质量,是建立品牌心智的关键 8。
断点续传设计:若用户播放被闹铃中断,前端需记录进度。下次恢复时,将 API 2 中存储的 progressSec 传入 startTime 属性,实现帧级别的无缝续播 8。
五、 分阶段开发计划 (2周 MVP 周期)
第 1-3 天:后端基础及数据爬取
搭建 Go 后端,配置 sundynix_ 前缀数据库和 UUID 10, 14。
编写定时任务(Cron Job),完成 GitHub 和 Product Hunt 数据抓取及 Goquery 标签清洗 5, 10。
第 4-7 天:AI 接入与存储链路构建
接入大模型 API 完成洗稿提炼 7。
集成 TTS 服务,完成音频生成。
搭建自建 MinIO 服务器,配置 Nginx HTTPS 反向代理与云端 CDN,使用 Go SDK 实现音频直推 MinIO 19, 补充。
第 8-10 天:小程序端播放器攻坚
搭建核心 UI,将 CDN 音频地址下发给小程序。
深度整合 BackgroundAudioManager,完成 startTime 断点续传逻辑和锁屏封面展示配置 8。
第 11-14 天:联调、扩展系统与合规审查
完成后端 API 与小程序联调。实现勋章成就系统的前端触发动画 14。
在显著位置增加“AIGC 生成内容”标识,完成政策合规 11。
六、 风险评估与对策
弱网延迟打断收听体验 11
对策:除了依赖自建 MinIO 前置的 CDN 节点加速外,小程序前端必须利用 BackgroundAudioManager 的机制,提前请求下一段音频的 src,实现主动缓冲。若检测到网络极差,动态请求备用的 MP3 低码率链接 8, 11。
系统级中断导致进度丢失 8
对策:妥善监听后台音频管理器的 onPause、onStop 和 onEnded 回调,实时向后端 API 2 上报 progressSec 8。
AIGC 政策合规风险 11
对策:严格遵循《网络视听数据质量及 AIGC 内容管理要求》,在前端 UI 必须带有清晰的 AI 标识,确保平台审核顺利通过 11。
-181
View File
@@ -1,181 +0,0 @@
中国垂直领域早安电台微信小程序产业研究与全链路建设报告
来源指南
中国垂直领域早安电台微信小程序产业研究与全链路建设报告
第一章:中国声音经济的宏观范式与长音频演进逻辑
在数字化媒介高度饱和的当下,视觉注意力的争夺已进入存量博弈阶段,而以声音为载体的“听觉经济”正成为新的蓝海。中国声音经济的崛起并非偶然,而是技术演进、消费升级与用户心理变迁共同驱动的结果。根据最新产业研究数据,2024年中国声音经济产业市场规模已达到5688.2亿元人民币,展现出强劲的增长韧性,预计到2029年这一数字将突破7400亿元 [1]。在这一宏大的背景下,垂直领域的早安电台小程序作为一种高频、高粘性的应用形态,正处于爆发的前夜。
1.1 长音频行业的增长驱动力与政策导向
中国长音频行业正经历从流量中心向内容中心的回归。政策层面的规范化是行业可持续发展的基石。政府通过加强版权保护、规范人工智能生成内容(AIGC)的标识、提升网络视听数据质量以及出台一系列税收优惠政策,为长音频行业创造了稳健的发展环境 [1]。这种政策引导不仅清除了市场中的侵权行为,更通过数字化转型推动了产业的结构性升级。
文娱消费的升级则从需求端重塑了市场。随着居民消费水平的提高,精神消费日益呈现出刚性化趋势。声音作为一种能够渗透生活间隙、构建深度陪伴价值的媒介,其不可替代性愈发凸显。对于垂直赛道的开发者而言,紧扣用户为优质内容和沉浸体验付费的意愿,是实现商业闭环的关键 [1]。
1.2 垂直化与场景化的深度融合
长音频行业的发展已进入“高质量与多元化”的新阶段。传统的全品类平台正面临内容同质化的挑战,而专注于特定领域、特定场景的垂直化策略则显示出极强的生命力。这种趋势不仅体现在内容的细分上,更体现在对用户特定时间段和特定心理需求的精准捕捉。
行业核心驱动因素
2024年市场表现
2029年预期/趋势
关键影响机制
声音经济产业总规模
5688.2 亿元 [1]
7400 亿元以上
居民精神消费刚性化驱动
AIGC 产业规模
471.7 亿元 [1]
2767.4 亿元 (2028)
降低内容生产边际成本 [1]
播客听众规模
1.34 亿人 [1]
1.79 亿人 (2027)
年轻精英群体知识获取需求 [1]
长音频市场增长率
14.8% (2024) [1]
持续平稳增长
垂类内容深化用户连接 [1]
早安电台作为一种特定的场景化产品,其核心逻辑在于“唤醒”与“伴随”。它利用了用户清晨洗漱、早餐及通勤的碎片化时间,将资讯获取、情绪调节与职业学习有机结合。这种高频的场景契合度,使得微信小程序成为了承载这一功能的最佳容器。
第二章:目标用户洞察与垂直赛道心理画像
垂直领域的早安电台若要获得成功,必须深入理解其核心受众的画像特征。当前,长音频的主要受众群体已呈现出清晰的结构化特征,这为产品的定位提供了精准的数据支持。
2.1 21-35岁年轻精英的消费逻辑
长音频,尤其是播客类内容的受众群体具有显著的“高价值”特征。统计数据显示,2024年中文播客听众中,63.3%的用户为21-35岁的年轻消费者 [1]。这一群体通常受过良好教育,处于事业的上升期,对时间成本极为敏感。
深入分析发现,72.1%的听众订阅节目的主要动机是由于节目的“专业性”或“干货多” [1]。这意味着对于早安电台而言,泛泛而谈的娱乐内容已失去吸引力,用户更倾向于在清晨获取能够启发思考、辅助职业决策或提升专业技能的垂直信息。
2.2 情感陪伴与情绪价值的刚需
尽管工具性需求(如知识获取)是用户收听的重要动因,但情感陪伴需求同样占据核心地位。数据表明,长音频在睡眠场景(39.36%)和日常放松场景中的使用率极高 [1]。早安电台恰好处于从“睡眠场景”向“职业场景”过渡的交汇点,用户期望从中获得不仅是信息,更是一种能够缓解早晨起床焦虑的“情绪价值”。
这种心理诉求直接影响了电台的音色选择、背景音乐节奏以及内容编排逻辑。垂直领域的小程序开发者应当通过声音技术的革新,如高保真音效或更具拟人化质感的AI配音,来增强这种沉浸式的陪伴感。
2.3 场景化收听时长分析
用户对长音频的粘性正在稳步提升。约88.3%的音频用户每天收听时长超过30分钟,甚至有相当比例的用户会同时活跃在四个以上的音频平台上 [1]。这种“多栖收听”的行为模式暗示了垂直电台的机会:只要内容足够细分且具差异化,用户不介意在已有的音频习惯中增加一个新的垂直入口。
用户核心收听场景
占比分布
核心心理诉求
垂直电台策略建议
睡眠/清晨醒来
39.36% [1]
情绪放松、温和过渡
采用轻柔BGM,渐进式内容播报
家务/洗漱
34.63% [1]
解放双手、背景伴随
简短有力的短资讯或单点知识
通勤/差旅
34.63% [1]
时间利用、资讯获取
侧重职业动态、深度垂直行业快讯
健身/运动
约25% (估算)
节奏激励、自我提升
提供节奏感强的行业前沿思考
第三章:微信小程序背景音频技术架构与实操规范
在微信小程序生态中构建早安电台,技术实现的优劣直接决定了用户在弱网或锁屏状态下的体验。微信提供的 BackgroundAudioManager 接口是实现此类功能的核心武器。
3.1 BackgroundAudioManager 的深度解构
与普通的音频播放组件不同,BackgroundAudioManager 允许音频在小程序进入后台甚至手机锁屏后依然持续播放。这对于早安场景至关重要,因为用户在晨间洗漱或整理时,往往不会保持手机屏幕常亮。
根据官方开发规范,当开发者通过该接口设置新的 src 属性时,系统会自动触发播放流程 [2]。这一机制虽然简化了操作,但也要求开发者在逻辑设计上必须严谨,以避免意外播放。目前,该接口支持的音频格式包括 m4a、aac、mp3 和 wav [2]。对于追求高质量垂直内容的电台,aac 格式在同等比特率下能提供更好的音质,而 mp3 则是兼容性最广的选择。
3.2 播放控制与用户状态同步
由于电台内容通常具有时效性,startTime 属性的使用变得尤为关键。它允许从小程序的业务逻辑层面控制音频从特定秒数开始播放,这在实现“断点续传”或“跳过前奏”等功能时非常有用 [2]。
此外,由于背景音频由系统管理,开发者必须妥善处理多个回调函数,如 onPlay、onPause、onStop 和 onEnded。特别是在早安电台的场景下,若用户收到来电或闹钟响起,系统会自动暂停背景音频,开发者需要确保在这些中断结束后,音频状态能够正确恢复,以保证收听体验的连贯性。
3.3 技术参数与兼容性一览
在实际开发中,开发者需严格遵循以下参数规范,以确保在不同版本的微信客户端和不同操作系统的设备上达到最佳表现。
属性名称
类型
必填
功能描述及开发者注意事项
src
String
音频数据源。设置后自动开始播放 [2]。建议配合CDN加速。
startTime
Number
音频开始播放的位置(秒)。可用于保存并恢复用户进度 [2]。
title
String
必填。音频标题,将显示在锁屏界面及系统的播放控件中。
singer
String
歌手名/主播名。有助于建立垂直领域的主播个人品牌。
epname
String
专辑名。可用于区分不同垂直主题的电台合集。
coverImgUrl
String
封面图URL。视觉呈现是品牌建立的重要一环。
第四章:AIGC 技术赋能:内容生产力的结构性变革
对于垂直赛道的创业者而言,高频率的每日内容更新是极大的运营挑战。AIGC(人工智能生成内容)技术的介入,正在从根本上解决内容生产的效率与成本问题。
4.1 从人力驱动向 AI 协同的转变
中国 AIGC 产业正处于爆发期。2024年市场规模已达471.7亿元人民币,同比增长高达494.8% [1]。这一技术趋势对长音频行业的重构体现在三个维度:效率、多样性与个性化。
传统的早安电台录制需要专业的录音间、主播及后期剪辑。而利用 AIGC 中的 TTS(文本转语音)技术,开发者只需输入垂直领域的资讯文本,即可生成具备自然语感、甚至带有特定情感色彩的音频内容。这使得每日更新的边际成本几乎降至为零。
4.2 “一千个人,一千个早安电台”
AI 技术的另一大价值在于实现高度的个性化匹配。基于多模态大模型,程序可以根据用户的个人职业背景、收听习惯甚至所在城市的天气,实时生成定制化的播报脚本。
• 动态脚本生成:针对职场人群,AI 自动从海量行业动态中抓取最相关的三条资讯,并自动转化为适合口播的短语。
• 虚拟主播矩阵:提供不同风格的 AI 虚拟主播供用户选择,从“温柔知性”到“睿智专业”,满足不同用户的审美偏好 [1]。
• 交互式内容:结合 AI 语音识别技术,未来的早安电台可以实现简单的语音互动,例如用户可以对某条早报内容提问,AI 实时给予深度解读。
4.3 生产模式的融合:PGC + AIGC
尽管 AI 极大地提升了效率,但在垂直领域,专业性(PGC)依然是核心壁垒。72.1%的听众是因为内容的专业性而留存 [1]。因此,最优的内容策略是:由垂直领域的行业专家(或资深编辑)负责内容的价值筛选与观点把控,而由 AI 负责语音合成、背景音乐自动配乐及跨平台分发。这种“专家灵魂 + AI 躯干”的模式,将是未来垂直领域电台的主流形态。
第五章:政策监管与微信生态下的准入壁垒分析
在中国,视听内容及资讯类服务的运营受到严格的法律法规限制。在微信小程序平台,针对不同的垂直内容,其类目要求和资质门槛差异巨大,这是开发者在项目立项阶段必须攻克的首要难关。
5.1 互联网新闻与视听节目资质
如果早安电台的内容涉及“时政新闻”或“社会动态”,其资质要求极高。根据微信平台的规范,此类服务通常需要上传《互联网新闻信息服务许可证》,且许可证的服务名称必须与小程序名称一致 [3]。此外,由于电台属于长音频范畴,可能还涉及《信息网络传播视听节目许可证》或相关的备案要求。
5.2 垂直行业特定资质:教育、汽车与心理
微信小程序对特定行业的准入有着精细化的管理逻辑。若开发者选择的垂直赛道涉及专业性服务,必须提供相应的行业资质 [4]。
• 教育与职业认证:如果电台定位于职业技能培训或资格考试辅导,门槛极高。例如,部分类目要求小程序主体在近90天内的教育培训类商品累计支付金额必须大于或等于50万元 [5]。同时,需提供《办学许可证》或实缴注册资金500万以上的《增值电信业务许可证》(ICP) [3, 5]。
• 资讯与媒体平台:对于汽车媒体等资讯平台,需提供汽车品牌主机厂的授权文件、营业执照,甚至是线下4S店的实拍照片及承诺函 [5]。
• 设计与装修:即便是在看似门槛较低的家装领域,也要求营业执照注册满2年,实缴资金100万以上,并提供住建部或装修协会颁发的资质证书 [3]。
5.3 运营者个人资质与平台合规性
除了机构资质,运营者的个人背景也可能成为审核的一部分。在某些教育或播音主持相关的垂直赛道,微信会核查运营者的学历背景(如影音游戏动画相关专业)、教师资格证书或播音员主持人证 [5]。
垂直领域
核心经营类目
核心资质要求 (部分)
准入关键点
时政/社会新闻
互联网内容资讯服务
《互联网新闻信息服务许可证》 [3]
需在稿源单位名单内
IT/职业教育
资格考试/IT资格认证
《办学许可证》及销售额达标 [5]
资金与合规双重门槛
汽车行业资讯
汽车媒体
品牌授权书及《留资承诺函》 [5]
定向招商与品牌背书
设计/家装
室内装饰/设计
行业协会证书、2年注册期、百万实缴 [3]
对主体存续期有要求
播音主持培训
常识兴趣/音乐
专业学历证书、主持人证 [5]
对运营人专业性核查
第六章:商业逻辑演进:从内容分发到生态服务
垂直领域的早安电台不应仅仅局限于内容输出,而应通过构建深度的用户价值,探索多元化的商业闭环。
6.1 精准流量下的场景化营销
早安场景具有极强的排他性,用户在洗漱或通勤时很难被其他媒介干扰。这种高纯度的注意力环境为品牌植入提供了绝佳机会。
• 原生音频植入:与全平台泛滥的硬广不同,垂直电台可以进行内容深耕。例如,职场早安电台可以与办公软件或在线教育品牌合作,通过“职场小贴士”的形式自然带出产品。
• 私域导流:利用小程序与微信群的天然关联,将高频听众引导至垂直赛道的社群中,实现从“听觉陪伴”到“社群交互”的转化。
6.2 订阅制与知识付费的深耕
鉴于 72.1% 的听众是因为内容的“专业性”而订阅,高质量内容的变现潜力巨大 [1]。
• 阶梯订阅模式:每日晨报保持免费以获取流量,而深度的行业解析、专家访谈或特定专题(如“21天职场心态重塑”)则采取付费订阅制。
• IP 价值延伸:长音频作为网络文学和垂直 IP 的重要表现形式,具有极强的长尾价值。开发者可以挖掘高价值的行业 IP 进行音频化转制,构建具有持久生命力的内容资产 [1]。
6.3 声音技术的增值服务
随着 AIGC 技术的发展,个性化定制正成为新的付费增长点。用户可能愿意为定制化的播报音色(如家人声音的克隆)、定制化的早晨唤醒语或专属的内容简报付费。这种基于 AI 的增值服务,不仅提升了用户的归属感,也增加了产品的竞争门槛。
第七章:风险管理与未来趋势展望
在快速增长的声音经济赛道中,垂直领域早安电台的开发者不仅要关注增长,更要警惕潜在的结构性风险。
7.1 技术瓶颈与网络延迟管理
尽管 5G 正在普及,但在地铁、电梯等清晨常见的收听场景中,网络波动依然是体验杀手。开发者必须在小程序前端实现精细化的缓存策略。利用 BackgroundAudioManager 的缓冲机制,提前预下载下一段音频片段,是保证流畅度的必要手段。同时,针对不同带宽的用户提供动态码率切换,也是成熟产品的标准配置。
7.2 版权保护与 AI 伦理
随着 AIGC 的广泛应用,版权归属及内容真实性问题愈发凸显。政策已明确要求规范 AI 生成内容的标识 [1]。在垂直电台运营中,开发者必须确保 AI 生成内容的素材来源合法,并在程序显著位置明确标识内容的 AI 生成属性,以符合监管要求并建立品牌信任。
7.3 趋势预测:向“全天候智能伴侣”进化
早安电台只是切口,未来的趋势是音频服务与物联网(IoT)的深度融合。随着智能家居的普及,微信小程序作为连接器,可以轻松将电台内容推送到智能音箱、车载系统或智能穿戴设备上。
1. 多端流转:用户在家中通过智能音箱收听早安电台,出门后通过微信小程序无缝接续到车载音响或耳机。
2. 情感计算:结合生物反馈技术,电台内容能根据用户的压力水平或心率,实时调整背景音乐的频率和内容的温和度。
3. 声音经济规模化:到2029年,随着中国声音经济规模突破7400亿元,垂直领域的“小而美”产品将通过极高的用户忠诚度,在巨头林立的市场中占据不可替代的生态位 [1]。
第八章:总结与实战策略建议
综合上述研究,对于想要构建垂直领域早安电台微信小程序的开发者,本报告提出以下三点核心建议:
第一,坚持“专业主义”的内容立身之本。在声音经济中,内容深度是抵抗 AIGC 同质化竞争的唯一防线。开发者应优先深耕自己擅长的特定垂直领域(如职业心理、行业资讯、特定技能学习),确保内容的不可替代性 [1]。
第二,充分利用微信生态的技术杠杆。熟练掌握 BackgroundAudioManager 等核心 API,通过精细的技术实现解决后台播放与缓存痛点 [2]。同时,积极拥抱 AIGC 降本增效,将有限的资源投入到内容策略和用户交互设计中。
第三,合规经营是项目长效发展的生命线。在立项之初就必须对照微信最新的类目资质要求(特别是涉及教育、新闻、视听等敏感领域),确保主体资格和相关许可证的完备,避免因监管原因导致的中途折戟 [3, 5]。
中国长音频行业正处于从“规模增长”转向“价值挖掘”的关键节点,垂直化的早安电台小程序凭借其特有的场景仪式感和深度陪伴价值,必将在 5000 亿规模的声音经济版图中展现出惊人的商业张力。
--------------------------------------------------------------------------------
1. 2025年中国长音频市场竞争格局分析报告 - 行业资讯_央广传媒, http://www.cnrmg.cn/xwzx1/hyzx/20251119/t20251119_527434585.html
2. uni.getBackgroundAudioManager() | uni-app官网 - DCloud, https://uniapp.dcloud.net.cn/api/media/background-audio-manager
3. 微信视频号资质管理规范, https://support.weixin.qq.com/cgi-bin/mmsupportacctnodeweb-bin/pages/CgFuOjwO6qjjLeHW
4. 微信视频号资质管理规范, https://support.weixin.qq.com/cgi-bin/mmsupportacctnodeweb-bin/pages/nnxkJtKmu2YJ5NDl
5. 微信视频号留资服务类目资质要求, https://support.weixin.qq.com/cgi-bin/mmsupportacctnodeweb-bin/pages/owkyDPAa4XB1mGbx
+3 -10
View File
@@ -79,8 +79,9 @@ func (a *VoiceApi) GetVoiceOptions(c *gin.Context) {
response.FailWithMsg(err.Error(), c) response.FailWithMsg(err.Error(), c)
return return
} }
response.OkWithData(response.ListResult{
response.OkWithData(list, c) List: list,
}, c)
} }
// SaveVoice 保存音色 // SaveVoice 保存音色
@@ -98,14 +99,6 @@ func (a *VoiceApi) SaveVoice(c *gin.Context) {
response.FailWithMsg("参数错误: "+err.Error(), c) response.FailWithMsg("参数错误: "+err.Error(), c)
return return
} }
// 检查speakerId是否已存在
existing, _ := voiceService.GetVoiceBySpeakerId(req.SpeakerId)
if existing != nil {
response.FailWithMsg("该音色ID已存在", c)
return
}
if err := voiceService.SaveVoice(req); err != nil { if err := voiceService.SaveVoice(req); err != nil {
global.Logger.Error("保存音色失败!", zap.Error(err)) global.Logger.Error("保存音色失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c) response.FailWithMsg(err.Error(), c)
+425
View File
@@ -1743,6 +1743,36 @@ const docTemplate = `{
} }
} }
}, },
"/radio/analytics/vip-stats": {
"get": {
"tags": [
"数据分析"
],
"summary": "获取VIP统计数据",
"parameters": [
{
"type": "string",
"description": "开始日期",
"name": "startDate",
"in": "query"
},
{
"type": "string",
"description": "结束日期",
"name": "endDate",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/radio/category/delete": { "/radio/category/delete": {
"post": { "post": {
"produces": [ "produces": [
@@ -2201,6 +2231,12 @@ const docTemplate = `{
"name": "id", "name": "id",
"in": "query", "in": "query",
"required": true "required": true
},
{
"type": "string",
"description": "音色(默认 zh_male_dayi_uranus_bigtts)",
"name": "speaker",
"in": "query"
} }
], ],
"responses": { "responses": {
@@ -2377,6 +2413,274 @@ const docTemplate = `{
} }
} }
}, },
"/radio/user/list": {
"get": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"用户管理"
],
"summary": "获取电台用户列表(含订阅/收听统计)",
"parameters": [
{
"type": "integer",
"description": "页码",
"name": "current",
"in": "query"
},
{
"type": "integer",
"description": "每页大小",
"name": "pageSize",
"in": "query"
},
{
"type": "string",
"description": "搜索关键字",
"name": "keyword",
"in": "query"
},
{
"type": "integer",
"description": "VIP筛选: 0全部 1VIP 2非VIP",
"name": "isVip",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/radio/voice/default": {
"get": {
"produces": [
"application/json"
],
"tags": [
"音色管理"
],
"summary": "获取默认音色",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/radio/voice/delete": {
"post": {
"produces": [
"application/json"
],
"tags": [
"音色管理"
],
"summary": "删除音色",
"parameters": [
{
"description": "音色ID列表",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.IdsReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/radio/voice/detail": {
"get": {
"produces": [
"application/json"
],
"tags": [
"音色管理"
],
"summary": "获取音色详情",
"parameters": [
{
"type": "string",
"description": "音色ID",
"name": "id",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/radio/voice/list": {
"post": {
"produces": [
"application/json"
],
"tags": [
"音色管理"
],
"summary": "获取音色列表",
"parameters": [
{
"description": "分页查询",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.GetVoiceList"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/radio/voice/options": {
"get": {
"produces": [
"application/json"
],
"tags": [
"音色管理"
],
"summary": "获取音色选项列表",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/radio/voice/save": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"音色管理"
],
"summary": "保存音色",
"parameters": [
{
"description": "音色信息",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.SaveVoice"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/radio/voice/set-default": {
"post": {
"produces": [
"application/json"
],
"tags": [
"音色管理"
],
"summary": "设置默认音色",
"parameters": [
{
"type": "string",
"description": "音色ID",
"name": "id",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/radio/voice/update": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"音色管理"
],
"summary": "更新音色",
"parameters": [
{
"description": "音色信息",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.UpdateVoice"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/role/delete": { "/role/delete": {
"post": { "post": {
"description": "删除角色", "description": "删除角色",
@@ -3394,6 +3698,9 @@ const docTemplate = `{
"description": "页码", "description": "页码",
"type": "integer" "type": "integer"
}, },
"isVip": {
"type": "integer"
},
"keyword": { "keyword": {
"description": "关键字", "description": "关键字",
"type": "string" "type": "string"
@@ -3407,6 +3714,31 @@ const docTemplate = `{
} }
} }
}, },
"request.GetVoiceList": {
"type": "object",
"properties": {
"current": {
"description": "页码",
"type": "integer"
},
"keyword": {
"description": "关键字",
"type": "string"
},
"name": {
"description": "音色名称",
"type": "string"
},
"pageSize": {
"description": "每页大小",
"type": "integer"
},
"status": {
"description": "状态",
"type": "integer"
}
}
},
"request.GrantMenu": { "request.GrantMenu": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -3600,6 +3932,51 @@ const docTemplate = `{
} }
} }
}, },
"request.SaveVoice": {
"type": "object",
"required": [
"name",
"speakerId"
],
"properties": {
"audioId": {
"description": "试听音频OSS ID",
"type": "string"
},
"description": {
"description": "音色描述",
"type": "string"
},
"gender": {
"description": "性别: male/female/neutral",
"type": "string"
},
"icon": {
"description": "音色图标URL",
"type": "string"
},
"isDefault": {
"description": "是否默认音色",
"type": "integer"
},
"name": {
"description": "音色名称",
"type": "string"
},
"sort": {
"description": "排序",
"type": "integer"
},
"speakerId": {
"description": "音色ID",
"type": "string"
},
"status": {
"description": "状态",
"type": "integer"
}
}
},
"request.ToggleLike": { "request.ToggleLike": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -3767,6 +4144,54 @@ const docTemplate = `{
} }
} }
}, },
"request.UpdateVoice": {
"type": "object",
"required": [
"id"
],
"properties": {
"audioId": {
"description": "试听音频OSS ID",
"type": "string"
},
"description": {
"description": "音色描述",
"type": "string"
},
"gender": {
"description": "性别",
"type": "string"
},
"icon": {
"description": "音色图标URL",
"type": "string"
},
"id": {
"description": "音色ID",
"type": "string"
},
"isDefault": {
"description": "是否默认音色",
"type": "integer"
},
"name": {
"description": "音色名称",
"type": "string"
},
"sort": {
"description": "排序",
"type": "integer"
},
"speakerId": {
"description": "音色ID",
"type": "string"
},
"status": {
"description": "状态",
"type": "integer"
}
}
},
"response.CaptchaRes": { "response.CaptchaRes": {
"type": "object", "type": "object",
"properties": { "properties": {
+425
View File
@@ -1736,6 +1736,36 @@
} }
} }
}, },
"/radio/analytics/vip-stats": {
"get": {
"tags": [
"数据分析"
],
"summary": "获取VIP统计数据",
"parameters": [
{
"type": "string",
"description": "开始日期",
"name": "startDate",
"in": "query"
},
{
"type": "string",
"description": "结束日期",
"name": "endDate",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/radio/category/delete": { "/radio/category/delete": {
"post": { "post": {
"produces": [ "produces": [
@@ -2194,6 +2224,12 @@
"name": "id", "name": "id",
"in": "query", "in": "query",
"required": true "required": true
},
{
"type": "string",
"description": "音色(默认 zh_male_dayi_uranus_bigtts)",
"name": "speaker",
"in": "query"
} }
], ],
"responses": { "responses": {
@@ -2370,6 +2406,274 @@
} }
} }
}, },
"/radio/user/list": {
"get": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"用户管理"
],
"summary": "获取电台用户列表(含订阅/收听统计)",
"parameters": [
{
"type": "integer",
"description": "页码",
"name": "current",
"in": "query"
},
{
"type": "integer",
"description": "每页大小",
"name": "pageSize",
"in": "query"
},
{
"type": "string",
"description": "搜索关键字",
"name": "keyword",
"in": "query"
},
{
"type": "integer",
"description": "VIP筛选: 0全部 1VIP 2非VIP",
"name": "isVip",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/radio/voice/default": {
"get": {
"produces": [
"application/json"
],
"tags": [
"音色管理"
],
"summary": "获取默认音色",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/radio/voice/delete": {
"post": {
"produces": [
"application/json"
],
"tags": [
"音色管理"
],
"summary": "删除音色",
"parameters": [
{
"description": "音色ID列表",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.IdsReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/radio/voice/detail": {
"get": {
"produces": [
"application/json"
],
"tags": [
"音色管理"
],
"summary": "获取音色详情",
"parameters": [
{
"type": "string",
"description": "音色ID",
"name": "id",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/radio/voice/list": {
"post": {
"produces": [
"application/json"
],
"tags": [
"音色管理"
],
"summary": "获取音色列表",
"parameters": [
{
"description": "分页查询",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.GetVoiceList"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/radio/voice/options": {
"get": {
"produces": [
"application/json"
],
"tags": [
"音色管理"
],
"summary": "获取音色选项列表",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/radio/voice/save": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"音色管理"
],
"summary": "保存音色",
"parameters": [
{
"description": "音色信息",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.SaveVoice"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/radio/voice/set-default": {
"post": {
"produces": [
"application/json"
],
"tags": [
"音色管理"
],
"summary": "设置默认音色",
"parameters": [
{
"type": "string",
"description": "音色ID",
"name": "id",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/radio/voice/update": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"音色管理"
],
"summary": "更新音色",
"parameters": [
{
"description": "音色信息",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.UpdateVoice"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/role/delete": { "/role/delete": {
"post": { "post": {
"description": "删除角色", "description": "删除角色",
@@ -3387,6 +3691,9 @@
"description": "页码", "description": "页码",
"type": "integer" "type": "integer"
}, },
"isVip": {
"type": "integer"
},
"keyword": { "keyword": {
"description": "关键字", "description": "关键字",
"type": "string" "type": "string"
@@ -3400,6 +3707,31 @@
} }
} }
}, },
"request.GetVoiceList": {
"type": "object",
"properties": {
"current": {
"description": "页码",
"type": "integer"
},
"keyword": {
"description": "关键字",
"type": "string"
},
"name": {
"description": "音色名称",
"type": "string"
},
"pageSize": {
"description": "每页大小",
"type": "integer"
},
"status": {
"description": "状态",
"type": "integer"
}
}
},
"request.GrantMenu": { "request.GrantMenu": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -3593,6 +3925,51 @@
} }
} }
}, },
"request.SaveVoice": {
"type": "object",
"required": [
"name",
"speakerId"
],
"properties": {
"audioId": {
"description": "试听音频OSS ID",
"type": "string"
},
"description": {
"description": "音色描述",
"type": "string"
},
"gender": {
"description": "性别: male/female/neutral",
"type": "string"
},
"icon": {
"description": "音色图标URL",
"type": "string"
},
"isDefault": {
"description": "是否默认音色",
"type": "integer"
},
"name": {
"description": "音色名称",
"type": "string"
},
"sort": {
"description": "排序",
"type": "integer"
},
"speakerId": {
"description": "音色ID",
"type": "string"
},
"status": {
"description": "状态",
"type": "integer"
}
}
},
"request.ToggleLike": { "request.ToggleLike": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -3760,6 +4137,54 @@
} }
} }
}, },
"request.UpdateVoice": {
"type": "object",
"required": [
"id"
],
"properties": {
"audioId": {
"description": "试听音频OSS ID",
"type": "string"
},
"description": {
"description": "音色描述",
"type": "string"
},
"gender": {
"description": "性别",
"type": "string"
},
"icon": {
"description": "音色图标URL",
"type": "string"
},
"id": {
"description": "音色ID",
"type": "string"
},
"isDefault": {
"description": "是否默认音色",
"type": "integer"
},
"name": {
"description": "音色名称",
"type": "string"
},
"sort": {
"description": "排序",
"type": "integer"
},
"speakerId": {
"description": "音色ID",
"type": "string"
},
"status": {
"description": "状态",
"type": "integer"
}
}
},
"response.CaptchaRes": { "response.CaptchaRes": {
"type": "object", "type": "object",
"properties": { "properties": {
+282
View File
@@ -241,6 +241,8 @@ definitions:
current: current:
description: 页码 description: 页码
type: integer type: integer
isVip:
type: integer
keyword: keyword:
description: 关键字 description: 关键字
type: string type: string
@@ -250,6 +252,24 @@ definitions:
phone: phone:
type: string type: string
type: object type: object
request.GetVoiceList:
properties:
current:
description: 页码
type: integer
keyword:
description: 关键字
type: string
name:
description: 音色名称
type: string
pageSize:
description: 每页大小
type: integer
status:
description: 状态
type: integer
type: object
request.GrantMenu: request.GrantMenu:
properties: properties:
menuIds: menuIds:
@@ -385,6 +405,39 @@ definitions:
- channelId - channelId
- title - title
type: object type: object
request.SaveVoice:
properties:
audioId:
description: 试听音频OSS ID
type: string
description:
description: 音色描述
type: string
gender:
description: '性别: male/female/neutral'
type: string
icon:
description: 音色图标URL
type: string
isDefault:
description: 是否默认音色
type: integer
name:
description: 音色名称
type: string
sort:
description: 排序
type: integer
speakerId:
description: 音色ID
type: string
status:
description: 状态
type: integer
required:
- name
- speakerId
type: object
request.ToggleLike: request.ToggleLike:
properties: properties:
programId: programId:
@@ -504,6 +557,41 @@ definitions:
- id - id
- price - price
type: object type: object
request.UpdateVoice:
properties:
audioId:
description: 试听音频OSS ID
type: string
description:
description: 音色描述
type: string
gender:
description: 性别
type: string
icon:
description: 音色图标URL
type: string
id:
description: 音色ID
type: string
isDefault:
description: 是否默认音色
type: integer
name:
description: 音色名称
type: string
sort:
description: 排序
type: integer
speakerId:
description: 音色ID
type: string
status:
description: 状态
type: integer
required:
- id
type: object
response.CaptchaRes: response.CaptchaRes:
properties: properties:
captcha: captcha:
@@ -1738,6 +1826,25 @@ paths:
summary: 获取用户留存分析 (Cohort) summary: 获取用户留存分析 (Cohort)
tags: tags:
- 数据分析 - 数据分析
/radio/analytics/vip-stats:
get:
parameters:
- description: 开始日期
in: query
name: startDate
type: string
- description: 结束日期
in: query
name: endDate
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.Response'
summary: 获取VIP统计数据
tags:
- 数据分析
/radio/category/delete: /radio/category/delete:
post: post:
parameters: parameters:
@@ -2028,6 +2135,10 @@ paths:
name: id name: id
required: true required: true
type: string type: string
- description: 音色(默认 zh_male_dayi_uranus_bigtts)
in: query
name: speaker
type: string
produces: produces:
- application/json - application/json
responses: responses:
@@ -2143,6 +2254,177 @@ paths:
summary: 解锁频道 summary: 解锁频道
tags: tags:
- 订阅管理 - 订阅管理
/radio/user/list:
get:
consumes:
- application/json
parameters:
- description: 页码
in: query
name: current
type: integer
- description: 每页大小
in: query
name: pageSize
type: integer
- description: 搜索关键字
in: query
name: keyword
type: string
- description: 'VIP筛选: 0全部 1VIP 2非VIP'
in: query
name: isVip
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.Response'
summary: 获取电台用户列表(含订阅/收听统计)
tags:
- 用户管理
/radio/voice/default:
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.Response'
summary: 获取默认音色
tags:
- 音色管理
/radio/voice/delete:
post:
parameters:
- description: 音色ID列表
in: body
name: data
required: true
schema:
$ref: '#/definitions/request.IdsReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.Response'
summary: 删除音色
tags:
- 音色管理
/radio/voice/detail:
get:
parameters:
- description: 音色ID
in: query
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.Response'
summary: 获取音色详情
tags:
- 音色管理
/radio/voice/list:
post:
parameters:
- description: 分页查询
in: body
name: data
required: true
schema:
$ref: '#/definitions/request.GetVoiceList'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.Response'
summary: 获取音色列表
tags:
- 音色管理
/radio/voice/options:
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.Response'
summary: 获取音色选项列表
tags:
- 音色管理
/radio/voice/save:
post:
consumes:
- application/json
parameters:
- description: 音色信息
in: body
name: data
required: true
schema:
$ref: '#/definitions/request.SaveVoice'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.Response'
summary: 保存音色
tags:
- 音色管理
/radio/voice/set-default:
post:
parameters:
- description: 音色ID
in: query
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.Response'
summary: 设置默认音色
tags:
- 音色管理
/radio/voice/update:
post:
consumes:
- application/json
parameters:
- description: 音色信息
in: body
name: data
required: true
schema:
$ref: '#/definitions/request.UpdateVoice'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.Response'
summary: 更新音色
tags:
- 音色管理
/role/delete: /role/delete:
post: post:
consumes: consumes:
-37
View File
@@ -1,37 +0,0 @@
package response
import "sundynix-go/model/system"
// VoiceResponse 音色响应
type VoiceResponse struct {
Id string `json:"id"`
SpeakerId string `json:"speakerId"`
Name string `json:"name"`
Description string `json:"description"`
Gender string `json:"gender"`
Icon string `json:"icon"`
AudioId string `json:"audioId"`
Audio *system.Oss `json:"audio"` // 试听音频OSS
Sort int `json:"sort"`
Status int `json:"status"`
IsDefault int `json:"isDefault"`
UseCount int `json:"useCount"`
}
// VoiceDetailResponse 音色详情响应
type VoiceDetailResponse struct {
Id string `json:"id"`
SpeakerId string `json:"speakerId"`
Name string `json:"name"`
Description string `json:"description"`
Gender string `json:"gender"`
Icon string `json:"icon"`
AudioId string `json:"audioId"`
Audio *system.Oss `json:"audio"` // 试听音频OSS
Sort int `json:"sort"`
Status int `json:"status"`
IsDefault int `json:"isDefault"`
UseCount int `json:"useCount"`
CreateTime string `json:"createTime"`
UpdateTime string `json:"updateTime"`
}
+2 -2
View File
@@ -13,7 +13,7 @@ type CategoryService struct{}
// GetCategoryList 获取分类列表 // GetCategoryList 获取分类列表
func (s *CategoryService) GetCategoryList(info radioReq.GetCategoryList) ([]radio.RadioCategory, int64, error) { func (s *CategoryService) GetCategoryList(info radioReq.GetCategoryList) ([]radio.RadioCategory, int64, error) {
db := global.DB.Model(&radio.RadioCategory{}) db := global.DB.Model(&radio.RadioCategory{}).Where("status = ?", 1)
var list []radio.RadioCategory var list []radio.RadioCategory
var total int64 var total int64
@@ -50,7 +50,7 @@ func (s *CategoryService) GetCategoryTree() ([]radio.RadioCategory, error) {
func (s *CategoryService) GetAllCategory() ([]radio.RadioCategory, error) { func (s *CategoryService) GetAllCategory() ([]radio.RadioCategory, error) {
var res []radio.RadioCategory var res []radio.RadioCategory
err := global.DB.Find(&res).Error err := global.DB.Where("status = ?", 1).Find(&res).Error
return res, err return res, err
} }
+2 -2
View File
@@ -15,7 +15,7 @@ import (
type ChannelService struct{} type ChannelService struct{}
func (s *ChannelService) GetFreeChannelList(req common.PageInfo) ([]radio.RadioChannel, int64, error) { func (s *ChannelService) GetFreeChannelList(req common.PageInfo) ([]radio.RadioChannel, int64, error) {
db := global.DB.Model(&radio.RadioChannel{}).Where("is_free = 1") db := global.DB.Model(&radio.RadioChannel{}).Where("is_free = 1").Where("status = ?", 1)
var list []radio.RadioChannel var list []radio.RadioChannel
var total int64 var total int64
err := db.Count(&total).Error err := db.Count(&total).Error
@@ -33,7 +33,7 @@ func (s *ChannelService) GetFreeChannelList(req common.PageInfo) ([]radio.RadioC
// GetChannelList 获取频道列表 // GetChannelList 获取频道列表
func (s *ChannelService) GetChannelList(userId string, info radioReq.GetChannelList) ([]radio.RadioChannel, int64, error) { func (s *ChannelService) GetChannelList(userId string, info radioReq.GetChannelList) ([]radio.RadioChannel, int64, error) {
db := global.DB.Model(&radio.RadioChannel{}) db := global.DB.Model(&radio.RadioChannel{}).Where("status = ?", 1)
var list []radio.RadioChannel var list []radio.RadioChannel
var total int64 var total int64
+1 -1
View File
@@ -13,7 +13,7 @@ type ProgramService struct{}
// GetProgramList 获取节目列表 // GetProgramList 获取节目列表
func (s *ProgramService) GetProgramList(info radioReq.GetProgramList) ([]radio.RadioProgram, int64, error) { func (s *ProgramService) GetProgramList(info radioReq.GetProgramList) ([]radio.RadioProgram, int64, error) {
db := global.DB.Model(&radio.RadioProgram{}).Preload("Audio") db := global.DB.Model(&radio.RadioProgram{}).Where("status = ?", 1).Preload("Audio")
var list []radio.RadioProgram var list []radio.RadioProgram
var total int64 var total int64
+8 -57
View File
@@ -4,7 +4,6 @@ import (
"sundynix-go/global" "sundynix-go/global"
"sundynix-go/model/radio" "sundynix-go/model/radio"
radioReq "sundynix-go/model/radio/request" radioReq "sundynix-go/model/radio/request"
radioRes "sundynix-go/model/radio/response"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -12,8 +11,8 @@ import (
type VoiceService struct{} type VoiceService struct{}
// GetVoiceList 获取音色列表 // GetVoiceList 获取音色列表
func (s *VoiceService) GetVoiceList(req radioReq.GetVoiceList) ([]radioRes.VoiceResponse, int64, error) { func (s *VoiceService) GetVoiceList(req radioReq.GetVoiceList) ([]radio.RadioVoice, int64, error) {
db := global.DB.Model(&radio.RadioVoice{}) db := global.DB.Model(&radio.RadioVoice{}).Preload("Audio")
var list []radio.RadioVoice var list []radio.RadioVoice
var total int64 var total int64
@@ -34,51 +33,14 @@ func (s *VoiceService) GetVoiceList(req radioReq.GetVoiceList) ([]radioRes.Voice
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
return list, total, nil
// 转换为响应结构
var resp []radioRes.VoiceResponse
for _, v := range list {
resp = append(resp, radioRes.VoiceResponse{
Id: v.Id,
SpeakerId: v.SpeakerId,
Name: v.Name,
Description: v.Description,
Gender: v.Gender,
Icon: v.Icon,
AudioId: v.AudioId,
Sort: v.Sort,
Status: v.Status,
IsDefault: v.IsDefault,
UseCount: v.UseCount,
})
}
return resp, total, nil
} }
// GetVoiceById 获取音色详情 // GetVoiceById 获取音色详情
func (s *VoiceService) GetVoiceById(id string) (*radioRes.VoiceDetailResponse, error) { func (s *VoiceService) GetVoiceById(id string) (*radio.RadioVoice, error) {
var voice radio.RadioVoice var voice radio.RadioVoice
err := global.DB.Preload("Audio").Where("id = ?", id).First(&voice).Error err := global.DB.Preload("Audio").Where("id = ?", id).First(&voice).Error
if err != nil { return &voice, err
return nil, err
}
return &radioRes.VoiceDetailResponse{
Id: voice.Id,
SpeakerId: voice.SpeakerId,
Name: voice.Name,
Description: voice.Description,
Gender: voice.Gender,
Icon: voice.Icon,
AudioId: voice.AudioId,
Audio: voice.Audio,
Sort: voice.Sort,
Status: voice.Status,
IsDefault: voice.IsDefault,
UseCount: voice.UseCount,
CreateTime: voice.CreatedAt.Format("2006-01-02 15:04:05"),
UpdateTime: voice.UpdatedAt.Format("2006-01-02 15:04:05"),
}, nil
} }
// GetVoiceBySpeakerId 根据SpeakerId获取音色 // GetVoiceBySpeakerId 根据SpeakerId获取音色
@@ -100,30 +62,19 @@ func (s *VoiceService) GetDefaultVoice() (*radio.RadioVoice, error) {
} }
// GetAllEnabledVoice 获取所有启用的音色(前端选择用) // GetAllEnabledVoice 获取所有启用的音色(前端选择用)
func (s *VoiceService) GetAllEnabledVoice() ([]radioRes.VoiceResponse, error) { func (s *VoiceService) GetAllEnabledVoice() ([]radio.RadioVoice, error) {
var list []radio.RadioVoice var list []radio.RadioVoice
err := global.DB.Where("status = ?", 1).Order("sort ASC").Find(&list).Error err := global.DB.Where("status = ?", 1).Order("sort ASC").Find(&list).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return list, nil
var resp []radioRes.VoiceResponse
for _, v := range list {
resp = append(resp, radioRes.VoiceResponse{
Id: v.Id,
SpeakerId: v.SpeakerId,
Name: v.Name,
Gender: v.Gender,
Icon: v.Icon,
IsDefault: v.IsDefault,
})
}
return resp, nil
} }
// SaveVoice 保存音色 // SaveVoice 保存音色
func (s *VoiceService) SaveVoice(req radioReq.SaveVoice) error { func (s *VoiceService) SaveVoice(req radioReq.SaveVoice) error {
return global.DB.Transaction(func(tx *gorm.DB) error { return global.DB.Transaction(func(tx *gorm.DB) error {
// 如果设置为默认音色,先取消其他默认 // 如果设置为默认音色,先取消其他默认
if req.IsDefault == 1 { if req.IsDefault == 1 {
if err := tx.Model(&radio.RadioVoice{}).Where("is_default = ?", 1).Update("is_default", 0).Error; err != nil { if err := tx.Model(&radio.RadioVoice{}).Where("is_default = ?", 1).Update("is_default", 0).Error; err != nil {