From fc585fa4df3c038461e70b0d1b099435dbfdcfc9 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 27 Feb 2026 13:54:01 +0800 Subject: [PATCH] first commit --- .DS_Store | Bin 0 -> 10244 bytes Data_Sources.md | 69 + Plan.md | 97 + README.md | 74 + README.zh.md | 73 + Source.md | 181 + Vertical_Directions.md | 47 + Walkthrough.md | 173 + api/v1/enter.go | 12 + api/v1/system/auth.go | 185 + api/v1/system/enter.go | 23 + api/v1/system/oss.go | 116 + api/v1/system/sys_client.go | 141 + api/v1/system/sys_menu.go | 166 + api/v1/system/sys_operation_record.go | 79 + api/v1/system/sys_role.go | 163 + api/v1/system/sys_user.go | 207 + config-dev.yaml | 90 + config-prod.yaml | 80 + config/config.go | 19 + config/config_db.go | 8 + config/config_jwt.go | 8 + config/config_redis.go | 10 + config/constans.go | 22 + config/db_list.go | 52 + config/gorm_mysql.go | 9 + config/gorm_pgsql.go | 17 + config/gorm_sqlite.go | 13 + config/mini_program.go | 6 + config/oss_minio.go | 11 + config/oss_tencent.go | 10 + config/rocket_mq.go | 12 + config/service_account.go | 6 + config/system.go | 9 + config/wechat_pay.go | 12 + config/zap.go | 81 + core/internal/constant.go | 8 + core/internal/cutter.go | 161 + core/internal/zap_core.go | 68 + core/viper.go | 58 + core/zap.go | 44 + docs/docs.go | 4405 ++++++++++++++++++ docs/swagger.json | 4379 +++++++++++++++++ docs/swagger.yaml | 2699 +++++++++++ global/enums.go | 42 + global/global.go | 27 + global/model.go | 35 + go.mod | 118 + go.sum | 347 ++ initialize/gorm.go | 46 + initialize/gorm_mysql.go | 43 + initialize/gorm_pgsql.go | 40 + initialize/gorm_sqlite.go | 37 + initialize/internal/gorm.go | 57 + initialize/internal/gorm_logger_writer.go | 39 + initialize/redis.go | 48 + initialize/router.go | 63 + initialize/timer.go | 8 + log/2026-02-25/error.log | 4 + log/2026-02-25/info.log | 12 + main.go | 51 + middleware/auth.go | 56 + middleware/operation.go | 77 + model/commom/request/common.go | 63 + model/commom/response/common.go | 12 + model/commom/response/response.go | 60 + model/system/oss.go | 15 + model/system/request/captcha.go | 15 + model/system/request/jwt.go | 18 + model/system/request/oss.go | 8 + model/system/request/sys_client.go | 9 + model/system/request/sys_menu.go | 6 + model/system/request/sys_operation_record.go | 14 + model/system/request/sys_role.go | 14 + model/system/request/sys_user.go | 26 + model/system/response/WxCode2SessionResp.go | 9 + model/system/response/oss.go | 7 + model/system/response/sys_captcha.go | 6 + model/system/response/sys_user.go | 9 + model/system/sys_client.go | 12 + model/system/sys_menu.go | 18 + model/system/sys_operation_record.go | 21 + model/system/sys_role.go | 11 + model/system/sys_role_menu.go | 6 + model/system/sys_user.go | 38 + model/system/sys_user_role.go | 6 + pkg/.DS_Store | Bin 0 -> 6148 bytes pkg/httpclient/http_client.go | 59 + router/enter.go | 12 + router/system/auth_router.go | 22 + router/system/client_router.go | 17 + router/system/enter.go | 24 + router/system/menu_router.go | 19 + router/system/operation_record.go | 16 + router/system/oss_router.go | 16 + router/system/role_router.go | 18 + router/system/user_router.go | 22 + service/enter.go | 11 + service/system/enter.go | 11 + service/system/oss.go | 115 + service/system/sys_client.go | 61 + service/system/sys_jwt.go | 26 + service/system/sys_menu.go | 127 + service/system/sys_operation_record.go | 57 + service/system/sys_role.go | 89 + service/system/sys_user.go | 256 + utils/async/async_task.go | 64 + utils/auth/claims.go | 130 + utils/auth/jwt.go | 89 + utils/captcha/redis.go | 5 + utils/directory.go | 20 + utils/hash.go | 32 + utils/hash_test.go | 111 + utils/location/location.go | 168 + utils/location/weather.go | 186 + utils/timer/human_duration.go | 30 + utils/timer/interval.go | 48 + utils/timer/timed_task.go | 219 + utils/uniqueid/id_generator.go | 41 + utils/uniqueid/out_trade_no.go | 32 + utils/upload/minio_oss.go | 106 + utils/upload/oss_instance.go | 31 + utils/upload/tencent_cos.go | 60 + utils/validator.go | 294 ++ utils/wechat/access_token.go | 53 + utils/wechat/pay.go | 39 + utils/wechat/safety.go | 156 + 127 files changed, 18548 insertions(+) create mode 100644 .DS_Store create mode 100644 Data_Sources.md create mode 100644 Plan.md create mode 100644 README.md create mode 100644 README.zh.md create mode 100644 Source.md create mode 100644 Vertical_Directions.md create mode 100644 Walkthrough.md create mode 100644 api/v1/enter.go create mode 100644 api/v1/system/auth.go create mode 100644 api/v1/system/enter.go create mode 100644 api/v1/system/oss.go create mode 100644 api/v1/system/sys_client.go create mode 100644 api/v1/system/sys_menu.go create mode 100644 api/v1/system/sys_operation_record.go create mode 100644 api/v1/system/sys_role.go create mode 100644 api/v1/system/sys_user.go create mode 100644 config-dev.yaml create mode 100644 config-prod.yaml create mode 100644 config/config.go create mode 100644 config/config_db.go create mode 100644 config/config_jwt.go create mode 100644 config/config_redis.go create mode 100644 config/constans.go create mode 100644 config/db_list.go create mode 100644 config/gorm_mysql.go create mode 100644 config/gorm_pgsql.go create mode 100644 config/gorm_sqlite.go create mode 100644 config/mini_program.go create mode 100644 config/oss_minio.go create mode 100644 config/oss_tencent.go create mode 100644 config/rocket_mq.go create mode 100644 config/service_account.go create mode 100644 config/system.go create mode 100644 config/wechat_pay.go create mode 100644 config/zap.go create mode 100644 core/internal/constant.go create mode 100644 core/internal/cutter.go create mode 100644 core/internal/zap_core.go create mode 100644 core/viper.go create mode 100644 core/zap.go create mode 100644 docs/docs.go create mode 100644 docs/swagger.json create mode 100644 docs/swagger.yaml create mode 100644 global/enums.go create mode 100644 global/global.go create mode 100644 global/model.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 initialize/gorm.go create mode 100644 initialize/gorm_mysql.go create mode 100644 initialize/gorm_pgsql.go create mode 100644 initialize/gorm_sqlite.go create mode 100644 initialize/internal/gorm.go create mode 100644 initialize/internal/gorm_logger_writer.go create mode 100644 initialize/redis.go create mode 100644 initialize/router.go create mode 100644 initialize/timer.go create mode 100644 log/2026-02-25/error.log create mode 100644 log/2026-02-25/info.log create mode 100644 main.go create mode 100644 middleware/auth.go create mode 100644 middleware/operation.go create mode 100644 model/commom/request/common.go create mode 100644 model/commom/response/common.go create mode 100644 model/commom/response/response.go create mode 100644 model/system/oss.go create mode 100644 model/system/request/captcha.go create mode 100644 model/system/request/jwt.go create mode 100644 model/system/request/oss.go create mode 100644 model/system/request/sys_client.go create mode 100644 model/system/request/sys_menu.go create mode 100644 model/system/request/sys_operation_record.go create mode 100644 model/system/request/sys_role.go create mode 100644 model/system/request/sys_user.go create mode 100644 model/system/response/WxCode2SessionResp.go create mode 100644 model/system/response/oss.go create mode 100644 model/system/response/sys_captcha.go create mode 100644 model/system/response/sys_user.go create mode 100644 model/system/sys_client.go create mode 100644 model/system/sys_menu.go create mode 100644 model/system/sys_operation_record.go create mode 100644 model/system/sys_role.go create mode 100644 model/system/sys_role_menu.go create mode 100644 model/system/sys_user.go create mode 100644 model/system/sys_user_role.go create mode 100644 pkg/.DS_Store create mode 100644 pkg/httpclient/http_client.go create mode 100644 router/enter.go create mode 100644 router/system/auth_router.go create mode 100644 router/system/client_router.go create mode 100644 router/system/enter.go create mode 100644 router/system/menu_router.go create mode 100644 router/system/operation_record.go create mode 100644 router/system/oss_router.go create mode 100644 router/system/role_router.go create mode 100644 router/system/user_router.go create mode 100644 service/enter.go create mode 100644 service/system/enter.go create mode 100644 service/system/oss.go create mode 100644 service/system/sys_client.go create mode 100644 service/system/sys_jwt.go create mode 100644 service/system/sys_menu.go create mode 100644 service/system/sys_operation_record.go create mode 100644 service/system/sys_role.go create mode 100644 service/system/sys_user.go create mode 100644 utils/async/async_task.go create mode 100644 utils/auth/claims.go create mode 100644 utils/auth/jwt.go create mode 100644 utils/captcha/redis.go create mode 100644 utils/directory.go create mode 100644 utils/hash.go create mode 100644 utils/hash_test.go create mode 100644 utils/location/location.go create mode 100644 utils/location/weather.go create mode 100644 utils/timer/human_duration.go create mode 100644 utils/timer/interval.go create mode 100644 utils/timer/timed_task.go create mode 100644 utils/uniqueid/id_generator.go create mode 100644 utils/uniqueid/out_trade_no.go create mode 100644 utils/upload/minio_oss.go create mode 100644 utils/upload/oss_instance.go create mode 100644 utils/upload/tencent_cos.go create mode 100644 utils/validator.go create mode 100644 utils/wechat/access_token.go create mode 100644 utils/wechat/pay.go create mode 100644 utils/wechat/safety.go diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..23575b11bcfb9a4bf492234f7f1d10fa7760749c GIT binary patch literal 10244 zcmeHM&u<$=6n>L9v7MyNrcFXALRj?$sU@VeLR1LVbyGovilaCRNq=D0_QY9by<_dJ z6Ot&%XE?zf?p!$`apA~?D>uY{09-l2_h#4a?#8*csLV(+Z+7Orc{AVae7kR#h(xvD zzD`skq5?8YYX-%L#QmI4rLnl>HAn$_Qmv+|nz*t~>6!+EfI+|@U=T0}7zF+s2;iB` zsW2T$QyK&e0tSI62rxgCkXcq@If$gD4iq{BfXty;C)D-JKV*p;ASEkvR$N8RNvBMI51sIP?%Dtxlp6NmCjG3<9SS;MjefZ0eD- z>L%m&F+N~R|LCO6CaFb=JlY|LIvHI*qdS|_r8lTelJ;qd4&kXwZrYlvu>LmT$;w#w z$cN|lnfBjj>~q@QCzsco8$YDkb`axPx ze4X(!NgZ;b*@U0W$~DHxm9wtW-m%{FxMPg*DBS?_?xuBluHt+G(JAbiha4hf7ATLh z=nzm$+2nzNt?ib0R%s86WlDPr+71wDV{}Oeusn@FkV)B0z=ro#j>09SxkxoyqYYZ6 z`^XRIj;`0}BkbHd-9ujua%0rKF>a}`#&NIjW$$2(*yjnvVy)&u(2Kum{m|^WP7p3H z|0>4Da}$&KLcTCvcwFCA2ldbn`$5(AAM(|PQf_ltx0{=eJZM<+ca;~~jwgL>;It)F z9)0Y1ZFNvpeXkv8+j`VdC>Dwh>(cS@t>x?2N;luRadNG6eCw@c)Nj0fa#AcTl;3=J zy|L$ZJ@p0d3nURg80lPe{iOU7wRkm7Yd>C%_+_}7-z{snp5oN>%(?Tk*4%}QbMtcx z3k!=cU3&TQE0-JA#irfb>-ap+yUJC5)$PlN9p7o!dv?q5I$NRqEUY_6vSD4wsIJTQ zCr_I;4tvm4%5~RN;DnCywjTvRc{P;XRc}YZ z<313s^|&k8lzsqt%4TcZ_I=yy$m-!^*KSI;=2cY~s_vH4QeHIkSv@Q+*L=xCmT&(R zfmp75IIO%LRV*q;6+}<-!&)pw`KRVU*Ec-n$zr*(7WEYIigOjW?tR+C4R}Of(KqxR z{YXF4Z}cbqE%L$=SHx9uQ@kf`ix0%AcpzG0S2%)?KGN85i(iedhouNmi??{c?t8BC zI%r|-JdTEo!|07V8k(w-lvdP-=?-Y|J`U9=5@eD{9+R1j#S!g}veR^tw42nF-UlH` z@-x)JG?CQJgOH(x2Z+P;oYrOvO=%ZXRDQ-9D7osKegp)xml>IfBRP)=5G@m^o27hB z9)BdrBcb_%5J+N0hD2Um5=m@xhDa7cT6_rk0JQWwL<2P8@KYYw+{*8UBj23#dsp(n z4SCB*gMdN6AYc%9J_w9QZcF_A|K8dE|DVs~j8}tzLExDnAaX196?`(%5#uP2h_R@I z{3$Xgjw}a})FCMfR^fL2IUX1MIbH|LXj|q!PN-L6Ifx`$ke>O^02BZ7Zx$2(>$~s& H8UOza&wY;| literal 0 HcmV?d00001 diff --git a/Data_Sources.md b/Data_Sources.md new file mode 100644 index 0000000..445abad --- /dev/null +++ b/Data_Sources.md @@ -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 以防内容重复。 \ No newline at end of file diff --git a/Plan.md b/Plan.md new file mode 100644 index 0000000..ed4d2ab --- /dev/null +++ b/Plan.md @@ -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。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8ee538 --- /dev/null +++ b/README.md @@ -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 & +~~~ \ No newline at end of file diff --git a/README.zh.md b/README.zh.md new file mode 100644 index 0000000..a5e28c4 --- /dev/null +++ b/README.zh.md @@ -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` diff --git a/Source.md b/Source.md new file mode 100644 index 0000000..953bfbc --- /dev/null +++ b/Source.md @@ -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 \ No newline at end of file diff --git a/Vertical_Directions.md b/Vertical_Directions.md new file mode 100644 index 0000000..217759c --- /dev/null +++ b/Vertical_Directions.md @@ -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)及考试工具链的引流。 \ No newline at end of file diff --git a/Walkthrough.md b/Walkthrough.md new file mode 100644 index 0000000..100c6d0 --- /dev/null +++ b/Walkthrough.md @@ -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 生成内容"标识 diff --git a/api/v1/enter.go b/api/v1/enter.go new file mode 100644 index 0000000..592d642 --- /dev/null +++ b/api/v1/enter.go @@ -0,0 +1,12 @@ +package v1 + +import ( + "sundynix-go/api/v1/system" +) + +var ApiGroupApp = new(ApiGroup) + +// ApiGroup 路由组 +type ApiGroup struct { + SystemApiGroup system.ApiGroup +} diff --git a/api/v1/system/auth.go b/api/v1/system/auth.go new file mode 100644 index 0000000..b83a6af --- /dev/null +++ b/api/v1/system/auth.go @@ -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) + +} diff --git a/api/v1/system/enter.go b/api/v1/system/enter.go new file mode 100644 index 0000000..343112f --- /dev/null +++ b/api/v1/system/enter.go @@ -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 +) diff --git a/api/v1/system/oss.go b/api/v1/system/oss.go new file mode 100644 index 0000000..cac3606 --- /dev/null +++ b/api/v1/system/oss.go @@ -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) +} diff --git a/api/v1/system/sys_client.go b/api/v1/system/sys_client.go new file mode 100644 index 0000000..3aea782 --- /dev/null +++ b/api/v1/system/sys_client.go @@ -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) + +} diff --git a/api/v1/system/sys_menu.go b/api/v1/system/sys_menu.go new file mode 100644 index 0000000..e5dbe2c --- /dev/null +++ b/api/v1/system/sys_menu.go @@ -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(¶m) + 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) +} diff --git a/api/v1/system/sys_operation_record.go b/api/v1/system/sys_operation_record.go new file mode 100644 index 0000000..6d06d92 --- /dev/null +++ b/api/v1/system/sys_operation_record.go @@ -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) +} diff --git a/api/v1/system/sys_role.go b/api/v1/system/sys_role.go new file mode 100644 index 0000000..c08bdca --- /dev/null +++ b/api/v1/system/sys_role.go @@ -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) +} diff --git a/api/v1/system/sys_user.go b/api/v1/system/sys_user.go new file mode 100644 index 0000000..c0f6824 --- /dev/null +++ b/api/v1/system/sys_user.go @@ -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) +} diff --git a/config-dev.yaml b/config-dev.yaml new file mode 100644 index 0000000..e2a7909 --- /dev/null +++ b/config-dev.yaml @@ -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 \ No newline at end of file diff --git a/config-prod.yaml b/config-prod.yaml new file mode 100644 index 0000000..64c0f4d --- /dev/null +++ b/config-prod.yaml @@ -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 \ No newline at end of file diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..7e7d181 --- /dev/null +++ b/config/config.go @@ -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"` //微信支付 +} diff --git a/config/config_db.go b/config/config_db.go new file mode 100644 index 0000000..92bb5c6 --- /dev/null +++ b/config/config_db.go @@ -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"` +} diff --git a/config/config_jwt.go b/config/config_jwt.go new file mode 100644 index 0000000..c95d30d --- /dev/null +++ b/config/config_jwt.go @@ -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"` // 签发者 +} diff --git a/config/config_redis.go b/config/config_redis.go new file mode 100644 index 0000000..011cbb8 --- /dev/null +++ b/config/config_redis.go @@ -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"` // 集群模式下的节点地址列表 +} diff --git a/config/constans.go b/config/constans.go new file mode 100644 index 0000000..1ce60ba --- /dev/null +++ b/config/constans.go @@ -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 // 钻石 +) diff --git a/config/db_list.go b/config/db_list.go new file mode 100644 index 0000000..5c9edf4 --- /dev/null +++ b/config/db_list.go @@ -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"` +} diff --git a/config/gorm_mysql.go b/config/gorm_mysql.go new file mode 100644 index 0000000..314c342 --- /dev/null +++ b/config/gorm_mysql.go @@ -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 +} diff --git a/config/gorm_pgsql.go b/config/gorm_pgsql.go new file mode 100644 index 0000000..acfbde4 --- /dev/null +++ b/config/gorm_pgsql.go @@ -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 +} diff --git a/config/gorm_sqlite.go b/config/gorm_sqlite.go new file mode 100644 index 0000000..d585670 --- /dev/null +++ b/config/gorm_sqlite.go @@ -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") +} diff --git a/config/mini_program.go b/config/mini_program.go new file mode 100644 index 0000000..2f3d146 --- /dev/null +++ b/config/mini_program.go @@ -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"` +} diff --git a/config/oss_minio.go b/config/oss_minio.go new file mode 100644 index 0000000..a0faac7 --- /dev/null +++ b/config/oss_minio.go @@ -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"` +} diff --git a/config/oss_tencent.go b/config/oss_tencent.go new file mode 100644 index 0000000..39a29d1 --- /dev/null +++ b/config/oss_tencent.go @@ -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"` +} diff --git a/config/rocket_mq.go b/config/rocket_mq.go new file mode 100644 index 0000000..5841bbe --- /dev/null +++ b/config/rocket_mq.go @@ -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"` +} diff --git a/config/service_account.go b/config/service_account.go new file mode 100644 index 0000000..3194e2c --- /dev/null +++ b/config/service_account.go @@ -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"` +} diff --git a/config/system.go b/config/system.go new file mode 100644 index 0000000..092626a --- /dev/null +++ b/config/system.go @@ -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"` +} diff --git a/config/wechat_pay.go b/config/wechat_pay.go new file mode 100644 index 0000000..5d3e4b9 --- /dev/null +++ b/config/wechat_pay.go @@ -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"` +} diff --git a/config/zap.go b/config/zap.go new file mode 100644 index 0000000..068ded3 --- /dev/null +++ b/config/zap.go @@ -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 + } +} diff --git a/core/internal/constant.go b/core/internal/constant.go new file mode 100644 index 0000000..a1e84e6 --- /dev/null +++ b/core/internal/constant.go @@ -0,0 +1,8 @@ +package internal + +const ( + ConfigDefaultFile = "config-dev.yaml" + ConfigProdFile = "config-prod.yaml" + ConfigDebugFile = "config-debug.yaml" + ConfigReleaseFile = "config-release.yaml" +) diff --git a/core/internal/cutter.go b/core/internal/cutter.go new file mode 100644 index 0000000..4eaa3ad --- /dev/null +++ b/core/internal/cutter.go @@ -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 + }) +} diff --git a/core/internal/zap_core.go b/core/internal/zap_core.go new file mode 100644 index 0000000..9121ebe --- /dev/null +++ b/core/internal/zap_core.go @@ -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() +} diff --git a/core/viper.go b/core/viper.go new file mode 100644 index 0000000..5217867 --- /dev/null +++ b/core/viper.go @@ -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 +} diff --git a/core/zap.go b/core/zap.go new file mode 100644 index 0000000..c31117f --- /dev/null +++ b/core/zap.go @@ -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 +} diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..010eaca --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,4405 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/auth/captcha": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "登录相关" + ], + "summary": "获取验证码", + "responses": { + "200": { + "description": "获取验证码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.CaptchaRes" + } + } + } + ] + } + } + } + } + }, + "/auth/getLocation": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "登录相关" + ], + "summary": "获取位置信息", + "parameters": [ + { + "type": "string", + "description": "longitude", + "name": "longitude", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "latitude", + "name": "latitude", + "in": "query", + "required": true + } + ], + "responses": {} + } + }, + "/auth/getPhone": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "登录相关" + ], + "summary": "获取手机号", + "parameters": [ + { + "type": "string", + "description": "code", + "name": "code", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "openId", + "name": "openId", + "in": "query", + "required": true + } + ], + "responses": {} + } + }, + "/auth/getWeather": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "登录相关" + ], + "summary": "获取天气信息", + "parameters": [ + { + "type": "string", + "description": "adcode", + "name": "adcode", + "in": "query", + "required": true + } + ], + "responses": {} + } + }, + "/auth/login": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "登录相关" + ], + "summary": "pc登录", + "parameters": [ + { + "description": "用户名, 密码, 验证码,验证码id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Login" + } + } + ], + "responses": { + "200": { + "description": "登录成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/auth/logout": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "登录相关" + ], + "summary": "pc登出", + "responses": { + "200": { + "description": "登出成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/auth/miniLogin": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "登录相关" + ], + "summary": "小程序登录", + "parameters": [ + { + "type": "string", + "description": "code", + "name": "code", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "小程序登录", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.LoginResponse" + } + } + } + ] + } + } + } + } + }, + "/classify/myClassifyLog": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "识别相关" + ], + "summary": "我的植物识别记录", + "parameters": [ + { + "description": "分页", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "识别记录", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/classify/plant": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "识别相关" + ], + "summary": "base64植物识别", + "parameters": [ + { + "type": "file", + "description": "植物识别", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "文件OCR", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/client/delete": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "客户端管理" + ], + "summary": "删除client", + "parameters": [ + { + "description": "ids", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "删除client", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/client/detail": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "id获取详情", + "produces": [ + "application/json" + ], + "tags": [ + "客户端管理" + ], + "summary": "获取client详情", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "获取client详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/system.Client" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/client/getClientList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "客户端管理" + ], + "summary": "获取client列表", + "parameters": [ + { + "description": "client", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetClientList" + } + } + ], + "responses": { + "200": { + "description": "获取client列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/client/save": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "客户端管理" + ], + "summary": "创建client", + "parameters": [ + { + "description": "client", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.Client" + } + } + ], + "responses": { + "200": { + "description": "创建client", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/client/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "客户端管理" + ], + "summary": "更新client", + "parameters": [ + { + "description": "client", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.Client" + } + } + ], + "responses": { + "200": { + "description": "更新client", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/config/badge/add": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "徽章配置" + ], + "summary": "添加徽章配置", + "parameters": [ + { + "description": "添加徽章配置", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreateBadge" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"添加成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/config/badge/delete": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "徽章配置" + ], + "summary": "删除徽章配置", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"删除成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/config/badge/find": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "徽章配置" + ], + "summary": "根据ID获取徽章配置", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/config/badge/tree": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "徽章配置" + ], + "summary": "获取徽章配置树形结构", + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/config/badge/update": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "徽章配置" + ], + "summary": "更新徽章配置", + "parameters": [ + { + "description": "更新参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.UpdateBadge" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"更新成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/config/level/add": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "等级配置" + ], + "summary": "添加等级配置", + "parameters": [ + { + "description": "添加等级配置", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreateLevelConf" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"添加成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/config/level/detail": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "等级配置" + ], + "summary": "等级配置详情", + "parameters": [ + { + "type": "string", + "description": "等级配置id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"查询成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/config/level/list": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "等级配置" + ], + "summary": "等级配置列表", + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"查询成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/config/level/update": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "等级配置" + ], + "summary": "修改等级配置", + "parameters": [ + { + "description": "修改等级配置", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.UpdateLevelConf" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"修改成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/menu/delete": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除menu", + "produces": [ + "application/json" + ], + "tags": [ + "菜单管理" + ], + "summary": "删除menu", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/detail": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "id获取详情", + "produces": [ + "application/json" + ], + "tags": [ + "菜单管理" + ], + "summary": "获取menu详情", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/system.Menu" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getAllMenuTree": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "菜单管理" + ], + "summary": "获取所有菜单树", + "parameters": [ + { + "description": "菜单信息", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetMenuTree" + } + } + ], + "responses": { + "200": { + "description": "获取所有菜单树", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/system.Menu" + } + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getUserMenuTree": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "菜单管理" + ], + "summary": "用户菜单数据", + "responses": { + "200": { + "description": "用户菜单数据", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/system.Menu" + } + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/route": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "菜单管理" + ], + "summary": "用户路由", + "responses": { + "200": { + "description": "用户route", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/system.Menu" + } + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/save": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "菜单管理" + ], + "summary": "新增菜单", + "parameters": [ + { + "description": "menu", + "name": "data", + "in": "body", + "schema": { + "$ref": "#/definitions/system.Menu" + } + } + ], + "responses": { + "200": { + "description": "新建菜单/按钮", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "菜单管理" + ], + "summary": "更新菜单", + "parameters": [ + { + "description": "menu", + "name": "data", + "in": "body", + "schema": { + "$ref": "#/definitions/system.Menu" + } + } + ], + "responses": { + "200": { + "description": "更新菜单", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/oss/delete": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "文件相关" + ], + "summary": "删除文件", + "parameters": [ + { + "description": "批量删除文件", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "删除文件", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/oss/detail": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "文件相关" + ], + "summary": "文件详情", + "parameters": [ + { + "type": "string", + "description": "文件id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "文件详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/oss/getFileList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "文件相关" + ], + "summary": "文件列表", + "parameters": [ + { + "description": "文件列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetOssFileList" + } + } + ], + "responses": { + "200": { + "description": "文件列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/oss/upload": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "文件相关" + ], + "summary": "文件上传", + "parameters": [ + { + "type": "file", + "description": "上传文件", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "上传文件", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/plant/add": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "添加植物", + "parameters": [ + { + "description": "创建植物", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreateMyPlant" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"添加成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/plant/completeTask": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "完成任务", + "parameters": [ + { + "description": "完成任务", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CompleteTask" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"完成任务\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/plant/deletePlan": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "删除任务", + "parameters": [ + { + "description": "删除植物", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"删除成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/plant/deletePlant": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "删除植物", + "parameters": [ + { + "description": "删除植物", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"删除成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/plant/detail": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "ById植物详情", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取ById成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/plant/growth/add": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "添加成长记录", + "parameters": [ + { + "description": "添加成长记录", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreateGrowthRecord" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"添加成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/plant/page": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "植物列表", + "parameters": [ + { + "description": "分页获取植物列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/plant/plan/add": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "添加养护事项", + "parameters": [ + { + "description": "添加养护事项", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreateCarePlan" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"添加成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/plant/plan/delete": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "删除养护事项", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"删除成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/plant/todayTask": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "今日任务", + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/plant/update": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "修改ById植物", + "parameters": [ + { + "description": "修改ById植物", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.UpdateMyPlant" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"修改ById成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/post/comment": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "帖子" + ], + "summary": "评论帖子", + "parameters": [ + { + "description": "评论帖子", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreateComment" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"评论成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/post/like": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "帖子" + ], + "summary": "点赞帖子", + "parameters": [ + { + "type": "string", + "description": "帖子id", + "name": "id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "点赞类型 1 点赞 2 取消点赞", + "name": "type", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"点赞成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/post/myPost": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "帖子" + ], + "summary": "我的发布", + "parameters": [ + { + "description": "分页获取帖子列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PostPage" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/post/page": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "帖子" + ], + "summary": "帖子列表", + "parameters": [ + { + "description": "分页获取帖子列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PostPage" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/post/publish": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "帖子" + ], + "summary": "发布帖子", + "parameters": [ + { + "description": "发布帖子", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreatePost" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发布成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/profile/detail": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "个人中心" + ], + "summary": "用户详情", + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"查询成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/profile/update": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "个人中心" + ], + "summary": "修改用户信息", + "parameters": [ + { + "description": "修改用户信息", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.UpdateProfile" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"修改成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/role/delete": { + "post": { + "description": "删除角色", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "删除角色", + "parameters": [ + { + "description": "批量删除角色", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "删除角色", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/role/detail": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "角色详情", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "角色详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/system.Role" + } + } + } + ] + } + } + } + } + }, + "/role/getRoleList": { + "post": { + "description": "获取角色列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "获取角色列表", + "parameters": [ + { + "description": "页码, 每页大小, 搜索条件", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetRoleList" + } + } + ], + "responses": { + "200": { + "description": "获取角色列表,返回包括列表,总数,页码,每页大小", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/role/grantMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "授权菜单给角色", + "parameters": [ + { + "description": "授权菜单给角色", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GrantMenu" + } + } + ], + "responses": { + "200": { + "description": "授权菜单给角色", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/role/save": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "创建角色", + "parameters": [ + { + "description": "角色信息", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.Role" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/role/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "修改角色", + "parameters": [ + { + "description": "角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.Role" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/topic/add": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "帖子话题" + ], + "summary": "修改话题", + "parameters": [ + { + "description": "修改话题", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.UpdateTopic" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发布成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/topic/delete": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "帖子话题" + ], + "summary": "删除任务", + "parameters": [ + { + "description": "删除话题", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"删除成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/topic/detail": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "帖子话题" + ], + "summary": "话题详情", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/topic/list": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "帖子话题" + ], + "summary": "话题列表", + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/topic/page": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "帖子话题" + ], + "summary": "话题分页", + "parameters": [ + { + "description": "分页获取话题列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/user/changePassword": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改密码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "修改密码", + "parameters": [ + { + "description": "用户id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ChangePwd" + } + } + ], + "responses": { + "200": { + "description": "修改密码成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/system.User" + } + } + } + ] + } + } + } + } + }, + "/user/delete": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "删除用户", + "parameters": [ + { + "description": "批量删除用户", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "删除用户", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/detail": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "获取用户详情", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "获取用户详情成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/system.User" + } + } + } + ] + } + } + } + } + }, + "/user/getUserList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "获取用户列表", + "parameters": [ + { + "description": "页码, 每页大小, 搜索条件", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetUserList" + } + } + ], + "responses": { + "200": { + "description": "获取用户列表,返回包括列表,总数,页码,每页大小", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/grantRole": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "给用户分配角色", + "parameters": [ + { + "description": "用户ID, 角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GrantRole" + } + } + ], + "responses": { + "200": { + "description": "{\"code\": 200, \"data\": [...]}", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/user/info": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "当前登录用户", + "responses": { + "200": { + "description": "{\"code\": 200, \"data\": {}, \"msg\": \"添加成功\"}", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/user/save": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "新增用户", + "parameters": [ + { + "description": "用户信息", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.User" + } + } + ], + "responses": { + "200": { + "description": "{\"code\": 200, \"data\": {}, \"msg\": \"添加成功\"}", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/user/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "更新用户", + "parameters": [ + { + "description": "用户ID,用户信息", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.User" + } + } + ], + "responses": { + "200": { + "description": "{\"code\": 200, \"data\": [...]}", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/wiki-class/add": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "百科分类" + ], + "summary": "添加分类", + "parameters": [ + { + "description": "添加分类", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreateWikiClass" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发布成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/wiki-class/delete": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "百科分类" + ], + "summary": "删除分类", + "parameters": [ + { + "description": "删除分类", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"删除成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/wiki-class/detail": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "百科分类" + ], + "summary": "分类详情", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/wiki-class/list": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "百科分类" + ], + "summary": "分类列表", + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/wiki-class/page": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "百科分类" + ], + "summary": "分类分页", + "parameters": [ + { + "description": "分页获取分类列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/wiki-class/update": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "百科分类" + ], + "summary": "修改分类(可直接传入ossId修改图片)", + "parameters": [ + { + "description": "修改分类", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.UpdateWikiClass" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发布成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/wiki/add": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "百科" + ], + "summary": "添加百科", + "parameters": [ + { + "description": "添加百科", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreateWiki" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发布成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/wiki/detail": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "百科" + ], + "summary": "百科详情", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/wiki/page": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "百科" + ], + "summary": "分页", + "parameters": [ + { + "description": "百科分页", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WikiPage" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发布成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/wiki/update": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "百科" + ], + "summary": "修改百科", + "parameters": [ + { + "description": "修改百科", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.UpdateWiki" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发布成功\"}", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "request.CarePlan": { + "type": "object", + "properties": { + "icon": { + "description": "icon信息", + "type": "string" + }, + "name": { + "description": "农事名称", + "type": "string" + }, + "period": { + "description": "周期", + "type": "integer" + } + } + }, + "request.ChangePwd": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "newPwd": { + "type": "string" + } + } + }, + "request.CompleteTask": { + "type": "object", + "required": [ + "taskId" + ], + "properties": { + "remark": { + "type": "string" + }, + "taskId": { + "type": "string" + } + } + }, + "request.CreateBadge": { + "type": "object", + "properties": { + "comparator": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dimension": { + "description": "维度: EXPERTISE(专家), PERSISTENCE(勤勉), JOURNAL(记录)", + "type": "string" + }, + "groupId": { + "description": "组ID: 用于前端聚合显示 (e.g. \"fertilizerMaster\")", + "type": "string" + }, + "iconId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "rewardSunlight": { + "type": "integer" + }, + "sort": { + "type": "integer" + }, + "targetAction": { + "description": "触发动作: ACT_FERTILIZE, ACT_WATER...", + "type": "string" + }, + "threshold": { + "type": "integer" + }, + "tier": { + "description": "等级: 1=铜, 2=银, 3=金", + "type": "integer" + } + } + }, + "request.CreateCarePlan": { + "type": "object", + "required": [ + "plantId" + ], + "properties": { + "icon": { + "description": "icon信息", + "type": "string" + }, + "name": { + "description": "农事名称", + "type": "string" + }, + "period": { + "description": "周期", + "type": "integer" + }, + "plantId": { + "type": "string" + } + } + }, + "request.CreateComment": { + "type": "object", + "required": [ + "content", + "postId" + ], + "properties": { + "content": { + "description": "评论内容", + "type": "string" + }, + "postId": { + "description": "帖子id", + "type": "string" + } + } + }, + "request.CreateGrowthRecord": { + "type": "object", + "required": [ + "plantId" + ], + "properties": { + "content": { + "type": "string" + }, + "desc": { + "type": "string" + }, + "name": { + "type": "string" + }, + "ossIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "plantId": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "request.CreateLevelConf": { + "type": "object", + "properties": { + "level": { + "description": "等级数值", + "type": "integer" + }, + "minSunlight": { + "description": "达到该等级所需的最小阳光值", + "type": "integer" + }, + "perks": { + "description": "解锁权益描述 (e.g., \"解锁智能诊断\")", + "type": "string" + }, + "title": { + "description": "等级称号 (e.g., \"萌芽园丁\")", + "type": "string" + } + } + }, + "request.CreateMyPlant": { + "type": "object", + "properties": { + "carePlans": { + "description": "养护计划", + "type": "array", + "items": { + "$ref": "#/definitions/request.CarePlan" + } + }, + "name": { + "description": "植物名称", + "type": "string" + }, + "ossIds": { + "description": "图片ids", + "type": "array", + "items": { + "type": "string" + } + }, + "placement": { + "description": "摆放位置", + "type": "string" + }, + "plantTime": { + "description": "种植时间", + "type": "string" + }, + "plantingMaterial": { + "description": "植料(即土的材质)", + "type": "string" + }, + "potMaterial": { + "description": "花盆材质", + "type": "string" + }, + "potSize": { + "description": "花盆大小 如直径 20cm × 高度 18cm", + "type": "string" + }, + "sunlight": { + "description": "光照条件如每日12小时", + "type": "string" + } + } + }, + "request.CreatePost": { + "type": "object", + "properties": { + "content": { + "description": "内容", + "type": "string" + }, + "location": { + "description": "位置", + "type": "string" + }, + "ossIds": { + "description": "图片id[]", + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "description": "标题 必须", + "type": "string" + } + } + }, + "request.CreateTopic": { + "type": "object", + "properties": { + "endTime": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "startTime": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "request.CreateWiki": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "aliases": { + "description": "别名", + "type": "string" + }, + "classIds": { + "description": "分类id", + "type": "array", + "items": { + "type": "string" + } + }, + "difficulty": { + "description": "种植难度 1-5级", + "type": "integer" + }, + "distributionArea": { + "description": "分布区域", + "type": "string" + }, + "flowerDiameter": { + "description": "花直径(cm)", + "type": "integer" + }, + "floweringColor": { + "description": "花色", + "type": "string" + }, + "floweringPeriod": { + "description": "开花期", + "type": "string" + }, + "floweringShape": { + "description": "花形", + "type": "string" + }, + "foliageColor": { + "description": "叶色", + "type": "string" + }, + "foliageShape": { + "description": "叶形", + "type": "string" + }, + "foliageType": { + "description": "叶型", + "type": "string" + }, + "fruit": { + "description": "果", + "type": "string" + }, + "genus": { + "description": "属", + "type": "string" + }, + "growthHabit": { + "description": "生长习性", + "type": "string" + }, + "height": { + "description": "高度(cm)", + "type": "integer" + }, + "isHot": { + "description": "是否热门", + "type": "integer" + }, + "latinName": { + "description": "拉丁名", + "type": "string" + }, + "lifeCycle": { + "description": "生命周期", + "type": "string" + }, + "lightIntensity": { + "description": "光照强度", + "type": "string" + }, + "lightType": { + "description": "光照类型(直射,散射等)", + "type": "string" + }, + "name": { + "description": "名称", + "type": "string" + }, + "optimalTempPeriod": { + "description": "最佳温度区间", + "type": "string" + }, + "ossIds": { + "description": "图片", + "type": "array", + "items": { + "type": "string" + } + }, + "pestsDiseases": { + "description": "常见病虫害", + "type": "string" + }, + "relatedWikiIds": { + "description": "相关推荐", + "type": "array", + "items": { + "type": "string" + } + }, + "reproductionMethod": { + "description": "繁殖方法", + "type": "string" + }, + "stem": { + "description": "茎", + "type": "string" + } + } + }, + "request.CreateWikiClass": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "分类名称", + "type": "string" + }, + "ossId": { + "description": "图片id", + "type": "string" + } + } + }, + "request.GetClientList": { + "type": "object", + "properties": { + "clientId": { + "type": "string" + }, + "current": { + "description": "页码", + "type": "integer" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "name": { + "type": "string" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + } + } + }, + "request.GetMenuTree": { + "type": "object", + "properties": { + "category": { + "type": "integer" + }, + "parentId": { + "type": "string" + } + } + }, + "request.GetOssFileList": { + "type": "object", + "properties": { + "current": { + "description": "页码", + "type": "integer" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "name": { + "type": "string" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + } + } + }, + "request.GetRoleList": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "current": { + "description": "页码", + "type": "integer" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "name": { + "type": "string" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + } + } + }, + "request.GetUserList": { + "type": "object", + "properties": { + "account": { + "type": "string" + }, + "current": { + "description": "页码", + "type": "integer" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + }, + "phone": { + "type": "string" + } + } + }, + "request.GrantMenu": { + "type": "object", + "properties": { + "menuIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "roleId": { + "type": "string" + } + } + }, + "request.GrantRole": { + "type": "object", + "properties": { + "roleIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "userId": { + "type": "string" + } + } + }, + "request.IdsReq": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "request.Login": { + "type": "object", + "properties": { + "account": { + "type": "string" + }, + "captcha": { + "type": "string" + }, + "captchaId": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "request.PageInfo": { + "type": "object", + "properties": { + "current": { + "description": "页码", + "type": "integer" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + } + } + }, + "request.PostPage": { + "type": "object", + "properties": { + "current": { + "description": "页码", + "type": "integer" + }, + "hasReviewed": { + "description": "是否审核通过", + "type": "integer" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + }, + "title": { + "description": "标题", + "type": "string" + } + } + }, + "request.UpdateBadge": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "comparator": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dimension": { + "description": "维度: EXPERTISE(专家), PERSISTENCE(勤勉), JOURNAL(记录)", + "type": "string" + }, + "groupId": { + "description": "组Id: 用于前端聚合显示 (e.g. \"fertilizerMaster\")", + "type": "string" + }, + "iconId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "rewardSunlight": { + "type": "integer" + }, + "sort": { + "type": "integer" + }, + "targetAction": { + "description": "触发动作: ACT_FERTILIZE, ACT_WATER...", + "type": "string" + }, + "threshold": { + "type": "integer" + }, + "tier": { + "description": "等级: 1=铜, 2=银, 3=金", + "type": "integer" + } + } + }, + "request.UpdateLevelConf": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string" + }, + "level": { + "description": "等级数值", + "type": "integer" + }, + "minSunlight": { + "description": "达到该等级所需的最小阳光值", + "type": "integer" + }, + "perks": { + "description": "解锁权益描述 (e.g., \"解锁智能诊断\")", + "type": "string" + }, + "title": { + "description": "等级称号 (e.g., \"萌芽园丁\")", + "type": "string" + } + } + }, + "request.UpdateMyPlant": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "carePlans": { + "type": "array", + "items": { + "$ref": "#/definitions/request.UpdatePlan" + } + }, + "id": { + "type": "string" + }, + "name": { + "description": "植物名称", + "type": "string" + }, + "placement": { + "description": "摆放位置", + "type": "string" + }, + "plantingMaterial": { + "description": "植料(即土的材质)", + "type": "string" + }, + "potMaterial": { + "description": "花盆材质", + "type": "string" + }, + "potSize": { + "description": "花盆大小 如直径 20cm × 高度 18cm", + "type": "string" + }, + "sunlight": { + "description": "光照条件如每日12小时", + "type": "string" + } + } + }, + "request.UpdatePlan": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "icon": { + "description": "icon信息", + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "description": "农事名称", + "type": "string" + }, + "period": { + "description": "周期", + "type": "integer" + } + } + }, + "request.UpdateProfile": { + "type": "object", + "properties": { + "avatarId": { + "type": "string" + }, + "nickname": { + "type": "string" + } + } + }, + "request.UpdateTopic": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "endTime": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "remark": { + "type": "string" + }, + "startTime": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "request.UpdateWiki": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "aliases": { + "description": "别名", + "type": "string" + }, + "difficulty": { + "description": "种植难度 1-5级", + "type": "integer" + }, + "distributionArea": { + "description": "分布区域", + "type": "string" + }, + "flowerDiameter": { + "description": "花直径(cm)", + "type": "integer" + }, + "floweringColor": { + "description": "花色", + "type": "string" + }, + "floweringPeriod": { + "description": "开花期", + "type": "string" + }, + "floweringShape": { + "description": "花形", + "type": "string" + }, + "foliageColor": { + "description": "叶色", + "type": "string" + }, + "foliageShape": { + "description": "叶形", + "type": "string" + }, + "foliageType": { + "description": "叶型", + "type": "string" + }, + "fruit": { + "description": "果", + "type": "string" + }, + "genus": { + "description": "属", + "type": "string" + }, + "growthHabit": { + "description": "生长习性", + "type": "string" + }, + "height": { + "description": "高度(cm)", + "type": "integer" + }, + "id": { + "type": "string" + }, + "latinName": { + "description": "拉丁名", + "type": "string" + }, + "lifeCycle": { + "description": "生命周期", + "type": "string" + }, + "lightIntensity": { + "description": "光照强度", + "type": "string" + }, + "lightType": { + "description": "光照类型(直射,散射等)", + "type": "string" + }, + "name": { + "description": "名称", + "type": "string" + }, + "optimalTempPeriod": { + "description": "最佳温度区间", + "type": "string" + }, + "pestsDiseases": { + "description": "常见病虫害", + "type": "string" + }, + "stem": { + "description": "茎", + "type": "string" + } + } + }, + "request.UpdateWikiClass": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "description": "分类名称", + "type": "string" + }, + "ossId": { + "description": "图片id", + "type": "string" + } + } + }, + "request.WikiPage": { + "type": "object", + "properties": { + "classId": { + "description": "分类id", + "type": "array", + "items": { + "type": "string" + } + }, + "current": { + "description": "页码", + "type": "integer" + }, + "isHot": { + "description": "是否热门 0否 1是", + "type": "integer" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "name": { + "description": "植物名称", + "type": "string" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + } + } + }, + "response.CaptchaRes": { + "type": "object", + "properties": { + "captcha": { + "type": "string" + }, + "captchaId": { + "type": "string" + } + } + }, + "response.LoginResponse": { + "type": "object", + "properties": { + "expiresAt": { + "type": "integer" + }, + "token": { + "type": "string" + }, + "user": { + "$ref": "#/definitions/system.User" + } + } + }, + "response.PageResult": { + "type": "object", + "properties": { + "list": {}, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "response.Response": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "msg": { + "type": "string" + } + } + }, + "system.Client": { + "type": "object", + "properties": { + "activeTimeout": { + "type": "integer" + }, + "additionalInfo": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "createdAtStr": { + "type": "string" + }, + "grantType": { + "type": "string" + }, + "id": { + "description": "主键ID", + "type": "string" + }, + "name": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, + "system.Menu": { + "type": "object", + "properties": { + "category": { + "type": "integer" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/system.Menu" + } + }, + "code": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "createdAtStr": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "id": { + "description": "主键ID", + "type": "string" + }, + "locale": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parentId": { + "type": "string" + }, + "path": { + "type": "string" + }, + "permission": { + "type": "string" + }, + "sort": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, + "system.Oss": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "createdAtStr": { + "type": "string" + }, + "id": { + "description": "主键ID", + "type": "string" + }, + "key": { + "type": "string" + }, + "md5": { + "type": "string" + }, + "name": { + "type": "string" + }, + "suffix": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "system.Role": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "createdAtStr": { + "type": "string" + }, + "id": { + "description": "主键ID", + "type": "string" + }, + "menus": { + "type": "array", + "items": { + "$ref": "#/definitions/system.Menu" + } + }, + "name": { + "type": "string" + }, + "sort": { + "type": "integer" + }, + "updatedAt": { + "type": "string" + } + } + }, + "system.User": { + "type": "object", + "properties": { + "account": { + "type": "string" + }, + "avatar": { + "$ref": "#/definitions/system.Oss" + }, + "avatarId": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "createdAtStr": { + "type": "string" + }, + "id": { + "description": "主键ID", + "type": "string" + }, + "miniOpenId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "nickName": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "saOpenId": { + "type": "string" + }, + "sessionKey": { + "type": "string" + }, + "tenantId": { + "type": "string" + }, + "unionId": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + } + }, + "securityDefinitions": { + "BearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "v1.0.0", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "Swagger API接口文档", + Description: "使用gin + gorm进行极速开发的全栈开发基础平台", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..36d7968 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,4379 @@ +{ + "swagger": "2.0", + "info": { + "description": "使用gin + gorm进行极速开发的全栈开发基础平台", + "title": "Swagger API接口文档", + "contact": {}, + "version": "v1.0.0" + }, + "paths": { + "/auth/captcha": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "登录相关" + ], + "summary": "获取验证码", + "responses": { + "200": { + "description": "获取验证码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.CaptchaRes" + } + } + } + ] + } + } + } + } + }, + "/auth/getLocation": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "登录相关" + ], + "summary": "获取位置信息", + "parameters": [ + { + "type": "string", + "description": "longitude", + "name": "longitude", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "latitude", + "name": "latitude", + "in": "query", + "required": true + } + ], + "responses": {} + } + }, + "/auth/getPhone": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "登录相关" + ], + "summary": "获取手机号", + "parameters": [ + { + "type": "string", + "description": "code", + "name": "code", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "openId", + "name": "openId", + "in": "query", + "required": true + } + ], + "responses": {} + } + }, + "/auth/getWeather": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "登录相关" + ], + "summary": "获取天气信息", + "parameters": [ + { + "type": "string", + "description": "adcode", + "name": "adcode", + "in": "query", + "required": true + } + ], + "responses": {} + } + }, + "/auth/login": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "登录相关" + ], + "summary": "pc登录", + "parameters": [ + { + "description": "用户名, 密码, 验证码,验证码id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Login" + } + } + ], + "responses": { + "200": { + "description": "登录成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/auth/logout": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "登录相关" + ], + "summary": "pc登出", + "responses": { + "200": { + "description": "登出成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/auth/miniLogin": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "登录相关" + ], + "summary": "小程序登录", + "parameters": [ + { + "type": "string", + "description": "code", + "name": "code", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "小程序登录", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.LoginResponse" + } + } + } + ] + } + } + } + } + }, + "/classify/myClassifyLog": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "识别相关" + ], + "summary": "我的植物识别记录", + "parameters": [ + { + "description": "分页", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "识别记录", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/classify/plant": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "识别相关" + ], + "summary": "base64植物识别", + "parameters": [ + { + "type": "file", + "description": "植物识别", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "文件OCR", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/client/delete": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "客户端管理" + ], + "summary": "删除client", + "parameters": [ + { + "description": "ids", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "删除client", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/client/detail": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "id获取详情", + "produces": [ + "application/json" + ], + "tags": [ + "客户端管理" + ], + "summary": "获取client详情", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "获取client详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/system.Client" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/client/getClientList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "客户端管理" + ], + "summary": "获取client列表", + "parameters": [ + { + "description": "client", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetClientList" + } + } + ], + "responses": { + "200": { + "description": "获取client列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/client/save": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "客户端管理" + ], + "summary": "创建client", + "parameters": [ + { + "description": "client", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.Client" + } + } + ], + "responses": { + "200": { + "description": "创建client", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/client/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "客户端管理" + ], + "summary": "更新client", + "parameters": [ + { + "description": "client", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.Client" + } + } + ], + "responses": { + "200": { + "description": "更新client", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/config/badge/add": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "徽章配置" + ], + "summary": "添加徽章配置", + "parameters": [ + { + "description": "添加徽章配置", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreateBadge" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"添加成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/config/badge/delete": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "徽章配置" + ], + "summary": "删除徽章配置", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"删除成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/config/badge/find": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "徽章配置" + ], + "summary": "根据ID获取徽章配置", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/config/badge/tree": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "徽章配置" + ], + "summary": "获取徽章配置树形结构", + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/config/badge/update": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "徽章配置" + ], + "summary": "更新徽章配置", + "parameters": [ + { + "description": "更新参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.UpdateBadge" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"更新成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/config/level/add": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "等级配置" + ], + "summary": "添加等级配置", + "parameters": [ + { + "description": "添加等级配置", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreateLevelConf" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"添加成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/config/level/detail": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "等级配置" + ], + "summary": "等级配置详情", + "parameters": [ + { + "type": "string", + "description": "等级配置id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"查询成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/config/level/list": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "等级配置" + ], + "summary": "等级配置列表", + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"查询成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/config/level/update": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "等级配置" + ], + "summary": "修改等级配置", + "parameters": [ + { + "description": "修改等级配置", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.UpdateLevelConf" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"修改成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/menu/delete": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除menu", + "produces": [ + "application/json" + ], + "tags": [ + "菜单管理" + ], + "summary": "删除menu", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/detail": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "id获取详情", + "produces": [ + "application/json" + ], + "tags": [ + "菜单管理" + ], + "summary": "获取menu详情", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/system.Menu" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getAllMenuTree": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "菜单管理" + ], + "summary": "获取所有菜单树", + "parameters": [ + { + "description": "菜单信息", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetMenuTree" + } + } + ], + "responses": { + "200": { + "description": "获取所有菜单树", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/system.Menu" + } + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getUserMenuTree": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "菜单管理" + ], + "summary": "用户菜单数据", + "responses": { + "200": { + "description": "用户菜单数据", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/system.Menu" + } + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/route": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "菜单管理" + ], + "summary": "用户路由", + "responses": { + "200": { + "description": "用户route", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/system.Menu" + } + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/save": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "菜单管理" + ], + "summary": "新增菜单", + "parameters": [ + { + "description": "menu", + "name": "data", + "in": "body", + "schema": { + "$ref": "#/definitions/system.Menu" + } + } + ], + "responses": { + "200": { + "description": "新建菜单/按钮", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "菜单管理" + ], + "summary": "更新菜单", + "parameters": [ + { + "description": "menu", + "name": "data", + "in": "body", + "schema": { + "$ref": "#/definitions/system.Menu" + } + } + ], + "responses": { + "200": { + "description": "更新菜单", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/oss/delete": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "文件相关" + ], + "summary": "删除文件", + "parameters": [ + { + "description": "批量删除文件", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "删除文件", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/oss/detail": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "文件相关" + ], + "summary": "文件详情", + "parameters": [ + { + "type": "string", + "description": "文件id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "文件详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/oss/getFileList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "文件相关" + ], + "summary": "文件列表", + "parameters": [ + { + "description": "文件列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetOssFileList" + } + } + ], + "responses": { + "200": { + "description": "文件列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/oss/upload": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "文件相关" + ], + "summary": "文件上传", + "parameters": [ + { + "type": "file", + "description": "上传文件", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "上传文件", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/plant/add": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "添加植物", + "parameters": [ + { + "description": "创建植物", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreateMyPlant" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"添加成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/plant/completeTask": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "完成任务", + "parameters": [ + { + "description": "完成任务", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CompleteTask" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"完成任务\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/plant/deletePlan": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "删除任务", + "parameters": [ + { + "description": "删除植物", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"删除成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/plant/deletePlant": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "删除植物", + "parameters": [ + { + "description": "删除植物", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"删除成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/plant/detail": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "ById植物详情", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取ById成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/plant/growth/add": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "添加成长记录", + "parameters": [ + { + "description": "添加成长记录", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreateGrowthRecord" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"添加成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/plant/page": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "植物列表", + "parameters": [ + { + "description": "分页获取植物列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/plant/plan/add": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "添加养护事项", + "parameters": [ + { + "description": "添加养护事项", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreateCarePlan" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"添加成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/plant/plan/delete": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "删除养护事项", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"删除成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/plant/todayTask": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "今日任务", + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/plant/update": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "我的植物" + ], + "summary": "修改ById植物", + "parameters": [ + { + "description": "修改ById植物", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.UpdateMyPlant" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"修改ById成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/post/comment": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "帖子" + ], + "summary": "评论帖子", + "parameters": [ + { + "description": "评论帖子", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreateComment" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"评论成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/post/like": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "帖子" + ], + "summary": "点赞帖子", + "parameters": [ + { + "type": "string", + "description": "帖子id", + "name": "id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "点赞类型 1 点赞 2 取消点赞", + "name": "type", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"点赞成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/post/myPost": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "帖子" + ], + "summary": "我的发布", + "parameters": [ + { + "description": "分页获取帖子列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PostPage" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/post/page": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "帖子" + ], + "summary": "帖子列表", + "parameters": [ + { + "description": "分页获取帖子列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PostPage" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/post/publish": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "帖子" + ], + "summary": "发布帖子", + "parameters": [ + { + "description": "发布帖子", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreatePost" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发布成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/profile/detail": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "个人中心" + ], + "summary": "用户详情", + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"查询成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/profile/update": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "个人中心" + ], + "summary": "修改用户信息", + "parameters": [ + { + "description": "修改用户信息", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.UpdateProfile" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"修改成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/role/delete": { + "post": { + "description": "删除角色", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "删除角色", + "parameters": [ + { + "description": "批量删除角色", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "删除角色", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/role/detail": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "角色详情", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "角色详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/system.Role" + } + } + } + ] + } + } + } + } + }, + "/role/getRoleList": { + "post": { + "description": "获取角色列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "获取角色列表", + "parameters": [ + { + "description": "页码, 每页大小, 搜索条件", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetRoleList" + } + } + ], + "responses": { + "200": { + "description": "获取角色列表,返回包括列表,总数,页码,每页大小", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/role/grantMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "授权菜单给角色", + "parameters": [ + { + "description": "授权菜单给角色", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GrantMenu" + } + } + ], + "responses": { + "200": { + "description": "授权菜单给角色", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/role/save": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "创建角色", + "parameters": [ + { + "description": "角色信息", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.Role" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/role/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "修改角色", + "parameters": [ + { + "description": "角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.Role" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/topic/add": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "帖子话题" + ], + "summary": "修改话题", + "parameters": [ + { + "description": "修改话题", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.UpdateTopic" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发布成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/topic/delete": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "帖子话题" + ], + "summary": "删除任务", + "parameters": [ + { + "description": "删除话题", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"删除成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/topic/detail": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "帖子话题" + ], + "summary": "话题详情", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/topic/list": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "帖子话题" + ], + "summary": "话题列表", + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/topic/page": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "帖子话题" + ], + "summary": "话题分页", + "parameters": [ + { + "description": "分页获取话题列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/user/changePassword": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改密码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "修改密码", + "parameters": [ + { + "description": "用户id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ChangePwd" + } + } + ], + "responses": { + "200": { + "description": "修改密码成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/system.User" + } + } + } + ] + } + } + } + } + }, + "/user/delete": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "删除用户", + "parameters": [ + { + "description": "批量删除用户", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "删除用户", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/detail": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "获取用户详情", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "获取用户详情成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/system.User" + } + } + } + ] + } + } + } + } + }, + "/user/getUserList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "获取用户列表", + "parameters": [ + { + "description": "页码, 每页大小, 搜索条件", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetUserList" + } + } + ], + "responses": { + "200": { + "description": "获取用户列表,返回包括列表,总数,页码,每页大小", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/grantRole": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "给用户分配角色", + "parameters": [ + { + "description": "用户ID, 角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GrantRole" + } + } + ], + "responses": { + "200": { + "description": "{\"code\": 200, \"data\": [...]}", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/user/info": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "当前登录用户", + "responses": { + "200": { + "description": "{\"code\": 200, \"data\": {}, \"msg\": \"添加成功\"}", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/user/save": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "新增用户", + "parameters": [ + { + "description": "用户信息", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.User" + } + } + ], + "responses": { + "200": { + "description": "{\"code\": 200, \"data\": {}, \"msg\": \"添加成功\"}", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/user/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "更新用户", + "parameters": [ + { + "description": "用户ID,用户信息", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.User" + } + } + ], + "responses": { + "200": { + "description": "{\"code\": 200, \"data\": [...]}", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/wiki-class/add": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "百科分类" + ], + "summary": "添加分类", + "parameters": [ + { + "description": "添加分类", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreateWikiClass" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发布成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/wiki-class/delete": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "百科分类" + ], + "summary": "删除分类", + "parameters": [ + { + "description": "删除分类", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"删除成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/wiki-class/detail": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "百科分类" + ], + "summary": "分类详情", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/wiki-class/list": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "百科分类" + ], + "summary": "分类列表", + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/wiki-class/page": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "百科分类" + ], + "summary": "分类分页", + "parameters": [ + { + "description": "分页获取分类列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/wiki-class/update": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "百科分类" + ], + "summary": "修改分类(可直接传入ossId修改图片)", + "parameters": [ + { + "description": "修改分类", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.UpdateWikiClass" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发布成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/wiki/add": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "百科" + ], + "summary": "添加百科", + "parameters": [ + { + "description": "添加百科", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreateWiki" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发布成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/wiki/detail": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "百科" + ], + "summary": "百科详情", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/wiki/page": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "百科" + ], + "summary": "分页", + "parameters": [ + { + "description": "百科分页", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WikiPage" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发布成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/wiki/update": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "百科" + ], + "summary": "修改百科", + "parameters": [ + { + "description": "修改百科", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.UpdateWiki" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发布成功\"}", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "request.CarePlan": { + "type": "object", + "properties": { + "icon": { + "description": "icon信息", + "type": "string" + }, + "name": { + "description": "农事名称", + "type": "string" + }, + "period": { + "description": "周期", + "type": "integer" + } + } + }, + "request.ChangePwd": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "newPwd": { + "type": "string" + } + } + }, + "request.CompleteTask": { + "type": "object", + "required": [ + "taskId" + ], + "properties": { + "remark": { + "type": "string" + }, + "taskId": { + "type": "string" + } + } + }, + "request.CreateBadge": { + "type": "object", + "properties": { + "comparator": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dimension": { + "description": "维度: EXPERTISE(专家), PERSISTENCE(勤勉), JOURNAL(记录)", + "type": "string" + }, + "groupId": { + "description": "组ID: 用于前端聚合显示 (e.g. \"fertilizerMaster\")", + "type": "string" + }, + "iconId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "rewardSunlight": { + "type": "integer" + }, + "sort": { + "type": "integer" + }, + "targetAction": { + "description": "触发动作: ACT_FERTILIZE, ACT_WATER...", + "type": "string" + }, + "threshold": { + "type": "integer" + }, + "tier": { + "description": "等级: 1=铜, 2=银, 3=金", + "type": "integer" + } + } + }, + "request.CreateCarePlan": { + "type": "object", + "required": [ + "plantId" + ], + "properties": { + "icon": { + "description": "icon信息", + "type": "string" + }, + "name": { + "description": "农事名称", + "type": "string" + }, + "period": { + "description": "周期", + "type": "integer" + }, + "plantId": { + "type": "string" + } + } + }, + "request.CreateComment": { + "type": "object", + "required": [ + "content", + "postId" + ], + "properties": { + "content": { + "description": "评论内容", + "type": "string" + }, + "postId": { + "description": "帖子id", + "type": "string" + } + } + }, + "request.CreateGrowthRecord": { + "type": "object", + "required": [ + "plantId" + ], + "properties": { + "content": { + "type": "string" + }, + "desc": { + "type": "string" + }, + "name": { + "type": "string" + }, + "ossIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "plantId": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "request.CreateLevelConf": { + "type": "object", + "properties": { + "level": { + "description": "等级数值", + "type": "integer" + }, + "minSunlight": { + "description": "达到该等级所需的最小阳光值", + "type": "integer" + }, + "perks": { + "description": "解锁权益描述 (e.g., \"解锁智能诊断\")", + "type": "string" + }, + "title": { + "description": "等级称号 (e.g., \"萌芽园丁\")", + "type": "string" + } + } + }, + "request.CreateMyPlant": { + "type": "object", + "properties": { + "carePlans": { + "description": "养护计划", + "type": "array", + "items": { + "$ref": "#/definitions/request.CarePlan" + } + }, + "name": { + "description": "植物名称", + "type": "string" + }, + "ossIds": { + "description": "图片ids", + "type": "array", + "items": { + "type": "string" + } + }, + "placement": { + "description": "摆放位置", + "type": "string" + }, + "plantTime": { + "description": "种植时间", + "type": "string" + }, + "plantingMaterial": { + "description": "植料(即土的材质)", + "type": "string" + }, + "potMaterial": { + "description": "花盆材质", + "type": "string" + }, + "potSize": { + "description": "花盆大小 如直径 20cm × 高度 18cm", + "type": "string" + }, + "sunlight": { + "description": "光照条件如每日12小时", + "type": "string" + } + } + }, + "request.CreatePost": { + "type": "object", + "properties": { + "content": { + "description": "内容", + "type": "string" + }, + "location": { + "description": "位置", + "type": "string" + }, + "ossIds": { + "description": "图片id[]", + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "description": "标题 必须", + "type": "string" + } + } + }, + "request.CreateTopic": { + "type": "object", + "properties": { + "endTime": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "startTime": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "request.CreateWiki": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "aliases": { + "description": "别名", + "type": "string" + }, + "classIds": { + "description": "分类id", + "type": "array", + "items": { + "type": "string" + } + }, + "difficulty": { + "description": "种植难度 1-5级", + "type": "integer" + }, + "distributionArea": { + "description": "分布区域", + "type": "string" + }, + "flowerDiameter": { + "description": "花直径(cm)", + "type": "integer" + }, + "floweringColor": { + "description": "花色", + "type": "string" + }, + "floweringPeriod": { + "description": "开花期", + "type": "string" + }, + "floweringShape": { + "description": "花形", + "type": "string" + }, + "foliageColor": { + "description": "叶色", + "type": "string" + }, + "foliageShape": { + "description": "叶形", + "type": "string" + }, + "foliageType": { + "description": "叶型", + "type": "string" + }, + "fruit": { + "description": "果", + "type": "string" + }, + "genus": { + "description": "属", + "type": "string" + }, + "growthHabit": { + "description": "生长习性", + "type": "string" + }, + "height": { + "description": "高度(cm)", + "type": "integer" + }, + "isHot": { + "description": "是否热门", + "type": "integer" + }, + "latinName": { + "description": "拉丁名", + "type": "string" + }, + "lifeCycle": { + "description": "生命周期", + "type": "string" + }, + "lightIntensity": { + "description": "光照强度", + "type": "string" + }, + "lightType": { + "description": "光照类型(直射,散射等)", + "type": "string" + }, + "name": { + "description": "名称", + "type": "string" + }, + "optimalTempPeriod": { + "description": "最佳温度区间", + "type": "string" + }, + "ossIds": { + "description": "图片", + "type": "array", + "items": { + "type": "string" + } + }, + "pestsDiseases": { + "description": "常见病虫害", + "type": "string" + }, + "relatedWikiIds": { + "description": "相关推荐", + "type": "array", + "items": { + "type": "string" + } + }, + "reproductionMethod": { + "description": "繁殖方法", + "type": "string" + }, + "stem": { + "description": "茎", + "type": "string" + } + } + }, + "request.CreateWikiClass": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "分类名称", + "type": "string" + }, + "ossId": { + "description": "图片id", + "type": "string" + } + } + }, + "request.GetClientList": { + "type": "object", + "properties": { + "clientId": { + "type": "string" + }, + "current": { + "description": "页码", + "type": "integer" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "name": { + "type": "string" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + } + } + }, + "request.GetMenuTree": { + "type": "object", + "properties": { + "category": { + "type": "integer" + }, + "parentId": { + "type": "string" + } + } + }, + "request.GetOssFileList": { + "type": "object", + "properties": { + "current": { + "description": "页码", + "type": "integer" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "name": { + "type": "string" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + } + } + }, + "request.GetRoleList": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "current": { + "description": "页码", + "type": "integer" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "name": { + "type": "string" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + } + } + }, + "request.GetUserList": { + "type": "object", + "properties": { + "account": { + "type": "string" + }, + "current": { + "description": "页码", + "type": "integer" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + }, + "phone": { + "type": "string" + } + } + }, + "request.GrantMenu": { + "type": "object", + "properties": { + "menuIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "roleId": { + "type": "string" + } + } + }, + "request.GrantRole": { + "type": "object", + "properties": { + "roleIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "userId": { + "type": "string" + } + } + }, + "request.IdsReq": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "request.Login": { + "type": "object", + "properties": { + "account": { + "type": "string" + }, + "captcha": { + "type": "string" + }, + "captchaId": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "request.PageInfo": { + "type": "object", + "properties": { + "current": { + "description": "页码", + "type": "integer" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + } + } + }, + "request.PostPage": { + "type": "object", + "properties": { + "current": { + "description": "页码", + "type": "integer" + }, + "hasReviewed": { + "description": "是否审核通过", + "type": "integer" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + }, + "title": { + "description": "标题", + "type": "string" + } + } + }, + "request.UpdateBadge": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "comparator": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dimension": { + "description": "维度: EXPERTISE(专家), PERSISTENCE(勤勉), JOURNAL(记录)", + "type": "string" + }, + "groupId": { + "description": "组Id: 用于前端聚合显示 (e.g. \"fertilizerMaster\")", + "type": "string" + }, + "iconId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "rewardSunlight": { + "type": "integer" + }, + "sort": { + "type": "integer" + }, + "targetAction": { + "description": "触发动作: ACT_FERTILIZE, ACT_WATER...", + "type": "string" + }, + "threshold": { + "type": "integer" + }, + "tier": { + "description": "等级: 1=铜, 2=银, 3=金", + "type": "integer" + } + } + }, + "request.UpdateLevelConf": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string" + }, + "level": { + "description": "等级数值", + "type": "integer" + }, + "minSunlight": { + "description": "达到该等级所需的最小阳光值", + "type": "integer" + }, + "perks": { + "description": "解锁权益描述 (e.g., \"解锁智能诊断\")", + "type": "string" + }, + "title": { + "description": "等级称号 (e.g., \"萌芽园丁\")", + "type": "string" + } + } + }, + "request.UpdateMyPlant": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "carePlans": { + "type": "array", + "items": { + "$ref": "#/definitions/request.UpdatePlan" + } + }, + "id": { + "type": "string" + }, + "name": { + "description": "植物名称", + "type": "string" + }, + "placement": { + "description": "摆放位置", + "type": "string" + }, + "plantingMaterial": { + "description": "植料(即土的材质)", + "type": "string" + }, + "potMaterial": { + "description": "花盆材质", + "type": "string" + }, + "potSize": { + "description": "花盆大小 如直径 20cm × 高度 18cm", + "type": "string" + }, + "sunlight": { + "description": "光照条件如每日12小时", + "type": "string" + } + } + }, + "request.UpdatePlan": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "icon": { + "description": "icon信息", + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "description": "农事名称", + "type": "string" + }, + "period": { + "description": "周期", + "type": "integer" + } + } + }, + "request.UpdateProfile": { + "type": "object", + "properties": { + "avatarId": { + "type": "string" + }, + "nickname": { + "type": "string" + } + } + }, + "request.UpdateTopic": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "endTime": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "remark": { + "type": "string" + }, + "startTime": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "request.UpdateWiki": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "aliases": { + "description": "别名", + "type": "string" + }, + "difficulty": { + "description": "种植难度 1-5级", + "type": "integer" + }, + "distributionArea": { + "description": "分布区域", + "type": "string" + }, + "flowerDiameter": { + "description": "花直径(cm)", + "type": "integer" + }, + "floweringColor": { + "description": "花色", + "type": "string" + }, + "floweringPeriod": { + "description": "开花期", + "type": "string" + }, + "floweringShape": { + "description": "花形", + "type": "string" + }, + "foliageColor": { + "description": "叶色", + "type": "string" + }, + "foliageShape": { + "description": "叶形", + "type": "string" + }, + "foliageType": { + "description": "叶型", + "type": "string" + }, + "fruit": { + "description": "果", + "type": "string" + }, + "genus": { + "description": "属", + "type": "string" + }, + "growthHabit": { + "description": "生长习性", + "type": "string" + }, + "height": { + "description": "高度(cm)", + "type": "integer" + }, + "id": { + "type": "string" + }, + "latinName": { + "description": "拉丁名", + "type": "string" + }, + "lifeCycle": { + "description": "生命周期", + "type": "string" + }, + "lightIntensity": { + "description": "光照强度", + "type": "string" + }, + "lightType": { + "description": "光照类型(直射,散射等)", + "type": "string" + }, + "name": { + "description": "名称", + "type": "string" + }, + "optimalTempPeriod": { + "description": "最佳温度区间", + "type": "string" + }, + "pestsDiseases": { + "description": "常见病虫害", + "type": "string" + }, + "stem": { + "description": "茎", + "type": "string" + } + } + }, + "request.UpdateWikiClass": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "description": "分类名称", + "type": "string" + }, + "ossId": { + "description": "图片id", + "type": "string" + } + } + }, + "request.WikiPage": { + "type": "object", + "properties": { + "classId": { + "description": "分类id", + "type": "array", + "items": { + "type": "string" + } + }, + "current": { + "description": "页码", + "type": "integer" + }, + "isHot": { + "description": "是否热门 0否 1是", + "type": "integer" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "name": { + "description": "植物名称", + "type": "string" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + } + } + }, + "response.CaptchaRes": { + "type": "object", + "properties": { + "captcha": { + "type": "string" + }, + "captchaId": { + "type": "string" + } + } + }, + "response.LoginResponse": { + "type": "object", + "properties": { + "expiresAt": { + "type": "integer" + }, + "token": { + "type": "string" + }, + "user": { + "$ref": "#/definitions/system.User" + } + } + }, + "response.PageResult": { + "type": "object", + "properties": { + "list": {}, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "response.Response": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "msg": { + "type": "string" + } + } + }, + "system.Client": { + "type": "object", + "properties": { + "activeTimeout": { + "type": "integer" + }, + "additionalInfo": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "createdAtStr": { + "type": "string" + }, + "grantType": { + "type": "string" + }, + "id": { + "description": "主键ID", + "type": "string" + }, + "name": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, + "system.Menu": { + "type": "object", + "properties": { + "category": { + "type": "integer" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/system.Menu" + } + }, + "code": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "createdAtStr": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "id": { + "description": "主键ID", + "type": "string" + }, + "locale": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parentId": { + "type": "string" + }, + "path": { + "type": "string" + }, + "permission": { + "type": "string" + }, + "sort": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, + "system.Oss": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "createdAtStr": { + "type": "string" + }, + "id": { + "description": "主键ID", + "type": "string" + }, + "key": { + "type": "string" + }, + "md5": { + "type": "string" + }, + "name": { + "type": "string" + }, + "suffix": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "system.Role": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "createdAtStr": { + "type": "string" + }, + "id": { + "description": "主键ID", + "type": "string" + }, + "menus": { + "type": "array", + "items": { + "$ref": "#/definitions/system.Menu" + } + }, + "name": { + "type": "string" + }, + "sort": { + "type": "integer" + }, + "updatedAt": { + "type": "string" + } + } + }, + "system.User": { + "type": "object", + "properties": { + "account": { + "type": "string" + }, + "avatar": { + "$ref": "#/definitions/system.Oss" + }, + "avatarId": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "createdAtStr": { + "type": "string" + }, + "id": { + "description": "主键ID", + "type": "string" + }, + "miniOpenId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "nickName": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "saOpenId": { + "type": "string" + }, + "sessionKey": { + "type": "string" + }, + "tenantId": { + "type": "string" + }, + "unionId": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + } + }, + "securityDefinitions": { + "BearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..0f4c487 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,2699 @@ +definitions: + request.CarePlan: + properties: + icon: + description: icon信息 + type: string + name: + description: 农事名称 + type: string + period: + description: 周期 + type: integer + type: object + request.ChangePwd: + properties: + id: + type: string + newPwd: + type: string + type: object + request.CompleteTask: + properties: + remark: + type: string + taskId: + type: string + required: + - taskId + type: object + request.CreateBadge: + properties: + comparator: + type: string + description: + type: string + dimension: + description: '维度: EXPERTISE(专家), PERSISTENCE(勤勉), JOURNAL(记录)' + type: string + groupId: + description: '组ID: 用于前端聚合显示 (e.g. "fertilizerMaster")' + type: string + iconId: + type: string + name: + type: string + rewardSunlight: + type: integer + sort: + type: integer + targetAction: + description: '触发动作: ACT_FERTILIZE, ACT_WATER...' + type: string + threshold: + type: integer + tier: + description: '等级: 1=铜, 2=银, 3=金' + type: integer + type: object + request.CreateCarePlan: + properties: + icon: + description: icon信息 + type: string + name: + description: 农事名称 + type: string + period: + description: 周期 + type: integer + plantId: + type: string + required: + - plantId + type: object + request.CreateComment: + properties: + content: + description: 评论内容 + type: string + postId: + description: 帖子id + type: string + required: + - content + - postId + type: object + request.CreateGrowthRecord: + properties: + content: + type: string + desc: + type: string + name: + type: string + ossIds: + items: + type: string + type: array + plantId: + type: string + tag: + type: string + required: + - plantId + type: object + request.CreateLevelConf: + properties: + level: + description: 等级数值 + type: integer + minSunlight: + description: 达到该等级所需的最小阳光值 + type: integer + perks: + description: 解锁权益描述 (e.g., "解锁智能诊断") + type: string + title: + description: 等级称号 (e.g., "萌芽园丁") + type: string + type: object + request.CreateMyPlant: + properties: + carePlans: + description: 养护计划 + items: + $ref: '#/definitions/request.CarePlan' + type: array + name: + description: 植物名称 + type: string + ossIds: + description: 图片ids + items: + type: string + type: array + placement: + description: 摆放位置 + type: string + plantTime: + description: 种植时间 + type: string + plantingMaterial: + description: 植料(即土的材质) + type: string + potMaterial: + description: 花盆材质 + type: string + potSize: + description: 花盆大小 如直径 20cm × 高度 18cm + type: string + sunlight: + description: 光照条件如每日12小时 + type: string + type: object + request.CreatePost: + properties: + content: + description: 内容 + type: string + location: + description: 位置 + type: string + ossIds: + description: 图片id[] + items: + type: string + type: array + title: + description: 标题 必须 + type: string + type: object + request.CreateTopic: + properties: + endTime: + type: string + remark: + type: string + startTime: + type: string + title: + type: string + type: object + request.CreateWiki: + properties: + aliases: + description: 别名 + type: string + classIds: + description: 分类id + items: + type: string + type: array + difficulty: + description: 种植难度 1-5级 + type: integer + distributionArea: + description: 分布区域 + type: string + flowerDiameter: + description: 花直径(cm) + type: integer + floweringColor: + description: 花色 + type: string + floweringPeriod: + description: 开花期 + type: string + floweringShape: + description: 花形 + type: string + foliageColor: + description: 叶色 + type: string + foliageShape: + description: 叶形 + type: string + foliageType: + description: 叶型 + type: string + fruit: + description: 果 + type: string + genus: + description: 属 + type: string + growthHabit: + description: 生长习性 + type: string + height: + description: 高度(cm) + type: integer + isHot: + description: 是否热门 + type: integer + latinName: + description: 拉丁名 + type: string + lifeCycle: + description: 生命周期 + type: string + lightIntensity: + description: 光照强度 + type: string + lightType: + description: 光照类型(直射,散射等) + type: string + name: + description: 名称 + type: string + optimalTempPeriod: + description: 最佳温度区间 + type: string + ossIds: + description: 图片 + items: + type: string + type: array + pestsDiseases: + description: 常见病虫害 + type: string + relatedWikiIds: + description: 相关推荐 + items: + type: string + type: array + reproductionMethod: + description: 繁殖方法 + type: string + stem: + description: 茎 + type: string + required: + - name + type: object + request.CreateWikiClass: + properties: + name: + description: 分类名称 + type: string + ossId: + description: 图片id + type: string + required: + - name + type: object + request.GetClientList: + properties: + clientId: + type: string + current: + description: 页码 + type: integer + keyword: + description: 关键字 + type: string + name: + type: string + pageSize: + description: 每页大小 + type: integer + type: object + request.GetMenuTree: + properties: + category: + type: integer + parentId: + type: string + type: object + request.GetOssFileList: + properties: + current: + description: 页码 + type: integer + keyword: + description: 关键字 + type: string + name: + type: string + pageSize: + description: 每页大小 + type: integer + type: object + request.GetRoleList: + properties: + code: + type: string + current: + description: 页码 + type: integer + keyword: + description: 关键字 + type: string + name: + type: string + pageSize: + description: 每页大小 + type: integer + type: object + request.GetUserList: + properties: + account: + type: string + current: + description: 页码 + type: integer + keyword: + description: 关键字 + type: string + pageSize: + description: 每页大小 + type: integer + phone: + type: string + type: object + request.GrantMenu: + properties: + menuIds: + items: + type: string + type: array + roleId: + type: string + type: object + request.GrantRole: + properties: + roleIds: + items: + type: string + type: array + userId: + type: string + type: object + request.IdsReq: + properties: + ids: + items: + type: string + type: array + type: object + request.Login: + properties: + account: + type: string + captcha: + type: string + captchaId: + type: string + password: + type: string + type: object + request.PageInfo: + properties: + current: + description: 页码 + type: integer + keyword: + description: 关键字 + type: string + pageSize: + description: 每页大小 + type: integer + type: object + request.PostPage: + properties: + current: + description: 页码 + type: integer + hasReviewed: + description: 是否审核通过 + type: integer + keyword: + description: 关键字 + type: string + pageSize: + description: 每页大小 + type: integer + title: + description: 标题 + type: string + type: object + request.UpdateBadge: + properties: + comparator: + type: string + description: + type: string + dimension: + description: '维度: EXPERTISE(专家), PERSISTENCE(勤勉), JOURNAL(记录)' + type: string + groupId: + description: '组Id: 用于前端聚合显示 (e.g. "fertilizerMaster")' + type: string + iconId: + type: string + id: + type: string + name: + type: string + rewardSunlight: + type: integer + sort: + type: integer + targetAction: + description: '触发动作: ACT_FERTILIZE, ACT_WATER...' + type: string + threshold: + type: integer + tier: + description: '等级: 1=铜, 2=银, 3=金' + type: integer + required: + - id + type: object + request.UpdateLevelConf: + properties: + id: + type: string + level: + description: 等级数值 + type: integer + minSunlight: + description: 达到该等级所需的最小阳光值 + type: integer + perks: + description: 解锁权益描述 (e.g., "解锁智能诊断") + type: string + title: + description: 等级称号 (e.g., "萌芽园丁") + type: string + required: + - id + type: object + request.UpdateMyPlant: + properties: + carePlans: + items: + $ref: '#/definitions/request.UpdatePlan' + type: array + id: + type: string + name: + description: 植物名称 + type: string + placement: + description: 摆放位置 + type: string + plantingMaterial: + description: 植料(即土的材质) + type: string + potMaterial: + description: 花盆材质 + type: string + potSize: + description: 花盆大小 如直径 20cm × 高度 18cm + type: string + sunlight: + description: 光照条件如每日12小时 + type: string + required: + - id + type: object + request.UpdatePlan: + properties: + icon: + description: icon信息 + type: string + id: + type: string + name: + description: 农事名称 + type: string + period: + description: 周期 + type: integer + required: + - id + type: object + request.UpdateProfile: + properties: + avatarId: + type: string + nickname: + type: string + type: object + request.UpdateTopic: + properties: + endTime: + type: string + id: + type: integer + remark: + type: string + startTime: + type: string + title: + type: string + required: + - id + type: object + request.UpdateWiki: + properties: + aliases: + description: 别名 + type: string + difficulty: + description: 种植难度 1-5级 + type: integer + distributionArea: + description: 分布区域 + type: string + flowerDiameter: + description: 花直径(cm) + type: integer + floweringColor: + description: 花色 + type: string + floweringPeriod: + description: 开花期 + type: string + floweringShape: + description: 花形 + type: string + foliageColor: + description: 叶色 + type: string + foliageShape: + description: 叶形 + type: string + foliageType: + description: 叶型 + type: string + fruit: + description: 果 + type: string + genus: + description: 属 + type: string + growthHabit: + description: 生长习性 + type: string + height: + description: 高度(cm) + type: integer + id: + type: string + latinName: + description: 拉丁名 + type: string + lifeCycle: + description: 生命周期 + type: string + lightIntensity: + description: 光照强度 + type: string + lightType: + description: 光照类型(直射,散射等) + type: string + name: + description: 名称 + type: string + optimalTempPeriod: + description: 最佳温度区间 + type: string + pestsDiseases: + description: 常见病虫害 + type: string + stem: + description: 茎 + type: string + required: + - id + type: object + request.UpdateWikiClass: + properties: + id: + type: string + name: + description: 分类名称 + type: string + ossId: + description: 图片id + type: string + required: + - id + type: object + request.WikiPage: + properties: + classId: + description: 分类id + items: + type: string + type: array + current: + description: 页码 + type: integer + isHot: + description: 是否热门 0否 1是 + type: integer + keyword: + description: 关键字 + type: string + name: + description: 植物名称 + type: string + pageSize: + description: 每页大小 + type: integer + type: object + response.CaptchaRes: + properties: + captcha: + type: string + captchaId: + type: string + type: object + response.LoginResponse: + properties: + expiresAt: + type: integer + token: + type: string + user: + $ref: '#/definitions/system.User' + type: object + response.PageResult: + properties: + list: {} + page: + type: integer + pageSize: + type: integer + total: + type: integer + type: object + response.Response: + properties: + code: + type: integer + data: {} + msg: + type: string + type: object + system.Client: + properties: + activeTimeout: + type: integer + additionalInfo: + type: string + clientId: + type: string + createdAt: + type: string + createdAtStr: + type: string + grantType: + type: string + id: + description: 主键ID + type: string + name: + type: string + updatedAt: + type: string + type: object + system.Menu: + properties: + category: + type: integer + children: + items: + $ref: '#/definitions/system.Menu' + type: array + code: + type: string + createdAt: + type: string + createdAtStr: + type: string + icon: + type: string + id: + description: 主键ID + type: string + locale: + type: string + name: + type: string + parentId: + type: string + path: + type: string + permission: + type: string + sort: + type: integer + title: + type: string + updatedAt: + type: string + type: object + system.Oss: + properties: + createdAt: + type: string + createdAtStr: + type: string + id: + description: 主键ID + type: string + key: + type: string + md5: + type: string + name: + type: string + suffix: + type: string + tag: + type: string + updatedAt: + type: string + url: + type: string + type: object + system.Role: + properties: + code: + type: string + createdAt: + type: string + createdAtStr: + type: string + id: + description: 主键ID + type: string + menus: + items: + $ref: '#/definitions/system.Menu' + type: array + name: + type: string + sort: + type: integer + updatedAt: + type: string + type: object + system.User: + properties: + account: + type: string + avatar: + $ref: '#/definitions/system.Oss' + avatarId: + type: string + clientId: + type: string + createdAt: + type: string + createdAtStr: + type: string + id: + description: 主键ID + type: string + miniOpenId: + type: string + name: + type: string + nickName: + type: string + phone: + type: string + saOpenId: + type: string + sessionKey: + type: string + tenantId: + type: string + unionId: + type: string + updatedAt: + type: string + type: object +info: + contact: {} + description: 使用gin + gorm进行极速开发的全栈开发基础平台 + title: Swagger API接口文档 + version: v1.0.0 +paths: + /auth/captcha: + get: + produces: + - application/json + responses: + "200": + description: 获取验证码 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.CaptchaRes' + type: object + summary: 获取验证码 + tags: + - 登录相关 + /auth/getLocation: + get: + parameters: + - description: longitude + in: query + name: longitude + required: true + type: string + - description: latitude + in: query + name: latitude + required: true + type: string + produces: + - application/json + responses: {} + summary: 获取位置信息 + tags: + - 登录相关 + /auth/getPhone: + get: + parameters: + - description: code + in: query + name: code + required: true + type: string + - description: openId + in: query + name: openId + required: true + type: string + produces: + - application/json + responses: {} + summary: 获取手机号 + tags: + - 登录相关 + /auth/getWeather: + get: + parameters: + - description: adcode + in: query + name: adcode + required: true + type: string + produces: + - application/json + responses: {} + summary: 获取天气信息 + tags: + - 登录相关 + /auth/login: + post: + consumes: + - application/json + parameters: + - description: 用户名, 密码, 验证码,验证码id + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.Login' + produces: + - application/json + responses: + "200": + description: 登录成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + summary: pc登录 + tags: + - 登录相关 + /auth/logout: + get: + produces: + - application/json + responses: + "200": + description: 登出成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: pc登出 + tags: + - 登录相关 + /auth/miniLogin: + get: + parameters: + - description: code + in: query + name: code + required: true + type: string + produces: + - application/json + responses: + "200": + description: 小程序登录 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.LoginResponse' + type: object + summary: 小程序登录 + tags: + - 登录相关 + /classify/myClassifyLog: + post: + consumes: + - application/json + parameters: + - description: 分页 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.PageInfo' + produces: + - application/json + responses: + "200": + description: 识别记录 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 我的植物识别记录 + tags: + - 识别相关 + /classify/plant: + post: + consumes: + - multipart/form-data + parameters: + - description: 植物识别 + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "200": + description: 文件OCR + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: base64植物识别 + tags: + - 识别相关 + /client/delete: + post: + consumes: + - application/json + parameters: + - description: ids + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.IdsReq' + produces: + - application/json + responses: + "200": + description: 删除client + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除client + tags: + - 客户端管理 + /client/detail: + get: + description: id获取详情 + parameters: + - description: id + in: query + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 获取client详情 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/system.Client' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取client详情 + tags: + - 客户端管理 + /client/getClientList: + post: + consumes: + - application/json + parameters: + - description: client + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetClientList' + produces: + - application/json + responses: + "200": + description: 获取client列表 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取client列表 + tags: + - 客户端管理 + /client/save: + post: + consumes: + - application/json + parameters: + - description: client + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.Client' + produces: + - application/json + responses: + "200": + description: 创建client + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 创建client + tags: + - 客户端管理 + /client/update: + post: + consumes: + - application/json + parameters: + - description: client + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.Client' + produces: + - application/json + responses: + "200": + description: 更新client + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 更新client + tags: + - 客户端管理 + /config/badge/add: + post: + consumes: + - application/json + parameters: + - description: 添加徽章配置 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.CreateBadge' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"添加成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 添加徽章配置 + tags: + - 徽章配置 + /config/badge/delete: + get: + parameters: + - description: id + in: query + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"删除成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 删除徽章配置 + tags: + - 徽章配置 + /config/badge/find: + get: + parameters: + - description: id + in: query + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"获取成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 根据ID获取徽章配置 + tags: + - 徽章配置 + /config/badge/tree: + get: + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"获取成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 获取徽章配置树形结构 + tags: + - 徽章配置 + /config/badge/update: + post: + consumes: + - application/json + parameters: + - description: 更新参数 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.UpdateBadge' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"更新成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 更新徽章配置 + tags: + - 徽章配置 + /config/level/add: + post: + consumes: + - application/json + parameters: + - description: 添加等级配置 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.CreateLevelConf' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"添加成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 添加等级配置 + tags: + - 等级配置 + /config/level/detail: + get: + parameters: + - description: 等级配置id + in: query + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"查询成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 等级配置详情 + tags: + - 等级配置 + /config/level/list: + get: + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"查询成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 等级配置列表 + tags: + - 等级配置 + /config/level/update: + post: + consumes: + - application/json + parameters: + - description: 修改等级配置 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.UpdateLevelConf' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"修改成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 修改等级配置 + tags: + - 等级配置 + /menu/delete: + get: + description: 删除menu + parameters: + - description: id + in: query + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 详情 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除menu + tags: + - 菜单管理 + /menu/detail: + get: + description: id获取详情 + parameters: + - description: id + in: query + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 详情 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/system.Menu' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取menu详情 + tags: + - 菜单管理 + /menu/getAllMenuTree: + post: + consumes: + - application/json + parameters: + - description: 菜单信息 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetMenuTree' + produces: + - application/json + responses: + "200": + description: 获取所有菜单树 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + items: + $ref: '#/definitions/system.Menu' + type: array + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取所有菜单树 + tags: + - 菜单管理 + /menu/getUserMenuTree: + get: + produces: + - application/json + responses: + "200": + description: 用户菜单数据 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + items: + $ref: '#/definitions/system.Menu' + type: array + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 用户菜单数据 + tags: + - 菜单管理 + /menu/route: + get: + produces: + - application/json + responses: + "200": + description: 用户route + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + items: + $ref: '#/definitions/system.Menu' + type: array + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 用户路由 + tags: + - 菜单管理 + /menu/save: + post: + consumes: + - application/json + parameters: + - description: menu + in: body + name: data + schema: + $ref: '#/definitions/system.Menu' + produces: + - application/json + responses: + "200": + description: 新建菜单/按钮 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 新增菜单 + tags: + - 菜单管理 + /menu/update: + post: + consumes: + - application/json + parameters: + - description: menu + in: body + name: data + schema: + $ref: '#/definitions/system.Menu' + produces: + - application/json + responses: + "200": + description: 更新菜单 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 更新菜单 + tags: + - 菜单管理 + /oss/delete: + post: + consumes: + - application/json + parameters: + - description: 批量删除文件 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.IdsReq' + produces: + - application/json + responses: + "200": + description: 删除文件 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除文件 + tags: + - 文件相关 + /oss/detail: + get: + parameters: + - description: 文件id + in: query + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 文件详情 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 文件详情 + tags: + - 文件相关 + /oss/getFileList: + post: + consumes: + - application/json + parameters: + - description: 文件列表 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetOssFileList' + produces: + - application/json + responses: + "200": + description: 文件列表 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 文件列表 + tags: + - 文件相关 + /oss/upload: + post: + consumes: + - multipart/form-data + parameters: + - description: 上传文件 + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "200": + description: 上传文件 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 文件上传 + tags: + - 文件相关 + /plant/add: + post: + consumes: + - application/json + parameters: + - description: 创建植物 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.CreateMyPlant' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"添加成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 添加植物 + tags: + - 我的植物 + /plant/completeTask: + post: + consumes: + - application/json + parameters: + - description: 完成任务 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.CompleteTask' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"完成任务"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 完成任务 + tags: + - 我的植物 + /plant/deletePlan: + post: + consumes: + - application/json + parameters: + - description: 删除植物 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.IdsReq' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"删除成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 删除任务 + tags: + - 我的植物 + /plant/deletePlant: + post: + consumes: + - application/json + parameters: + - description: 删除植物 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.IdsReq' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"删除成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 删除植物 + tags: + - 我的植物 + /plant/detail: + get: + parameters: + - description: id + in: query + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"获取ById成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: ById植物详情 + tags: + - 我的植物 + /plant/growth/add: + post: + consumes: + - application/json + parameters: + - description: 添加成长记录 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.CreateGrowthRecord' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"添加成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 添加成长记录 + tags: + - 我的植物 + /plant/page: + post: + consumes: + - application/json + parameters: + - description: 分页获取植物列表 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.PageInfo' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"获取成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 植物列表 + tags: + - 我的植物 + /plant/plan/add: + post: + consumes: + - application/json + parameters: + - description: 添加养护事项 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.CreateCarePlan' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"添加成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 添加养护事项 + tags: + - 我的植物 + /plant/plan/delete: + get: + parameters: + - description: id + in: query + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"删除成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 删除养护事项 + tags: + - 我的植物 + /plant/todayTask: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"获取成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 今日任务 + tags: + - 我的植物 + /plant/update: + post: + consumes: + - application/json + parameters: + - description: 修改ById植物 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.UpdateMyPlant' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"修改ById成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 修改ById植物 + tags: + - 我的植物 + /post/comment: + post: + consumes: + - application/json + parameters: + - description: 评论帖子 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.CreateComment' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"评论成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 评论帖子 + tags: + - 帖子 + /post/like: + get: + parameters: + - description: 帖子id + in: query + name: id + required: true + type: string + - description: 点赞类型 1 点赞 2 取消点赞 + in: query + name: type + required: true + type: string + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"点赞成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 点赞帖子 + tags: + - 帖子 + /post/myPost: + post: + consumes: + - application/json + parameters: + - description: 分页获取帖子列表 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.PostPage' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"获取成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 我的发布 + tags: + - 帖子 + /post/page: + post: + consumes: + - application/json + parameters: + - description: 分页获取帖子列表 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.PostPage' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"获取成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 帖子列表 + tags: + - 帖子 + /post/publish: + post: + consumes: + - application/json + parameters: + - description: 发布帖子 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.CreatePost' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"发布成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 发布帖子 + tags: + - 帖子 + /profile/detail: + get: + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"查询成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 用户详情 + tags: + - 个人中心 + /profile/update: + post: + consumes: + - application/json + parameters: + - description: 修改用户信息 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.UpdateProfile' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"修改成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 修改用户信息 + tags: + - 个人中心 + /role/delete: + post: + consumes: + - application/json + description: 删除角色 + parameters: + - description: 批量删除角色 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.IdsReq' + produces: + - application/json + responses: + "200": + description: 删除角色 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + summary: 删除角色 + tags: + - 角色管理 + /role/detail: + get: + parameters: + - description: id + in: query + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 角色详情 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/system.Role' + type: object + security: + - ApiKeyAuth: [] + summary: 角色详情 + tags: + - 角色管理 + /role/getRoleList: + post: + consumes: + - application/json + description: 获取角色列表 + parameters: + - description: 页码, 每页大小, 搜索条件 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetRoleList' + produces: + - application/json + responses: + "200": + description: 获取角色列表,返回包括列表,总数,页码,每页大小 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + summary: 获取角色列表 + tags: + - 角色管理 + /role/grantMenu: + post: + consumes: + - application/json + parameters: + - description: 授权菜单给角色 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GrantMenu' + produces: + - application/json + responses: + "200": + description: 授权菜单给角色 + schema: + $ref: '#/definitions/response.Response' + security: + - ApiKeyAuth: [] + summary: 授权菜单给角色 + tags: + - 角色管理 + /role/save: + post: + consumes: + - application/json + parameters: + - description: 角色信息 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.Role' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + security: + - ApiKeyAuth: [] + summary: 创建角色 + tags: + - 角色管理 + /role/update: + post: + consumes: + - application/json + parameters: + - description: 角色ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.Role' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + security: + - ApiKeyAuth: [] + summary: 修改角色 + tags: + - 角色管理 + /topic/add: + post: + consumes: + - application/json + parameters: + - description: 修改话题 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.UpdateTopic' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"发布成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 修改话题 + tags: + - 帖子话题 + /topic/delete: + post: + consumes: + - application/json + parameters: + - description: 删除话题 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.IdsReq' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"删除成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 删除任务 + tags: + - 帖子话题 + /topic/detail: + get: + parameters: + - description: id + in: query + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"获取成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 话题详情 + tags: + - 帖子话题 + /topic/list: + get: + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"获取成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 话题列表 + tags: + - 帖子话题 + /topic/page: + post: + consumes: + - application/json + parameters: + - description: 分页获取话题列表 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.PageInfo' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"获取成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 话题分页 + tags: + - 帖子话题 + /user/changePassword: + post: + consumes: + - application/json + description: 修改密码 + parameters: + - description: 用户id + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.ChangePwd' + produces: + - application/json + responses: + "200": + description: 修改密码成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/system.User' + type: object + security: + - ApiKeyAuth: [] + summary: 修改密码 + tags: + - 用户管理 + /user/delete: + post: + consumes: + - application/json + parameters: + - description: 批量删除用户 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.IdsReq' + produces: + - application/json + responses: + "200": + description: 删除用户 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除用户 + tags: + - 用户管理 + /user/detail: + get: + parameters: + - description: id + in: query + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 获取用户详情成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/system.User' + type: object + security: + - ApiKeyAuth: [] + summary: 获取用户详情 + tags: + - 用户管理 + /user/getUserList: + post: + consumes: + - application/json + parameters: + - description: 页码, 每页大小, 搜索条件 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetUserList' + produces: + - application/json + responses: + "200": + description: 获取用户列表,返回包括列表,总数,页码,每页大小 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取用户列表 + tags: + - 用户管理 + /user/grantRole: + post: + consumes: + - application/json + parameters: + - description: 用户ID, 角色ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GrantRole' + produces: + - application/json + responses: + "200": + description: '{"code": 200, "data": [...]}' + schema: + $ref: '#/definitions/response.Response' + security: + - ApiKeyAuth: [] + summary: 给用户分配角色 + tags: + - 用户管理 + /user/info: + get: + produces: + - application/json + responses: + "200": + description: '{"code": 200, "data": {}, "msg": "添加成功"}' + schema: + $ref: '#/definitions/response.Response' + security: + - ApiKeyAuth: [] + summary: 当前登录用户 + tags: + - 用户管理 + /user/save: + post: + consumes: + - application/json + parameters: + - description: 用户信息 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.User' + produces: + - application/json + responses: + "200": + description: '{"code": 200, "data": {}, "msg": "添加成功"}' + schema: + $ref: '#/definitions/response.Response' + security: + - ApiKeyAuth: [] + summary: 新增用户 + tags: + - 用户管理 + /user/update: + post: + consumes: + - application/json + parameters: + - description: 用户ID,用户信息 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.User' + produces: + - application/json + responses: + "200": + description: '{"code": 200, "data": [...]}' + schema: + $ref: '#/definitions/response.Response' + security: + - ApiKeyAuth: [] + summary: 更新用户 + tags: + - 用户管理 + /wiki-class/add: + post: + consumes: + - application/json + parameters: + - description: 添加分类 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.CreateWikiClass' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"发布成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 添加分类 + tags: + - 百科分类 + /wiki-class/delete: + post: + consumes: + - application/json + parameters: + - description: 删除分类 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.IdsReq' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"删除成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 删除分类 + tags: + - 百科分类 + /wiki-class/detail: + get: + parameters: + - description: id + in: query + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"获取成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 分类详情 + tags: + - 百科分类 + /wiki-class/list: + get: + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"获取成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 分类列表 + tags: + - 百科分类 + /wiki-class/page: + post: + consumes: + - application/json + parameters: + - description: 分页获取分类列表 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.PageInfo' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"获取成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 分类分页 + tags: + - 百科分类 + /wiki-class/update: + post: + consumes: + - application/json + parameters: + - description: 修改分类 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.UpdateWikiClass' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"发布成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 修改分类(可直接传入ossId修改图片) + tags: + - 百科分类 + /wiki/add: + post: + consumes: + - application/json + parameters: + - description: 添加百科 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.CreateWiki' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"发布成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 添加百科 + tags: + - 百科 + /wiki/detail: + get: + parameters: + - description: id + in: query + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"获取成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 百科详情 + tags: + - 百科 + /wiki/page: + post: + consumes: + - application/json + parameters: + - description: 百科分页 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.WikiPage' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"发布成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 分页 + tags: + - 百科 + /wiki/update: + post: + consumes: + - application/json + parameters: + - description: 修改百科 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.UpdateWiki' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"发布成功"}' + schema: + type: string + security: + - BearerAuth: [] + summary: 修改百科 + tags: + - 百科 +securityDefinitions: + BearerAuth: + in: header + name: Authorization + type: apiKey +swagger: "2.0" diff --git a/global/enums.go b/global/enums.go new file mode 100644 index 0000000..1d861a0 --- /dev/null +++ b/global/enums.go @@ -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" // 处理失败 +) diff --git a/global/global.go b/global/global.go new file mode 100644 index 0000000..7b9ec7c --- /dev/null +++ b/global/global.go @@ -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 +) diff --git a/global/model.go b/global/model.go new file mode 100644 index 0000000..27e1fc9 --- /dev/null +++ b/global/model.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..798a7b8 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2cd1e9c --- /dev/null +++ b/go.sum @@ -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= diff --git a/initialize/gorm.go b/initialize/gorm.go new file mode 100644 index 0000000..d4499fb --- /dev/null +++ b/initialize/gorm.go @@ -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") +} diff --git a/initialize/gorm_mysql.go b/initialize/gorm_mysql.go new file mode 100644 index 0000000..fd2dd58 --- /dev/null +++ b/initialize/gorm_mysql.go @@ -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 + } +} diff --git a/initialize/gorm_pgsql.go b/initialize/gorm_pgsql.go new file mode 100644 index 0000000..809735d --- /dev/null +++ b/initialize/gorm_pgsql.go @@ -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 + } +} diff --git a/initialize/gorm_sqlite.go b/initialize/gorm_sqlite.go new file mode 100644 index 0000000..ac0e467 --- /dev/null +++ b/initialize/gorm_sqlite.go @@ -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 + } +} diff --git a/initialize/internal/gorm.go b/initialize/internal/gorm.go new file mode 100644 index 0000000..247807f --- /dev/null +++ b/initialize/internal/gorm.go @@ -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, + } +} diff --git a/initialize/internal/gorm_logger_writer.go b/initialize/internal/gorm_logger_writer.go new file mode 100644 index 0000000..691e9aa --- /dev/null +++ b/initialize/internal/gorm_logger_writer.go @@ -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 + } +} diff --git a/initialize/redis.go b/initialize/redis.go new file mode 100644 index 0000000..2616e4e --- /dev/null +++ b/initialize/redis.go @@ -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 +} diff --git a/initialize/router.go b/initialize/router.go new file mode 100644 index 0000000..5df0eed --- /dev/null +++ b/initialize/router.go @@ -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)) + +} diff --git a/initialize/timer.go b/initialize/timer.go new file mode 100644 index 0000000..8f9de77 --- /dev/null +++ b/initialize/timer.go @@ -0,0 +1,8 @@ +package initialize + +func InitTimer() { + go func() { + // var option []cron.Option + // option = append(option, cron.WithSeconds()) + }() +} diff --git a/log/2026-02-25/error.log b/log/2026-02-25/error.log new file mode 100644 index 0000000..f35a507 --- /dev/null +++ b/log/2026-02-25/error.log @@ -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是否为规定结构 diff --git a/log/2026-02-25/info.log b/log/2026-02-25/info.log new file mode 100644 index 0000000..876821d --- /dev/null +++ b/log/2026-02-25/info.log @@ -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} diff --git a/main.go b/main.go new file mode 100644 index 0000000..41ee800 --- /dev/null +++ b/main.go @@ -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() +} diff --git a/middleware/auth.go b/middleware/auth.go new file mode 100644 index 0000000..ec24fde --- /dev/null +++ b/middleware/auth.go @@ -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() + } +} diff --git a/middleware/operation.go b/middleware/operation.go new file mode 100644 index 0000000..825a5ee --- /dev/null +++ b/middleware/operation.go @@ -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)) + } + } +} diff --git a/model/commom/request/common.go b/model/commom/request/common.go new file mode 100644 index 0000000..0e98c78 --- /dev/null +++ b/model/commom/request/common.go @@ -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 +} diff --git a/model/commom/response/common.go b/model/commom/response/common.go new file mode 100644 index 0000000..19b618c --- /dev/null +++ b/model/commom/response/common.go @@ -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"` +} diff --git a/model/commom/response/response.go b/model/commom/response/response.go new file mode 100644 index 0000000..503168b --- /dev/null +++ b/model/commom/response/response.go @@ -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, + }) +} diff --git a/model/system/oss.go b/model/system/oss.go new file mode 100644 index 0000000..bb508f3 --- /dev/null +++ b/model/system/oss.go @@ -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"` +} diff --git a/model/system/request/captcha.go b/model/system/request/captcha.go new file mode 100644 index 0000000..3da0d01 --- /dev/null +++ b/model/system/request/captcha.go @@ -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 +} diff --git a/model/system/request/jwt.go b/model/system/request/jwt.go new file mode 100644 index 0000000..28c8448 --- /dev/null +++ b/model/system/request/jwt.go @@ -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 +} diff --git a/model/system/request/oss.go b/model/system/request/oss.go new file mode 100644 index 0000000..d0d6573 --- /dev/null +++ b/model/system/request/oss.go @@ -0,0 +1,8 @@ +package request + +import common "sundynix-go/model/commom/request" + +type GetOssFileList struct { + common.PageInfo + Name string `json:"name" form:"name"` +} diff --git a/model/system/request/sys_client.go b/model/system/request/sys_client.go new file mode 100644 index 0000000..2f6d4b9 --- /dev/null +++ b/model/system/request/sys_client.go @@ -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"` +} diff --git a/model/system/request/sys_menu.go b/model/system/request/sys_menu.go new file mode 100644 index 0000000..b204b49 --- /dev/null +++ b/model/system/request/sys_menu.go @@ -0,0 +1,6 @@ +package request + +type GetMenuTree struct { + Category int `json:"category" form:"category"` + ParentId string `json:"parentId" form:"parentId"` +} diff --git a/model/system/request/sys_operation_record.go b/model/system/request/sys_operation_record.go new file mode 100644 index 0000000..a6359c4 --- /dev/null +++ b/model/system/request/sys_operation_record.go @@ -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"` +} diff --git a/model/system/request/sys_role.go b/model/system/request/sys_role.go new file mode 100644 index 0000000..d669b38 --- /dev/null +++ b/model/system/request/sys_role.go @@ -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"` +} diff --git a/model/system/request/sys_user.go b/model/system/request/sys_user.go new file mode 100644 index 0000000..efb5e85 --- /dev/null +++ b/model/system/request/sys_user.go @@ -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"` +} diff --git a/model/system/response/WxCode2SessionResp.go b/model/system/response/WxCode2SessionResp.go new file mode 100644 index 0000000..cacc3f3 --- /dev/null +++ b/model/system/response/WxCode2SessionResp.go @@ -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"` +} diff --git a/model/system/response/oss.go b/model/system/response/oss.go new file mode 100644 index 0000000..56b90e4 --- /dev/null +++ b/model/system/response/oss.go @@ -0,0 +1,7 @@ +package response + +import "sundynix-go/model/system" + +type UploadFileResponse struct { + File system.Oss `json:"file"` +} diff --git a/model/system/response/sys_captcha.go b/model/system/response/sys_captcha.go new file mode 100644 index 0000000..40a1842 --- /dev/null +++ b/model/system/response/sys_captcha.go @@ -0,0 +1,6 @@ +package response + +type CaptchaRes struct { + CaptchaId string `json:"captchaId"` + Captcha string `json:"captcha"` +} diff --git a/model/system/response/sys_user.go b/model/system/response/sys_user.go new file mode 100644 index 0000000..d83fb3d --- /dev/null +++ b/model/system/response/sys_user.go @@ -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"` +} diff --git a/model/system/sys_client.go b/model/system/sys_client.go new file mode 100644 index 0000000..7801e10 --- /dev/null +++ b/model/system/sys_client.go @@ -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"` +} diff --git a/model/system/sys_menu.go b/model/system/sys_menu.go new file mode 100644 index 0000000..0cad773 --- /dev/null +++ b/model/system/sys_menu.go @@ -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:"-"` +} diff --git a/model/system/sys_operation_record.go b/model/system/sys_operation_record.go new file mode 100644 index 0000000..f979edc --- /dev/null +++ b/model/system/sys_operation_record.go @@ -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"` +} diff --git a/model/system/sys_role.go b/model/system/sys_role.go new file mode 100644 index 0000000..9524930 --- /dev/null +++ b/model/system/sys_role.go @@ -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;"` +} diff --git a/model/system/sys_role_menu.go b/model/system/sys_role_menu.go new file mode 100644 index 0000000..7c9d0e9 --- /dev/null +++ b/model/system/sys_role_menu.go @@ -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"` +} diff --git a/model/system/sys_user.go b/model/system/sys_user.go new file mode 100644 index 0000000..78c5da5 --- /dev/null +++ b/model/system/sys_user.go @@ -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 +} diff --git a/model/system/sys_user_role.go b/model/system/sys_user_role.go new file mode 100644 index 0000000..9ff8b92 --- /dev/null +++ b/model/system/sys_user_role.go @@ -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"` +} diff --git a/pkg/.DS_Store b/pkg/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..ba16104846d075129489b5649bf2beed50d08685 GIT binary patch literal 6148 zcmeHKPfrs;6n_I%wji=VtB5h#*oz57ix49*hFUP%7z3dMi-2`^JCqH}OtZVC0wL*H zPksPDfZxExi$^bBy%|4(UOnlXKO4cq%@{-WC2xLj=FNMv@3+~R9RMK3W^Dq%1ONk7 zV5%3JV?_9cc1V{VNg)!M!v!BapuiyNn4JtH3?vNvZw!cUw-+|?dySw0hrZvE&t1po z4*JFBP(;1{Qwmxq#5k?6p<~`T4c`l+eEu`(>Q0?G+hg>J4m-BHRS(2d&vKs!MZZZ`>w#A*H*D8q z^-4rvL}hP}R?VS5rY_Rjv)@BYyA7O^`8^=hJqRdeGT;2hMo6Q|A$z524uo#NeSd+6 zUgUAMvKAuerHF2q*apWHcaZP-hH!<;GzbwpVk#?k5ZJ6vi@WQ-?NEQ76?qi#?TY7e zrk;F2rp4|;Kt;;j%y-3TuJ9-}t|`NWoHFoK$;6gMlKbGarM<*Bqv>2>L3z@60*%2v zcnr(11$*!s-oRUU4iv_=R6l{kSC| zTF1WdopQi@&g$roW8$Gil{<~zG4s+j^mb|SJ2p2sKSBBl`)TAndln|k*njlC8q-^ zC`{UffrNph40NlmDbD}}wTf<8+ahy{&pg*bwe{0K-Ik|tr`pEB?Zr&rwr literal 0 HcmV?d00001 diff --git a/pkg/httpclient/http_client.go b/pkg/httpclient/http_client.go new file mode 100644 index 0000000..6fd2eb4 --- /dev/null +++ b/pkg/httpclient/http_client.go @@ -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 +} diff --git a/router/enter.go b/router/enter.go new file mode 100644 index 0000000..f1b3d88 --- /dev/null +++ b/router/enter.go @@ -0,0 +1,12 @@ +package router + +import ( + "sundynix-go/router/system" +) + +var GroupApp = new(Group) + +// Group 路由组 +type Group struct { + System system.SysRouterGroup +} diff --git a/router/system/auth_router.go b/router/system/auth_router.go new file mode 100644 index 0000000..b12da76 --- /dev/null +++ b/router/system/auth_router.go @@ -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) // 获取天气 + } +} diff --git a/router/system/client_router.go b/router/system/client_router.go new file mode 100644 index 0000000..fbbc9b4 --- /dev/null +++ b/router/system/client_router.go @@ -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) + } +} diff --git a/router/system/enter.go b/router/system/enter.go new file mode 100644 index 0000000..ad78fb8 --- /dev/null +++ b/router/system/enter.go @@ -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 +) diff --git a/router/system/menu_router.go b/router/system/menu_router.go new file mode 100644 index 0000000..033f473 --- /dev/null +++ b/router/system/menu_router.go @@ -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) + } +} diff --git a/router/system/operation_record.go b/router/system/operation_record.go new file mode 100644 index 0000000..d752b13 --- /dev/null +++ b/router/system/operation_record.go @@ -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) // 批量删除操作记录 + } +} diff --git a/router/system/oss_router.go b/router/system/oss_router.go new file mode 100644 index 0000000..1408478 --- /dev/null +++ b/router/system/oss_router.go @@ -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) + } +} diff --git a/router/system/role_router.go b/router/system/role_router.go new file mode 100644 index 0000000..2b9ef6f --- /dev/null +++ b/router/system/role_router.go @@ -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) + } +} diff --git a/router/system/user_router.go b/router/system/user_router.go new file mode 100644 index 0000000..fc01c2e --- /dev/null +++ b/router/system/user_router.go @@ -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) + } +} diff --git a/service/enter.go b/service/enter.go new file mode 100644 index 0000000..467e190 --- /dev/null +++ b/service/enter.go @@ -0,0 +1,11 @@ +package service + +import ( + "sundynix-go/service/system" +) + +var GroupApp = new(Group) + +type Group struct { + SystemServiceGroup system.ServiceGroup +} diff --git a/service/system/enter.go b/service/system/enter.go new file mode 100644 index 0000000..f9f4f48 --- /dev/null +++ b/service/system/enter.go @@ -0,0 +1,11 @@ +package system + +type ServiceGroup struct { + JwtService + UserService + ClientService + RoleService + MenuService + OperationRecordService + OssService +} diff --git a/service/system/oss.go b/service/system/oss.go new file mode 100644 index 0000000..50e9b78 --- /dev/null +++ b/service/system/oss.go @@ -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 +} diff --git a/service/system/sys_client.go b/service/system/sys_client.go new file mode 100644 index 0000000..58bd663 --- /dev/null +++ b/service/system/sys_client.go @@ -0,0 +1,61 @@ +package system + +import ( + "errors" + "sundynix-go/global" + common "sundynix-go/model/commom/request" + "sundynix-go/model/system" + systemReq "sundynix-go/model/system/request" + + "gorm.io/gorm" +) + +type ClientService struct{} + +var ClientServiceApp = new(ClientService) + +func (s *ClientService) SaveClient(client system.Client) error { + if !errors.Is(global.DB.Where("client_id = ?", client.ClientId).First(&system.Client{}).Error, gorm.ErrRecordNotFound) { + return errors.New("存在重复clientId,请修改clientId") + } + return global.DB.Create(&client).Error +} + +func (s *ClientService) UpdateClient(client system.Client) error { + return global.DB.Model(&client).Where("id = ?", client.Id).Updates(&client).Error +} + +func (s *ClientService) GetClientList(info systemReq.GetClientList) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Current - 1) + db := global.DB.Model(&system.Client{}) + var clientList []system.Client + if info.ClientId != "" { + db = db.Where("client_id = ?", info.ClientId) + } + 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).Find(&clientList).Error + return clientList, total, err +} + +func (s *ClientService) DeleteClientByIds(ids common.IdsReq) (err error) { + return global.DB.Where("id IN (?)", ids.Ids).Delete(&system.Client{}).Error +} + +func (s *ClientService) GetClientById(id string) (client system.Client, err error) { + var c system.Client + err = global.DB.Where("id = ?", id).First(&c).Error + return c, err +} + +func (s *ClientService) GetClientByClientId(clientId string) (client *system.Client, err error) { + var c system.Client + err = global.DB.Where("client_id = ?", clientId).First(&c).Error + return &c, err +} diff --git a/service/system/sys_jwt.go b/service/system/sys_jwt.go new file mode 100644 index 0000000..9394fb3 --- /dev/null +++ b/service/system/sys_jwt.go @@ -0,0 +1,26 @@ +package system + +import ( + "context" + "sundynix-go/global" + "sundynix-go/utils/timer" +) + +type JwtService struct{} + +var JwtServiceApp = new(JwtService) + +// 登出,禁用jwt +func (s *JwtService) PutBlacklist(userId string, token string) (err error) { + expire, err := timer.ParseDuration(global.Config.JWT.ExpiresTime) + if err != nil { + return err + } + err = global.Redis.Set(context.Background(), userId, token, expire).Err() + return err +} + +func (s *JwtService) IsInBlacklist(userId string, token string) bool { + val, err := global.Redis.Get(context.Background(), userId).Result() + return err == nil && val == token +} diff --git a/service/system/sys_menu.go b/service/system/sys_menu.go new file mode 100644 index 0000000..2e324fd --- /dev/null +++ b/service/system/sys_menu.go @@ -0,0 +1,127 @@ +package system + +import ( + "errors" + "sundynix-go/global" + "sundynix-go/model/system" + + "gorm.io/gorm" +) + +type MenuService struct{} + +var MenuServiceApp = new(MenuService) + +func (s *MenuService) SaveMenu(menu system.Menu) error { + //1.根据code和name查询是否存在重名 + if err := global.DB.Where("code = ? or name = ?", menu.Code, menu.Name).First(&system.Menu{}).Error; err == nil { + return errors.New("菜单已存在") + } + return global.DB.Create(&menu).Error +} + +func (s *MenuService) UpdateMenu(menu *system.Menu) (err error) { + var sysMenu system.Menu + menuMap := map[string]interface{}{ + "Category": menu.Category, + "Name": menu.Name, + "Title": menu.Title, + "Code": menu.Code, + "path": menu.Path, + "Permission": menu.Permission, + "Locale": menu.Locale, + "Icon": menu.Icon, + "Sort": menu.Sort, + } + err = global.DB.Where("id = ?", menu.Id).First(&sysMenu).Error + if err != nil { + global.Logger.Debug(err.Error()) + return errors.New("查询菜单失败") + } + err = global.DB.Model(&sysMenu).Updates(menuMap).Error + return err +} + +func (s *MenuService) DeleteMenu(id string) (err error) { + err = global.DB.First(&system.Menu{}, "parent_id = ?", id).Error + if err == nil { + return errors.New("请先删除子菜单") + } + var menu system.Menu + err = global.DB.Where("id = ?", id).First(&menu).Error + if err != nil { + return errors.New("菜单记录不存在") + } + // 同步删除menu表和role-menu表数据 + return global.DB.Transaction(func(tx *gorm.DB) error { + if err = tx.Where("id = ?", id).Delete(&system.Menu{}).Error; err != nil { + return err + } + if err = tx.Where("menu_id = ?", id).Delete(&system.RoleMenu{}).Error; err != nil { + return err + } + return nil + }) +} + +func (s *MenuService) GetMenuById(id string) (menu system.Menu, err error) { + var m system.Menu + err = global.DB.Where("id = ?", id).First(&m).Error + return m, err +} + +func (s *MenuService) GetAllMenuTree(category int, parentId string) (menus []*system.Menu, err error) { + //1,先根据category和parentId获取所有菜单 category默认为0,parentId默认为0 + //2.讲查询出的列表构建为树结构 + var menuList []*system.Menu + db := global.DB.Model(&system.Menu{}) + if category != 0 { + db.Where("category = ?", category) + } + if parentId != "0" { + db.Where("parent_id = ?", parentId) + } + err = db.Order("sort asc").Find(&menuList).Error + if err != nil { + return nil, err + } + tree := buildMenuTree(menuList) + return tree, nil +} + +func (s *MenuService) GetUserRoutes(userId string) (menus []*system.Menu, err error) { + //1.根据userId 查询角色 根据角色查询菜单 去重 + //2.构建树结构 + var roleIds []string + err = global.DB.Model(&system.UserRole{}).Where("user_id = ?", userId).Pluck("role_id", &roleIds).Error + var menuIds []string + err = global.DB.Model(&system.RoleMenu{}).Where("role_id in ?", roleIds).Pluck("menu_id", &menuIds).Error + var menuList []*system.Menu + err = global.DB.Model(&system.Menu{}).Where("id in ? and category = 1 ", menuIds).Order("sort asc").Find(&menuList).Error + return buildMenuTree(menuList), nil +} + +func buildMenuTree(list []*system.Menu) []*system.Menu { + //1.定义一个map + menuMap := make(map[string]*system.Menu) + for _, item := range list { + menuMap[item.Id] = item + } + //构建树结构 + var treeList []*system.Menu + for _, item := range list { + if item.ParentId == "0" { + // 如果没有父节点,直接添加到树中 + treeList = append(treeList, item) + } else { + if parent, exists := menuMap[item.ParentId]; exists { + // 如果有父节点,将当前节点添加到父节点的Children中 + parent.Children = append(parent.Children, item) + } else { + // 如果没有父节点,将当前节点添加到树中 + treeList = append(treeList, item) + } + } + } + return treeList +} diff --git a/service/system/sys_operation_record.go b/service/system/sys_operation_record.go new file mode 100644 index 0000000..c548dfb --- /dev/null +++ b/service/system/sys_operation_record.go @@ -0,0 +1,57 @@ +package system + +import ( + "sundynix-go/global" + common "sundynix-go/model/commom/request" + "sundynix-go/model/system" + systemReq "sundynix-go/model/system/request" +) + +type OperationRecordService struct{} + +var OperationRecordServiceApp = new(OperationRecordService) + +func (o *OperationRecordService) CreateOperationRecord(operationRecord system.SysOperationRecord) (err error) { + return global.DB.Create(&operationRecord).Error +} + +func (o *OperationRecordService) GetRecordList(info systemReq.GetOperationRecordList) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Current - 1) + db := global.DB.Model(&system.SysOperationRecord{}) + var operationRecordList []system.SysOperationRecord + + if info.Ip != "" { + db = db.Where("ip = ?", info.Method) + } + if info.Method != "" { + db = db.Where("method = ?", info.Method) + } + if info.Path != "" { + db = db.Where("path = ?", info.Path) + } + if info.UserId != "" { + db = db.Where("status = ?", info.UserId) + } + if info.Status != 0 { + db = db.Where("status = ?", info.Status) + } + err = db.Count(&total).Error + if err != nil { + return + } + err = db.Limit(limit).Offset(offset).Find(&operationRecordList).Error + return operationRecordList, total, err +} + +func (o *OperationRecordService) GetRecordById(id string) (record system.SysOperationRecord, err error) { + var r system.SysOperationRecord + err = global.DB.Where("id = ?", id).First(&r).Error + return r, err +} + +func (o *OperationRecordService) DeleteRecordsByIds(ids common.IdsReq) (err error) { + // Unscoped()禁用软删除 --> 永久物理删除 + err = global.DB.Where("id in ?", ids.Ids).Unscoped().Delete(&system.SysOperationRecord{}).Error + return err +} diff --git a/service/system/sys_role.go b/service/system/sys_role.go new file mode 100644 index 0000000..479a212 --- /dev/null +++ b/service/system/sys_role.go @@ -0,0 +1,89 @@ +package system + +import ( + "errors" + "sundynix-go/global" + common "sundynix-go/model/commom/request" + "sundynix-go/model/system" + systemReq "sundynix-go/model/system/request" + + "gorm.io/gorm" +) + +type RoleService struct { +} + +var RoleServiceApp = new(RoleService) + +func (s *RoleService) SaveRole(role system.Role) error { + if !errors.Is(global.DB.Where("code = ?", role.Code).First(&system.Role{}).Error, gorm.ErrRecordNotFound) { + return errors.New("存在重复角色") + } + return global.DB.Create(&role).Error +} + +func (s *RoleService) UpdateRole(role system.Role) error { + return global.DB.Model(&role).Where("id = ?", role.Id).Updates(&role).Error +} + +func (s *RoleService) GetRoleList(info systemReq.GetRoleList) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Current - 1) + db := global.DB.Model(&system.Role{}) + var roleList []system.Role + if info.Code != "" { + db = db.Where("code = ?", info.Code) + } + 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).Find(&roleList).Error + return roleList, total, err +} + +func (s *RoleService) DeleteRoleByIds(ids common.IdsReq) error { + return global.DB.Where("id in ?", ids.Ids).Delete(&system.Role{}).Error +} + +func (s *RoleService) GetRoleById(id string) (role system.Role, err error) { + var r system.Role + err = global.DB.Where("id = ?", id).First(&r).Error + return r, err +} + +func (s *RoleService) GrantRole(userId string, roleIds []string) error { + //1. 检查是否存在userid的授权记录 存在就删除 不存在就插入 + //2. 插入新的数据 + return global.DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Where("user_id = ?", userId).Delete(&system.UserRole{}).Error; err != nil { + return err + } + for _, roleId := range roleIds { + if err := tx.Create(&system.UserRole{UserId: userId, RoleId: roleId}).Error; err != nil { + return err + } + } + return nil + }) + +} + +func (s *RoleService) GrantMenu(roleId string, menuIds []string) error { + //1. 检查是否存在userid的授权记录 存在就删除 不存在就插入 + //2. 插入新的数据 + return global.DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Where("role_id = ?", roleId).Delete(&system.RoleMenu{}).Error; err != nil { + return err + } + for _, menuId := range menuIds { + if err := tx.Create(&system.RoleMenu{RoleId: roleId, MenuId: menuId}).Error; err != nil { + return err + } + } + return nil + }) +} diff --git a/service/system/sys_user.go b/service/system/sys_user.go new file mode 100644 index 0000000..d6ea35d --- /dev/null +++ b/service/system/sys_user.go @@ -0,0 +1,256 @@ +package system + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + url2 "net/url" + "strconv" + "sundynix-go/global" + common "sundynix-go/model/commom/request" + "sundynix-go/model/system" + systemReq "sundynix-go/model/system/request" + systemResp "sundynix-go/model/system/response" + "sundynix-go/pkg/httpclient" + "sundynix-go/utils" + location "sundynix-go/utils/location" + "sundynix-go/utils/uniqueid" + "sundynix-go/utils/wechat" + + "go.uber.org/zap" + "gorm.io/gorm" +) + +type UserService struct{} + +var UserServiceApp = new(UserService) + +func (userService *UserService) Login(u *system.User) (userInfo *system.User, err error) { + var user system.User + // 查询出用户信息的同时查询出角色信息 + err = global.DB.Model(&system.User{}).Where("account = ?", u.Account).First(&user).Error + if err == nil { + if ok := utils.BcryptCheck(u.Password, user.Password); !ok { + return nil, errors.New("密码错误") + } + } + return &user, err +} + +func (userService *UserService) SaveUser(user system.User) error { + if !errors.Is(global.DB.Where("account = ?", user.Account).First(&system.User{}).Error, gorm.ErrRecordNotFound) { + return errors.New("存在重复Account,请修改Account") + } + user.Password = utils.BcryptHash(user.Password) + return global.DB.Create(&user).Error +} + +func (userService *UserService) UpdateUser(user *system.User) (err error) { + var sysUser system.User + userMap := map[string]interface{}{ + "account": user.Account, + "phone": user.Phone, + "name": user.Name, + "avatar_id": user.AvatarId, + } + err = global.DB.Where("id = ?", user.Id).First(&sysUser).Error + if err != nil { + global.Logger.Debug(err.Error()) + return errors.New("查询用户失败") + } + err = global.DB.Model(&sysUser).Updates(userMap).Error + return err +} + +func (userService *UserService) GetUserList(info systemReq.GetUserList) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Current - 1) + db := global.DB.Model(&system.User{}) + var userList []system.User + + if info.Account != "" { + db = db.Where("account LIKE ?", "%"+info.Account+"%") + } + if info.Phone != "" { + db = db.Where("phone LIKE ?", "%"+info.Phone+"%") + } + err = db.Count(&total).Error + if err != nil { + return + } + err = db.Limit(limit).Offset(offset).Find(&userList).Error + return userList, total, err +} + +func (userService *UserService) DeleteUserByIds(ids common.IdsReq) error { + return global.DB.Where("id IN (?)", ids.Ids).Delete(&system.User{}).Error +} + +func (userService *UserService) GetUserById(id string) (user *system.User, err error) { + var u system.User + err = global.DB.Where("id = ?", id).Preload("Avatar").First(&u).Error + return &u, err +} + +func (userService *UserService) ChangePassword(id string, pwd string) (err error) { + return global.DB.Model(&system.User{}).Where("id = ?", id).Update("password", utils.BcryptHash(pwd)).Error +} + +func (userService *UserService) MiniLogin(code string) (result *system.User, err error) { + //构建参数 + params := url2.Values{} + params.Set("appid", global.Config.MiniProgram.AppId) + params.Set("secret", global.Config.MiniProgram.AppSecret) + params.Set("js_code", code) + params.Set("grant_type", "authorization_code") + fullURL := "https://api.weixin.qq.com/sns/jscode2session?" + params.Encode() + + //1. 获取全局 HTTP Client(复用连接池) + myHttpClient := httpclient.GetClient() + //2.发起请求 + resp, err := myHttpClient.Get(fullURL) + if err != nil { + global.Logger.Error("微信登录接口请求失败", zap.Error(err)) + return nil, fmt.Errorf("微信登录接口请求失败: %w", err) + } + defer resp.Body.Close() + //3.读取响应 + body, err := io.ReadAll(resp.Body) + if err != nil { + global.Logger.Error("读取微信接口响应失败", zap.Error(err)) + return nil, fmt.Errorf("读取微信登录接口响应失败: %w", err) + } + // 4. 解析JSON(用结构体替代map,提升效率+类型安全) + var wxResp systemResp.WxCode2SessionResp + if err = json.Unmarshal(body, &wxResp); err != nil { + global.Logger.Error("解析微信接口响应失败", zap.Error(err)) + return nil, fmt.Errorf("解析微信登录接口响应失败: %w", err) + } + // 5. 检查微信接口错误码(关键:原代码未处理errcode,导致无法定位真实错误) + if wxResp.Errcode != 0 { + errMsg := fmt.Sprintf("微信接口返回错误: errcode=%d, errmsg=%s", wxResp.Errcode, wxResp.Errmsg) + global.Logger.Error(errMsg) + return nil, errors.New(errMsg) + } + // 6. 校验openid(空值直接返回) + if wxResp.Openid == "" { + global.Logger.Error("微信接口返回openid为空") + return nil, errors.New("openid为空") + } + + // 7. 根据openid查询用户 存在--> 更新session_key 返回数据 + var user system.User + err = global.DB.Where("mini_open_id = ?", wxResp.Openid).Preload("Avatar").First(&user).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + // 8. 使用 Transaction 闭包管理事务 + err = global.DB.Transaction(func(tx *gorm.DB) error { + // 创建新用户 + newUser := system.User{ + Name: uniqueid.GenerateName(), + MiniOpenId: wxResp.Openid, + SessionKey: wxResp.SessionKey, + } + if err := tx.Create(&newUser).Error; err != nil { + return err + } + // 赋值给外部变量以便返回 + user = newUser + return nil + }) + if err != nil { + global.Logger.Error("创建用户失败", zap.Error(err)) + return nil, fmt.Errorf("登录失败: %w", err) + } + return &user, nil + } + if err == nil && user.Id != "" { + // UpdateColumn:只更新字段,不触发模型钩子,比Update更高效 + if err = global.DB.Model(&user).UpdateColumn("session_key", wxResp.SessionKey).Error; err != nil { + global.Logger.Error("更新session_key失败", zap.Error(err)) + return nil, fmt.Errorf("更新session_key失败: %w", err) + } + return &user, nil + } + return nil, errors.New("登录失败") +} + +func (userService *UserService) LoginByPhone(code string, openId string) (result *system.User, err error) { + token := wechat.GetMiniAccessToken() + url := "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + token + data := map[string]interface{}{ + "code": code, + } + jsonData, _ := json.Marshal(data) + resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + log.Fatalf("Error making POST request: %s", err) + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Error reading response body: %s", err) + } + var dataMap map[string]interface{} + err = json.Unmarshal([]byte(body), &dataMap) + if err != nil { + log.Fatalf("Error unmarshalling JSON: %s", err) + } + //用户已经存在 --> 如果有手机号直接返回user,没有则更新手机号并返回user + if openId != "" { + var user system.User + err = global.DB.Where("mini_open_id = ?", openId).First(&user).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("用户不存在") + } + if err == nil && user.Id != "" { + if user.Phone != "" { + return &user, nil + } else { + user.Phone = dataMap["phone_info"].(map[string]interface{})["phoneNumber"].(string) + return &user, global.DB.Save(&user).Error + } + } + } + return nil, errors.New("登录失败") +} + +// GetLocation 获取位置信息 +func (userService *UserService) GetLocation(longitude, latitude string) (res map[string]interface{}, err error) { + long, err := strconv.ParseFloat(longitude, 32) + if err != nil { + return res, err + } + lati, err := strconv.ParseFloat(latitude, 32) + if err != nil { + return res, err + } + + entity, err := location.Point2code(float32(long), float32(lati)) + result := map[string]interface{}{ + "city": entity.AddressComponent.City.String(), + "adcode": entity.AddressComponent.Adcode, + } + return result, err +} + +// GetWeather 获取天气信息 adcode 行政区划代码 +func (userService *UserService) GetWeather(adcode string) (res map[string]interface{}, err error) { + weatherResp, err := location.GetWeather(adcode, "base") + live := weatherResp.Lives[0] // 实时天气数组仅1条数据 + result := map[string]interface{}{ + "province": live.Province, + "city": live.City, + "adcode": live.Adcode, + "weather": live.Weather, + "temperature": live.Temperature, + "windPower": live.WindPower, + "windDirection": live.WindDirection, + "humidity": live.Humidity, + "reportTime": live.ReportTime, + } + return result, err +} diff --git a/utils/async/async_task.go b/utils/async/async_task.go new file mode 100644 index 0000000..dab2c22 --- /dev/null +++ b/utils/async/async_task.go @@ -0,0 +1,64 @@ +package async + +import ( + "context" + "runtime/debug" + "sundynix-go/global" + "sync" + "time" + + "go.uber.org/zap" +) + +type AsyncTask func(ctx context.Context) + +type namedTask struct { + name string + fn AsyncTask +} + +type TaskRunner struct { + mu sync.Mutex + tasks []namedTask +} + +// Add 添加任务 +func (tr *TaskRunner) Add(name string, task AsyncTask) { + if task == nil { + return + } + tr.mu.Lock() + defer tr.mu.Unlock() + tr.tasks = append(tr.tasks, namedTask{name: name, fn: task}) +} + +// RunAll 安全执行 +func (tr *TaskRunner) RunAll() { + tr.mu.Lock() + todoTasks := tr.tasks + tr.tasks = nil + tr.mu.Unlock() + + for _, task := range todoTasks { + t := task + go func() { + defer func() { + if r := recover(); r != nil { + // 使用全局 Zap 记录结构化日志 + // 这里的 global.Logger 替换为你实际的全局变量名 + global.Logger.Error("异步任务异常崩溃", + zap.String("task_name", t.name), + zap.Any("panic_info", r), + zap.String("stack", string(debug.Stack())), + ) + } + }() + + // 异步任务执行,设置独立的超时控制 + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + t.fn(ctx) + }() + } +} diff --git a/utils/auth/claims.go b/utils/auth/claims.go new file mode 100644 index 0000000..b68049a --- /dev/null +++ b/utils/auth/claims.go @@ -0,0 +1,130 @@ +package auth + +import ( + "net" + "strings" + "sundynix-go/global" + "sundynix-go/model/system" + systemReq "sundynix-go/model/system/request" + + "github.com/gin-gonic/gin" +) + +// GetLoginToken 获取登录token +func GetLoginToken(user system.Login) (token string, claims systemReq.CustomClaims, err error) { + j := NewJWT() + claims = j.CreateClaims(systemReq.BaseClaims{ + Account: user.GetAccount(), + ID: user.GetUserId(), + }) + token, err = j.CreateToken(claims) + return +} + +// GetToken 从请求头中获取JWT token,并确保其有效性 +// +// 参数: +// - c: *gin.Context, Gin框架的上下文对象,用于获取请求信息和设置响应。 +// +// 返回值: +// - string: 获取到的JWT token,如果获取失败则返回空字符串。 +func GetToken(c *gin.Context) string { + // 从请求头中获取Authorization字段的值 + token := c.Request.Header.Get("Authorization") + prefix := strings.HasPrefix(token, "Bearer ") + if prefix { + token = strings.TrimPrefix(token, "Bearer ") + } + // 返回获取到的token + return token +} + +// ClearToken 清除Cookie中的token +func ClearToken(c *gin.Context) { + host, _, err := net.SplitHostPort(c.Request.Host) + if err != nil { + host = c.Request.Host + } + if net.ParseIP(host) != nil { + c.SetCookie("sundynix-token", "", -1, "/", "", false, false) + } else { + c.SetCookie("sundynix-token", "", -1, "/", host, false, false) + } +} + +// GetUserInfo 从 gin.Context 中获取用户信息,并返回 CustomClaims 类型的指针。 +// 该函数首先尝试从上下文中获取已存在的 claims,如果不存在,则调用 GetClaims 函数获取 claims。 +// 如果获取 claims 失败,则返回 nil。 +// +// 参数: +// - c: *gin.Context, gin 框架的上下文对象,用于获取请求相关的信息。 +// +// 返回值: +// - *systemReq.CustomClaims: 返回用户的自定义 claims 信息,如果获取失败则返回 nil。 +func GetUserInfo(c *gin.Context) *systemReq.CustomClaims { + // 尝试从上下文中获取已存在的 claims + if claims, exists := c.Get("claims"); !exists { + // 如果 claims 不存在,则调用 GetClaims 函数获取 claims + if cl, err := GetClaims(c); err != nil { + // 如果获取 claims 失败,返回 nil + return nil + } else { + // 成功获取 claims,返回 claims + return cl + } + } else { + // 如果 claims 存在,将其转换为 CustomClaims 类型并返回 + waitUse := claims.(*systemReq.CustomClaims) + return waitUse + } +} + +// GetUserId 从 gin.Context 中获取用户 ID,并返回 uint 类型的 ID。 +// 该函数首先尝试从上下文中获取已存在的 claims,如果不存在,则调用 GetClaims 函数获取 claims。 +// 如果获取 claims 失败,则返回 0。 +// +// 参数: +// - c: *gin.Context, gin 框架的上下文对象,用于获取请求相关的信息。 +// +// 返回值: +// - uint: 返回用户的 ID,如果获取失败则返回 0。 +func GetUserId(c *gin.Context) string { + if claims, exists := c.Get("claims"); !exists { + if cl, err := GetClaims(c); err != nil { + return "0" + } else { + return cl.BaseClaims.ID + } + } else { + waitUse := claims.(*systemReq.CustomClaims) + return waitUse.BaseClaims.ID + } +} + +// GetClaims 从 Gin 上下文中提取并解析 JWT 令牌,返回自定义的 claims 信息。 +// 该函数首先从请求头中获取 JWT 令牌,然后使用 JWT 解析器解析令牌并返回 claims。 +// 如果解析过程中发生错误,函数会记录错误日志并返回错误信息。 +// +// 参数: +// - c: *gin.Context, Gin 上下文对象,用于获取请求头中的 JWT 令牌。 +// +// 返回值: +// - *systemReq.CustomClaims: 解析后的自定义 claims 信息。 +// - error: 解析过程中发生的错误,如果解析成功则为 nil。 +func GetClaims(c *gin.Context) (*systemReq.CustomClaims, error) { + // 从 Gin 上下文中获取 JWT 令牌 + token := GetToken(c) + + // 创建新的 JWT 解析器 + j := NewJWT() + + // 解析 JWT 令牌并获取 claims 信息 + claims, err := j.ParseToken(token) + if err != nil { + // 如果解析失败,记录错误日志 + global.Logger.Error("获取用户信息失败,请检查请求头是否存在x-token且claims是否为规定结构") + } + + // 返回解析后的 claims 信息和可能的错误 + return claims, err +} diff --git a/utils/auth/jwt.go b/utils/auth/jwt.go new file mode 100644 index 0000000..5e76fb7 --- /dev/null +++ b/utils/auth/jwt.go @@ -0,0 +1,89 @@ +package auth + +import ( + "errors" + "sundynix-go/global" + "sundynix-go/model/system/request" + "sundynix-go/utils/timer" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type JWT struct { + SigningKey []byte +} + +var ( + TokenValid = errors.New("未知错误") + TokenExpired = errors.New("token已过期") + TokenNotValidYet = errors.New("token尚未激活") + TokenMalformed = errors.New("这不是一个token") + TokenSignatureInvalid = errors.New("无效签名") + TokenInvalid = errors.New("无法处理此token") +) + +// NewJWT 初始化JWT +func NewJWT() *JWT { + return &JWT{ + SigningKey: []byte("gin-blog-key"), + } +} + +// CreateClaims 创建Claims +func (j *JWT) CreateClaims(baseClaims request.BaseClaims) request.CustomClaims { + bf, _ := timer.ParseDuration(global.Config.JWT.BufferTime) + ep, _ := timer.ParseDuration(global.Config.JWT.ExpiresTime) + claims := request.CustomClaims{ + BaseClaims: baseClaims, + BufferTime: int64(bf / time.Second), // 缓冲时间1天 缓冲时间内会获得新的token刷新令牌 此时一个用户会存在两个有效令牌 但是前端只留一个 另一个会丢失 + RegisteredClaims: jwt.RegisteredClaims{ + Audience: jwt.ClaimStrings{"sundynix"}, + NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)), // 签名生效时间 + ExpiresAt: jwt.NewNumericDate(time.Now().Add(ep)), // 过期时间 7天 配置文件 + Issuer: global.Config.JWT.Issuer, + }, + } + return claims +} + +// CreateToken 创建一个token +func (j *JWT) CreateToken(claims request.CustomClaims) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(j.SigningKey) +} + +// RefreshToken 刷新token +func (j *JWT) RefreshToken(oldTokenString string, claims request.CustomClaims) (string, error) { + v, err, _ := global.ConcurrencyControl.Do("JWT:"+oldTokenString, func() (interface{}, error) { + return j.CreateToken(claims) + }) + return v.(string), err +} + +// ParseToken 解析token +func (j *JWT) ParseToken(tokenString string) (*request.CustomClaims, error) { + token, err := jwt.ParseWithClaims(tokenString, &request.CustomClaims{}, func(token *jwt.Token) (i interface{}, e error) { + return j.SigningKey, nil + }) + if err != nil { + switch { + case errors.Is(err, jwt.ErrTokenExpired): + return nil, TokenExpired + case errors.Is(err, jwt.ErrTokenNotValidYet): + return nil, TokenNotValidYet + case errors.Is(err, jwt.ErrTokenMalformed): + return nil, TokenMalformed + case errors.Is(err, jwt.ErrTokenSignatureInvalid): + return nil, TokenSignatureInvalid + default: + return nil, TokenInvalid + } + } + if token != nil { + if claims, ok := token.Claims.(*request.CustomClaims); ok && token.Valid { + return claims, nil + } + } + return nil, TokenInvalid +} diff --git a/utils/captcha/redis.go b/utils/captcha/redis.go new file mode 100644 index 0000000..6e09bc6 --- /dev/null +++ b/utils/captcha/redis.go @@ -0,0 +1,5 @@ +package captcha + +import "github.com/mojocn/base64Captcha" + +var CaptchaStore = base64Captcha.DefaultMemStore diff --git a/utils/directory.go b/utils/directory.go new file mode 100644 index 0000000..c2bd8ae --- /dev/null +++ b/utils/directory.go @@ -0,0 +1,20 @@ +package utils + +import ( + "errors" + "os" +) + +func PathExist(path string) (bool, error) { + stat, err := os.Stat(path) + if err == nil { + if stat.IsDir() { + return true, nil + } + return false, errors.New("存在同名文件") + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} diff --git a/utils/hash.go b/utils/hash.go new file mode 100644 index 0000000..e7f23aa --- /dev/null +++ b/utils/hash.go @@ -0,0 +1,32 @@ +package utils + +import ( + "crypto/md5" + "encoding/hex" + + "golang.org/x/crypto/bcrypt" +) + +// BcryptHash 使用 bcrypt 对密码进行加密 +func BcryptHash(password string) string { + bytes, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes) +} + +// BcryptCheck 对比明文密码和数据库的哈希值 +func BcryptCheck(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: MD5V +//@description: md5加密 +//@param: str []byte +//@return: string + +func MD5V(str []byte, b ...byte) string { + h := md5.New() + h.Write(str) + return hex.EncodeToString(h.Sum(b)) +} diff --git a/utils/hash_test.go b/utils/hash_test.go new file mode 100644 index 0000000..e04d239 --- /dev/null +++ b/utils/hash_test.go @@ -0,0 +1,111 @@ +package utils + +import ( + "fmt" + "log" + "sundynix-go/utils/location" + "sundynix-go/utils/timer" + "sundynix-go/utils/uniqueid" + "testing" + "time" +) + +func TestHashPwd(t *testing.T) { + hash := BcryptHash("sundynix") + fmt.Println(hash) // $2a$10$QC/zkQ/ohPmvjF/goDyicu7cHgAEj8gHg6OTDHWhbYQMHHn4dwxX2 + + //check := BcryptCheck("sundynix", "$2a$10$QC/zkQ/ohPmvjF/goDyicu7cHgAEj8gHg6OTDHWhbYQMHHn4dwxX2") + //fmt.Println(check) +} + +func TestUuid(t *testing.T) { + id := uniqueid.GenerateId() + fmt.Println(id) +} + +func TestTimePeriod(t *testing.T) { + str := "2025-11-19 10:29:20.597" + parse, _ := time.ParseInLocation("2006-01-02 15:04:05.999999999", str, time.Local) + interval := timer.TimeInterval(parse) + fmt.Println(interval) +} + +func TestPoint2Code(t *testing.T) { + //latitude := float32(39.90882) + //longitude := float32(116.39748) + longitude := float32(102.74837) + latitude := float32(25.02847) + // 调用逆地理编码 + entity, err := location.Point2code(longitude, latitude) + if err != nil { + log.Fatalf("获取地址失败: %v", err) + } + + // 打印结果 + // 打印结果 + fmt.Println("=== 解析结果 ===") + fmt.Println("格式化地址:", entity.Address) + fmt.Println("省份:", entity.AddressComponent.Province.String()) + fmt.Println("城市:", entity.AddressComponent.City.String()) + fmt.Println("区县:", entity.AddressComponent.District.String()) + fmt.Println("乡镇:", entity.AddressComponent.Township) + fmt.Println("街道:", entity.AddressComponent.Street) + fmt.Println("门牌号:", entity.AddressComponent.StreetNumber.Number) + fmt.Println("门牌号所属街道:", entity.AddressComponent.StreetNumber.Street) + fmt.Println("行政区划编码:", entity.AddressComponent.Adcode) + fmt.Println("国家:", entity.AddressComponent.Country) +} + +func TestWeather(t *testing.T) { + + adcode := "532325" + // 可选:extensions="all" 查询实时+未来3天预报 + weatherResp, err := location.GetWeather(adcode, "base") + if err != nil { + log.Fatalf("查询天气失败: %v", err) + } + + // 打印实时天气 + fmt.Println("=== 实时天气 ===") + live := weatherResp.Lives[0] // 实时天气数组仅1条数据 + fmt.Printf("省份:%s\n", live.Province) + fmt.Printf("城市:%s\n", live.City) + fmt.Printf("行政区划编码:%s\n", live.Adcode) + fmt.Printf("天气:%s\n", live.Weather) + fmt.Printf("实时气温:%s℃\n", live.Temperature) + fmt.Printf("风向:%s\n", live.WindDirection) + fmt.Printf("风力:%s级\n", live.WindPower) + fmt.Printf("湿度:%s%%\n", live.Humidity) + fmt.Printf("数据更新时间:%s\n", live.ReportTime) + + // 若查询的是all(实时+预报),打印预报数据 + // if len(weatherResp.Forecasts) > 0 { + // fmt.Println("\n=== 未来3天预报 ===") + // forecast := weatherResp.Forecasts[0] + // for _, day := range forecast.Casts { + // fmt.Printf("\n日期:%s(星期%s)\n", day.Date, day.Week) + // fmt.Printf("白天天气:%s,温度:%s℃\n", day.DayWeather, day.DayTemp) + // fmt.Printf("夜间天气:%s,温度:%s℃\n", day.NightWeather, day.NightTemp) + // fmt.Printf("白天风向/风力:%s/%s级\n", day.DayWindDir, day.DayWindPower) + // } + // } +} + +func TestGenOrderNo(t *testing.T) { + no := uniqueid.GenOrderNo() + fmt.Println(no) +} + +func TestTime(t *testing.T) { + milliTimestamp := time.Now().UnixMilli() + microTimestamp := time.Now().UnixMicro() + + fmt.Printf("毫秒级时间戳: %d\n", milliTimestamp) + fmt.Printf("微秒级时间戳: %d\n", microTimestamp) +} + +func TestGetZeroTime(t *testing.T) { + zeroTime := timer.GetZeroTime() + fmt.Printf("当天零点: %v\n", zeroTime) + fmt.Printf("时间戳(秒): %v\n", zeroTime.Unix()) +} diff --git a/utils/location/location.go b/utils/location/location.go new file mode 100644 index 0000000..ee50e4d --- /dev/null +++ b/utils/location/location.go @@ -0,0 +1,168 @@ +package location + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "time" +) + +// StringOrArray ########################### 核心:兼容字符串/数组的自定义类型 ########################### +type StringOrArray string + +// UnmarshalJSON 处理字符串/数组两种格式 +func (s *StringOrArray) UnmarshalJSON(data []byte) error { + // 尝试解析为纯字符串 + var str string + if err := json.Unmarshal(data, &str); err == nil { + *s = StringOrArray(str) + return nil + } + + // 尝试解析为字符串数组 + var strArr []string + if err := json.Unmarshal(data, &strArr); err != nil { + return fmt.Errorf("字段解析失败: %v, 原始数据: %s", err, string(data)) + } + + // 数组取第一个元素(高德返回的数组通常仅1个元素) + if len(strArr) > 0 { + *s = StringOrArray(strArr[0]) + } else { + *s = StringOrArray("") + } + return nil +} + +// String 转为普通字符串,方便使用 +func (s StringOrArray) String() string { + return string(s) +} + +// ########################### 严格对齐高德API的结构体 ########################### +// AMapEntity 最终返回的实体(对应Java的AMapEntity) +type AMapEntity struct { + AddressComponent AddressComponent `json:"addresscomponent"` + Address StringOrArray `json:"formatted_address"` // 改为兼容类型 +} + +// StreetNumber 门牌号嵌套对象(高德固定返回对象) +type StreetNumber struct { + Number StringOrArray `json:"number"` // 门牌号 + Location StringOrArray `json:"location"` // 经纬度 + Direction StringOrArray `json:"direction"` // 方向 + Distance StringOrArray `json:"distance"` // 距离 + Street StringOrArray `json:"street"` // 所属街道 +} + +// AddressComponent 地址组件(全量兼容) +type AddressComponent struct { + Province StringOrArray `json:"province"` // 省(字符串/数组) + City StringOrArray `json:"city"` // 市(字符串/数组) + District StringOrArray `json:"district"` // 区(字符串/数组) + Township StringOrArray `json:"township"` // 乡镇(纯字符串) + Street StringOrArray `json:"street"` // 街道(纯字符串) + StreetNumber StreetNumber `json:"streetnumber"` // 门牌号(对象) + Adcode StringOrArray `json:"adcode"` // 行政区划编码(纯字符串) + Country StringOrArray `json:"country"` // 国家(纯字符串) + CountryCode StringOrArray `json:"countrycode"` // 国家编码(纯字符串) +} + +// AMapResponse 高德API顶层响应 +type AMapResponse struct { + Regeocode Regeocode `json:"regeocode"` // 核心数据 + Status string `json:"status"` // 1=成功,0=失败 + Info string `json:"info"` // 错误信息 + Infocode string `json:"infocode"` // 错误码(精准定位问题) +} + +// Regeocode 逆地理编码核心数据 +type Regeocode struct { + FormattedAddress StringOrArray `json:"formatted_address"` // 改为兼容类型 + AddressComponent AddressComponent `json:"addresscomponent"` // 地址组件 +} + +// ########################### 配置常量 ########################### +const ( + appKey = "1b8dd8848bcc062ff1f2cec6db683673" // 替换为你的有效Key + amapApiUrl = "https://restapi.amap.com/v3/geocode/regeo" +) + +// Point2code 经纬度转地址(完整错误处理+兼容) +func Point2code(longitude, latitude float32) (*AMapEntity, error) { + // 1. 基础参数校验 + if longitude == 0 || latitude == 0 { + return nil, errors.New("经纬度不能为0") + } + + // 2. 构建请求URL + baseURL, err := url.Parse(amapApiUrl) + if err != nil { + log.Printf("URL解析失败: %v", err) + return nil, err + } + + // 设置查询参数 + params := url.Values{} + params.Set("key", appKey) + params.Set("location", fmt.Sprintf("%f,%f", longitude, latitude)) + baseURL.RawQuery = params.Encode() + + // 3. 全局HTTP客户端(建议单例复用) + client := &http.Client{Timeout: 10 * time.Second} + + // 4. 发送GET请求 + req, err := http.NewRequest(http.MethodGet, baseURL.String(), nil) + if err != nil { + log.Printf("请求构建失败: %v", err) + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + log.Printf("请求发送失败: %v", err) + return nil, err + } + defer resp.Body.Close() // 强制关闭响应体,避免内存泄漏 + + // 5. 校验HTTP状态码 + if resp.StatusCode != http.StatusOK { + errMsg := fmt.Sprintf("HTTP请求失败,状态码: %d", resp.StatusCode) + log.Println(errMsg) + return nil, errors.New(errMsg) + } + + // 6. 读取响应体 + bodyBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Printf("响应体读取失败: %v", err) + return nil, err + } + + // 7. 解析JSON响应 + var amapResp AMapResponse + if err := json.Unmarshal(bodyBytes, &amapResp); err != nil { + log.Printf("JSON解析失败: %v, 响应内容: %s", err, string(bodyBytes)) + return nil, err + } + + // 8. 校验高德API业务状态 + if amapResp.Status != "1" { + errMsg := fmt.Sprintf("高德API返回失败: status=%s, info=%s, infocode=%s", + amapResp.Status, amapResp.Info, amapResp.Infocode) + log.Println(errMsg) + return nil, errors.New(errMsg) + } + + // 9. 构造最终返回实体 + result := &AMapEntity{ + AddressComponent: amapResp.Regeocode.AddressComponent, + Address: amapResp.Regeocode.FormattedAddress, + } + + return result, nil +} diff --git a/utils/location/weather.go b/utils/location/weather.go new file mode 100644 index 0000000..b3f73b5 --- /dev/null +++ b/utils/location/weather.go @@ -0,0 +1,186 @@ +package location + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "time" +) + +// ########################### 1. 结构体定义(对齐高德天气API返回格式) ########################### +// WeatherResponse 高德天气API顶层响应结构体 +type WeatherResponse struct { + Status string `json:"status"` // 1=成功,0=失败 + Info string `json:"info"` // 错误信息 + Infocode string `json:"infocode"` // 错误码 + Lives []LiveWeather `json:"lives"` // 实时天气(数组,仅1条数据) + Forecasts []Forecast `json:"forecasts"` // 天气预报(数组,仅1条数据) +} + +// LiveWeather 实时天气结构体 +type LiveWeather struct { + Province string `json:"province"` // 省份 + City string `json:"city"` // 城市 + Adcode string `json:"adcode"` // 行政区划编码 + Weather string `json:"weather"` // 天气现象(如晴、阴) + Temperature string `json:"temperature"` // 实时气温(℃) + WindDirection string `json:"winddirection"` // 风向(如东、西南) + WindPower string `json:"windpower"` // 风力(如3级) + Humidity string `json:"humidity"` // 湿度(%) + ReportTime string `json:"reporttime"` // 数据更新时间 +} + +// Forecast 天气预报顶层结构体(包含多日预报) +type Forecast struct { + Province string `json:"province"` // 省份 + City string `json:"city"` // 城市 + Adcode string `json:"adcode"` // 行政区划编码 + ReportTime string `json:"reporttime"` // 预报发布时间 + Casts []ForecastDay `json:"casts"` // 每日预报(未来3天) +} + +// ForecastDay 单日天气预报 +type ForecastDay struct { + Date string `json:"date"` // 日期(yyyy-MM-dd) + Week string `json:"week"` // 星期(1=周一,7=周日) + DayWeather string `json:"dayweather"` // 白天天气 + NightWeather string `json:"nightweather"` // 夜间天气 + DayTemp string `json:"daytemp"` // 白天温度 + NightTemp string `json:"nighttemp"` // 夜间温度 + DayWindDir string `json:"daywinddir"` // 白天风向 + NightWindDir string `json:"nightwinddir"` // 夜间风向 + DayWindPower string `json:"daywindpower"` // 白天风力 + NightWindPower string `json:"nightwindpower"` // 夜间风力 +} + +// ########################### 2. 配置常量 ########################### +const ( + amapWeatherApi = "https://restapi.amap.com/v3/weather/weatherInfo" +) + +// GetWeather 根据行政区划编码查询天气 +// adcode: 行政区划编码(如110101=北京市东城区) +// extensions: 查询类型(base=实时,all=实时+预报) +func GetWeather(adcode string, extensions string) (*WeatherResponse, error) { + // 1. 参数校验 + if adcode == "" { + return nil, errors.New("行政区划编码adcode不能为空") + } + if extensions == "" { + extensions = "base" // 默认查实时天气 + } + if extensions != "base" && extensions != "all" { + return nil, errors.New("extensions仅支持base(实时)或all(实时+预报)") + } + + // 2. 构建请求URL和参数 + baseURL, err := url.Parse(amapWeatherApi) + if err != nil { + log.Printf("解析基础URL失败: %v", err) + return nil, err + } + + // 设置查询参数 + params := url.Values{} + params.Set("key", appKey) + params.Set("city", adcode) // 核心参数:行政区划编码 + params.Set("extensions", extensions) + params.Set("output", "json") // 固定返回JSON格式 + baseURL.RawQuery = params.Encode() + + // 3. 发送HTTP请求(全局客户端,复用连接) + client := &http.Client{ + Timeout: 10 * time.Second, // 10秒超时 + } + req, err := http.NewRequest(http.MethodGet, baseURL.String(), nil) + if err != nil { + log.Printf("构建请求失败: %v", err) + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + log.Printf("发送请求失败: %v", err) + return nil, err + } + defer resp.Body.Close() // 强制关闭响应体 + + // 4. 检查HTTP状态码 + if resp.StatusCode != http.StatusOK { + errMsg := fmt.Sprintf("请求失败,状态码: %d", resp.StatusCode) + log.Println(errMsg) + return nil, errors.New(errMsg) + } + + // 5. 读取响应体 + bodyBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Printf("读取响应体失败: %v", err) + return nil, err + } + + // 6. 解析JSON响应 + var weatherResp WeatherResponse + if err := json.Unmarshal(bodyBytes, &weatherResp); err != nil { + log.Printf("解析JSON失败: %v, 响应内容: %s", err, string(bodyBytes)) + return nil, err + } + + // 7. 检查API业务状态 + if weatherResp.Status != "1" { + errMsg := fmt.Sprintf("高德天气API返回失败: status=%s, info=%s, infocode=%s", + weatherResp.Status, weatherResp.Info, weatherResp.Infocode) + log.Println(errMsg) + return nil, errors.New(errMsg) + } + + // 8. 校验返回数据非空 + if extensions == "base" && len(weatherResp.Lives) == 0 { + return nil, errors.New("未查询到实时天气数据") + } + if extensions == "all" && (len(weatherResp.Lives) == 0 || len(weatherResp.Forecasts) == 0) { + return nil, errors.New("未查询到天气数据(实时/预报)") + } + + return &weatherResp, nil +} + +// ########################### 4. 测试示例 ########################### +func main() { + // 示例1:查询北京市东城区(adcode=110101)实时天气 + adcode := "110101" + // 可选:extensions="all" 查询实时+未来3天预报 + weatherResp, err := GetWeather(adcode, "base") + if err != nil { + log.Fatalf("查询天气失败: %v", err) + } + + // 打印实时天气 + fmt.Println("=== 实时天气 ===") + live := weatherResp.Lives[0] // 实时天气数组仅1条数据 + fmt.Printf("省份:%s\n", live.Province) + fmt.Printf("城市:%s\n", live.City) + fmt.Printf("行政区划编码:%s\n", live.Adcode) + fmt.Printf("天气:%s\n", live.Weather) + fmt.Printf("实时气温:%s℃\n", live.Temperature) + fmt.Printf("风向:%s\n", live.WindDirection) + fmt.Printf("风力:%s级\n", live.WindPower) + fmt.Printf("湿度:%s%%\n", live.Humidity) + fmt.Printf("数据更新时间:%s\n", live.ReportTime) + + // 若查询的是all(实时+预报),打印预报数据 + // if len(weatherResp.Forecasts) > 0 { + // fmt.Println("\n=== 未来3天预报 ===") + // forecast := weatherResp.Forecasts[0] + // for _, day := range forecast.Casts { + // fmt.Printf("\n日期:%s(星期%s)\n", day.Date, day.Week) + // fmt.Printf("白天天气:%s,温度:%s℃\n", day.DayWeather, day.DayTemp) + // fmt.Printf("夜间天气:%s,温度:%s℃\n", day.NightWeather, day.NightTemp) + // fmt.Printf("白天风向/风力:%s/%s级\n", day.DayWindDir, day.DayWindPower) + // } + // } +} diff --git a/utils/timer/human_duration.go b/utils/timer/human_duration.go new file mode 100644 index 0000000..fd9a59b --- /dev/null +++ b/utils/timer/human_duration.go @@ -0,0 +1,30 @@ +package timer + +import ( + "strconv" + "strings" + "time" +) + +// ParseDuration 解析时间 +func ParseDuration(d string) (time.Duration, error) { + d = strings.TrimSpace(d) + dr, err := time.ParseDuration(d) + if err == nil { + return dr, nil + } + if strings.Contains(d, "d") { + index := strings.Index(d, "d") + + hour, _ := strconv.Atoi(d[:index]) + dr = time.Hour * 24 * time.Duration(hour) + ndr, err := time.ParseDuration(d[index+1:]) + if err != nil { + return dr, nil + } + return dr + ndr, nil + } + + dv, err := strconv.ParseInt(d, 10, 64) + return time.Duration(dv), err +} diff --git a/utils/timer/interval.go b/utils/timer/interval.go new file mode 100644 index 0000000..3951722 --- /dev/null +++ b/utils/timer/interval.go @@ -0,0 +1,48 @@ +package timer + +import ( + "fmt" + "time" +) + +// TimeInterval 计算两个时间的间隔,超过24小时返回天数,否则返回几小时前 +func TimeInterval(targetTime time.Time) string { + // 1. 将当前时间和目标时间统一转换为本地时区(time.Local) + now := time.Now().In(time.Local) // 当前时间转为本地时区 + target := targetTime.In(time.Local) // 目标时间转为本地时区 + + // 2. 计算时间差(取绝对值,避免因目标时间在未来导致负数) + var diff time.Duration + if now.After(target) { + diff = now.Sub(target) + } else { + diff = target.Sub(now) + } + + // 3. 转换为总小时数(取整数部分,自动截断小数) + // 转换为总分钟数(取整数部分,自动截断秒数) + totalMinutes := int(diff.Minutes()) + + // 按不同阈值返回对应格式 + switch { + case totalMinutes >= 24*60: // 24小时 = 1440分钟 + days := totalMinutes / (24 * 60) + return fmt.Sprintf("%d天前", days) + case totalMinutes >= 60: // 1小时 = 60分钟 + hours := totalMinutes / 60 + return fmt.Sprintf("%d小时前", hours) + case totalMinutes < 1: + return "刚刚" + default: // 不足1小时 + return fmt.Sprintf("%d分钟前", totalMinutes) + } +} + +// GetZeroTime 获取当天零点时间 +func GetZeroTime() time.Time { + now := time.Now() + + // 2. 使用当天的年月日,将时分秒纳秒设为0,并保留原时区(Location) + zeroTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local) + return zeroTime +} diff --git a/utils/timer/timed_task.go b/utils/timer/timed_task.go new file mode 100644 index 0000000..0a93dd2 --- /dev/null +++ b/utils/timer/timed_task.go @@ -0,0 +1,219 @@ +package timer + +import ( + "sync" + + "github.com/robfig/cron/v3" +) + +type Timer interface { + FindCronList() map[string]*taskManager + AddTaskByFuncWithSecond(cronName string, spec string, fun func(), taskName string, options ...cron.Option) (cron.EntryID, error) // 添加Task Func以秒的形式加入 + AddTaskByJobWithSecond(cronName string, spec string, job interface{ Run() }, taskName string, options ...cron.Option) (cron.EntryID, error) // 添加Task Job以秒的形式加入 + AddTaskByFunc(cronName string, spec string, task func(), taskName string, options ...cron.Option) (cron.EntryID, error) // 添加Task Func加入 + AddTaskByJob(cronName string, spec string, job interface{ Run() }, taskName string, options ...cron.Option) (cron.EntryID, error) // 添加Task Job加入 + FindCron(cronName string) (*taskManager, bool) //获取对应taskName的cron 可能会为空 + StartCron(cronName string) // 启动对应cron + StopCron(cronName string) // 停止对应cron + FindTask(cronName string, taskName string) (*task, bool) //获取对应taskName的task + RemoveTask(cronName string, id int) //删除对应taskName的task + RemoveTaskByName(cronName string, taskName string) //删除对应taskName的task + Clear(cronName string) //清空对应cronName的task + Close() // 关闭所有定时任务 +} + +type task struct { + EntryId cron.EntryID + Spec string + TaskName string +} +type taskManager struct { + corn *cron.Cron + tasks map[cron.EntryID]*task +} + +// timer 定时任务管理 +type timer struct { + cronList map[string]*taskManager + sync.Mutex +} + +// AddTaskByFuncWithSecond 通过函数的方法使用WithSeconds添加任务 +func (t *timer) AddTaskByFuncWithSecond(cronName string, spec string, fun func(), taskName string, option ...cron.Option) (cron.EntryID, error) { + t.Lock() + defer t.Unlock() + option = append(option, cron.WithSeconds()) + if _, ok := t.cronList[cronName]; !ok { + tasks := make(map[cron.EntryID]*task) + t.cronList[cronName] = &taskManager{ + corn: cron.New(option...), + tasks: tasks, + } + } + id, err := t.cronList[cronName].corn.AddFunc(spec, fun) + t.cronList[cronName].corn.Start() + t.cronList[cronName].tasks[id] = &task{ + EntryId: id, + Spec: spec, + TaskName: taskName, + } + return id, err +} + +// AddTaskByFunc 通过函数的方法添加任务 +func (t *timer) AddTaskByFunc(cronName string, spec string, fun func(), taskName string, option ...cron.Option) (cron.EntryID, error) { + t.Lock() + defer t.Unlock() + if _, ok := t.cronList[cronName]; !ok { + tasks := make(map[cron.EntryID]*task) + t.cronList[cronName] = &taskManager{ + corn: cron.New(option...), + tasks: tasks, + } + } + id, err := t.cronList[cronName].corn.AddFunc(spec, fun) + t.cronList[cronName].corn.Start() + t.cronList[cronName].tasks[id] = &task{ + EntryId: id, + Spec: spec, + TaskName: taskName, + } + return id, err +} + +// AddTaskByJobWithSecond 通过Job的方法使用WithSeconds添加任务 +func (t *timer) AddTaskByJobWithSecond(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) { + t.Lock() + defer t.Unlock() + option = append(option, cron.WithSeconds()) + if _, ok := t.cronList[cronName]; !ok { + tasks := make(map[cron.EntryID]*task) + t.cronList[cronName] = &taskManager{ + corn: cron.New(option...), + tasks: tasks, + } + } + id, err := t.cronList[cronName].corn.AddJob(spec, job) + t.cronList[cronName].corn.Start() + t.cronList[cronName].tasks[id] = &task{ + EntryId: id, + Spec: spec, + TaskName: taskName, + } + return id, err +} + +// AddTaskByJob 通过Job的方法添加任务 +func (t *timer) AddTaskByJob(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) { + t.Lock() + defer t.Unlock() + if _, ok := t.cronList[cronName]; !ok { + tasks := make(map[cron.EntryID]*task) + t.cronList[cronName] = &taskManager{ + corn: cron.New(option...), + tasks: tasks, + } + } + id, err := t.cronList[cronName].corn.AddJob(spec, job) + t.cronList[cronName].corn.Start() + t.cronList[cronName].tasks[id] = &task{ + EntryId: id, + Spec: spec, + TaskName: taskName, + } + return id, err +} + +// FindCron 获取对应cronName的cron 可能会为空 +func (t *timer) FindCron(cronName string) (*taskManager, bool) { + t.Lock() + defer t.Unlock() + v, ok := t.cronList[cronName] + return v, ok +} + +// FindTask 获取对应taskName的task +func (t *timer) FindTask(cronName string, taskName string) (*task, bool) { + t.Lock() + defer t.Unlock() + v, ok := t.cronList[cronName] + if !ok { + return nil, false + } + for _, t2 := range v.tasks { + if t2.TaskName == taskName { + return t2, true + } + } + return nil, false +} + +// FindCronList 获取所有cron +func (t *timer) FindCronList() map[string]*taskManager { + t.Lock() + defer t.Unlock() + return t.cronList +} + +// StartCron 启动对应cron +func (t *timer) StartCron(cronName string) { + t.Lock() + defer t.Unlock() + if v, ok := t.cronList[cronName]; ok { + v.corn.Start() + } +} + +// StopCron 停止对应cron +func (t *timer) StopCron(cronName string) { + t.Lock() + defer t.Unlock() + if v, ok := t.cronList[cronName]; ok { + v.corn.Stop() + } +} + +// RemoveTask 从cronName 删除指定任务 +func (t *timer) RemoveTask(cronName string, id int) { + t.Lock() + defer t.Unlock() + if v, ok := t.cronList[cronName]; ok { + v.corn.Remove(cron.EntryID(id)) + delete(v.tasks, cron.EntryID(id)) + } +} + +// RemoveTaskByName 从cronName 删除指定任务 +func (t *timer) RemoveTaskByName(cronName string, taskName string) { + fTask, ok := t.FindTask(cronName, taskName) + if !ok { + return + } + t.RemoveTask(cronName, int(fTask.EntryId)) +} + +// Clear 清空对应cronName的task +func (t *timer) Clear(cronName string) { + t.Lock() + defer t.Unlock() + if v, ok := t.cronList[cronName]; ok { + v.corn.Stop() + delete(t.cronList, cronName) + } +} + +// Close 关闭所有定时任务 释放资源 +func (t *timer) Close() { + t.Lock() + defer t.Unlock() + for _, v := range t.cronList { + v.corn.Stop() + } +} + +// NewTimerTask 创建定时任务 +func NewTimerTask() Timer { + return &timer{ + cronList: make(map[string]*taskManager), + } +} diff --git a/utils/uniqueid/id_generator.go b/utils/uniqueid/id_generator.go new file mode 100644 index 0000000..26baa94 --- /dev/null +++ b/utils/uniqueid/id_generator.go @@ -0,0 +1,41 @@ +package uniqueid + +import ( + "crypto/rand" + "fmt" + "math/big" + "strings" + + "github.com/google/uuid" +) + +func GenerateId() string { + uuidV1, err := uuid.NewUUID() + if err != nil { + panic(err) + } + return uuidV1.String() +} + +func GenerateName() string { + str := uuid.New().String() + //生成一个用户名 比如花友u278bb 中文后的字符随机生成 不可重复 取str的前六位 + return "花友" + str[6:12] + +} + +// GenerateRandomCode 生成邀请码 +func GenerateRandomCode(length int) string { + const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + var sb strings.Builder + for i := 0; i < length; i++ { + n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + sb.WriteByte(charset[n.Int64()]) + } + return sb.String() +} + +// GenCodeKey 生成邀请码的key +func GenCodeKey(userId string) string { + return fmt.Sprintf("code:%s", userId) +} diff --git a/utils/uniqueid/out_trade_no.go b/utils/uniqueid/out_trade_no.go new file mode 100644 index 0000000..e652148 --- /dev/null +++ b/utils/uniqueid/out_trade_no.go @@ -0,0 +1,32 @@ +package uniqueid + +import ( + "fmt" + "time" +) + +func GenOrderNo() string { + /* + 支付宝订单号示例:1217752501201407033233368028 + 分析可能格式: + - 前14位:时间戳(年月日时分秒)20250112 151420 + - 中间6位:商户/业务标识 + - 最后8位:随机数或序列号 + */ + + now := time.Now() + + // 时间部分:年月日时分秒 + timePart := now.Format("20060102150405") // 14位 + + // 业务标识:机器ID + 进程ID + 随机数 + machineID := 1 + pid := now.Nanosecond() % 1000 + businessPart := fmt.Sprintf("%02d%03d%01d", machineID, pid, now.Second()%10) // 6位 + + // 随机部分:纳秒取模 + random1 := fmt.Sprintf("%04d", now.Nanosecond()%10000) + random2 := fmt.Sprintf("%04d", (now.Nanosecond()/10000)%10000) + + return timePart + businessPart + random1 + random2 +} diff --git a/utils/upload/minio_oss.go b/utils/upload/minio_oss.go new file mode 100644 index 0000000..07ae159 --- /dev/null +++ b/utils/upload/minio_oss.go @@ -0,0 +1,106 @@ +package upload + +import ( + "bytes" + "context" + "errors" + "io" + "mime/multipart" + "path/filepath" + "strconv" + "strings" + "sundynix-go/global" + "sundynix-go/utils" + "time" + + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "go.uber.org/zap" +) + +var MinioClient *Minio // 优化性能,但是不支持动态配置 +type Minio struct { + Client *minio.Client + bucket string +} + +func GetMinio(endpoint, accessKey, secretKey, bucketName string, useSSL bool) (*Minio, error) { + if MinioClient != nil { + return MinioClient, nil + } + // Initialize minio client object. + minioClient, err := minio.New(endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(accessKey, secretKey, ""), + Secure: useSSL, // Set to true if using https + }) + if err != nil { + return nil, err + } + + // 创建bucket + err = minioClient.MakeBucket(context.Background(), bucketName, minio.MakeBucketOptions{}) + if err != nil { + // 判断是否已经存在 + exists, errBucketExists := minioClient.BucketExists(context.Background(), bucketName) + if errBucketExists == nil && exists { + global.Logger.Info("Bucket already exists") + } else { + return nil, err + } + } + MinioClient = &Minio{ + Client: minioClient, + bucket: bucketName, + } + return MinioClient, nil +} + +func (m *Minio) UploadFile(file *multipart.FileHeader) (filePathres, key string, uploadErr error) { + f, openErr := file.Open() + // mutipart.File to os.File + if openErr != nil { + global.Logger.Error("function file.Open() Failed", zap.Any("err", openErr.Error())) + return "", "", errors.New("function file.Open() Failed, err:" + openErr.Error()) + } + buffer := bytes.Buffer{} + _, err := io.Copy(&buffer, f) + if err != nil { + global.Logger.Error("读取文件失败", zap.Any("err", err.Error())) + return "", "", errors.New("读取文件失败, err:" + err.Error()) + } + f.Close() // 创建文件 defer 关闭 + + //对文件名进行加密存储 + ext := filepath.Ext(file.Filename) + filename := utils.MD5V([]byte(strings.TrimSuffix(file.Filename, ext))) + ext + timestamp := time.Now().UnixMicro() + timestr := strconv.FormatInt(timestamp, 10) + if global.Config.Minio.BasePath == "" { + filePathres = "uploads/" + time.Now().Format("2006-01-02") + "/" + timestr + "-" + filename // uploads/2025-09-17/xxxx.png + } else { + filePathres = global.Config.Minio.BasePath + "/" + time.Now().Format("2006-01-02") + "/" + timestr + "-" + filename + } + // 设置超时10分钟 + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10) + defer cancel() + + //大文件自动切换为分片上传 + info, err := m.Client.PutObject(ctx, global.Config.Minio.BucketName, filePathres, &buffer, file.Size, minio.PutObjectOptions{ + ContentType: "application/octet-stream", + }) + if err != nil { + global.Logger.Error("上传文件到minio失败", zap.Any("err", err.Error())) + return "", "", errors.New("上传文件到minio失败, err:" + err.Error()) + } + //http://127.0.0.1:9000/planting-fun/uploads/2025-09-17/211476f3837fc7acbaebf0f901c1bd68.png + return global.Config.Minio.BucketUrl + "/" + info.Key, filePathres, nil + +} + +func (m *Minio) DeleteFile(key string) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + err := m.Client.RemoveObject(ctx, m.bucket, key, minio.RemoveObjectOptions{}) + return err +} diff --git a/utils/upload/oss_instance.go b/utils/upload/oss_instance.go new file mode 100644 index 0000000..fd64016 --- /dev/null +++ b/utils/upload/oss_instance.go @@ -0,0 +1,31 @@ +package upload + +import ( + "fmt" + "mime/multipart" + "sundynix-go/global" +) + +// Oss 对象存储接口 +type Oss interface { + UploadFile(file *multipart.FileHeader) (string, string, error) + DeleteFile(key string) error +} + +// OssInstance 实例化oos方法 +func OssInstance() Oss { + switch global.Config.System.OssType { + case "local": + fmt.Println("local") + case "tencent-cos": + return &TencentCOS{} + case "minio": + minioClient, err := GetMinio(global.Config.Minio.Endpoint, global.Config.Minio.AccessKeyId, global.Config.Minio.AccessKeySecret, global.Config.Minio.BucketName, global.Config.Minio.UseSSL) + if err != nil { + global.Logger.Warn("minio初始化失败,请检查minio可用性或安全配置:" + err.Error()) + panic("minio初始化失败,请检查minio可用性或安全配置") + } + return minioClient + } + return nil +} diff --git a/utils/upload/tencent_cos.go b/utils/upload/tencent_cos.go new file mode 100644 index 0000000..440f7b6 --- /dev/null +++ b/utils/upload/tencent_cos.go @@ -0,0 +1,60 @@ +package upload + +import ( + "context" + "errors" + "fmt" + "mime/multipart" + "net/http" + "net/url" + "sundynix-go/global" + "time" + + "github.com/tencentyun/cos-go-sdk-v5" + "go.uber.org/zap" +) + +type TencentCOS struct{} + +// NewClient 创建一个腾讯云COS客户端 +func NewClient() *cos.Client { + urlStr, _ := url.Parse("https://" + global.Config.TencentCOS.Bucket + ".cos." + global.Config.TencentCOS.Region + ".myqcloud.com") + baseURL := &cos.BaseURL{BucketURL: urlStr} + client := cos.NewClient(baseURL, &http.Client{ + Transport: &cos.AuthorizationTransport{ + SecretID: global.Config.TencentCOS.SecretID, + SecretKey: global.Config.TencentCOS.SecretKey, + }, + }) + return client +} + +// UploadFile upload file to COS +func (*TencentCOS) UploadFile(file *multipart.FileHeader) (string, string, error) { + client := NewClient() + f, openError := file.Open() + if openError != nil { + global.Logger.Error("function file.Open() failed", zap.Any("err", openError.Error())) + return "", "", errors.New("function file.Open() failed, err:" + openError.Error()) + } + defer f.Close() // 创建文件 defer 关闭 + fileKey := fmt.Sprintf("%d%s", time.Now().Unix(), file.Filename) + + _, err := client.Object.Put(context.Background(), global.Config.TencentCOS.PathPrefix+"/"+fileKey, f, nil) + if err != nil { + panic(err) + } + return global.Config.TencentCOS.BaseURL + "/" + global.Config.TencentCOS.PathPrefix + "/" + fileKey, fileKey, nil +} + +// DeleteFile delete file form COS +func (*TencentCOS) DeleteFile(key string) error { + client := NewClient() + name := global.Config.TencentCOS.PathPrefix + "/" + key + _, err := client.Object.Delete(context.Background(), name) + if err != nil { + global.Logger.Error("function bucketManager.Delete() failed", zap.Any("err", err.Error())) + return errors.New("function bucketManager.Delete() failed, err:" + err.Error()) + } + return nil +} diff --git a/utils/validator.go b/utils/validator.go new file mode 100644 index 0000000..a56dac0 --- /dev/null +++ b/utils/validator.go @@ -0,0 +1,294 @@ +package utils + +import ( + "errors" + "reflect" + "regexp" + "strconv" + "strings" +) + +type Rules map[string][]string + +type RulesMap map[string]Rules + +var CustomizeMap = make(map[string]Rules) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: RegisterRule +//@description: 注册自定义规则方案建议在路由初始化层即注册 +//@param: key string, rule Rules +//@return: err error + +func RegisterRule(key string, rule Rules) (err error) { + if CustomizeMap[key] != nil { + return errors.New(key + "已注册,无法重复注册") + } else { + CustomizeMap[key] = rule + return nil + } +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: NotEmpty +//@description: 非空 不能为其对应类型的0值 +//@return: string + +func NotEmpty() string { + return "notEmpty" +} + +// @author: [zooqkl](https://github.com/zooqkl) +// @function: RegexpMatch +// @description: 正则校验 校验输入项是否满足正则表达式 +// @param: rule string +// @return: string + +func RegexpMatch(rule string) string { + return "regexp=" + rule +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Lt +//@description: 小于入参(<) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Lt(mark string) string { + return "lt=" + mark +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Le +//@description: 小于等于入参(<=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Le(mark string) string { + return "le=" + mark +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Eq +//@description: 等于入参(==) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Eq(mark string) string { + return "eq=" + mark +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Ne +//@description: 不等于入参(!=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Ne(mark string) string { + return "ne=" + mark +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Ge +//@description: 大于等于入参(>=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Ge(mark string) string { + return "ge=" + mark +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Gt +//@description: 大于入参(>) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Gt(mark string) string { + return "gt=" + mark +} + +// +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Verify +//@description: 校验方法 +//@param: st interface{}, roleMap Rules(入参实例,规则map) +//@return: err error + +func Verify(st interface{}, roleMap Rules) (err error) { + compareMap := map[string]bool{ + "lt": true, + "le": true, + "eq": true, + "ne": true, + "ge": true, + "gt": true, + } + + typ := reflect.TypeOf(st) + val := reflect.ValueOf(st) // 获取reflect.Type类型 + + kd := val.Kind() // 获取到st对应的类别 + if kd != reflect.Struct { + return errors.New("expect struct") + } + num := val.NumField() + // 遍历结构体的所有字段 + for i := 0; i < num; i++ { + tagVal := typ.Field(i) + val := val.Field(i) + if tagVal.Type.Kind() == reflect.Struct { + if err = Verify(val.Interface(), roleMap); err != nil { + return err + } + } + if len(roleMap[tagVal.Name]) > 0 { + for _, v := range roleMap[tagVal.Name] { + switch { + case v == "notEmpty": + if isBlank(val) { + return errors.New(tagVal.Name + "值不能为空") + } + case strings.Split(v, "=")[0] == "regexp": + if !regexpMatch(strings.Split(v, "=")[1], val.String()) { + return errors.New(tagVal.Name + "格式校验不通过") + } + case compareMap[strings.Split(v, "=")[0]]: + if !compareVerify(val, v) { + return errors.New(tagVal.Name + "长度或值不在合法范围," + v) + } + } + } + } + } + return nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: compareVerify +//@description: 长度和数字的校验方法 根据类型自动校验 +//@param: value reflect.Value, VerifyStr string +//@return: bool + +func compareVerify(value reflect.Value, VerifyStr string) bool { + switch value.Kind() { + case reflect.String: + return compare(len([]rune(value.String())), VerifyStr) + case reflect.Slice, reflect.Array: + return compare(value.Len(), VerifyStr) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return compare(value.Uint(), VerifyStr) + case reflect.Float32, reflect.Float64: + return compare(value.Float(), VerifyStr) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return compare(value.Int(), VerifyStr) + default: + return false + } +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: isBlank +//@description: 非空校验 +//@param: value reflect.Value +//@return: bool + +func isBlank(value reflect.Value) bool { + switch value.Kind() { + case reflect.String, reflect.Slice: + return value.Len() == 0 + case reflect.Bool: + return !value.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return value.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return value.Uint() == 0 + case reflect.Float32, reflect.Float64: + return value.Float() == 0 + case reflect.Interface, reflect.Ptr: + return value.IsNil() + } + return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface()) +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: compare +//@description: 比较函数 +//@param: value interface{}, VerifyStr string +//@return: bool + +func compare(value interface{}, VerifyStr string) bool { + VerifyStrArr := strings.Split(VerifyStr, "=") + val := reflect.ValueOf(value) + switch val.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + VInt, VErr := strconv.ParseInt(VerifyStrArr[1], 10, 64) + if VErr != nil { + return false + } + switch { + case VerifyStrArr[0] == "lt": + return val.Int() < VInt + case VerifyStrArr[0] == "le": + return val.Int() <= VInt + case VerifyStrArr[0] == "eq": + return val.Int() == VInt + case VerifyStrArr[0] == "ne": + return val.Int() != VInt + case VerifyStrArr[0] == "ge": + return val.Int() >= VInt + case VerifyStrArr[0] == "gt": + return val.Int() > VInt + default: + return false + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + VInt, VErr := strconv.Atoi(VerifyStrArr[1]) + if VErr != nil { + return false + } + switch { + case VerifyStrArr[0] == "lt": + return val.Uint() < uint64(VInt) + case VerifyStrArr[0] == "le": + return val.Uint() <= uint64(VInt) + case VerifyStrArr[0] == "eq": + return val.Uint() == uint64(VInt) + case VerifyStrArr[0] == "ne": + return val.Uint() != uint64(VInt) + case VerifyStrArr[0] == "ge": + return val.Uint() >= uint64(VInt) + case VerifyStrArr[0] == "gt": + return val.Uint() > uint64(VInt) + default: + return false + } + case reflect.Float32, reflect.Float64: + VFloat, VErr := strconv.ParseFloat(VerifyStrArr[1], 64) + if VErr != nil { + return false + } + switch { + case VerifyStrArr[0] == "lt": + return val.Float() < VFloat + case VerifyStrArr[0] == "le": + return val.Float() <= VFloat + case VerifyStrArr[0] == "eq": + return val.Float() == VFloat + case VerifyStrArr[0] == "ne": + return val.Float() != VFloat + case VerifyStrArr[0] == "ge": + return val.Float() >= VFloat + case VerifyStrArr[0] == "gt": + return val.Float() > VFloat + default: + return false + } + default: + return false + } +} + +func regexpMatch(rule, matchStr string) bool { + return regexp.MustCompile(rule).MatchString(matchStr) +} diff --git a/utils/wechat/access_token.go b/utils/wechat/access_token.go new file mode 100644 index 0000000..e3f873c --- /dev/null +++ b/utils/wechat/access_token.go @@ -0,0 +1,53 @@ +package wechat + +import ( + "context" + "encoding/json" + "errors" + "io" + "log" + "sundynix-go/global" + "sundynix-go/pkg/httpclient" + "time" + + "github.com/redis/go-redis/v9" +) + +// GetMiniAccessToken 获取小程序的access_token +func GetMiniAccessToken() string { + ak, err := global.Redis.Get(context.Background(), "zeeq_mini_access_token").Result() + if errors.Is(err, redis.Nil) { + // 从微信服务器获取 + //重新从微信服务器获取 + url := "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + global.Config.MiniProgram.AppId + "&secret=" + global.Config.MiniProgram.AppSecret + myHttpClient := httpclient.GetClient() + resp, err := myHttpClient.Get(url) + if err != nil { + log.Fatalf("Error making GET request: %s", err) + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + log.Fatalf("Error closing response body: %s", err) + } + }(resp.Body) + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Error reading response body: %s", err) + } + res := string(body) + + var data map[string]interface{} + err = json.Unmarshal([]byte(res), &data) + if err != nil { + log.Fatalf("Error unmarshalling JSON: %s", err) + } + ak = data["access_token"].(string) + ex := data["expires_in"].(float64) + global.Redis.Set(context.Background(), "zeeq_mini_access_token", ak, time.Duration(ex)*time.Second) //秒 + } else if err != nil { + log.Fatalf("Error getting access token from Redis: %s", err) + } + return ak + +} diff --git a/utils/wechat/pay.go b/utils/wechat/pay.go new file mode 100644 index 0000000..bcd97b4 --- /dev/null +++ b/utils/wechat/pay.go @@ -0,0 +1,39 @@ +package wechat + +import ( + "context" + "fmt" + "sundynix-go/global" + + "github.com/wechatpay-apiv3/wechatpay-go/core" + "github.com/wechatpay-apiv3/wechatpay-go/core/option" + "github.com/wechatpay-apiv3/wechatpay-go/utils" +) + +// GetWxPayClient 初始化微信支付客户端 +func GetWxPayClient() (*core.Client, error) { + + //2.加载私钥 + mchPrivateKey, err := utils.LoadPrivateKeyWithPath(global.Config.WechatPay.PrivateKeyPath) + if err != nil { + return nil, err + } + mchPublicKey, err := utils.LoadPublicKeyWithPath(global.Config.WechatPay.PublicKeyPath) + if err != nil { + return nil, err + } + ctx := context.Background() + // 3. 创建客户端配置 使用商户私钥等初始化 client,并使它具有自动定时获取微信支付平台证书的能力 + opts := []core.ClientOption{ + option.WithWechatPayPublicKeyAuthCipher(global.Config.WechatPay.MchId, + global.Config.WechatPay.MchCertificateSerialNumber, + mchPrivateKey, global.Config.WechatPay.PublicKeyId, mchPublicKey), // 自动处理签名/验签 + } + client, err := core.NewClient(ctx, opts...) + + if err != nil { + fmt.Printf("new wechat pay client err:%s", err) + } + return client, err + +} diff --git a/utils/wechat/safety.go b/utils/wechat/safety.go new file mode 100644 index 0000000..07fe0f4 --- /dev/null +++ b/utils/wechat/safety.go @@ -0,0 +1,156 @@ +package wechat + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "sundynix-go/global" + + "go.uber.org/zap" +) + +// WeChatCommonResponse 微信通用响应 +type WeChatCommonResponse struct { + Errcode int `json:"errcode"` + Errmsg string `json:"errmsg"` +} + +// MsgSecCheckDetail 文本检测结果详情 +type MsgSecCheckDetail struct { + Strategy string `json:"strategy"` + Errcode int `json:"errcode"` + Suggest string `json:"suggest"` + Label int `json:"label"` + Prob int `json:"prob"` +} + +// MsgSecCheckResult 文本检测结果 +type MsgSecCheckResult struct { + Suggest string `json:"suggest"` + Label int `json:"label"` +} + +// MsgSecCheckResponse 文本检测响应 +type MsgSecCheckResponse struct { + WeChatCommonResponse + Result MsgSecCheckResult `json:"result"` + Detail []MsgSecCheckDetail `json:"detail"` +} + +// MediaCheckAsyncResponse 媒体检测异步响应 +type MediaCheckAsyncResponse struct { + WeChatCommonResponse + TraceId string `json:"trace_id"` +} + +// MsgSecCheck 文本内容安全识别 +// content: 需检测的文本内容 +// openid: 用户的openid(用户需在近两小时访问过小程序) +// return: true-通过, false-不通过 +func MsgSecCheck(content string, openid string) bool { + // 2024-05-15: 微信api调整 + // https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/sec-center/sec-check/msgSecCheck.html + accessToken := GetMiniAccessToken() + url := "https://api.weixin.qq.com/wxa/msg_sec_check?access_token=" + accessToken + + payload := map[string]interface{}{ + "version": 2, + "openid": openid, + "scene": 2, // 2-资料说明 + "content": content, + } + + jsonData, err := json.Marshal(payload) + if err != nil { + global.Logger.Error("MsgSecCheck json marshal error", zap.Error(err)) + return false + } + + resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + global.Logger.Error("MsgSecCheck http post error", zap.Error(err)) + return false + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + global.Logger.Error("MsgSecCheck read body error", zap.Error(err)) + return false + } + + var response MsgSecCheckResponse + if err := json.Unmarshal(body, &response); err != nil { + global.Logger.Error("MsgSecCheck json unmarshal error", zap.Error(err)) + return false + } + + if response.Errcode != 0 { + global.Logger.Error("MsgSecCheck api error", zap.Int("errcode", response.Errcode), zap.String("errmsg", response.Errmsg)) + return false + } + + // 检查 result.suggest + if response.Result.Suggest == "pass" { + return true + } + + global.Logger.Warn("MsgSecCheck risky content", + zap.String("content", content), + zap.String("suggest", response.Result.Suggest), + zap.Int("label", response.Result.Label), + ) + return false +} + +// MediaCheckAsync 多媒体内容安全识别(异步) +// mediaUrl: 需检测的多媒体url +// mediaType: 1:音频; 2:图片 +// openid: 用户的openid +// return: trace_id, error +func MediaCheckAsync(mediaUrl string, mediaType int, openid string) (string, error) { + accessToken := GetMiniAccessToken() + url := "https://api.weixin.qq.com/wxa/media_check_async?access_token=" + accessToken + + payload := map[string]interface{}{ + "media_url": mediaUrl, + "media_type": mediaType, + "version": 2, + "openid": openid, + "scene": 2, + } + + jsonData, err := json.Marshal(payload) + if err != nil { + return "", err + } + + resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + var response MediaCheckAsyncResponse + if err := json.Unmarshal(body, &response); err != nil { + return "", err + } + + if response.Errcode != 0 { + global.Logger.Error("MediaCheckAsync api error", zap.Int("errcode", response.Errcode), zap.String("errmsg", response.Errmsg)) + return "", fmt.Errorf("errcode: %d, errmsg: %s", response.Errcode, response.Errmsg) + } + + if response.TraceId != "" { + return response.TraceId, nil + } + + return "", fmt.Errorf("no trace_id returned") +}