first commit

This commit is contained in:
Blizzard
2026-02-27 13:54:01 +08:00
commit fc585fa4df
127 changed files with 18548 additions and 0 deletions
Vendored
BIN
View File
Binary file not shown.
+69
View File
@@ -0,0 +1,69 @@
# 早安电台:垂直领域内容抓取源清单 (2026版)
这份清单为不同场景的电台提供“食材”来源,建议通过 Go 后端定时任务 (Cron Job) 自动抓取并推送到 LLM 进行摘要改写。
---
## 1. 【硬核职场】AI 商业机会方向
*目标:提供最具时效性的“信息差”和变现线索。*
### 核心抓取源:
* **技术趋势 API:**
* **GitHub API:** 抓取 `/search/repositories`,筛选过去 24 小时内 Star 增长最快的 AI/Agent 相关项目。
* **Hugging Face:** 监控其 `Trending` 页面,提取每日热门开源模型及其应用场景。
* **产品发布与商业线索:**
* **Product Hunt API:** 获取每日 Upvote 前 10 的产品描述,由 AI 筛选其中具有“盈利模型”的项目。
* **Indie Hackers:** 爬取 `Revenue` 板块,获取独立开发者的最新收入报告和成功路径。
* **行业动态:**
* **深度快讯 (DeepSeek/Google/OpenAI):** 订阅其开发者博客的 RSS,实时监控模型降价或新功能发布。
---
## 2. 【效率健康】个人数字健康方向
*目标:结合实时运动数据与专业健康建议。*
### 核心抓取源:
* **用户实时数据 (小程序端):**
* **WeRun (微信运动):** 接口 `wx.getWeRunData` 获取步数(需用户授权)。
* **专业健康内容:**
* **PubMed/Nature API:** 抓取关于“运动表现”或“睡眠科学”的最新研究摘要,转译为科普短语音。
* **Healthline/WebMD:** 爬取每日健康小贴士(如:今日最适合的燃脂食物建议)。
* **环境与压力感知:**
* **QWeather (和风天气) API:** 不仅获取气温,重点关注“运动指数”、“过敏指数”及“紫外线强度”。
---
## 3. 【极简生活】脱手式晨间管家方向
*目标:提供本地化、高实用性的生活情报。*
### 核心抓取源:
* **本地化民生信息:**
* **本地政府/电力/供水公示:** 监控特定城市的政务 RSS,抓取停水、停电或道路临时封堵信息。
* **小红书 (RedNote) 热榜:** 通过搜索接口抓取当地“今日热门打卡点”或“避雷指南”。
* **消费与价格:**
* **每日生鲜价格 API:** 监控如美团买菜/叮咚买菜的每日低价、特价商品(通过爬虫抓取首页 Banner 信息)。
* **时间/日程管理:**
* **中国万年历 API:** 播报今日忌宜、节气特征及重要的法定节假日提醒。
---
## 4. 【知识胶囊】备考与职场进阶方向
*目标:将长文本转化为碎片化听觉知识点。*
### 核心抓取源:
* **权威教育资源:**
* **Khan Academy API:** 获取各学科的知识节点摘要,适合做每日一词/一理。
* **Quizlet API:** 接入用户的生词本或知识集,实现“个性化错题/重点”音频播报。
* **行业标准与规范:**
* **Go 语言官方博客/文档:** 定时监控 `golang.org/doc` 的更新,播报最新的语法改进或最佳实践。
* **CSDN/掘金热榜:** 抓取每日技术高赞文章,提取其中的核心结论(而非正文)。
* **英语/语言学习:**
* **BBC Learning English / VOA Special English:** 抓取最新的每日短音频或新闻稿件。
---
## 💡 技术实现小贴士 (For Developer)
1. **频率控制:** 建议“硬核职场”每 2 小时更新一次,“生活管家”每天清晨 5 点更新一次。
2. **数据清洗:** 抓取回来的 HTML 需使用 `Goquery` (Golang) 库进行清洗,只保留核心文本,减少 Token 浪费。
3. **UUID 存储:** 每个抓取到的内容条目,在存入 `sundynix_audio_content` 表时,务必生成唯一的 UUID 以防内容重复。
+97
View File
@@ -0,0 +1,97 @@
《“早安电台”项目完整架构设计与开发计划书》
一、 产品愿景与垂直领域选择
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。
+74
View File
@@ -0,0 +1,74 @@
## server项目结构
```shell
├── api
│   └── v1
├── config
├── core
├── docs
├── global
├── initialize
│   └── internal
├── middleware
├── model
│   ├── request
│   └── response
├── pkg
│   ├── example
├── packfile
├── resource
│   ├── excel
│   ├── page
│   └── template
├── router
├── service
├── source
└── utils
├── timer
└── upload
```
| 文件夹 | 说明 | 描述 |
| ------------ | ----------------------- | --------------------------- |
| `api` | api层 | api层 |
| `--v1` | v1版本接口 | v1版本接口 |
| `config` | 配置包 | config.yaml对应的配置结构体 |
| `core` | 核心文件 | 核心组件(zap, viper, server)的初始化 |
| `docs` | swagger文档目录 | swagger文档目录 |
| `global` | 全局对象 | 全局对象 |
| `initialize` | 初始化 | router,redis,gorm,validator, timer的初始化 |
| `--internal` | 初始化内部函数 | gorm 的 logger 自定义,在此文件夹的函数只能由 `initialize` 层进行调用 |
| `middleware` | 中间件层 | 用于存放 `gin` 中间件代码 |
| `model` | 模型层 | 模型对应数据表 |
| `pkg` | 公共包 | 存放一些公共函数 |
| `--request` | 入参结构体 | 接收前端发送到后端的数据。 |
| `--response` | 出参结构体 | 返回给前端的数据结构体 |
| `packfile` | 静态文件打包 | 静态文件打包 |
| `resource` | 静态资源文件夹 | 负责存放静态文件 |
| `--excel` | excel导入导出默认路径 | excel导入导出默认路径 |
| `--page` | 表单生成器 | 表单生成器 打包后的dist |
| `--template` | 模板 | 模板文件夹,存放的是代码生成器的模板 |
| `router` | 路由层 | 路由层 |
| `service` | service层 | 存放业务逻辑问题 |
| `source` | source层 | 存放初始化数据的函数 |
| `utils` | 工具包 | 工具函数封装 |
| `--timer` | timer | 定时器接口封装 |
| `--upload` | oss | oss接口封装 |
## 一、部署
### 1.1 传统方式
1.1.1 Mac下交叉编译到Windows和Linux
~~~
# 交叉编译到Windows
GOOS=windows GOARCH=amd64 go build -o sundynix-plant-go.exe
# 交叉编译到Linux
GOOS=linux GOARCH=amd64 go build -o sundynix-plant-go
~~~
1.1.2 Rocky9.x下运行
上传build好的可执行文件和配置文件到生产目录下 执行如下命令
~~~
nohup ./sundynix-plant -c config-prod.yaml > console.log 2>&1 &
~~~
+73
View File
@@ -0,0 +1,73 @@
# Sundynix Plant Server 项目文档
## 项目简介
本项目是一个基于 Go 语言 (Gin 框架) 开发的后台管理系统,主要用于植物领养、种植乐趣及周边社区的运营管理。项目采用了模块化的架构设计,集成了系统管理(RBAC、日志、OSS)与业务模块(植物图鉴、领养、订单、社区)。
## 目录结构
```shell
├── api # 接口控制层 (Controller)
│   └── v1 # v1 版本接口
├── config # 配置文件结构定义
├── core # 核心组件初始化 (Zap, Viper, Server)
├── global # 全局对象 (DB, Redis, Config)
├── initialize # 初始化流程 (Router, Redis, Gorm, Validator, Timer)
├── middleware # Gin 中间件
├── model # 数据模型 (Structs)
├── router # 路由定义
│ ├── plant # 业务路由 (植物、订单、社区等)
│ └── system # 系统路由 (用户、角色、菜单等)
├── service # 业务逻辑层 (Service)
└── utils # 工具函数集合
```
## 基本功能逻辑
项目主要分为 **系统管理****业务管理 (Plant)** 两大模块:
### 1. 系统管理模块 (System)
- **用户鉴权 (Auth/User)**: 支持用户注册、登录及 JWT 鉴权。
- **权限管理 (RBAC)**: 基于角色 (Role) 和菜单 (Menu) 的权限控制体系,支持动态菜单。
- **操作日志 (Operation Record)**: 记录用户的敏感操作行为,便于审计。
- **文件上传 (OSS)**: 集成对象存储,用于处理图片、文件上传。
- **客户端管理 (Client)**: 管理接入的客户端信息。
### 2. 业务管理模块 (Plant)
- **植物图鉴 (Library/Classification)**: 维护植物的基础信息与分类,构建植物百科。
- **植物领养 (Claim Plant)**:
- 用户可以浏览并领养植物。
- 支持配置领养规则。
- 用户可查看 "我的领养" 记录。
- **订单系统 (Order/Pay)**:
- 处理领养或购买产生的订单。
- 支持订单发货、详情查询、删除及导出功能。
- 集成支付功能。
- **社区互动 (Post/Comment)**:
- 用户发布帖子 (Post) 分享种植心得。
- 支持对帖子进行评论 (Comment)。
- **其他功能**:
- **徽章系统 (Badge)**: 用户成就体系。
- **OCR 识别**: 植物图片识别功能。
- **个人中心 (Personal)**: 用户个人数据管理。
## 基本流程 (Workflow)
### 1. 植物领养流程
1. **浏览图鉴**: 用户查看植物图鉴 (Library),选择感兴趣的植物。
2. **发起领养**: 用户发起领养请求 (Claim),系统根据配置 (`claimPlantApi`) 处理领养逻辑。
3. **生成记录**: 成功后在 "我的领养" (`MyClaim`) 中生成记录,并可能关联具体的植物 ID。
### 2. 订单处理流程
1. **创建订单**: 用户进行支付或确认领养后生成订单。
2. **后台发货**: 管理员在后台查看订单列表 (`OrderPage`),核实信息后进行发货操作 (`ShipOrder`)。
3. **订单完成**: 用户确认收货,订单流程结束。
4. **数据导出**: 运营人员可将订单数据导出 (`ExportOrder`) 用于分析。
### 3. 内容社区流程
1. **发布内容**: 用户上传植物照片或心得,发布帖子 (`Post`)。
2. **互动交流**: 其他用户浏览帖子,并进行评论 (`PostComment`) 或点赞。
3. **审核管理**: 管理员可对违规内容进行管理或删除。
## 部署说明 (参考)
项目支持跨平台编译:
- **Windows**: `GOOS=windows GOARCH=amd64 go build -o planting-fun.exe`
- **Linux**: `GOOS=linux GOARCH=amd64 go build -o sundynix-plant`
+181
View File
@@ -0,0 +1,181 @@
中国垂直领域早安电台微信小程序产业研究与全链路建设报告
来源指南
中国垂直领域早安电台微信小程序产业研究与全链路建设报告
第一章:中国声音经济的宏观范式与长音频演进逻辑
在数字化媒介高度饱和的当下,视觉注意力的争夺已进入存量博弈阶段,而以声音为载体的“听觉经济”正成为新的蓝海。中国声音经济的崛起并非偶然,而是技术演进、消费升级与用户心理变迁共同驱动的结果。根据最新产业研究数据,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
+47
View File
@@ -0,0 +1,47 @@
# 早安电台:2026年垂直领域方向深度分析
## 1. 【硬核职场】AI 商业机会与搞钱情报站
* **适用场景:** 通勤 / 差旅 / 早餐。
* **目标受众:** 开发者、独立创业者、产品经理、对 AI 变现感兴趣的职场人。
* **核心逻辑:** 针对 2026 年信息极度碎片化、AI 工具爆发的现状,提供“零点击”的信息提炼,直接关联用户的收益增长。
* **垂直内容点:**
* **每日 AI 变现线索:** 自动抓取 GitHub Trending、Product Hunt、HuggingFace 热榜,由 AI 改写成“今日可落地的 3 个变现点”。
* **差旅异地情报:** 结合 LBS 播报目的地城市的行业沙龙、高新园区政策或当地大厂动态。
* **API 调价快报:** 实时监控主流模型(OpenAI, Anthropic, DeepSeek 等)的价格波动与技术更新。
* **商业价值:** 极高。可转化为付费会员订阅制或高端行业社群入口。
---
## 2. 【效率健康】个人数字健康同步站
* **适用场景:** 健身 / 运动 / 晨跑。
* **目标受众:** 健身爱好者、减脂人群、追求生活规律的自律族。
* **核心逻辑:** 顺应 2026 年“个人数字健康生态”趋势。电台不再是单向输出,而是通过微信运动数据实现“状态感知的实时播报”。
* **垂直内容点:**
* **多巴胺语音激励:** 监测到步数或心率达到目标区间时,AI 自动切换为“热血教练模式”进行即时鼓励。
* **身心调频流:** 运动结束后自动无缝切换为舒缓的“呼吸冥想”或“肌肉拉伸”语音指导。
* **营养补给建议:** 根据当日卡路里消耗,AI 实时推荐附近的低脂餐厅或早餐食谱。
* **商业价值:** 中高。适合切入运动装备、健康补剂的精准广告推送。
---
## 3. 【极简生活】脱手式晨间管家
* **适用场景:** 家务 / 洗漱 / 烹饪。
* **目标受众:** 精致独居青年、高效家庭主妇/主夫、追求晨间仪式感的人群。
* **核心逻辑:** 解决“洗漱时无法操作手机”的痛点,将电台转化为精准的时间刻度工具。
* **垂直内容点:**
* **分段洗漱播报:** 严格匹配洗漱流程(如:前 3 分钟刷牙播报天气与穿衣,后 5 分钟早餐播报今日重要日程)。
* **家务背景流:** 洗衣服或整理房间时,播报“极简主义生活贴士”或本地社区的民生快报(停水停电、生鲜折扣)。
* **语音交互助手:** 支持全语音指令(如:“下一条”、“详细讲讲这个生活妙招”)。
* **商业价值:** 中。适合切入生鲜电商导流、日用品订阅服务。
---
## 4. 【知识胶囊】备考与职场进阶自习室
* **适用场景:** 所有空闲场景(尤其是长途通勤)。
* **目标受众:** 学生、考公考研人群、语言学习者、程序员(架构师进阶)。
* **核心逻辑:** 将枯燥的知识体系碎片化,利用“听觉记忆”在碎片时间内完成复习。
* **垂直内容点:**
* **碎片复习流:** AI 将用户上传的教材或代码规范改写成“对话式小剧场”进行播报。
* **每日一词/一题:** 针对特定领域(如英语、Go 语言、算法)进行每日深度拆解。
* **备考情绪疏导:** 结合备考周期,定期推送减压语录或学霸经验分享。
* **商业价值:** 极高。知识付费、课程分销(CPS)及考试工具链的引流。
+173
View File
@@ -0,0 +1,173 @@
# 项目改造 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 生成内容"标识
+12
View File
@@ -0,0 +1,12 @@
package v1
import (
"sundynix-go/api/v1/system"
)
var ApiGroupApp = new(ApiGroup)
// ApiGroup 路由组
type ApiGroup struct {
SystemApiGroup system.ApiGroup
}
+185
View File
@@ -0,0 +1,185 @@
package system
import (
"sundynix-go/global"
"sundynix-go/model/commom/response"
"sundynix-go/model/system"
systemReq "sundynix-go/model/system/request"
systemRes "sundynix-go/model/system/response"
"sundynix-go/utils/auth"
"github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha"
"go.uber.org/zap"
)
var store = base64Captcha.DefaultMemStore
type AuthApi struct{}
// Login
// @Tags 登录相关
// @Summary pc登录
// @accept application/json
// @Produce application/json
// @Param data body systemReq.Login true "用户名, 密码, 验证码,验证码id"
// @Success 200 {object} response.Response{msg=string} "登录成功"
// @Router /auth/login [post]
func (a *AuthApi) Login(c *gin.Context) {
var l systemReq.Login
err := c.ShouldBindJSON(&l)
if err != nil {
response.FailWithMsg(err.Error(), c)
return
}
if l.CaptchaId != "" && l.Captcha != "" && store.Verify(l.CaptchaId, l.Captcha, true) {
u := &system.User{Account: l.Account, Password: l.Password}
user, err := userService.Login(u)
if err != nil {
global.Logger.Error("登录失败! 用户名不存在或者密码错误!", zap.Error(err))
response.FailWithMsg("用户名不存在或者密码错误", c)
return
}
a.GetToken(c, *user)
return
}
response.FailWithMsg("验证码错误", c)
}
// Logout
// @Tags 登录相关
// @Summary pc登出
// @Security ApiKeyAuth
// @Produce application/json
// @Success 200 {object} response.Response{msg=string} "登出成功"
// @Router /auth/logout [get]
func (a *AuthApi) Logout(c *gin.Context) {
token := auth.GetToken(c)
userId := auth.GetUserId(c)
err := jwtService.PutBlacklist(userId, token)
if err != nil {
global.Logger.Error("登出失败!", zap.Error(err))
response.FailWithMsg("登出失败", c)
return
}
auth.ClearToken(c)
response.OkWithMsg("登出成功", c)
}
// Captcha
// @Tags 登录相关
// @Summary 获取验证码
// @Produce application/json
// @Success 200 {object} response.Response{data=systemRes.CaptchaRes} "获取验证码"
// @Router /auth/captcha [get]
func (a *AuthApi) Captcha(c *gin.Context) {
var driver = base64Captcha.DriverString{
Height: 80,
Width: 240,
NoiseCount: 2,
ShowLineOptions: 4,
Length: 4,
Source: "1234567890",
}
cp := base64Captcha.NewCaptcha(&driver, store)
id, b64s, _, err := cp.Generate()
if err != nil {
global.Logger.Error("GenerateCaptcha err", zap.Error(err))
response.FailWithMsg("GenerateCaptcha err", c)
return
}
response.OkWithData(systemRes.CaptchaRes{
CaptchaId: id,
Captcha: b64s,
}, c)
}
func (a *AuthApi) GetToken(c *gin.Context, user system.User) {
token, claims, err := auth.GetLoginToken(&user)
if err != nil {
global.Logger.Error("GetToken err", zap.Error(err))
response.FailWithMsg("GetToken err", c)
}
response.OkWithData(systemRes.LoginResponse{
ExpiresAt: claims.RegisteredClaims.ExpiresAt.Unix() * 1000,
Token: token,
User: user,
}, c)
}
// MiniLogin
// @Tags 登录相关
// @Summary 小程序登录
// @Produce application/json
// @Param code query string true "code"
// @Success 200 {object} response.Response{data=systemRes.LoginResponse} "小程序登录"
// @Router /auth/miniLogin [get]
func (a *AuthApi) MiniLogin(c *gin.Context) {
jsCode := c.Query("code")
user, err := userService.MiniLogin(jsCode)
if err != nil {
global.Logger.Error("登录失败!", zap.Error(err))
response.FailWithMsg("登录失败", c)
return
}
a.GetToken(c, *user)
return
}
// GetPhone
// @Tags 登录相关
// @Summary 获取手机号
// @Produce application/json
// @Param code query string true "code"
// @Param openId query string true "openId"
// @Router /auth/getPhone [get]
func (a *AuthApi) GetPhone(c *gin.Context) {
jsCode := c.Query("code")
openId := c.Query("openId")
user, err := userService.LoginByPhone(jsCode, openId)
if err != nil {
global.Logger.Error("登录失败!", zap.Error(err))
response.FailWithMsg("登录失败", c)
return
}
response.OkWithData(user, c)
}
// GetLocation
// @Tags 登录相关
// @Summary 获取位置信息
// @Produce application/json
// @Param longitude query string true "longitude"
// @Param latitude query string true "latitude"
// @Router /auth/getLocation [get]
func (a *AuthApi) GetLocation(c *gin.Context) {
longitude := c.Query("longitude") //经度
latitude := c.Query("latitude")
location, err := userService.GetLocation(longitude, latitude)
if err != nil {
global.Logger.Error("获取位置信息失败!", zap.Error(err))
response.FailWithMsg("获取位置信息失败", c)
return
}
response.OkWithData(location, c)
}
// GetWeather
// @Tags 登录相关
// @Summary 获取天气信息
// @Produce application/json
// @Param adcode query string true "adcode"
// @Router /auth/getWeather [get]
func (a *AuthApi) GetWeather(c *gin.Context) {
value := c.Query("adcode")
weather, err := userService.GetWeather(value)
if err != nil {
global.Logger.Error("获取天气信息失败!", zap.Error(err))
response.FailWithMsg("获取天气信息失败", c)
return
}
response.OkWithData(weather, c)
}
+23
View File
@@ -0,0 +1,23 @@
package system
import "sundynix-go/service"
type ApiGroup struct {
AuthApi
UserApi
ClientApi
RoleApi
MenuApi
OperationRecordApi
OssApi
}
var (
jwtService = service.GroupApp.SystemServiceGroup.JwtService
userService = service.GroupApp.SystemServiceGroup.UserService
clientService = service.GroupApp.SystemServiceGroup.ClientService
roleService = service.GroupApp.SystemServiceGroup.RoleService
menuService = service.GroupApp.SystemServiceGroup.MenuService
operationRecordService = service.GroupApp.SystemServiceGroup.OperationRecordService
ossService = service.GroupApp.SystemServiceGroup.OssService
)
+116
View File
@@ -0,0 +1,116 @@
package system
import (
"sundynix-go/global"
"sundynix-go/model/commom/request"
"sundynix-go/model/commom/response"
"sundynix-go/model/system"
sysReq "sundynix-go/model/system/request"
sysResp "sundynix-go/model/system/response"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type OssApi struct {
}
// UploadFile
// @tags 文件相关
// @Summary 文件上传
// @Security ApiKeyAuth
// @accept multipart/form-data
// @Produce application/json
// @Param file formData file true "上传文件"
// @Success 200 {object} response.Response{msg=string} "上传文件"
// @router /oss/upload [post]
func (o *OssApi) UploadFile(c *gin.Context) {
var file system.Oss
multipartFile, header, err := c.Request.FormFile("file")
if err != nil {
global.Logger.Error("接收文件失败!", zap.Error(err))
response.FailWithMsg("接收文件失败!", c)
return
}
file, err = ossService.Upload(multipartFile, header) //上传完成后拿到文件信息
if err != nil {
global.Logger.Error("上传文件失败!", zap.Error(err))
response.FailWithMsg("上传文件失败!", c)
return
}
response.OkWithData(sysResp.UploadFileResponse{File: file}, c)
}
// DeleteFile
// @tags 文件相关
// @Summary 删除文件
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.IdsReq true "批量删除文件"
// @Success 200 {object} response.Response{msg=string} "删除文件"
// @router /oss/delete [post]
func (o *OssApi) DeleteFile(c *gin.Context) {
var ids request.IdsReq
err := c.ShouldBindJSON(&ids)
if err != nil {
response.FailWithMsg(err.Error(), c)
return
}
err = ossService.DeleteFileByIds(ids)
if err != nil {
global.Logger.Error("删除文件失败!", zap.Error(err))
response.FailWithMsg("删除文件失败!", c)
return
}
response.OkWithMsg("删除文件成功!", c)
}
// GetFileList
// @tags 文件相关
// @Summary 文件列表
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param data body sysReq.GetOssFileList true "文件列表"
// @Success 200 {object} response.Response{data=string} "文件列表"
// @router /oss/getFileList [post]
func (o *OssApi) GetFileList(c *gin.Context) {
var pageInfo sysReq.GetOssFileList
err := c.ShouldBindJSON(&pageInfo)
if err != nil {
response.FailWithMsg(err.Error(), c)
return
}
list, total, err := ossService.GetFileList(pageInfo)
if err != nil {
global.Logger.Error("获取文件列表失败!", zap.Error(err))
response.FailWithMsg("获取文件列表失败!", c)
return
}
response.OkWithData(response.PageResult{
List: list,
Total: total,
Page: pageInfo.Current,
PageSize: pageInfo.PageSize,
}, c)
}
// Detail
// @tags 文件相关
// @Summary 文件详情
// @Security ApiKeyAuth
// @Produce application/json
// @Param id query string true "文件id"
// @Success 200 {object} response.Response{data=string} "文件详情"
// @router /oss/detail [get]
func (o *OssApi) Detail(c *gin.Context) {
id := c.Query("id")
file, err := ossService.GetById(id)
if err != nil {
global.Logger.Error("获取文件详情失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithData(file, c)
}
+141
View File
@@ -0,0 +1,141 @@
package system
import (
"sundynix-go/global"
"sundynix-go/model/commom/request"
"sundynix-go/model/commom/response"
"sundynix-go/model/system"
systemReq "sundynix-go/model/system/request"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type ClientApi struct {
}
// SaveClient
// @Tags 客户端管理
// @Summary 创建client
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body system.Client true "client"
// @Success 200 {object} response.Response{msg=string} "创建client"
// @Router /client/save [post]
func (s *ClientApi) SaveClient(c *gin.Context) {
var client system.Client
err := c.ShouldBindJSON(&client)
if err != nil {
response.FailWithMsg(err.Error(), c)
return
}
err = clientService.SaveClient(client)
if err != nil {
global.Logger.Error("保存客户端失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithMsg("保存客户端成功!", c)
}
// UpdateClient
// @Tags 客户端管理
// @Summary 更新client
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body system.Client true "client"
// @Success 200 {object} response.Response{msg=string} "更新client"
// @Router /client/update [post]
func (s *ClientApi) UpdateClient(c *gin.Context) {
var client system.Client
err := c.ShouldBindJSON(&client)
if err != nil {
response.FailWithMsg(err.Error(), c)
return
}
err = clientService.UpdateClient(client)
if err != nil {
global.Logger.Error("更新客户端失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithMsg("更新客户端成功!", c)
}
// GetClientList
// @Tags 客户端管理
// @Summary 获取client列表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body systemReq.GetClientList true "client"
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取client列表"
// @Router /client/getClientList [post]
func (s *ClientApi) GetClientList(c *gin.Context) {
var pageInfo systemReq.GetClientList
err := c.ShouldBindJSON(&pageInfo)
if err != nil {
response.FailWithMsg(err.Error(), c)
return
}
list, total, err := clientService.GetClientList(pageInfo)
if err != nil {
global.Logger.Error("获取客户端列表失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithData(response.PageResult{
List: list,
Total: total,
Page: pageInfo.Current,
PageSize: pageInfo.PageSize,
}, c)
}
// Delete
// @Tags 客户端管理
// @Summary 删除client
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.IdsReq true "ids"
// @Success 200 {object} response.Response{msg=string} "删除client"
// @Router /client/delete [post]
func (s *ClientApi) Delete(c *gin.Context) {
var ids request.IdsReq
err := c.ShouldBindJSON(&ids)
if err != nil {
response.FailWithMsg(err.Error(), c)
return
}
err = clientService.DeleteClientByIds(ids)
if err != nil {
global.Logger.Error("删除客户端失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithMsg("删除客户端成功!", c)
}
// Detail
// @Tags 客户端管理
// @Summary 获取client详情
// @Description id获取详情
// @Security ApiKeyAuth
// @Produce application/json
// @Param id query string true "id"
// @Success 200 {object} response.Response{data=system.Client,msg=string} "获取client详情"
// @Router /client/detail [get]
func (s *ClientApi) Detail(c *gin.Context) {
id := c.Query("id")
client, err := clientService.GetClientById(id)
if err != nil {
global.Logger.Error("获取客户端详情失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithData(client, c)
}
+166
View File
@@ -0,0 +1,166 @@
package system
import (
"sundynix-go/global"
"sundynix-go/model/commom/response"
"sundynix-go/model/system"
systemReq "sundynix-go/model/system/request"
"sundynix-go/utils/auth"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type MenuApi struct {
}
// SaveMenu
// @Tags 菜单管理
// @Summary 新增菜单
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body system.Menu false "menu"
// @Success 200 {object} response.Response{msg=string} "新建菜单/按钮"
// @Router /menu/save [post]
func (m *MenuApi) SaveMenu(c *gin.Context) {
var menu system.Menu
err := c.ShouldBindJSON(&menu)
if err != nil {
response.FailWithMsg("参数错误:"+err.Error(), c)
return
}
if err = menuService.SaveMenu(menu); err != nil {
global.Logger.Error("保存菜单失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
} else {
response.OkWithMsg("保存菜单成功!", c)
}
}
// UpdateMenu
// @Tags 菜单管理
// @Summary 更新菜单
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body system.Menu false "menu"
// @Success 200 {object} response.Response{msg=string} "更新菜单"
// @Router /menu/update [post]
func (m *MenuApi) UpdateMenu(c *gin.Context) {
var menu system.Menu
err := c.ShouldBindJSON(&menu)
if err != nil {
response.FailWithMsg("参数错误:"+err.Error(), c)
return
}
if err = menuService.UpdateMenu(&menu); err != nil {
global.Logger.Error("更新菜单失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
} else {
response.OkWithMsg("更新菜单成功!", c)
}
}
// DeleteMenu
// @Tags 菜单管理
// @Summary 删除menu
// @Description 删除menu
// @Security ApiKeyAuth
// @Produce application/json
// @Param id query string true "id"
// @Success 200 {object} response.Response{msg=string} "详情"
// @Router /menu/delete [get]
func (m *MenuApi) DeleteMenu(c *gin.Context) {
id := c.Query("id")
err := menuService.DeleteMenu(id)
if err != nil {
global.Logger.Error("删除菜单失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithMsg("删除菜单成功!", c)
}
// Detail
// @Tags 菜单管理
// @Summary 获取menu详情
// @Description id获取详情
// @Security ApiKeyAuth
// @Produce application/json
// @Param id query string true "id"
// @Success 200 {object} response.Response{data=system.Menu,msg=string} "详情"
// @Router /menu/detail [get]
func (m *MenuApi) Detail(c *gin.Context) {
id := c.Query("id")
menu, err := menuService.GetMenuById(id)
if err != nil {
global.Logger.Error("获取菜单详情失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithData(menu, c)
}
// GetAllMenuTree
// @Tags 菜单管理
// @Summary 获取所有菜单树
// @Security ApiKeyAuth
// @Accept json
// @Produce json
// @Param data body systemReq.GetMenuTree true "菜单信息"
// @Success 200 {object} response.Response{data=[]system.Menu,msg=string} "获取所有菜单树"
// @Router /menu/getAllMenuTree [post]
func (m *MenuApi) GetAllMenuTree(c *gin.Context) {
var param systemReq.GetMenuTree
err := c.ShouldBindJSON(&param)
if err != nil {
response.FailWithMsg(err.Error(), c)
return
}
menus, err := menuService.GetAllMenuTree(param.Category, param.ParentId)
if err != nil {
global.Logger.Error("获取菜单树结构失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithData(menus, c)
}
// GetUserMenuTree
// @Tags 菜单管理
// @Summary 用户菜单数据
// @Security ApiKeyAuth
// @Produce json
// @Success 200 {object} response.Response{data=[]system.Menu,msg=string} "用户菜单数据"
// @Router /menu/getUserMenuTree [get]
func (m *MenuApi) GetUserMenuTree(c *gin.Context) {
userId := auth.GetUserId(c)
routes, err := menuService.GetUserRoutes(userId)
if err != nil {
global.Logger.Error("获取用户菜单失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithData(routes, c)
}
// Route
// @Tags 菜单管理
// @Summary 用户路由
// @Security ApiKeyAuth
// @Produce json
// @Success 200 {object} response.Response{data=[]system.Menu,msg=string} "用户route"
// @Router /menu/route [get]
func (m *MenuApi) Route(c *gin.Context) {
userId := auth.GetUserId(c)
routes, err := menuService.GetUserRoutes(userId)
if err != nil {
global.Logger.Error("获取用户菜单失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithData(routes, c)
}
+79
View File
@@ -0,0 +1,79 @@
package system
import (
"sundynix-go/global"
"sundynix-go/model/commom/request"
"sundynix-go/model/commom/response"
"sundynix-go/model/system"
systemReq "sundynix-go/model/system/request"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type OperationRecordApi struct {
}
func (s *OperationRecordApi) CreateOperationRecord(c *gin.Context) {
var sysOperationRecord system.SysOperationRecord
err := c.ShouldBindJSON(&sysOperationRecord)
if err != nil {
response.FailWithMsg(err.Error(), c)
return
}
err = operationRecordService.CreateOperationRecord(sysOperationRecord)
if err != nil {
global.Logger.Error("创建操作记录失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithMsg("创建操作记录成功!", c)
}
func (s *OperationRecordApi) GetRecordList(c *gin.Context) {
var pageInfo systemReq.GetOperationRecordList
err := c.ShouldBindJSON(&pageInfo)
if err != nil {
response.FailWithMsg(err.Error(), c)
return
}
list, total, err := operationRecordService.GetRecordList(pageInfo)
if err != nil {
global.Logger.Error("获取操作记录列表失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithData(response.PageResult{
List: list,
Total: total,
Page: pageInfo.Current,
PageSize: pageInfo.PageSize,
}, c)
}
func (s *OperationRecordApi) GetRecordById(c *gin.Context) {
id := c.Query("id")
record, err := operationRecordService.GetRecordById(id)
if err != nil {
global.Logger.Error("获取操作记录详情失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithData(record, c)
}
func (s *OperationRecordApi) DeleteRecordsByIds(c *gin.Context) {
var ids request.IdsReq
err := c.ShouldBindJSON(&ids)
if err != nil {
response.FailWithMsg(err.Error(), c)
return
}
err = operationRecordService.DeleteRecordsByIds(ids)
if err != nil {
global.Logger.Error("删除操作记录失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithMsg("删除操作记录成功!", c)
}
+163
View File
@@ -0,0 +1,163 @@
package system
import (
"sundynix-go/global"
"sundynix-go/model/commom/request"
"sundynix-go/model/commom/response"
"sundynix-go/model/system"
systemreq "sundynix-go/model/system/request"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type RoleApi struct {
}
// SaveRole
// @tags 角色管理
// @Summary 创建角色
// @Security ApiKeyAuth
// @accept json
// @Produce json
// @Param data body system.Role true "角色信息"
// @Success 200 {object} response.Response
// @Router /role/save [post]
func (a *RoleApi) SaveRole(context *gin.Context) {
var role system.Role
err := context.ShouldBindJSON(&role)
if err != nil {
response.FailWithMsg("参数错误"+err.Error(), context)
return
}
err = roleService.SaveRole(role)
if err != nil {
global.Logger.Error("保存角色失败!", zap.Error(err))
response.FailWithMsg(err.Error(), context)
return
}
response.OkWithMsg("保存角色成功!", context)
}
// UpdateRole
// @tags 角色管理
// @Summary 修改角色
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body system.Role true "角色ID"
// @Success 200 {object} response.Response"
// @Router /role/update [post]
func (a *RoleApi) UpdateRole(context *gin.Context) {
var role system.Role
err := context.ShouldBindJSON(&role)
if err != nil {
response.FailWithMsg(err.Error(), context)
return
}
err = roleService.UpdateRole(role)
if err != nil {
global.Logger.Error("更新角色失败!", zap.Error(err))
response.FailWithMsg(err.Error(), context)
return
}
response.OkWithMsg("更新角色成功!", context)
}
// GetRoleList
// @tags 角色管理
// @Summary 获取角色列表
// @Description 获取角色列表
// @Accept application/json
// @Produce application/json
// @Param data body systemreq.GetRoleList true "页码, 每页大小, 搜索条件"
// @success 200 {object} response.Response{data=response.PageResult,msg=string} "获取角色列表,返回包括列表,总数,页码,每页大小"
// @Router /role/getRoleList [post]
func (a *RoleApi) GetRoleList(c *gin.Context) {
var pageInfo systemreq.GetRoleList
err := c.ShouldBindJSON(&pageInfo)
if err != nil {
response.FailWithMsg(err.Error(), c)
return
}
list, total, err := roleService.GetRoleList(pageInfo)
if err != nil {
global.Logger.Error("获取角色列表失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
}
response.OkWithData(response.PageResult{
List: list,
Total: total,
Page: pageInfo.Current,
PageSize: pageInfo.PageSize,
}, c)
}
// Delete
// @Tags 角色管理
// @Summary 删除角色
// @Description 删除角色
// @Accept application/json
// @Produce application/json
// @Param data body request.IdsReq true "批量删除角色"
// @Success 200 {object} response.Response{msg=string} "删除角色"
// @Router /role/delete [post]
func (a *RoleApi) Delete(context *gin.Context) {
var ids request.IdsReq
err := context.ShouldBindJSON(&ids)
if err != nil {
response.FailWithMsg(err.Error(), context)
return
}
err = roleService.DeleteRoleByIds(ids)
if err != nil {
global.Logger.Error("删除角色失败!", zap.Error(err))
response.FailWithMsg(err.Error(), context)
return
}
response.OkWithMsg("删除角色成功!", context)
}
// Detail
// @Tags 角色管理
// @Summary 角色详情
// @Security ApiKeyAuth
// @Produce application/json
// @Param id query string true "id"
// @Success 200 {object} response.Response{data=system.Role} "角色详情"
// @Router /role/detail [get]
func (a *RoleApi) Detail(context *gin.Context) {
id := context.Query("id")
role, err := roleService.GetRoleById(id)
if err != nil {
global.Logger.Error("获取角色详情失败!", zap.Error(err))
response.FailWithMsg(err.Error(), context)
return
}
response.OkWithData(role, context)
}
// GrantMenu
// @tags 角色管理
// @Summary 授权菜单给角色
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body systemreq.GrantMenu true "授权菜单给角色"
// @success 200 {object} response.Response "授权菜单给角色"
// @Router /role/grantMenu [post]
func (a *RoleApi) GrantMenu(c *gin.Context) {
var grantMenu systemreq.GrantMenu
err := c.ShouldBindJSON(&grantMenu)
if err != nil {
response.FailWithMsg(err.Error(), c)
return
}
err = roleService.GrantMenu(grantMenu.RoleId, grantMenu.MenuIds)
if err != nil {
global.Logger.Error("授权菜单失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithMsg("授权菜单成功!", c)
}
+207
View File
@@ -0,0 +1,207 @@
package system
import (
"sundynix-go/global"
"sundynix-go/model/commom/request"
"sundynix-go/model/commom/response"
"sundynix-go/model/system"
systemReq "sundynix-go/model/system/request"
"sundynix-go/utils/auth"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type UserApi struct {
}
// CurrentUser
// @tags 用户管理
// @Summary 当前登录用户
// @Security ApiKeyAuth
// @Produce json
// @Success 200 {object} response.Response "{"code": 200, "data": {}, "msg": "添加成功"}"
// @Router /user/info [get]
func (u *UserApi) CurrentUser(c *gin.Context) {
userId := auth.GetUserId(c)
user, err := userService.GetUserById(userId)
if err != nil {
global.Logger.Error("获取用户信息失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithData(user, c)
}
// SaveUser
// @tags 用户管理
// @Summary 新增用户
// @Security ApiKeyAuth
// @accept json
// @Produce json
// @Param data body system.User true "用户信息"
// @Success 200 {object} response.Response "{"code": 200, "data": {}, "msg": "添加成功"}"
// @Router /user/save [post]
func (u *UserApi) SaveUser(c *gin.Context) {
var user system.User
err := c.ShouldBindJSON(&user)
if err != nil {
response.FailWithMsg("参数错误:"+err.Error(), c)
return
}
if err = userService.SaveUser(user); err != nil {
global.Logger.Error("保存用户失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
} else {
response.OkWithMsg("保存用户成功!", c)
}
}
// UpdateUser
// @tags 用户管理
// @Summary 更新用户
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body system.User true "用户ID,用户信息"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /user/update [post]
func (u *UserApi) UpdateUser(c *gin.Context) {
var user system.User
err := c.ShouldBindJSON(&user)
if err != nil {
response.FailWithMsg("参数错误:"+err.Error(), c)
return
}
if err = userService.UpdateUser(&user); err != nil {
global.Logger.Error("更新用户失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
} else {
response.OkWithMsg("更新用户成功!", c)
}
}
// GetUserList
// @tags 用户管理
// @Summary 获取用户列表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body systemReq.GetUserList true "页码, 每页大小, 搜索条件"
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取用户列表,返回包括列表,总数,页码,每页大小"
// @Router /user/getUserList [post]
func (u *UserApi) GetUserList(c *gin.Context) {
var pageInfo systemReq.GetUserList
err := c.ShouldBindJSON(&pageInfo)
if err != nil {
response.FailWithMsg(err.Error(), c)
return
}
list, total, err := userService.GetUserList(pageInfo)
if err != nil {
global.Logger.Error("获取用户列表失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithData(response.PageResult{
List: list,
Total: total,
Page: pageInfo.Current,
PageSize: pageInfo.PageSize,
}, c)
}
// Delete
// @Tags 用户管理
// @Summary 删除用户
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.IdsReq true "批量删除用户"
// @Success 200 {object} response.Response{msg=string} "删除用户"
// @Router /user/delete [post]
func (u *UserApi) Delete(c *gin.Context) {
var ids request.IdsReq
err := c.ShouldBindJSON(&ids)
if err != nil {
response.FailWithMsg(err.Error(), c)
return
}
err = userService.DeleteUserByIds(ids)
if err != nil {
global.Logger.Error("删除用户失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithMsg("删除用户成功!", c)
}
// Detail
// @Tags 用户管理
// @Summary 获取用户详情
// @Security ApiKeyAuth
// @Produce application/json
// @Param id query string true "id"
// @Success 200 {object} response.Response{data=system.User} "获取用户详情成功"
// @Router /user/detail [get]
func (u *UserApi) Detail(c *gin.Context) {
id := c.Query("id")
user, err := userService.GetUserById(id)
if err != nil {
global.Logger.Error("获取用户详情失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithData(user, c)
}
// ChangePassword
// @Tags 用户管理
// @Summary 修改密码
// @Security ApiKeyAuth
// @Description 修改密码
// @accept json
// @Produce application/json
// @Param data body request.ChangePwd true "用户id"
// @Success 200 {object} response.Response{data=system.User} "修改密码成功"
// @Router /user/changePassword [post]
func (u *UserApi) ChangePassword(c *gin.Context) {
var changePwd systemReq.ChangePwd
err := c.ShouldBindJSON(&changePwd)
if err != nil {
response.FailWithMsg(err.Error(), c)
return
}
err = userService.ChangePassword(changePwd.Id, changePwd.NewPwd)
if err != nil {
global.Logger.Error("修改密码失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithMsg("修改密码成功", c)
}
// GrantRole
// @Tags 用户管理
// @Summary 给用户分配角色
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body systemReq.GrantRole true "用户ID, 角色ID"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /user/grantRole [post]
func (u *UserApi) GrantRole(c *gin.Context) {
var grantRole systemReq.GrantRole
err := c.ShouldBindJSON(&grantRole)
if err != nil {
response.FailWithMsg(err.Error(), c)
return
}
err = roleService.GrantRole(grantRole.UserId, grantRole.RoleIds)
if err != nil {
global.Logger.Error("授权角色失败!", zap.Error(err))
response.FailWithMsg(err.Error(), c)
return
}
response.OkWithMsg("授权角色成功!", c)
}
+90
View File
@@ -0,0 +1,90 @@
system:
addr: 8889
db-type: mysql
router-prefix: ""
enable-captcha: 0
oss-type: minio
# oss-type: tencent-cos
jwt:
buffer-time: 2h
expires-time: 2h
issuer: sundynix
signing-key: 9149f2eb-d517-4a50-a03a-231dbcf0d872
# 早安电台微信小程序
mini-program:
app-id: wxb463820bf36dd5d6
app-secret: 731784a74c76c6d31fa00bb847af2c7d
# 服务号
service-account:
app-id: wxc236cddde8e7f863
app-secret: 26c1fcecfc98a748d8916355623c975c
# 微信支付
wechat-pay:
mch-id: 1735188493 # 商户号
mch-certificate-serial-number: 3725BFCA9CA3AF819AEC5D0CB7D3540BBC67F2CF # 商户证书序列号
public-key-id: PUB_KEY_ID_0117351884932025120900181833003602 # 商户APIv3密钥对应的公钥
mch-api-v3-key: a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6 # 商户APIv3密钥
private-key-path: /Users/blizzard/privateFolder/cert/apiclient_key.pem # 商户APIv3密钥对应的私钥
public-key-path: /Users/blizzard/privateFolder/cert/pub_key.pem # 商户APIv3密钥对应的公钥
notify-url: https://prod.sundynix.cn/api/wechatpay/notify # 微信支付结果通知回调地址
# MinIO 对象存储 (音频存储)
minio:
access-key-id: qP5QXP3g6Axw1hkwX21Y
access-key-secret: sddT6J3S6yDn9m1wfth0pzelPg9KWmbHjMAUF5S9
base-path: ""
bucket-name: sundynix-audios
bucket-url: https://res.sundynix.cn/sundynix-audios
endpoint: 129.28.103.17:3407
use-ssl: false
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
access-key: ""
secret-key: ""
enable-ssl: false
log-enabled: false
mysql:
config: charset=utf8mb4&parseTime=True&loc=Local
db-name: sundynix_radio
engine: ""
log-mode: error
log-zap: true
max-idle-conns: 10
max-open-conns: 100
host: 129.28.103.17
port: "3413"
prefix: "sundynix_"
singular: true
user: root
password: root
redis:
addr: 127.0.0.1:6379
clusteraddrs:
- 172.21.0.3:7000
- 172.21.0.4:7001
- 172.21.0.2:7002
db: 1
# name: ""
# password: "sundynix"
cluster: false
zap:
director: log
encode-level: LowercaseColorLevelEncoder
format: console
level: debug
log-in-console: true
prefix: '[sundynix-radio-server]'
retention-day: 5
show-line: true
stacktrace-key: stacktrace
+80
View File
@@ -0,0 +1,80 @@
system:
addr: 8889
db-type: mysql
router-prefix: ""
enable-captcha: 0
oss-type: minio
# oss-type: tencent-cos
jwt:
buffer-time: 2h
expires-time: 2h
issuer: sundynix
signing-key: 9149f2eb-d517-4a50-a03a-231dbcf0d872
# 植趣微信小程序
mini-program:
app-id: wxb463820bf36dd5d6
app-secret: 731784a74c76c6d31fa00bb847af2c7d
# 植趣服务号
service-account:
app-id: wxc236cddde8e7f863
app-secret: 26c1fcecfc98a748d8916355623c975c
minio:
access-key-id: qP5QXP3g6Axw1hkwX21Y
access-key-secret: sddT6J3S6yDn9m1wfth0pzelPg9KWmbHjMAUF5S9
base-path: ""
bucket-name: sundynix-plant
bucket-url: https://res.sundynix.cn/sundynix-plant
endpoint: 129.28.103.17:3407
use-ssl: false
rocket-mq:
name-space: 192.168.100.140:9876
endpoint: 192.168.100.140:8081 # 5.x版本使用了proxy 默认proxy地址是8081
consumer-group: sundynix-plant
topic: sundynix-plant-generate-library
access-key: ""
secret-key: ""
enable-ssl: false
log-enabled: false
mysql:
config: charset=utf8mb4&parseTime=True&loc=Local
db-name: sundynix_plant
engine: ""
log-mode: error
log-zap: true
max-idle-conns: 10
max-open-conns: 100
host: 192.168.100.127
port: "3306"
prefix: "sundynix_"
singular: true
user: root
password: root
redis:
addr: 192.168.100.127:6379
clusteraddrs:
- 172.21.0.3:7000
- 172.21.0.4:7001
- 172.21.0.2:7002
db: 5
name: ""
password: ""
cluster: false
zap:
director: log
encode-level: LowercaseColorLevelEncoder
format: console
level: debug
log-in-console: true
prefix: '[sundynix-go]'
retention-day: 5
show-line: true
stacktrace-key: stacktrace
+19
View File
@@ -0,0 +1,19 @@
package config
type Config struct {
JWT JWT `mapstructure:"jwt" json:"jwt" yaml:"jwt"`
System System `mapstructure:"system" json:"system" yaml:"system"`
Mysql Mysql `mapstructure:"mysql" json:"mysql" yaml:"mysql"`
Pgsql Pgsql `mapstructure:"pgsql" json:"pgsql" yaml:"pgsql"`
Sqlite Sqlite `mapstructure:"sqlite" json:"sqlite" yaml:"sqlite"`
Redis Redis `mapstructure:"redis" json:"redis" yaml:"redis"`
Zap Zap `mapstructure:"zap" json:"zap" yaml:"zap"`
Minio Minio `mapstructure:"minio" json:"minio" yaml:"minio"`
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"` //微信支付
}
+8
View File
@@ -0,0 +1,8 @@
package config
type DB struct {
Host string `mapstructure:"host" json:"host" yaml:"host"`
Port int `mapstructure:"port" json:"port" yaml:"port"`
User string `mapstructure:"user" json:"user" yaml:"user"`
Password string `mapstructure:"password" json:"password" yaml:"password"`
}
+8
View File
@@ -0,0 +1,8 @@
package config
type JWT struct {
SigningKey string `mapstructure:"signing-key" json:"signing-key" yaml:"signing-key"` // jwt签名
ExpiresTime string `mapstructure:"expires-time" json:"expires-time" yaml:"expires-time"` // 过期时间
BufferTime string `mapstructure:"buffer-time" json:"buffer-time" yaml:"buffer-time"` // 缓冲时间
Issuer string `mapstructure:"issuer" json:"issuer" yaml:"issuer"` // 签发者
}
+10
View File
@@ -0,0 +1,10 @@
package config
type Redis struct {
Name string `mapstructure:"name" json:"name" yaml:"name"` // 代表当前实例的名字
Addr string `mapstructure:"addr" json:"addr" yaml:"addr"` // 服务器地址:端口
Password string `mapstructure:"password" json:"password" yaml:"password"` // 密码
DB int `mapstructure:"db" json:"db" yaml:"db"` // 单实例模式下redis的哪个数据库
Cluster bool `mapstructure:"cluster" json:"cluster" yaml:"cluster"` // 是否使用集群模式
ClusterAddrs []string `mapstructure:"clusterAddrs" json:"clusterAddrs" yaml:"clusterAddrs"` // 集群模式下的节点地址列表
}
+22
View File
@@ -0,0 +1,22 @@
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 // 钻石
)
+52
View File
@@ -0,0 +1,52 @@
package config
import (
"gorm.io/gorm/logger"
"strings"
)
type DsnProvider interface {
Dsn() string
}
// Embeded 结构体可以压平到上一层,从而保持 config 文件的结构和原来一样
// 见 playground: https://go.dev/play/p/KIcuhqEoxmY
// GeneralDB 也被 Pgsql 和 Mysql 原样使用
type GeneralDB struct {
Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"` // 数据库前缀
Port string `mapstructure:"port" json:"port" yaml:"port"` // 数据库端口
Config string `mapstructure:"config" json:"config" yaml:"config"` // 高级配置
Dbname string `mapstructure:"db-name" json:"db-name" yaml:"db-name"` // 数据库名
User string `mapstructure:"user" json:"user" yaml:"user"` // 数据库账号
Password string `mapstructure:"password" json:"password" yaml:"password"` // 数据库密码
Host string `mapstructure:"host" json:"host" yaml:"host"` // 数据库地址
Engine string `mapstructure:"engine" json:"engine" yaml:"engine" default:"InnoDB"` // 数据库引擎,默认InnoDB
LogMode string `mapstructure:"log-mode" json:"log-mode" yaml:"log-mode"` // 是否开启Gorm全局日志
MaxIdleConns int `mapstructure:"max-idle-conns" json:"max-idle-conns" yaml:"max-idle-conns"` // 空闲中的最大连接数
MaxOpenConns int `mapstructure:"max-open-conns" json:"max-open-conns" yaml:"max-open-conns"` // 打开到数据库的最大连接数
Singular bool `mapstructure:"singular" json:"singular" yaml:"singular"` // 是否开启全局禁用复数,true表示开启
LogZap bool `mapstructure:"log-zap" json:"log-zap" yaml:"log-zap"` // 是否通过zap写入日志文件
}
func (c GeneralDB) LogLevel() logger.LogLevel {
switch strings.ToLower(c.LogMode) {
case "silent", "Silent":
return logger.Silent
case "error", "Error":
return logger.Error
case "warn", "Warn":
return logger.Warn
case "info", "Info":
return logger.Info
default:
return logger.Info
}
}
type SpecializedDB struct {
Type string `mapstructure:"type" json:"type" yaml:"type"`
AliasName string `mapstructure:"alias-name" json:"alias-name" yaml:"alias-name"`
GeneralDB `yaml:",inline" mapstructure:",squash"`
Disable bool `mapstructure:"disable" json:"disable" yaml:"disable"`
}
+9
View File
@@ -0,0 +1,9 @@
package config
type Mysql struct {
GeneralDB `yaml:",inline" mapstructure:",squash"`
}
func (m *Mysql) Dsn() string {
return m.User + ":" + m.Password + "@tcp(" + m.Host + ":" + m.Port + ")/" + m.Dbname + "?" + m.Config
}
+17
View File
@@ -0,0 +1,17 @@
package config
type Pgsql struct {
GeneralDB `yaml:",inline" mapstructure:",squash"`
}
// Dsn 基于配置文件获取 dsn
// Author [SliverHorn](https://github.com/SliverHorn)
func (p *Pgsql) Dsn() string {
return "host=" + p.Host + " user=" + p.User + " password=" + p.Password + " dbname=" + p.Dbname + " port=" + p.Port + " " + p.Config
}
// LinkDsn 根据 dbname 生成 dsn
// Author [SliverHorn](https://github.com/SliverHorn)
func (p *Pgsql) LinkDsn(dbname string) string {
return "host=" + p.Host + " user=" + p.User + " password=" + p.Password + " dbname=" + dbname + " port=" + p.Port + " " + p.Config
}
+13
View File
@@ -0,0 +1,13 @@
package config
import (
"path/filepath"
)
type Sqlite struct {
GeneralDB `yaml:",inline" mapstructure:",squash"`
}
func (s *Sqlite) Dsn() string {
return filepath.Join(s.Host, s.Dbname+".db")
}
+6
View File
@@ -0,0 +1,6 @@
package config
type MiniProgram struct {
AppId string `mapstructure:"app-id" json:"app-id" yaml:"app-id"`
AppSecret string `mapstructure:"app-secret" json:"app-secret" yaml:"app-secret"`
}
+11
View File
@@ -0,0 +1,11 @@
package config
type Minio struct {
Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint"`
AccessKeyId string `mapstructure:"access-key-id" json:"access-key-id" yaml:"access-key-id"`
AccessKeySecret string `mapstructure:"access-key-secret" json:"access-key-secret" yaml:"access-key-secret"`
BucketName string `mapstructure:"bucket-name" json:"bucket-name" yaml:"bucket-name"`
UseSSL bool `mapstructure:"use-ssl" json:"use-ssl" yaml:"use-ssl"`
BasePath string `mapstructure:"base-path" json:"base-path" yaml:"base-path"`
BucketUrl string `mapstructure:"bucket-url" json:"bucket-url" yaml:"bucket-url"`
}
+10
View File
@@ -0,0 +1,10 @@
package config
type TencentCOS struct {
Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"`
Region string `mapstructure:"region" json:"region" yaml:"region"`
SecretID string `mapstructure:"secret-id" json:"secret-id" yaml:"secret-id"`
SecretKey string `mapstructure:"secret-key" json:"secret-key" yaml:"secret-key"`
BaseURL string `mapstructure:"base-url" json:"base-url" yaml:"base-url"`
PathPrefix string `mapstructure:"path-prefix" json:"path-prefix" yaml:"path-prefix"`
}
+12
View File
@@ -0,0 +1,12 @@
package config
type RocketMQConfig struct {
NameSpace string `mapstructure:"name-space" json:"nameSpace" yaml:"name-space"`
Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint"`
ConsumerGroup string `mapstructure:"consumer-group" json:"consumerGroup" yaml:"consumer-group"`
AccessKey string `mapstructure:"access-key" json:"accessKey" yaml:"access-key"`
SecretKey string `mapstructure:"secret-key" json:"secretKey" yaml:"secret-key"`
Topic string `mapstructure:"topic" json:"topic" yaml:"topic"`
EnableSSL bool `mapstructure:"enable-ssl" json:"enableSSL" yaml:"enable-ssl"`
LogEnabled bool `mapstructure:"log-enabled" json:"logEnabled" yaml:"log-enabled"`
}
+6
View File
@@ -0,0 +1,6 @@
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"`
}
+9
View File
@@ -0,0 +1,9 @@
package config
type System struct {
Addr int `mapstructure:"addr" json:"addr" yaml:"addr"`
DbType string `mapstructure:"db-type" json:"db-type" yaml:"db-type"`
RouterPrefix string `mapstructure:"router-prefix" json:"router-prefix" yaml:"router-prefix"`
EnableCaptcha int `mapstructure:"enable-captcha" json:"enable-captcha" yaml:"enable-captcha"`
OssType string `mapstructure:"oss-type" json:"oss-type" yaml:"oss-type"`
}
+12
View File
@@ -0,0 +1,12 @@
package config
// WechatPay 微信支付
type WechatPay struct {
MchId string `mapstructure:"mch-id" json:"mch-id" yaml:"mch-id"`
PublicKeyId string `mapstructure:"public-key-id" json:"public-key-id" yaml:"public-key-id"`
MchCertificateSerialNumber string `mapstructure:"mch-certificate-serial-number" json:"mch-certificate-serial-number" yaml:"mch-certificate-serial-number"`
MchAPIv3Key string `mapstructure:"mch-api-v3-key" json:"mch-api-v3-key" yaml:"mch-api-v3-key"`
PrivateKeyPath string `mapstructure:"private-key-path" json:"private-key-path" yaml:"private-key-path"`
PublicKeyPath string `mapstructure:"public-key-path" json:"public-key-path" yaml:"public-key-path"`
NotifyUrl string `mapstructure:"notify-url" json:"notify-url" yaml:"notify-url"`
}
+81
View File
@@ -0,0 +1,81 @@
package config
import (
"go.uber.org/zap/zapcore"
"time"
)
type Zap struct {
Level string `mapstructure:"level" json:"level" yaml:"level"`
Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"`
Format string `mapstructure:"format" json:"format" yaml:"format"`
Director string `mapstructure:"director" json:"director" yaml:"director"`
EncodeLevel string `mapstructure:"encode-level" json:"encode-level" yaml:"encode-level"`
StacktraceKey string `mapstructure:"stacktrace-key" json:"stacktrace-key" yaml:"stacktrace-key"`
ShowLine bool `mapstructure:"show-line" json:"show-line" yaml:"show-line"`
LogInConsole bool `mapstructure:"log-in-console" json:"log-in-console" yaml:"log-in-console"`
RetentionDay int `mapstructure:"retention-day" json:"retention-day" yaml:"retention-day"`
}
// Levels 返回一个基于 Zap 实例中配置的日志级别的 zapcore.Level 切片。
// 该切片从配置的日志级别开始,包含所有更高严重性级别,直到 FatalLevel。
// 如果无法解析配置的日志级别,则默认使用 DebugLevel。
//
// 返回值:
// - []zapcore.Level: 包含从配置的日志级别(或解析失败时的 DebugLevel)到 FatalLevel 的所有日志级别的切片。
func (c *Zap) Levels() []zapcore.Level {
// 初始化一个容量为 7 的空切片,用于存储日志级别。
levels := make([]zapcore.Level, 0, 7)
// 解析配置的日志级别。如果解析失败,则默认使用 DebugLevel。
level, err := zapcore.ParseLevel(c.Level)
if err != nil {
level = zapcore.DebugLevel
}
// 从解析的(或默认的)日志级别开始,迭代到 FatalLevel,并将每个级别追加到切片中 按照日志级别分片存储
for ; level <= zapcore.FatalLevel; level++ {
levels = append(levels, level)
}
// 返回填充好的日志级别切片
return levels
}
// Encoder 返回一个 zapcore.Encoder,用于编码日志记录。
func (c *Zap) Encoder() zapcore.Encoder {
config := zapcore.EncoderConfig{
TimeKey: "time",
NameKey: "name",
LevelKey: "level",
CallerKey: "caller",
MessageKey: "message",
StacktraceKey: c.StacktraceKey,
LineEnding: zapcore.DefaultLineEnding,
EncodeTime: func(t time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(c.Prefix + t.Format("2006-01-02 15:04:05"))
},
EncodeLevel: c.LevelEncoder(),
EncodeCaller: zapcore.FullCallerEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
}
if c.Format == "json" {
return zapcore.NewJSONEncoder(config)
}
return zapcore.NewConsoleEncoder(config)
}
func (c *Zap) LevelEncoder() zapcore.LevelEncoder {
switch {
case c.EncodeLevel == "LowercaseLevelEncoder":
return zapcore.LowercaseLevelEncoder
case c.EncodeLevel == "LowercaseColorLevelEncoder":
return zapcore.LowercaseColorLevelEncoder
case c.EncodeLevel == "CapitalLevelEncoder":
return zapcore.CapitalLevelEncoder
case c.EncodeLevel == "CapitalColorLevelEncoder":
return zapcore.CapitalColorLevelEncoder
default:
return zapcore.LowercaseLevelEncoder
}
}
+8
View File
@@ -0,0 +1,8 @@
package internal
const (
ConfigDefaultFile = "config-dev.yaml"
ConfigProdFile = "config-prod.yaml"
ConfigDebugFile = "config-debug.yaml"
ConfigReleaseFile = "config-release.yaml"
)
+161
View File
@@ -0,0 +1,161 @@
package internal
import (
"os"
"path/filepath"
"sync"
"time"
)
type Cutter struct {
level string // 日志级别
layout string //时间格式
formats []string //自定义参数
director string //日志文件夹
retentionDay int //保留天数
file *os.File //文件
mutex *sync.RWMutex // 读写锁
}
type CutterOption func(c *Cutter)
// 设置时间格式
func CutterWithLayout(layout string) CutterOption {
return func(c *Cutter) {
c.layout = layout
}
}
// 格式化参数
func CutterWithFormats(format ...string) CutterOption {
return func(c *Cutter) {
if len(format) > 0 {
c.formats = format
}
}
}
// NewCutter 创建一个新的 Cutter 实例,用于管理日志文件的切割和保留。
//
// 参数:
// - directory: 日志文件存储的目录路径。
// - level: 日志级别,用于标识日志的严重程度。
// - retentionDay: 日志文件保留的天数,超过该天数的日志文件将被删除。
// - options: 可选的 CutterOption 函数,用于对 Cutter 实例进行额外的配置。
//
// 返回值:
// - *Cutter: 返回一个初始化后的 Cutter 实例。
func NewCutter(directory string, level string, retentionDay int, options ...CutterOption) *Cutter {
// 初始化 Cutter 实例,设置日志级别、目录、保留天数以及互斥锁
rotate := &Cutter{
level: level,
director: directory,
retentionDay: retentionDay,
mutex: new(sync.RWMutex),
}
// 应用所有传入的 CutterOption 配置函数
for i := 0; i < len(options); i++ {
options[i](rotate)
}
return rotate
}
// Write 方法将给定的字节数据写入到日志文件中。该方法会确保日志文件的目录存在,并根据配置的格式生成文件名。
// 如果日志文件已经存在,数据将被追加到文件末尾。如果文件不存在,则会创建新文件。
// 该方法还会定期清理超过保留天数的日志文件夹。
//
// 参数:
// - bytes: 要写入的字节数据。
//
// 返回值:
// - n: 成功写入的字节数。
// - err: 如果发生错误,返回错误信息;否则返回 nil。
func (c *Cutter) Write(bytes []byte) (n int, err error) {
// 加锁以确保并发安全
c.mutex.Lock()
defer func() {
// 在函数结束时关闭文件并释放锁
if c.file != nil {
_ = c.file.Close()
c.file = nil
}
c.mutex.Unlock()
}()
// 生成日志文件名
length := len(c.formats)
values := make([]string, 0, 3+length)
values = append(values, c.director)
if c.layout != "" {
values = append(values, time.Now().Format(c.layout))
}
for i := 0; i < length; i++ {
values = append(values, c.formats[i])
}
values = append(values, c.level+".log")
filename := filepath.Join(values...)
// 确保日志文件所在的目录存在
directory := filepath.Dir(filename)
err = os.MkdirAll(directory, os.ModePerm)
if err != nil {
return 0, nil
}
// 清理超过保留天数的日志文件夹
err = removeNDaysFolders(c.director, c.retentionDay)
if err != nil {
return 0, err
}
// 打开或创建日志文件,并追加写入数据
c.file, err = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return 0, err
}
// 将数据写入文件并返回写入的字节数
return c.file.Write(bytes)
}
// Sync 方法用于将当前文件的内容同步到磁盘,确保所有缓冲区的数据都写入磁盘。
// 该方法在调用时会先获取互斥锁,以确保在同步过程中不会有其他操作干扰。
// 如果当前 Cutter 实例中的文件对象不为 nil,则调用文件对象的 Sync 方法进行同步操作。
// 如果文件对象为 nil,则直接返回 nil,表示无需同步。
//
// 返回值:
// - error: 如果同步过程中发生错误,则返回该错误;否则返回 nil。
func (c *Cutter) Sync() error {
c.mutex.Lock()
defer c.mutex.Unlock()
// 如果文件对象存在,则调用其 Sync 方法进行同步
if c.file != nil {
return c.file.Sync()
}
// 文件对象不存在,直接返回 nil
return nil
}
// removeNDaysFolders 删除指定目录下,指定天数前的文件夹
func removeNDaysFolders(dir string, days int) error {
if days <= 0 {
return nil
}
cutoff := time.Now().AddDate(0, 0, -days)
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() && info.ModTime().Before(cutoff) && path != dir {
err = os.RemoveAll(path)
if err != nil {
return err
}
}
return nil
})
}
+68
View File
@@ -0,0 +1,68 @@
package internal
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"os"
"sundynix-go/global"
"time"
)
type ZapCore struct {
level zapcore.Level
zapcore.Core
}
// NewZapCore 创建一个 zapcore.Core
func NewZapCore(level zapcore.Level) *ZapCore {
entity := &ZapCore{level: level}
syncer := entity.WriteSyncer()
levelEnabler := zap.LevelEnablerFunc(func(l zapcore.Level) bool { return l == level })
entity.Core = zapcore.NewCore(global.Config.Zap.Encoder(), syncer, levelEnabler)
return entity
}
// WriteSyncer 创建一个 zapcore.WriteSyncer
func (z *ZapCore) WriteSyncer(formats ...string) zapcore.WriteSyncer {
cutter := NewCutter(
global.Config.Zap.Director,
z.level.String(),
global.Config.Zap.RetentionDay,
CutterWithLayout(time.DateOnly),
CutterWithFormats(formats...),
)
if global.Config.Zap.LogInConsole {
multiSyncer := zapcore.NewMultiWriteSyncer(os.Stdout, cutter)
return zapcore.AddSync(multiSyncer)
}
return zapcore.AddSync(cutter)
}
func (z *ZapCore) Enabled(level zapcore.Level) bool {
return z.level == level
}
func (z *ZapCore) With(fields []zapcore.Field) zapcore.Core {
return z.Core.With(fields)
}
func (z *ZapCore) Check(entry zapcore.Entry, check *zapcore.CheckedEntry) *zapcore.CheckedEntry {
if z.Enabled(entry.Level) {
return check.AddCore(entry, z)
}
return check
}
func (z *ZapCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
for i := 0; i < len(fields); i++ {
if fields[i].Key == "business" || fields[i].Key == "folder" || fields[i].Key == "directory" {
syncer := z.WriteSyncer(fields[i].String)
z.Core = zapcore.NewCore(global.Config.Zap.Encoder(), syncer, z.level)
}
}
return z.Core.Write(entry, fields)
}
func (z *ZapCore) Sync() error {
return z.Core.Sync()
}
+58
View File
@@ -0,0 +1,58 @@
package core
import (
"flag"
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"os"
"sundynix-go/core/internal"
"sundynix-go/global"
)
func Viper() *viper.Viper {
config := getConfigPath()
v := viper.New()
v.SetConfigFile(config)
v.SetConfigType("yaml")
err := v.ReadInConfig()
if err != nil {
panic(fmt.Errorf("fatal error config file: %w", err))
}
//监听配置文件变化并热加载
v.WatchConfig()
//监听配置文件变化事件
v.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("config file changed:", e.Name)
if err = v.Unmarshal(&global.Config); err != nil {
fmt.Println(err)
}
})
//将读取的配置信息反序列化到全局变量Conf中
if err = v.Unmarshal(&global.Config); err != nil {
panic(fmt.Errorf("fatal error unmarshal config: %w", err))
}
return v
}
// 获取配置文件路径 优先级: 命令行 > 环境变量 > 默认值
func getConfigPath() (config string) {
flag.StringVar(&config, "c", "", "choose config file")
flag.Parse()
// 命令行参数不为空 将值赋值于config
if config != "" {
fmt.Printf("正在使用命令行的 '-c' 参数传递的值, config 的路径为 %s\n", config)
return
}
_, err := os.Stat(config)
if err != nil || os.IsNotExist(err) {
config = internal.ConfigDefaultFile
fmt.Printf("配置文件路径不存在, 使用默认配置文件路径: %s\n", config)
}
return
}
+44
View File
@@ -0,0 +1,44 @@
package core
import (
"fmt"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"os"
"sundynix-go/core/internal"
"sundynix-go/global"
"sundynix-go/utils"
)
// Zap 函数用于初始化并返回一个 zap.Logger 实例。
// 该函数会检查日志目录是否存在,如果不存在则创建该目录。
// 根据配置中的日志级别,创建对应的 zapcore.Core,并将它们合并为一个 zap.Logger。
// 如果配置中启用了显示行号,则会在日志中添加调用者信息。
// 返回值:
// - logger: 初始化后的 zap.Logger 实例,用于记录日志。
func Zap() (logger *zap.Logger) {
// 检查日志目录是否存在,如果不存在则创建
if ok, _ := utils.PathExist(global.Config.Zap.Director); !ok {
fmt.Printf("日志目录 %v 不存在,创建中...\n", global.Config.Zap.Director)
_ = os.Mkdir(global.Config.Zap.Director, os.ModePerm)
}
// 获取配置中的日志级别,并初始化对应的 zapcore.Core
levels := global.Config.Zap.Levels()
length := len(levels)
cores := make([]zapcore.Core, 0, length)
for i := 0; i < length; i++ {
core := internal.NewZapCore(levels[i])
cores = append(cores, core)
}
// 将所有的 zapcore.Core 合并为一个 zap.Logger
logger = zap.New(zapcore.NewTee(cores...))
// 如果配置中启用了显示行号,则添加调用者信息
if global.Config.Zap.ShowLine {
logger = logger.WithOptions(zap.AddCaller())
}
return logger
}
+4405
View File
File diff suppressed because it is too large Load Diff
+4379
View File
File diff suppressed because it is too large Load Diff
+2699
View File
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
package global
// 1. 勋章类型定义 (Medal Type) - 早安电台成就系统
const (
MedalEarlyBird7 = "EARLY_BIRD_7" // 早起达人 - 连续7天收听
MedalEarlyBird30 = "EARLY_BIRD_30" // 晨间守护者 - 连续30天收听
MedalEarlyBird100 = "EARLY_BIRD_100" // 百日坚持 - 连续100天收听
MedalExplorer = "EXPLORER" // 探索者 - 收听3个不同场景
MedalAllRounder = "ALL_ROUNDER" // 全能达人 - 收听全部场景
MedalFirstListen = "FIRST_LISTEN" // 初次体验 - 首次完整收听
)
// 2. 场景定义 (Scene) - 对应垂直领域方向
const (
SceneMorningCareer = "morning_career" // 硬核职场 - AI 商业机会
SceneMorningHealth = "morning_health" // 效率健康 - 数字健康
SceneMorningLife = "morning_life" // 极简生活 - 晨间管家
SceneMorningStudy = "morning_study" // 知识胶囊 - 备考进阶
)
// 3. 抓取源类型 (Source Type)
const (
SourceTypeGithub = "github" // GitHub API
SourceTypeProductH = "producthunt" // Product Hunt API
SourceTypeRSS = "rss" // RSS 订阅
SourceTypeHuggingF = "huggingface" // HuggingFace Trending
SourceTypeWebScrape = "webscrape" // 网页爬取
)
// 4. 音频状态 (Audio Status)
const (
AudioStatusDraft = 0 // 草稿
AudioStatusPublished = 1 // 已发布
AudioStatusOffline = 2 // 已下线
)
// 5. 抓取记录状态 (Crawl Status)
const (
CrawlStatusPending = "pending" // 待处理
CrawlStatusProcessed = "processed" // 已处理
CrawlStatusFailed = "failed" // 处理失败
)
+27
View File
@@ -0,0 +1,27 @@
package global
import (
"sundynix-go/config"
"sundynix-go/utils/timer"
"github.com/bsm/redislock"
"github.com/redis/go-redis/v9"
"github.com/spf13/viper"
"github.com/wechatpay-apiv3/wechatpay-go/core"
"go.uber.org/zap"
"golang.org/x/sync/singleflight"
"gorm.io/gorm"
)
// 全局变量 加载在内存中
var (
Viper *viper.Viper
Logger *zap.Logger
Config *config.Config
DB *gorm.DB
Redis redis.UniversalClient
Locker *redislock.Client // 分布式锁
ConcurrencyControl = &singleflight.Group{}
Timer timer.Timer = timer.NewTimerTask()
WxPayClient *core.Client
)
+35
View File
@@ -0,0 +1,35 @@
package global
import (
"sundynix-go/utils/uniqueid"
"time"
"gorm.io/gorm"
)
type BaseModel struct {
Id string `gorm:"size:50;primaryKey" json:"id"` // 主键ID
CreatedAt time.Time `json:"createdAt" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updatedAt" gorm:"autoCreateTime;autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 删除时间
CreatedAtStr string `json:"createdAtStr" gorm:"-"`
}
// BeforeCreate 定义一个钩子,在创建之前执行自动插入字段
func (model *BaseModel) BeforeCreate(db *gorm.DB) (err error) {
//生成主键的string uniqueid
db.Statement.SetColumn("id", uniqueid.GenerateId())
return
}
// BeforeUpdate 定义一个钩子,在更新之前执行自动更新字段
func (model *BaseModel) BeforeUpdate(db *gorm.DB) (err error) {
db.Statement.SetColumn("updated_at", time.Now())
return
}
// AfterFind 钩子,在查询之后执行
func (model *BaseModel) AfterFind(tx *gorm.DB) (err error) {
model.CreatedAtStr = model.CreatedAt.Format("2006-01-02 15:04:05")
return
}
+118
View File
@@ -0,0 +1,118 @@
module sundynix-go
go 1.24.0
toolchain go1.24.2
require (
github.com/bsm/redislock v0.9.4
github.com/fsnotify/fsnotify v1.8.0
github.com/gin-gonic/gin v1.10.1
github.com/glebarez/sqlite v1.11.0
github.com/golang-jwt/jwt/v5 v5.2.3
github.com/google/uuid v1.6.0
github.com/minio/minio-go/v7 v7.0.95
github.com/mojocn/base64Captcha v1.3.8
github.com/redis/go-redis/v9 v9.7.3
github.com/robfig/cron/v3 v3.0.1
github.com/spf13/viper v1.20.1
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6
github.com/tencentyun/cos-go-sdk-v5 v0.7.70
github.com/wechatpay-apiv3/wechatpay-go v0.2.21
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.45.0
golang.org/x/sync v0.18.0
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.11
gorm.io/gorm v1.26.0
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj v1.8.4 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-openapi/jsonpointer v0.22.0 // indirect
github.com/go-openapi/jsonreference v0.21.1 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.24.1 // indirect
github.com/go-openapi/swag/cmdutils v0.24.0 // indirect
github.com/go-openapi/swag/conv v0.24.0 // indirect
github.com/go-openapi/swag/fileutils v0.24.0 // indirect
github.com/go-openapi/swag/jsonname v0.24.0 // indirect
github.com/go-openapi/swag/jsonutils v0.24.0 // indirect
github.com/go-openapi/swag/loading v0.24.0 // indirect
github.com/go-openapi/swag/mangling v0.24.0 // indirect
github.com/go-openapi/swag/netutils v0.24.0 // indirect
github.com/go-openapi/swag/stringutils v0.24.0 // indirect
github.com/go-openapi/swag/typeutils v0.24.0 // indirect
github.com/go-openapi/swag/yamlutils v0.24.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/go-sql-driver/mysql v1.9.2 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/google/go-querystring v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.4 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/minio/crc64nvme v1.0.2 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mozillazg/go-httpheader v0.2.1 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tinylib/msgp v1.3.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.21.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/image v0.26.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.38.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.64.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.10.0 // indirect
modernc.org/sqlite v1.37.0 // indirect
)
+347
View File
@@ -0,0 +1,347 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw=
github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bsm/redislock v0.9.4 h1:X/Wse1DPpiQgHbVYRE9zv6m070UcKoOGekgvpNhiSvw=
github.com/bsm/redislock v0.9.4/go.mod h1:Epf7AJLiSFwLCiZcfi6pWFO/8eAYrYpQXFxEDPoDeAk=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM=
github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU=
github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA=
github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8=
github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A=
github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I=
github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8=
github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik=
github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c=
github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak=
github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90=
github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k=
github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q=
github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts=
github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0=
github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc=
github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk=
github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk=
github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc=
github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w=
github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM=
github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM=
github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w=
github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw=
github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI=
github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c=
github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg=
github.com/minio/crc64nvme v1.0.2/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mojocn/base64Captcha v1.3.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg=
github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4=
github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ=
github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0=
github.com/tencentyun/cos-go-sdk-v5 v0.7.70 h1:gkBkSfrDvUg4ZIjwYAfjbNCCclen9LCRNHhBNz+yjEQ=
github.com/tencentyun/cos-go-sdk-v5 v0.7.70/go.mod h1:STbTNaNKq03u+gscPEGOahKzLcGSYOj6Dzc5zNay7Pg=
github.com/tencentyun/qcloud-cos-sts-sdk v0.0.0-20250515025012-e0eec8a5d123/go.mod h1:b18KQa4IxHbxeseW1GcZox53d7J0z39VNONTxvvlkXw=
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/wechatpay-apiv3/wechatpay-go v0.2.21 h1:uIyMpzvcaHA33W/QPtHstccw+X52HO1gFdvVL9O6Lfs=
github.com/wechatpay-apiv3/wechatpay-go v0.2.21/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.26.0 h1:9lqQVPG5aNNS6AyHdRiwScAVnXHg/L/Srzx55G5fOgs=
gorm.io/gorm v1.26.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
modernc.org/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA=
modernc.org/cc/v4 v4.26.0/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc=
modernc.org/ccgo/v4 v4.26.0/go.mod h1:Sem8f7TFUtVXkG2fiaChQtyyfkqhJBg/zjEJBkmuAVY=
modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.64.0 h1:U0k8BD2d3cD3e9I8RLcZgJBHAcsJzbXx5mKGSb5pyJA=
modernc.org/libc v1.64.0/go.mod h1:7m9VzGq7APssBTydds2zBcxGREwvIGpuUBaKTXdm2Qs=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+46
View File
@@ -0,0 +1,46 @@
package initialize
import (
"os"
"sundynix-go/global"
"sundynix-go/model/system"
"go.uber.org/zap"
"gorm.io/gorm"
)
// Gorm 根据全局配置中的数据库类型返回对应的 *gorm.DB 实例。
// 该函数通过检查 global.Config.System.DbType 的值来决定使用哪种数据库连接。
//
// 返回值:
// - *gorm.DB: 返回对应数据库类型的 *gorm.DB 实例
func Gorm() *gorm.DB {
switch global.Config.System.DbType {
case "mysql":
return GormMysql() // 返回 MySQL 数据库的 *gorm.DB 实例
case "pgsql":
return GromPgsql() // 返回 PostgreSQL 数据库的 *gorm.DB 实例
case "sqlite":
return GormSqlite() // 返回 SQLite 数据库的 *gorm.DB 实例
default:
return GormMysql() // 默认返回 MySQL 数据库 of *gorm.DB 实例
}
}
// MigrateTable 创建数据库表结构。
func MigrateTable() {
db := global.DB
err := db.AutoMigrate(
system.User{},
system.Client{},
system.Role{},
system.Menu{},
system.SysOperationRecord{},
system.Oss{},
)
if err != nil {
global.Logger.Error("Migrate table failed,err:", zap.Error(err))
os.Exit(0)
}
global.Logger.Info("Migrate table success")
}
+43
View File
@@ -0,0 +1,43 @@
package initialize
import (
"sundynix-go/config"
"sundynix-go/global"
"sundynix-go/initialize/internal"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// GormMysql 初始化Mysql数据库
func GormMysql() *gorm.DB {
m := global.Config.Mysql
return initMysqlDatabase(m)
}
// GromMysqlByConfig 根据配置初始化Mysql数据库
func GromMysqlByConfig(m config.Mysql) *gorm.DB {
return initMysqlDatabase(m)
}
// initMysqlDatabase 初始化Mysql数据库的辅助函数
func initMysqlDatabase(m config.Mysql) *gorm.DB {
if m.Dbname == "" {
return nil
}
mysqlConfig := mysql.Config{
DSN: m.Dsn(), //dsn
DefaultStringSize: 191, // 默认字符串类型长度
SkipInitializeWithVersion: false, // 根据版本自动配置
}
if db, err := gorm.Open(mysql.New(mysqlConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil {
panic(err)
} else {
db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine)
sqlDb, _ := db.DB()
sqlDb.SetMaxIdleConns(m.MaxIdleConns)
sqlDb.SetMaxOpenConns(m.MaxOpenConns)
global.Logger.Info("Mysql connect success")
return db
}
}
+40
View File
@@ -0,0 +1,40 @@
package initialize
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
"sundynix-go/config"
"sundynix-go/global"
"sundynix-go/initialize/internal"
)
// GromPgsql 初始化 Postgresql 数据库
func GromPgsql() *gorm.DB {
p := global.Config.Pgsql
return initPgsqlDatabase(p)
}
// GormPgsqlByConfig 根据配置文件初始化 Postgresql 数据库
func GormPgsqlByConfig(p config.Pgsql) *gorm.DB {
return initPgsqlDatabase(p)
}
// initPgsqlDatabase 初始化 Postgresql 数据库的辅助函数
func initPgsqlDatabase(p config.Pgsql) *gorm.DB {
if p.Dbname == "" {
return nil
}
pgsqlConfig := postgres.Config{
DSN: p.Dsn(), // DSN 数据库连接串
PreferSimpleProtocol: false, // 禁用隐式 prepared statement
}
if db, err := gorm.Open(postgres.New(pgsqlConfig), internal.Gorm.Config(p.Prefix, p.Singular)); err != nil {
panic(err)
} else {
sqlDb, _ := db.DB()
sqlDb.SetMaxIdleConns(p.MaxIdleConns)
sqlDb.SetMaxOpenConns(p.MaxOpenConns)
global.Logger.Info("postgresql connect success")
return db
}
}
+37
View File
@@ -0,0 +1,37 @@
package initialize
import (
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"sundynix-go/config"
"sundynix-go/global"
"sundynix-go/initialize/internal"
)
// GormSqlite 初始化Sqlite数据库
func GormSqlite() *gorm.DB {
s := global.Config.Sqlite
return initSqliteDatabase(s)
}
// GormSqliteByConfig 初始化Sqlite数据库用过传入配置
func GormSqliteByConfig(s config.Sqlite) *gorm.DB {
return initSqliteDatabase(s)
}
// initSqliteDatabase 初始化Sqlite数据库辅助函数
func initSqliteDatabase(s config.Sqlite) *gorm.DB {
if s.Dbname == "" {
return nil
}
if db, err := gorm.Open(sqlite.Open(s.Dsn()), internal.Gorm.Config(s.Prefix, s.Singular)); err != nil {
panic(err)
} else {
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(s.MaxIdleConns)
sqlDB.SetMaxOpenConns(s.MaxOpenConns)
global.Logger.Info("sqlite connect success")
return db
}
}
+57
View File
@@ -0,0 +1,57 @@
package internal
import (
"sundynix-go/config"
"sundynix-go/global"
"time"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
)
var Gorm = new(_gorm)
type _gorm struct{}
// Config 函数用于根据数据库类型和配置生成 GORM 的配置对象
// 该函数会根据全局配置中的数据库类型选择相应的通用配置,并返回一个配置好的 *gorm.Config 对象。
//
// 参数:
// - prefix: 表名前缀,用于在生成表名时添加到表名前。
// - singular: 是否禁用复数表名,true 表示禁用复数表名,false 表示使用复数表名。
//
// 返回值:
// - *gorm.Config: 配置好的 GORM 配置对象,包含日志、命名策略等配置。
func (g *_gorm) Config(prefix string, singular bool) *gorm.Config {
// 根据全局配置中的数据库类型选择相应的通用配置
var general config.GeneralDB
switch global.Config.System.DbType {
case "mysql":
general = global.Config.Mysql.GeneralDB
case "pgsql":
general = global.Config.Pgsql.GeneralDB
case "sqlite":
general = global.Config.Sqlite.GeneralDB
default:
// 默认使用 MySQL 的通用配置
general = global.Config.Mysql.GeneralDB
}
// 返回配置好的 GORM 配置对象
return &gorm.Config{
// 配置日志记录器,使用自定义的日志写入器,并设置慢查询阈值、日志级别和颜色输出
Logger: logger.New(NewWriter(general), logger.Config{
SlowThreshold: 200 * time.Millisecond,
LogLevel: general.LogLevel(),
Colorful: true,
}),
// 配置命名策略,设置表前缀和是否禁用复数表名
NamingStrategy: schema.NamingStrategy{
TablePrefix: prefix, // 表前缀
SingularTable: singular, // 禁用复数表名
},
// 禁用自动创建外键约束
DisableForeignKeyConstraintWhenMigrating: true,
}
}
+39
View File
@@ -0,0 +1,39 @@
package internal
import (
"fmt"
"gorm.io/gorm/logger"
"sundynix-go/config"
"sundynix-go/global"
)
type Writer struct {
config config.GeneralDB
writer logger.Writer
}
// NewWriter 创建一个Writer
func NewWriter(config config.GeneralDB) *Writer {
return &Writer{config: config}
}
// Printf 格式化打印日志
func (w *Writer) Printf(message string, data ...any) {
fmt.Printf(message, data)
//当开启了zap的情况下,会打印到日志记录中
if w.config.LogZap {
switch w.config.LogLevel() {
case logger.Silent:
global.Logger.Debug(fmt.Sprintf(message, data))
case logger.Error:
global.Logger.Error(fmt.Sprintf(message, data))
case logger.Warn:
global.Logger.Warn(fmt.Sprintf(message, data))
case logger.Info:
global.Logger.Info(fmt.Sprintf(message, data))
default:
global.Logger.Info(fmt.Sprintf(message, data))
}
return
}
}
+48
View File
@@ -0,0 +1,48 @@
package initialize
import (
"context"
"sundynix-go/config"
"sundynix-go/global"
"github.com/bsm/redislock"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
)
// Redis
func Redis() {
client, err := initRedisClient(global.Config.Redis)
if err != nil {
global.Logger.Error("Redis connect failed,err:", zap.Error(err))
return
}
global.Redis = client
global.Locker = redislock.New(client)
}
// 初始化Redis
func initRedisClient(redisConfig config.Redis) (redis.UniversalClient, error) {
var client redis.UniversalClient
//集群模式
if redisConfig.Cluster {
client = redis.NewClusterClient(&redis.ClusterOptions{
Addrs: redisConfig.ClusterAddrs,
Password: redisConfig.Password,
})
} else {
//单例模式
client = redis.NewClient(&redis.Options{
Addr: redisConfig.Addr,
Password: redisConfig.Password,
DB: redisConfig.DB,
})
}
pong, err := client.Ping(context.Background()).Result()
if err != nil {
global.Logger.Error("Redis connect ping failed,err:", zap.String("name", redisConfig.Name), zap.Error(err))
return nil, err
}
global.Logger.Info("Redis connect ping response:", zap.String("name", redisConfig.Name), zap.String("pong", pong))
return client, nil
}
+63
View File
@@ -0,0 +1,63 @@
package initialize
import (
"fmt"
"sundynix-go/docs"
"sundynix-go/global"
"sundynix-go/middleware"
"sundynix-go/router"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"go.uber.org/zap"
)
// Routers 初始化总路由
func Routers() {
Router := gin.New()
Router.Use(gin.Recovery())
if gin.Mode() == gin.DebugMode {
Router.Use(gin.Logger())
}
docs.SwaggerInfo.BasePath = global.Config.System.RouterPrefix
Router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// 系统组路由
systemRouter := router.GroupApp.System
NeedAuthGroup := Router.Group(global.Config.System.RouterPrefix)
PublicGroup := Router.Group(global.Config.System.RouterPrefix)
//鉴权中间件
NeedAuthGroup.Use(middleware.AuthMiddleware())
{
//无须鉴权的路由
systemRouter.InitAuthRouter(PublicGroup) //登录不需要鉴权
}
{
//需要鉴权的路由
systemRouter.InitUserRouter(NeedAuthGroup) //用户相关
systemRouter.InitClientRouter(NeedAuthGroup) //客户端相关
systemRouter.InitRoleRouter(NeedAuthGroup) //角色相关
systemRouter.InitMenuRouter(NeedAuthGroup) //菜单相关
systemRouter.InitOssRouter(NeedAuthGroup) //OSS相关
}
address := fmt.Sprintf(":%d", global.Config.System.Addr)
fmt.Printf(`
欢迎使用 sundynix-service
项目地址:
默认自动化文档地址:http://127.0.0.1%s/swagger/index.html
默认前端文件运行地址:http://127.0.0.1:8080
`, address)
err := Router.Run(address)
if err != nil {
global.Logger.Error("Gin run failed", zap.Error(err))
}
global.Logger.Info("Gin run success", zap.String("address", address))
}
+8
View File
@@ -0,0 +1,8 @@
package initialize
func InitTimer() {
go func() {
// var option []cron.Option
// option = append(option, cron.WithSeconds())
}()
}
+4
View File
@@ -0,0 +1,4 @@
[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
@@ -0,0 +1,12 @@
[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}
+51
View File
@@ -0,0 +1,51 @@
package main
import (
"database/sql"
"sundynix-go/core"
"sundynix-go/global"
"sundynix-go/initialize"
"sundynix-go/pkg/httpclient"
"go.uber.org/zap"
)
// @title Swagger API接口文档
// @version v1.0.0
// @description 使用gin + gorm进行极速开发的全栈开发基础平台
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @BasePath /
func main() {
//viper
global.Viper = core.Viper()
//canzap
global.Logger = core.Zap()
//swap
zap.ReplaceGlobals(global.Logger)
//初始化Gorm 连接数据库
global.DB = initialize.Gorm()
//redis
initialize.Redis()
// timer
initialize.InitTimer()
// httpclient 主动初始化 HTTP Client(可选,也可依赖懒加载)饿汉加载
httpclient.InitHttpClient()
//迁移数据库
if global.DB != nil {
initialize.MigrateTable() // 迁移数据库结构
db, _ := global.DB.DB()
defer func(db *sql.DB) {
err := db.Close()
if err != nil {
global.Logger.Error("db close failed", zap.Error(err))
}
}(db)
}
//初始化路由
initialize.Routers()
}
+56
View File
@@ -0,0 +1,56 @@
package middleware
import (
"errors"
"sundynix-go/global"
"sundynix-go/model/commom/response"
"sundynix-go/service"
"sundynix-go/utils/auth"
"sundynix-go/utils/timer"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
var jwtService = service.GroupApp.SystemServiceGroup.JwtService
// AuthMiddleware 验证token有效性
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := auth.GetToken(c)
if token == "" {
response.NoAuth("未登录或非法访问", c)
c.Abort()
return
}
userId := auth.GetUserId(c)
if jwtService.IsInBlacklist(userId, token) {
response.NoAuth("未登录或令牌失效", c)
c.Abort()
return
}
j := auth.NewJWT()
// 解析token信息
claims, err := j.ParseToken(token)
if err != nil {
if errors.Is(err, auth.TokenExpired) {
response.NoAuth("登录过期", c)
auth.ClearToken(c)
c.Abort()
return
}
response.NoAuth(err.Error(), c)
auth.ClearToken(c)
c.Abort()
return
}
c.Set("claims", claims)
if claims.ExpiresAt.Unix()-time.Now().Unix() < claims.BufferTime {
dr, _ := timer.ParseDuration(global.Config.JWT.ExpiresTime)
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(dr))
}
c.Next()
}
}
+77
View File
@@ -0,0 +1,77 @@
package middleware
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/url"
"strings"
"sundynix-go/global"
"sundynix-go/model/system"
"sundynix-go/service"
"sundynix-go/utils/auth"
"sync"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
var operationService = service.GroupApp.SystemServiceGroup.OperationRecordService
var respPool sync.Pool
var bufferSize = 1024
func init() {
respPool = sync.Pool{
New: func() interface{} {
return make([]byte, bufferSize)
},
}
}
func OperationRecord() gin.HandlerFunc {
return func(c *gin.Context) {
var body []byte
var userId string
if c.Request.Method != http.MethodGet {
var err error
body, err = io.ReadAll(c.Request.Body)
if err != nil {
global.Logger.Error("read body from request error:", zap.Error(err))
} else {
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
}
} else {
query := c.Request.URL.RawQuery
query, _ = url.QueryUnescape(query)
split := strings.Split(query, "&")
m := make(map[string]string)
for _, v := range split {
kv := strings.Split(v, "=")
if len(kv) == 2 {
m[kv[0]] = kv[1]
}
}
body, _ = json.Marshal(&m)
}
claims, _ := auth.GetClaims(c)
if claims != nil && claims.BaseClaims.ID != "" {
userId = claims.BaseClaims.ID
} else {
userId = c.Request.Header.Get("x-user-id")
}
record := system.SysOperationRecord{
Ip: c.ClientIP(),
Method: c.Request.Method,
Path: c.Request.URL.Path,
Agent: c.Request.UserAgent(),
Body: string(body),
UserId: userId,
}
if err := operationService.CreateOperationRecord(record); err != nil {
global.Logger.Error("create operation record error:", zap.Error(err))
}
}
}
+63
View File
@@ -0,0 +1,63 @@
package request
import (
"gorm.io/gorm"
)
// PageInfo Paging common input parameter structure
type PageInfo struct {
Current int `json:"current" form:"current"` // 页码
PageSize int `json:"pageSize" form:"pageSize"` // 每页大小
Keyword string `json:"keyword" form:"keyword"` // 关键字
}
func (r *PageInfo) Paginate() func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if r.Current <= 0 {
r.Current = 1
}
switch {
case r.PageSize > 100:
r.PageSize = 100
case r.PageSize <= 0:
r.PageSize = 10
}
offset := (r.Current - 1) * r.PageSize
return db.Offset(offset).Limit(r.PageSize)
}
}
// GetById Find by id structure
type GetById struct {
ID string `json:"id" form:"id"` // 主键ID
}
func (r *GetById) Uint() string {
return string(r.ID)
}
type IdsReq struct {
Ids []string `json:"ids" form:"ids"`
}
// GetAuthorityId Get role by id structure
type GetAuthorityId struct {
AuthorityId string `json:"authorityId" form:"authorityId"` // 角色ID
}
type Empty struct{}
type UploadOss struct {
Id string `json:"id" binding:"required"` //数据主键
OssIds []string `json:"ossIds" binding:"required"` // ossIds
}
type DeleteOss struct {
Id string `json:"id" binding:"required"` //数据主键
OssIds []string `json:"ossIds" binding:"required"`
}
type UploadFile struct {
Id string `json:"id" binding:"required"` //数据主键
OssId string `json:"ossIds" binding:"required"` // ossId
}
+12
View File
@@ -0,0 +1,12 @@
package response
type PageResult struct {
List interface{} `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
type ListResult struct {
List interface{} `json:"list"`
}
+60
View File
@@ -0,0 +1,60 @@
package response
import (
"github.com/gin-gonic/gin"
"net/http"
)
type Response struct {
Code int `json:"code"`
Data interface{} `json:"data"`
Msg string `json:"msg"`
}
const (
SUCCESS = 200
ERROR = 7
)
// Result 返回结果
func Result(code int, data interface{}, msg string, c *gin.Context) {
c.JSON(http.StatusOK, Response{
code,
data,
msg,
})
}
// Ok 成功返回
func Ok(c *gin.Context) {
Result(SUCCESS, map[string]interface{}{}, "操作成功", c)
}
// OkWithData 带数据成功返回
func OkWithData(data interface{}, c *gin.Context) {
Result(SUCCESS, data, "操作成功", c)
}
// OkWithMsg 带信息成功返回
func OkWithMsg(msg string, c *gin.Context) {
Result(SUCCESS, map[string]interface{}{}, msg, c)
}
// Fail 失败返回
func Fail(code int, msg string, c *gin.Context) {
Result(code, map[string]interface{}{}, "操作失败", c)
}
// FailWithMsg 带信息失败返回
func FailWithMsg(msg string, c *gin.Context) {
Result(ERROR, map[string]interface{}{}, msg, c)
}
// NoAuth 未授权返回
func NoAuth(message string, c *gin.Context) {
c.JSON(http.StatusUnauthorized, Response{
7,
nil,
message,
})
}
+15
View File
@@ -0,0 +1,15 @@
package system
import (
"sundynix-go/global"
)
type Oss struct {
global.BaseModel
Name string `json:"name" form:"name" gorm:"column:name;comment:文件名"`
Url string `json:"url" form:"url" gorm:"column:url;comment:文件地址"`
Tag string `json:"tag" form:"tag" gorm:"column:tag;comment:文件标签"`
Key string `json:"key" form:"key" gorm:"column:key;comment:文件key"`
Suffix string `json:"suffix" form:"suffix" gorm:"column:suffix;comment:文件后缀"`
MD5 string `json:"md5" form:"md5" gorm:"column:md5;comment:文件md5"`
}
+15
View File
@@ -0,0 +1,15 @@
package request
import "github.com/mojocn/base64Captcha"
// configJsonBody json request body.
type CaptchaReqBody struct {
Id string
CaptchaType string
VerifyValue string
DriverAudio *base64Captcha.DriverAudio
DriverString *base64Captcha.DriverString
DriverChinese *base64Captcha.DriverChinese
DriverMath *base64Captcha.DriverMath
DriverDigit *base64Captcha.DriverDigit
}
+18
View File
@@ -0,0 +1,18 @@
package request
import (
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
type CustomClaims struct {
BaseClaims
BufferTime int64
jwt.RegisteredClaims
}
type BaseClaims struct {
UUID uuid.UUID
ID string
Account string
}
+8
View File
@@ -0,0 +1,8 @@
package request
import common "sundynix-go/model/commom/request"
type GetOssFileList struct {
common.PageInfo
Name string `json:"name" form:"name"`
}
+9
View File
@@ -0,0 +1,9 @@
package request
import common "sundynix-go/model/commom/request"
type GetClientList struct {
common.PageInfo
ClientId string `json:"clientId" form:"clientId"`
Name string `json:"name" form:"name"`
}
+6
View File
@@ -0,0 +1,6 @@
package request
type GetMenuTree struct {
Category int `json:"category" form:"category"`
ParentId string `json:"parentId" form:"parentId"`
}
@@ -0,0 +1,14 @@
package request
import (
common "sundynix-go/model/commom/request"
)
type GetOperationRecordList struct {
common.PageInfo
Ip string `json:"ip" form:"ip"`
Method string `json:"method" form:"method"`
Path string `json:"path" form:"path"`
UserId string `json:"userId" form:"userId"`
Status int `json:"status" form:"status"`
}
+14
View File
@@ -0,0 +1,14 @@
package request
import common "sundynix-go/model/commom/request"
type GetRoleList struct {
common.PageInfo
Code string `json:"code" form:"code"`
Name string `json:"name" form:"name"`
}
type GrantMenu struct {
RoleId string `json:"roleId"`
MenuIds []string `json:"menuIds"`
}
+26
View File
@@ -0,0 +1,26 @@
package request
import common "sundynix-go/model/commom/request"
type Login struct {
Account string `json:"account"`
Password string `json:"password"`
Captcha string `json:"captcha"`
CaptchaId string `json:"captchaId"`
}
type GetUserList struct {
common.PageInfo
Account string `json:"account" form:"account"`
Phone string `json:"phone" form:"phone"`
}
type ChangePwd struct {
Id string `json:"id"`
NewPwd string `json:"newPwd"`
}
type GrantRole struct {
UserId string `json:"userId"`
RoleIds []string `json:"roleIds"`
}
@@ -0,0 +1,9 @@
package response
type WxCode2SessionResp struct {
SessionKey string `json:"session_key"`
Unionid string `json:"unionid"`
Openid string `json:"openid"`
Errcode int32 `json:"errcode"`
Errmsg string `json:"errmsg"`
}
+7
View File
@@ -0,0 +1,7 @@
package response
import "sundynix-go/model/system"
type UploadFileResponse struct {
File system.Oss `json:"file"`
}
+6
View File
@@ -0,0 +1,6 @@
package response
type CaptchaRes struct {
CaptchaId string `json:"captchaId"`
Captcha string `json:"captcha"`
}
+9
View File
@@ -0,0 +1,9 @@
package response
import "sundynix-go/model/system"
type LoginResponse struct {
User system.User `json:"user"`
Token string `json:"token"`
ExpiresAt int64 `json:"expiresAt"`
}
+12
View File
@@ -0,0 +1,12 @@
package system
import "sundynix-go/global"
type Client struct {
global.BaseModel
ClientId string `gorm:"size:20;" json:"clientId"`
Name string `gorm:"size:50;" json:"name"`
GrantType string `gorm:"size:50;" json:"grantType"`
AdditionalInfo string `gorm:"type:text" json:"additionalInfo"`
ActiveTimeout int64 `json:"activeTimeout"`
}
+18
View File
@@ -0,0 +1,18 @@
package system
import "sundynix-go/global"
type Menu struct {
global.BaseModel
ParentId string `gorm:"size:100;default:'0'" json:"parentId" form:"parentId"`
Category int `json:"category" form:"category"`
Name string `gorm:"size:20" json:"name" form:"name"`
Title string `gorm:"size:20" json:"title" form:"title"`
Code string `gorm:"size:50" json:"code" form:"code"`
Path string `gorm:"size:100" json:"path" form:"path"`
Permission string `gorm:"size:20" json:"permission" form:"permission"`
Locale string `gorm:"size:50" json:"locale" form:"locale"`
Icon string `gorm:"size:20" json:"icon" form:"icon"`
Sort int `json:"sort" form:"sort"`
Children []*Menu `json:"children" gorm:"-"`
}
+21
View File
@@ -0,0 +1,21 @@
package system
import (
"sundynix-go/global"
"time"
)
type SysOperationRecord struct {
global.BaseModel
Ip string `json:"ip" form:"ip" gorm:"column:ip;comment:请求ip"` // 请求ip
Method string `json:"method" form:"method" gorm:"column:method;comment:请求方法"` // 请求方法
Path string `json:"path" form:"path" gorm:"column:path;comment:请求路径"` // 请求路径
Status int `json:"status" form:"status" gorm:"column:status;comment:请求状态"` // 请求状态
Latency time.Duration `json:"latency" form:"latency" gorm:"column:latency;comment:延迟" swaggertype:"string"` // 延迟
Agent string `json:"agent" form:"agent" gorm:"type:text;column:agent;comment:代理"` // 代理
ErrorMessage string `json:"erroMessage" form:"error_message" gorm:"column:error_message;comment:错误信息"` // 错误信息
Body string `json:"body" form:"body" gorm:"type:text;column:body;comment:请求Body"` // 请求Body
Resp string `json:"resp" form:"resp" gorm:"type:text;column:resp;comment:响应Body"` // 响应Body
UserId string `json:"userId" form:"user_id" gorm:"column:user_id;comment:用户id"` // 用户id
User User `json:"user"`
}
+11
View File
@@ -0,0 +1,11 @@
package system
import "sundynix-go/global"
type Role struct {
global.BaseModel
Name string `gorm:"size:20" json:"name" form:"name"`
Code string `gorm:"size:20" json:"code" form:"code"`
Sort int `json:"sort" form:"sort"`
Menus []*Menu `gorm:"many2many:role_menu;"`
}
+6
View File
@@ -0,0 +1,6 @@
package system
type RoleMenu struct {
RoleId string `json:"roleId" gorm:"size:100;column:role_id;comment:角色id"`
MenuId string `json:"menuId" gorm:"size:100;column:menu_id;comment:菜单id"`
}
+38
View File
@@ -0,0 +1,38 @@
package system
import (
"sundynix-go/global"
)
type Login interface {
GetAccount() string
GetUserId() string
GetUserInfo() any
}
type User struct {
global.BaseModel
TenantId string `gorm:"size:20;" json:"tenantId" form:"tenantId"`
ClientId string `gorm:"size:20;" json:"clientId"`
Name string `gorm:"size:20" json:"name" form:"name"`
Account string `gorm:"size:11;" json:"account" form:"account"`
Password string `gorm:"size:100;" json:"-" form:"password"`
NickName string `gorm:"size:20;column:nick_name" json:"nickName" form:"nickName"`
Phone string `gorm:"size:20;column:phone" json:"phone" form:"phone"`
SessionKey string `gorm:"size:80;column:session_key" json:"sessionKey" form:"sessionKey"`
UnionId string `gorm:"size:80;column:union_id" json:"unionId"`
MiniOpenId string `gorm:"size:80;column:mini_open_id" json:"miniOpenId" form:"miniOpenId"`
SaOpenId string `gorm:"size:80;column:sa_open_id" json:"saOpenId"`
AvatarId string `gorm:"size:50;column:avatar_id" json:"avatarId"`
Avatar *Oss `gorm:"foreignKey:AvatarId" json:"avatar"`
}
func (u *User) GetAccount() string {
return u.Account
}
func (u *User) GetUserId() string {
return u.Id
}
func (u *User) GetUserInfo() any {
return *u
}
+6
View File
@@ -0,0 +1,6 @@
package system
type UserRole struct {
UserId string `json:"userId" gorm:"size:100;column:user_id;comment:用户id"`
RoleId string `json:"roleId" gorm:"size:100;column:role_id;comment:角色id"`
}
BIN
View File
Binary file not shown.
+59
View File
@@ -0,0 +1,59 @@
package httpclient
import (
"net"
"net/http"
"sync"
"time"
)
// 全局单例变量
var (
once sync.Once // 保证初始化只执行一次
globalClient *http.Client // 复用的 HTTP Client 实例
)
// InitHttpClient 初始化全局 HTTP Client(手动调用,可选)
// 配置连接池参数,优化连接复用
func InitHttpClient() {
once.Do(func() {
// 核心:配置 Transport(连接池核心参数)
transport := &http.Transport{
// 全局最大空闲连接数(避免连接数过多)
MaxIdleConns: 100,
// 空闲连接超时时间(超时后关闭,释放资源)
IdleConnTimeout: 30 * time.Second,
// 每个目标主机的最大空闲连接数(避免单主机占用过多连接)
MaxIdleConnsPerHost: 10,
MaxConnsPerHost: 20, // 新增:每个目标主机的最大并发连接数(防止打满服务器)
// 禁用压缩(根据业务需求调整,比如调用微信接口可开启)
DisableCompression: false,
// TCP 连接建立超时(防止连接挂死)
DialContext: (&net.Dialer{
Timeout: 10 * time.Second, // 连接建立超时
KeepAlive: 30 * time.Second, // TCP 保活时间(维持长连接)
}).DialContext,
// TLS 握手超时(HTTPS 场景必配)
TLSHandshakeTimeout: 5 * time.Second,
/// 等待响应头的超时
ResponseHeaderTimeout: 15 * time.Second,
}
// 构建 HTTP Client
globalClient = &http.Client{
Timeout: 20 * time.Second, // 整个请求的超时(连接+读取+写入)
Transport: transport,
// 可选:禁用自动重定向(根据业务需求,比如微信接口无需重定向)
// CheckRedirect: func(req *http.Request, via []*http.Request) error {
// return http.ErrUseLastResponse
// },
}
})
}
// GetClient 获取全局 HTTP Client 实例(推荐使用此方法调用)
// 懒加载:如果未初始化,自动触发初始化
func GetClient() *http.Client {
InitHttpClient() // 兜底:确保初始化
return globalClient
}
+12
View File
@@ -0,0 +1,12 @@
package router
import (
"sundynix-go/router/system"
)
var GroupApp = new(Group)
// Group 路由组
type Group struct {
System system.SysRouterGroup
}
+22
View File
@@ -0,0 +1,22 @@
package system
import (
"github.com/gin-gonic/gin"
)
type AuthRouter struct {
}
// InitAuthRouter 初始化登录路由
func (s *AuthRouter) InitAuthRouter(Router *gin.RouterGroup) {
loginRouter := Router.Group("auth")
{
loginRouter.POST("login", authApi.Login)
loginRouter.GET("captcha", authApi.Captcha)
loginRouter.GET("logout", authApi.Logout) // 服务端不保存任何登录状态 退出实际是禁用了当前的jwt
loginRouter.GET("miniLogin", authApi.MiniLogin) //小程序登录
loginRouter.GET("getPhone", authApi.GetPhone) // 获取手机号
loginRouter.GET("getLocation", authApi.GetLocation) // 获取位置
loginRouter.GET("getWeather", authApi.GetWeather) // 获取天气
}
}
+17
View File
@@ -0,0 +1,17 @@
package system
import "github.com/gin-gonic/gin"
type ClientRouter struct {
}
func (s *ClientRouter) InitClientRouter(Router *gin.RouterGroup) {
clientRouter := Router.Group("client")
{
clientRouter.POST("save", clientApi.SaveClient)
clientRouter.POST("update", clientApi.UpdateClient)
clientRouter.POST("getClientList", clientApi.GetClientList)
clientRouter.POST("delete", clientApi.Delete)
clientRouter.GET("detail", clientApi.Detail)
}
}
+24
View File
@@ -0,0 +1,24 @@
package system
import v1 "sundynix-go/api/v1"
type SysRouterGroup struct {
AuthRouter
UserRouter
ClientRouter
RoleRouter
MenuRouter
OperationRecordRouter
OssRouter
}
// 初始化路由
var (
authApi = v1.ApiGroupApp.SystemApiGroup.AuthApi
userApi = v1.ApiGroupApp.SystemApiGroup.UserApi
clientApi = v1.ApiGroupApp.SystemApiGroup.ClientApi
roleApi = v1.ApiGroupApp.SystemApiGroup.RoleApi
menuApi = v1.ApiGroupApp.SystemApiGroup.MenuApi
operationRecordApi = v1.ApiGroupApp.SystemApiGroup.OperationRecordApi
ossApi = v1.ApiGroupApp.SystemApiGroup.OssApi
)
+19
View File
@@ -0,0 +1,19 @@
package system
import "github.com/gin-gonic/gin"
type MenuRouter struct {
}
func (m *MenuRouter) InitMenuRouter(Router *gin.RouterGroup) {
menuRouter := Router.Group("menu")
{
menuRouter.GET("route", menuApi.Route)
menuRouter.POST("getAllMenuTree", menuApi.GetAllMenuTree)
menuRouter.GET("getUserMenuTree", menuApi.GetUserMenuTree)
menuRouter.POST("save", menuApi.SaveMenu)
menuRouter.POST("update", menuApi.UpdateMenu)
menuRouter.GET("delete", menuApi.DeleteMenu)
menuRouter.GET("detail", menuApi.Detail)
}
}
+16
View File
@@ -0,0 +1,16 @@
package system
import "github.com/gin-gonic/gin"
type OperationRecordRouter struct {
}
func (o *OperationRecordRouter) InitOperationRecordRouter(Router *gin.RouterGroup) {
operationRecordRouter := Router.Group("operationRecord")
{
operationRecordRouter.POST("createOperationRecord", operationRecordApi.CreateOperationRecord) // 新增操作记录
operationRecordRouter.GET("getOperationRecordList", operationRecordApi.GetRecordList) // 获取操作记录列表
operationRecordRouter.GET("getOperationRecordById", operationRecordApi.GetRecordById) // 获取操作记录
operationRecordRouter.DELETE("delete", operationRecordApi.DeleteRecordsByIds) // 批量删除操作记录
}
}
+16
View File
@@ -0,0 +1,16 @@
package system
import "github.com/gin-gonic/gin"
type OssRouter struct {
}
func (f *OssRouter) InitOssRouter(Router *gin.RouterGroup) {
ossRouter := Router.Group("oss")
{
ossRouter.POST("upload", ossApi.UploadFile)
ossRouter.POST("delete", ossApi.DeleteFile)
ossRouter.POST("getFileList", ossApi.GetFileList)
ossRouter.GET("getFile", ossApi.Detail)
}
}
+18
View File
@@ -0,0 +1,18 @@
package system
import "github.com/gin-gonic/gin"
type RoleRouter struct {
}
func (r *RoleRouter) InitRoleRouter(router *gin.RouterGroup) {
roleRouter := router.Group("role")
{
roleRouter.POST("save", roleApi.SaveRole)
roleRouter.POST("update", roleApi.UpdateRole)
roleRouter.POST("getRoleList", roleApi.GetRoleList)
roleRouter.POST("delete", roleApi.Delete)
roleRouter.GET("detail", roleApi.Detail)
roleRouter.POST("grantMenu", roleApi.GrantMenu)
}
}
+22
View File
@@ -0,0 +1,22 @@
package system
import (
"github.com/gin-gonic/gin"
)
type UserRouter struct {
}
func (s *UserRouter) InitUserRouter(Router *gin.RouterGroup) {
userRouter := Router.Group("user")
{
userRouter.GET("info", userApi.CurrentUser)
userRouter.POST("save", userApi.SaveUser)
userRouter.POST("update", userApi.UpdateUser)
userRouter.POST("getUserList", userApi.GetUserList)
userRouter.POST("delete", userApi.Delete)
userRouter.GET("detail", userApi.Detail)
userRouter.POST("changePassword", userApi.ChangePassword)
userRouter.POST("grantRole", userApi.GrantRole)
}
}
+11
View File
@@ -0,0 +1,11 @@
package service
import (
"sundynix-go/service/system"
)
var GroupApp = new(Group)
type Group struct {
SystemServiceGroup system.ServiceGroup
}
+11
View File
@@ -0,0 +1,11 @@
package system
type ServiceGroup struct {
JwtService
UserService
ClientService
RoleService
MenuService
OperationRecordService
OssService
}
+115
View File
@@ -0,0 +1,115 @@
package system
import (
"crypto/md5"
"errors"
"fmt"
_ "image/jpeg"
_ "image/png"
"io"
"mime/multipart"
"strings"
"sundynix-go/global"
common "sundynix-go/model/commom/request"
"sundynix-go/model/system"
sysReq "sundynix-go/model/system/request"
"sundynix-go/utils/upload"
"go.uber.org/zap"
"gorm.io/gorm"
)
type OssService struct {
}
var OssServiceApp = new(OssService)
func (o *OssService) Save(file system.Oss) error {
return global.DB.Create(&file).Error
}
func (o *OssService) Upload(multipartFile multipart.File, header *multipart.FileHeader) (file system.Oss, err error) {
//1.检查是否已有此文件
temp, err := header.Open()
if err != nil {
return file, err
}
defer temp.Close()
hasher := md5.New()
if _, copyErr := io.Copy(hasher, temp); copyErr != nil {
return file, copyErr
}
// 步骤3: 计算哈希值并转换为十六进制字符串
hashBytes := hasher.Sum(nil)
hashString := fmt.Sprintf("%x", hashBytes)
var exist system.Oss
findErr := global.DB.Where("md5 = ?", hashString).First(&exist).Error
if findErr == nil && exist.Id != "" {
return exist, nil
}
if errors.Is(findErr, gorm.ErrRecordNotFound) {
//不存在的时候保存
instance := upload.OssInstance()
filepath, key, uploadErr := instance.UploadFile(header)
if uploadErr != nil {
return file, uploadErr
}
//文件后缀
s := strings.Split(header.Filename, ".")
//仅当可能是图片时 才计算图片宽高
f := system.Oss{
Key: key, // uploads/2025-09-17/
Name: header.Filename,
Suffix: s[len(s)-1],
Tag: s[len(s)-1],
Url: filepath, // http://127.0.0.1:9000/planting-fun/uploads/2025-09-17/211476f3837fc7acbaebf0f901c1bd68.png
MD5: hashString,
}
return f, global.DB.Create(&f).Error
}
return file, err
}
func (o *OssService) DeleteFileByIds(ids common.IdsReq) error {
//循环删除
instance := upload.OssInstance()
for _, id := range ids.Ids {
file, err := o.GetById(id)
if err != nil {
return err
}
if err = instance.DeleteFile(file.Key); err != nil {
global.Logger.Error("删除文件失败!", zap.Error(err))
return err
}
}
err := global.DB.Where("id IN (?)", ids.Ids).Delete(&system.Oss{}).Error
return err
}
func (o *OssService) GetById(id string) (system.Oss, error) {
var file system.Oss
err := global.DB.Where("id = ?", id).First(&file).Error
//不存在的时候不要返回错误,而是返回nil
if err != nil {
return file, nil
}
return file, err
}
func (o *OssService) GetFileList(info sysReq.GetOssFileList) (list interface{}, total int64, err error) {
limit := info.PageSize
offset := info.PageSize * (info.Current - 1)
db := global.DB.Model(&system.Oss{})
var files []system.Oss
if info.Name != "" {
db = db.Where("name LIKE ?", "%"+info.Name+"%")
}
err = db.Count(&total).Error
if err != nil {
return
}
err = db.Limit(limit).Offset(offset).Order("created_at desc").Find(&files).Error
return files, total, err
}

Some files were not shown because too many files have changed in this diff Show More