mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-08-22 07:17:13 +00:00
Compare commits
175 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64f8693871 | |||
| 4270847b5d | |||
| 3eb0177188 | |||
| 48203e0d31 | |||
| 6e6a496f1b | |||
| ddfa588163 | |||
| 809d8d71bd | |||
| 49254b2a32 | |||
| 5979451ea6 | |||
| 46c3d65bea | |||
| c4b44d84e3 | |||
| 822d1831cd | |||
| 0d28402aa3 | |||
| 359999c045 | |||
| 2c6eee7fc1 | |||
| c5badb3e13 | |||
| 65fb58585c | |||
| 02aedd9edc | |||
| 896b5de0a4 | |||
| 1ca58606da | |||
| 3d8c37cb38 | |||
| c8eec6c97e | |||
| 330ac397d4 | |||
| 8aa75a49d2 | |||
| 5d1cbb9050 | |||
| 8cf40ec6fd | |||
| 37b757d910 | |||
| ef6b642011 | |||
| 8b2cf90aab | |||
| 18961d3c26 | |||
| 976b71e592 | |||
| da63446e64 | |||
| 2522be1738 | |||
| 8f37ab859b | |||
| 1cb3c42bb9 | |||
| a60ffca135 | |||
| 1ff0636745 | |||
| 26f3db09a7 | |||
| 52d297624d | |||
| 53866d1461 | |||
| 54c8856adf | |||
| ead30c8779 | |||
| f8e4d2880f | |||
| 85b4cc0a3c | |||
| 3271b924fa | |||
| 2e2b4ee370 | |||
| 14524f0559 | |||
| 957954f5ee | |||
| 3531c7f356 | |||
| 9a4239290b | |||
| 0585edd895 | |||
| 52313fc7f6 | |||
| 9254b8fafe | |||
| c83c88ef27 | |||
| 3c065b99fb | |||
| 3a6f8ccc16 | |||
| 539e91c12e | |||
| 696ef20a80 | |||
| ebaaefaf7a | |||
| 77a8969f73 | |||
| b3b1981264 | |||
| b6d81890cf | |||
| 728de61bd6 | |||
| fb76e24c51 | |||
| d9a177864d | |||
| 536b4b8056 | |||
| 7fb0aad3c7 | |||
| b05334bb77 | |||
| b155597e28 | |||
| 0956bef9db | |||
| 6242f648f1 | |||
| c0832a24b9 | |||
| c9a7449d2d | |||
| f82270d097 | |||
| 5a4778074e | |||
| 9070531531 | |||
| ad52613738 | |||
| e7db720986 | |||
| c651103eff | |||
| 80ee41aee7 | |||
| 000969d902 | |||
| 9a94d98725 | |||
| c0a89d6f32 | |||
| 604ce985bd | |||
| 8c03ecad2b | |||
| af31f91973 | |||
| 0ce5df6811 | |||
| 4e237c9560 | |||
| e456210944 | |||
| cb7235bb83 | |||
| eab8265b9c | |||
| 5f24df6cee | |||
| e8e3783af0 | |||
| ec00f156f0 | |||
| bb6e90d50a | |||
| 8d2519d5a1 | |||
| 6c35c69ed7 | |||
| e994060e93 | |||
| f7cf992598 | |||
| 92915f7678 | |||
| 54f8494b5c | |||
| 068b5ddeef | |||
| 8168377e47 | |||
| 4d048dbfa7 | |||
| 0ee230cf41 | |||
| 9fba68fb14 | |||
| ff96fada02 | |||
| c19b7db8c0 | |||
| 6cfc7175e8 | |||
| a3f6a641aa | |||
| 54fe49de5d | |||
| 454dfc1aa7 | |||
| 3156701d4e | |||
| 41eb0e634a | |||
| 73d003d6c3 | |||
| 5e4ba6d971 | |||
| 76d32c78d8 | |||
| 66776556d8 | |||
| 149f598f6d | |||
| 1d6f0ab714 | |||
| 545f257476 | |||
| 6cb1f16f56 | |||
| 598f6c48fb | |||
| 97e489901a | |||
| eea57790de | |||
| 81b32523ed | |||
| 6b6fe1bebd | |||
| a7063bf30a | |||
| 32fc4d86a2 | |||
| e685876cc0 | |||
| 41e4b1c7ac | |||
| 76a3ada85f | |||
| b1ddcef593 | |||
| 94a5187e75 | |||
| 521ca77541 | |||
| 5dd3c1835a | |||
| 9fb01ee3ee | |||
| 9d72edc048 | |||
| a9505ff72d | |||
| 628ae15fd7 | |||
| 1ce71374ac | |||
| 7c81d946a7 | |||
| cdaeb2a404 | |||
| ca2de54438 | |||
| 303e9ed052 | |||
| 347b640614 | |||
| 19099aed6f | |||
| 615515094b | |||
| c4fe6c825e | |||
| 26c18fcd5a | |||
| a914994483 | |||
| 9edd6621b1 | |||
| 10e3e61b2c | |||
| 643cf6085a | |||
| fa74ae18ee | |||
| dffdbf697b | |||
| 5e59b3a708 | |||
| 7bc55f3ed1 | |||
| 73f5a44e0a | |||
| 2c6abbe7e4 | |||
| 1f0cf11636 | |||
| c44f5d40fe | |||
| 314d81303b | |||
| b9859e5591 | |||
| a3d65ba939 | |||
| 8a2d2f66b5 | |||
| d1c9fd6eba | |||
| d629d842be | |||
| f752ec5b06 | |||
| c7b09f29ca | |||
| 51c270fb29 | |||
| b97d4b7895 | |||
| 0627109b2b | |||
| c2d4530395 | |||
| c3be47d4ce |
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: frontend-developer
|
||||
description: Use this agent when you need assistance with frontend development tasks including Vue.js components, UI implementation, styling, responsive design, state management, or frontend architecture decisions. Examples: <example>Context: User is working on a Vue.js component and needs help with implementing a responsive layout. user: 'I need to create a mobile-friendly chat interface component' assistant: 'I'll use the frontend-developer agent to help design and implement this responsive chat component' <commentary>Since this involves frontend development work with Vue.js and responsive design, use the frontend-developer agent.</commentary></example> <example>Context: User encounters styling issues with Element Plus components. user: 'The Element Plus dialog is not displaying correctly on mobile devices' assistant: 'Let me use the frontend-developer agent to troubleshoot this mobile styling issue' <commentary>This is a frontend styling problem that requires expertise in Element Plus and responsive design.</commentary></example>
|
||||
color: purple
|
||||
---
|
||||
|
||||
You are a Senior Frontend Development Engineer with deep expertise in modern web development technologies, particularly Vue.js 3, Element Plus, Vant, and responsive design patterns. You specialize in creating high-quality, maintainable frontend applications with excellent user experience.
|
||||
|
||||
Your core responsibilities include:
|
||||
- Developing Vue.js 3 components using Composition API and best practices
|
||||
- Implementing responsive designs that work seamlessly across desktop and mobile devices
|
||||
- Working with Element Plus for desktop UI and Vant for mobile components
|
||||
- Managing application state using Pinia store patterns
|
||||
- Styling with Stylus preprocessor and Tailwind CSS utilities
|
||||
- Optimizing build processes with Vite and ensuring proper code organization
|
||||
- Implementing theme switching (dark/light mode) and accessibility features
|
||||
- Follow decoupled development, with HTML, CSS, and JS codes placed in separate files for easier maintenance
|
||||
|
||||
When working on frontend tasks, you will:
|
||||
1. Analyze requirements and suggest the most appropriate Vue.js patterns and component structures
|
||||
2. Ensure responsive design principles are followed, considering both desktop and mobile viewports
|
||||
3. Choose appropriate UI components from Element Plus (desktop) or Vant (mobile) libraries
|
||||
4. Write clean, maintainable code following Vue.js 3 Composition API best practices
|
||||
5. Consider performance implications and suggest optimizations when relevant
|
||||
6. Ensure proper state management using Pinia when component state needs to be shared
|
||||
7. Follow the project's established patterns for routing, API integration, and component organization
|
||||
8. Provide specific code examples and explain the reasoning behind architectural decisions
|
||||
|
||||
You have deep knowledge of:
|
||||
- Vue.js 3 ecosystem (Vue Router, Pinia, Composition API)
|
||||
- Modern CSS techniques and preprocessors (Stylus, Tailwind)
|
||||
- Component library integration (Element Plus, Vant)
|
||||
- Build tools and development workflow (Vite, npm scripts)
|
||||
- Cross-browser compatibility and mobile-first design principles
|
||||
- Performance optimization and code splitting strategies
|
||||
|
||||
Always consider the user experience, code maintainability, and alignment with modern frontend development standards. When suggesting solutions, provide clear explanations and consider both immediate needs and long-term scalability.
|
||||
@@ -0,0 +1,6 @@
|
||||
重构当前页面代码
|
||||
|
||||
1. 把当前页面 JS 代码全部抽离,然后是采用 Pinia 重构
|
||||
2. 把当前页面 CSS 代码全部抽离,如果是 stylus 语法代码,则需要改成 SCSS 语法代码
|
||||
3. 尽量做到代码的复用性,不要重复造轮子
|
||||
4. 移动端的 css 和 js 分别放到对应的 mobile 目录下,不要覆盖 PC 端的代码
|
||||
@@ -0,0 +1,5 @@
|
||||
web/src/assets/fonts filter=lfs diff=lfs merge=lfs -text
|
||||
web/src/assets/iconfont filter=lfs diff=lfs merge=lfs -text
|
||||
web/src/assets/img filter=lfs diff=lfs merge=lfs -text
|
||||
web/public filter=lfs diff=lfs merge=lfs -text
|
||||
api/res filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -1,5 +1,59 @@
|
||||
# 更新日志
|
||||
|
||||
## v4.2.7
|
||||
|
||||
- Bug 修复:修复超级管理员无法修改密码的 Bug
|
||||
- Bug 修复:微信登录配置更新后,没有同步更新到系统配置
|
||||
- 功能优化: 给 AI 对话 API 加上线程锁,确保同一个用户同时只有一个对话请求
|
||||
- 功能新增:支持即梦 AI 4.0 图片编辑,即梦 AI 数字人,动作迁移功能。🔥🔥🔥
|
||||
- 功能新增:新增 AI 对话编辑功能,并优化了重新生成逻辑
|
||||
|
||||
## v4.2.6
|
||||
|
||||
- 功能重构:优化系统配置管理功能,把 OSS,支付,短信,邮件等配置全部迁移到管理后台,无需通过修改配置文档的方式修改 🎉🎉🎉
|
||||
- 功能优化:重构 API 授权代码,采用中间件鉴权方式,实现更加精准的 API 鉴权 🎉🎉🎉
|
||||
- 功能优化:优化 PC 端的 Suno 音乐,视频生成,以及即梦 AI 页面 UI
|
||||
- 功能优化:重构登录和注册页面,兼容移动端和 PC 端,并且所有的登录组件共用了同一套组件代码,大大降低维护成本 🎉🎉🎉
|
||||
- 功能优化:管理后台增加模型批量删除功能
|
||||
- 功能优化:优化 Table 组件 UI,并支持 dark 主题
|
||||
- 功能优化:移动端对话页面支持上传文件和图片
|
||||
- 功能新增:新增微信扫码登录支持
|
||||
- 功能新增:新增安全监控,内容审核功能,支持敏感内容过滤拦截
|
||||
- 功能新增:DALL-E 绘图支持参 Google Banana 图片编辑功能
|
||||
|
||||
## v4.2.5
|
||||
|
||||
- 功能优化:在代码右下角增加复制代码功能按钮,增加收起和展开代码功能
|
||||
- Bug 修复:修复 Shift + Enter 不换行的 Bug
|
||||
- Bug 修复:修复管理后台菜单添加页面的文本错误
|
||||
- Bug 修复:解决聊天页面异常退出不断重连的 bug
|
||||
- 功能优化:把 Luma 和可灵视频生成页面整合成一个视频创作中心页面,统一管理视频任务
|
||||
- 功能新增:增加即梦 AI 专题页面,支持即梦官方原生 API 的图片和视频生成 🎉🎉🎉
|
||||
|
||||
## v4.2.4
|
||||
|
||||
- 功能优化:更改前端构建技术选型,使用 Vite 构建,提升构建速度和兼容性
|
||||
- 功能优化:使用 SSE 发送消息,替换原来的 Websocket 消息方案
|
||||
- 功能新增:管理后台支持设置默认昵称
|
||||
- 功能优化:支持 Suno v4.5 模型支持
|
||||
- 功能新增:用户注册和用户登录增加用户协议和隐私政策功能,需要用户同意协议才可注册和登录。
|
||||
- 功能优化:修改重新回答功能,撤回千面的问答内容为可编辑内容,撤回的内容不会增加额外的上下文
|
||||
- 功能优化:优化聊天记录的存储结构,增加模型名称字段,支持存储更长的模型名称
|
||||
- Bug 修复:聊天应用绑定模型后无效,还是会轮询 API KEY,导致一会成功,一会请求失败。
|
||||
- 功能优化:如果管理后台没有启用会员充值菜单,移动端也不显示充值套餐功能
|
||||
|
||||
## v4.2.3
|
||||
|
||||
- 功能优化:增加模型分组与模型描述,采用卡片展示模式改进模型选择功能体验
|
||||
- 功能优化:化思维导图下载图片的清晰度以及解决拖动、缩放操作后下载图片内容不全问题
|
||||
- Bug 修复:修复 MJ 画图页面已画出的图,点复制指令无效问题
|
||||
- 功能优化:MJ 画图的分辨率支持自定义,优先使用 prompt 中--ar 参数
|
||||
- Bug 修复:修复 MJ 绘画 U1-V1,拼写错误
|
||||
- 功能优化:支持自动迁移数据表结构,无需在手动执行 SQL 了
|
||||
- 功能优化:移除首页的文字动画效果
|
||||
- 功能优化:在聊天页面增加对话列表展开和隐藏功能
|
||||
- 功能优化:聊天页面增加 AI 思考中动画效果
|
||||
|
||||
## v4.2.2
|
||||
|
||||
- 功能优化:开启图形验证码功能的时候现检查是否配置了 API 服务,防止开启之后没法登录的 Bug。
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Build Commands
|
||||
|
||||
### Go Backend (api/)
|
||||
- **Development**: `cd api && go run main.go` (uses config.toml)
|
||||
- **Build**: `cd api && make` (builds both amd64 and arm64 binaries)
|
||||
- **Individual builds**: `make amd64` or `make arm64`
|
||||
- **Clean**: `make clean`
|
||||
- **Config**: Copy `config.sample.toml` to `config.toml` and configure
|
||||
|
||||
### Web Frontend (web/)
|
||||
- **Development**: `cd web && npm run dev` (runs on Vite dev server with --host)
|
||||
- **Build**: `cd web && npm run build`
|
||||
- **Lint**: `cd web && npm run lint` (ESLint with auto-fix)
|
||||
|
||||
### Testing
|
||||
- Backend tests: `cd api/test && bash run_crawler_test.sh`
|
||||
- No specific frontend test configuration found
|
||||
|
||||
## Project Architecture
|
||||
|
||||
### Backend (Go)
|
||||
- **Framework**: Gin web framework with dependency injection via uber-go/fx
|
||||
- **Database**: GORM with MySQL, Redis for caching, LevelDB for local storage
|
||||
- **Authentication**: JWT tokens with Redis session storage
|
||||
- **Middleware**: CORS, authorization, parameter handling, static resource serving
|
||||
- **Structure**:
|
||||
- `handler/`: HTTP request handlers (REST API endpoints)
|
||||
- `service/`: Business logic services (AI integrations, payments, etc.)
|
||||
- `store/`: Database models and data access layer
|
||||
- `core/`: Application server and middleware configuration
|
||||
- `utils/`: Utility functions and helpers
|
||||
|
||||
### Frontend (Vue.js)
|
||||
- **Framework**: Vue 3 with Composition API
|
||||
- **UI Components**: Element Plus + Vant (mobile components)
|
||||
- **State Management**: Pinia
|
||||
- **Routing**: Vue Router with nested routes
|
||||
- **Build Tool**: Vite
|
||||
- **CSS**: Stylus preprocessor with Tailwind CSS utilities
|
||||
- **Features**: Responsive design (desktop/mobile views), theme switching (dark/light)
|
||||
|
||||
### Key Features
|
||||
- **AI Chat**: Multiple chat models and conversation management
|
||||
- **Image Generation**: MidJourney, Stable Diffusion, DALL-E integration
|
||||
- **Audio/Video**: Suno music creation, Luma/KeLing video generation
|
||||
- **User Management**: Authentication, payments, power logs, invitations
|
||||
- **Admin Panel**: Comprehensive management interface
|
||||
|
||||
### Database Models
|
||||
Key entities: User, ChatItem, ChatMessage, ChatRole, ChatModel, Order, Product, AdminUser, and various job types for AI services.
|
||||
|
||||
### API Structure
|
||||
- User APIs: `/api/user/*` (auth, profile, settings)
|
||||
- Chat APIs: `/api/chat/*` (conversations, messages)
|
||||
- AI Service APIs: `/api/mj/*`, `/api/sd/*`, `/api/dall/*`, `/api/suno/*`, `/api/video/*`
|
||||
- Admin APIs: `/api/admin/*` (management functions)
|
||||
|
||||
### Configuration
|
||||
- Backend: TOML configuration file (`config.toml`)
|
||||
- Database: MySQL with automatic migrations
|
||||
- Services: Redis, various AI API integrations
|
||||
- File Storage: Local, Aliyun OSS, MinIO, Qiniu options
|
||||
@@ -1,13 +1,67 @@
|
||||
# GeekAI-PLUS
|
||||
# 🚀 GeekAI-PLUS:一站式 AI 创意生产力平台
|
||||
|
||||
**重新定义 AI 创作体验,让每个人都能成为内容创作大师**
|
||||
|
||||
基于 GeekAI 项目开发的高级版,增加了很多高级功能,比如思维导图,Dalle 绘画等。**高级版源码不会一次性开放,只提供镜像给大家免费使用**,源码会逐步逐步按照版同步迁移到[社区版(GeekAI)](https://github.com/yangjian102621/geekai)。所以如果大家想要二次开发,请移步去社区版。
|
||||
|
||||
## ✨ 核心特色
|
||||
|
||||
### 🎨 **全能 AI 创作矩阵**
|
||||
|
||||
- **智能对话**:集成 ChatGPT、Claude 等多款顶级 AI 模型,支持角色扮演和专业对话
|
||||
- **图像生成**:整合 MidJourney、DALL-E、Stable Diffusion 三大主流 AI 绘画引擎
|
||||
- **音频创作**:Suno AI 音乐生成,从旋律到歌词一键创作专属音乐
|
||||
- **视频制作**:Luma 和 KeLing,即梦,Veo3 视频 AI,文本到视频,创意无限
|
||||
- **思维导图**:AI 辅助思维整理,复杂想法可视化呈现
|
||||
|
||||
### 🏗️ **企业级技术架构**
|
||||
|
||||
- **高性能后端**:Go + Gin + MySQL + Redis,支持高并发访问
|
||||
- **现代化前端**:Vue3 + Element Plus + Vant,桌面移动双端适配
|
||||
- **智能缓存**:多层缓存策略,响应速度提升 80%
|
||||
- **弹性部署**:Docker 容器化部署,一键启动,轻松扩展
|
||||
- **私有化部署**:支持私有化部署,私有化部署不支持升级,需要手动升级
|
||||
- **文档支持**:丰富且详细的部署和 API 开发文档支持,二次开发轻松上手
|
||||
|
||||
### 💼 **商业化就绪**
|
||||
|
||||
- **完整用户系统**:注册登录、权限管理、积分充值
|
||||
- **灵活计费模式**:支持按次付费、包月订阅等多种商业模式
|
||||
- **数据统计分析**:用户行为、消费记录、系统性能全方位监控
|
||||
- **管理后台**:功能完备的管理员界面,运营数据一目了然
|
||||
|
||||
### 🎯 **用户体验优势**
|
||||
|
||||
- **响应式设计**:完美适配桌面、平板、手机等全终端设备
|
||||
- **暗黑模式**:支持明暗主题切换,护眼舒适
|
||||
- **实时交互**:WebSocket 实时通信,创作过程流畅无卡顿
|
||||
- **文件管理**:支持多种云存储,作品安全可靠
|
||||
|
||||
## 🎪 **应用场景**
|
||||
|
||||
- **内容创作者**:博客写作、社交媒体素材、短视频制作
|
||||
- **企业营销**:品牌宣传材料、产品介绍、创意广告
|
||||
- **教育培训**:课件制作、知识图谱、互动内容
|
||||
- **个人娱乐**:AI 聊天、创意绘画、音乐创作
|
||||
|
||||
## 🔥 **为什么选择 GeekAI-PLUS?**
|
||||
|
||||
1. **技术领先**:集成当前最先进的 AI 技术,始终保持创新前沿
|
||||
2. **开箱即用**:完整的商业化解决方案,无需从零开发
|
||||
3. **高度定制**:模块化架构设计,支持个性化功能扩展
|
||||
4. **稳定可靠**:经过大量用户验证,性能稳定,安全可信
|
||||
5. **持续更新**:紧跟 AI 技术发展,功能持续迭代升级
|
||||
|
||||
## 演示站点
|
||||
|
||||
[Geek-AI 创作系统](https://www.geekai.me)
|
||||
|
||||
## 文档地址
|
||||
|
||||
[Geek-AI 文档](https://www.geekai.me/docs/)
|
||||
|
||||
## 部署
|
||||
|
||||
1. 安装 docker 和 docker-compose 程序,这个自行解决。
|
||||
2. 直接在项目根目录运行启动命令:
|
||||
```shell
|
||||
@@ -15,5 +69,9 @@
|
||||
```
|
||||
|
||||
## 功能截图
|
||||
|
||||
请参考 [GeekAI 项目介绍](https://docs.geekai.me/info/)。
|
||||
|
||||
---
|
||||
|
||||
_让 AI 成为你最强大的创作伙伴,开启无限创意可能!_
|
||||
|
||||
@@ -17,5 +17,6 @@ bin
|
||||
data
|
||||
config.toml
|
||||
static/upload
|
||||
static/audio
|
||||
storage.json
|
||||
res/certs/wechat/apiclient_key.pem
|
||||
|
||||
+15
-315
@@ -8,83 +8,72 @@ package core
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/imroc/req/v3"
|
||||
"github.com/nfnt/resize"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
"golang.org/x/image/webp"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AppServer struct {
|
||||
Debug bool
|
||||
Config *types.AppConfig
|
||||
Engine *gin.Engine
|
||||
SysConfig *types.SystemConfig // system config cache
|
||||
Redis *redis.Client
|
||||
}
|
||||
|
||||
func NewServer(appConfig *types.AppConfig) *AppServer {
|
||||
func NewServer(appConfig *types.AppConfig, redis *redis.Client, sysConfig *types.SystemConfig) *AppServer {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
gin.DefaultWriter = io.Discard
|
||||
return &AppServer{
|
||||
Debug: false,
|
||||
Config: appConfig,
|
||||
Engine: gin.Default(),
|
||||
Config: appConfig,
|
||||
Redis: redis,
|
||||
Engine: gin.Default(),
|
||||
SysConfig: sysConfig,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AppServer) Init(debug bool, client *redis.Client) {
|
||||
s.Engine.Use(corsMiddleware())
|
||||
s.Engine.Use(staticResourceMiddleware())
|
||||
s.Engine.Use(authorizeMiddleware(s, client))
|
||||
s.Engine.Use(parameterHandlerMiddleware())
|
||||
func (s *AppServer) Init(client *redis.Client) {
|
||||
s.Engine.Use(middleware.ParameterHandlerMiddleware())
|
||||
s.Engine.Use(errorHandler)
|
||||
// 添加静态资源访问
|
||||
s.Engine.Static("/static", s.Config.StaticDir)
|
||||
s.Engine.Use(middleware.StaticMiddleware())
|
||||
}
|
||||
|
||||
func (s *AppServer) Run(db *gorm.DB) error {
|
||||
// load system configs
|
||||
var sysConfig model.Config
|
||||
err := db.Where("marker", "system").First(&sysConfig).Error
|
||||
err := db.Where("name", "system").First(&sysConfig).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load system config: %v", err)
|
||||
}
|
||||
err = utils.JsonDecode(sysConfig.Config, &s.SysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &s.SysConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode system config: %v", err)
|
||||
}
|
||||
|
||||
// 统计安装信息
|
||||
go func() {
|
||||
info, err := host.Info()
|
||||
if err == nil {
|
||||
apiURL := fmt.Sprintf("%s/%s", s.Config.ApiConfig.ApiURL, "api/installs/push")
|
||||
apiURL := fmt.Sprintf("%s/api/installs/push", types.GeekAPIURL)
|
||||
timestamp := time.Now().Unix()
|
||||
product := "geekai-plus"
|
||||
signStr := fmt.Sprintf("%s#%s#%d", product, info.HostID, timestamp)
|
||||
sign := utils.Sha256(signStr)
|
||||
resp, err := req.C().R().SetBody(map[string]interface{}{"product": product, "device_id": info.HostID, "timestamp": timestamp, "sign": sign}).Post(apiURL)
|
||||
if err != nil {
|
||||
logger.Errorf("register install info failed: %v", err)
|
||||
} else {
|
||||
if err == nil {
|
||||
logger.Debugf("register install info success: %v", resp.String())
|
||||
}
|
||||
}
|
||||
@@ -106,292 +95,3 @@ func errorHandler(c *gin.Context) {
|
||||
//加载完 defer recover,继续后续接口调用
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// 跨域中间件设置
|
||||
func corsMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
method := c.Request.Method
|
||||
origin := c.Request.Header.Get("Origin")
|
||||
|
||||
// 设置允许的请求源
|
||||
if origin != "" {
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
} else {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
}
|
||||
|
||||
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
|
||||
//允许跨域设置可以返回其他子段,可以自定义字段
|
||||
c.Header("Access-Control-Allow-Headers", "Authorization, Body-Length, Body-Type, Admin-Authorization,content-type")
|
||||
// 允许浏览器(客户端)可以解析的头部 (重要)
|
||||
c.Header("Access-Control-Expose-Headers", "Body-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
|
||||
//设置缓存时间
|
||||
c.Header("Access-Control-Max-Age", "172800")
|
||||
//允许客户端传递校验信息比如 cookie (重要)
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
if method == http.MethodOptions {
|
||||
c.JSON(http.StatusOK, "ok!")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Info("Panic info is: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// 用户授权验证
|
||||
func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
clientProtocols := c.GetHeader("Sec-WebSocket-Protocol")
|
||||
var tokenString string
|
||||
isAdminApi := strings.Contains(c.Request.URL.Path, "/api/admin/")
|
||||
if isAdminApi { // 后台管理 API
|
||||
tokenString = c.GetHeader(types.AdminAuthHeader)
|
||||
} else if clientProtocols != "" { // Websocket 连接
|
||||
// 解析子协议内容
|
||||
protocols := strings.Split(clientProtocols, ",")
|
||||
if protocols[0] == "realtime" {
|
||||
tokenString = strings.TrimSpace(protocols[1][25:])
|
||||
} else if protocols[0] == "token" {
|
||||
tokenString = strings.TrimSpace(protocols[1])
|
||||
}
|
||||
} else {
|
||||
tokenString = c.GetHeader(types.UserAuthHeader)
|
||||
}
|
||||
|
||||
if tokenString == "" {
|
||||
if needLogin(c) {
|
||||
resp.NotAuth(c, "You should put Authorization in request headers")
|
||||
c.Abort()
|
||||
return
|
||||
} else { // 直接放行
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok && needLogin(c) {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
if isAdminApi {
|
||||
return []byte(s.Config.AdminSession.SecretKey), nil
|
||||
} else {
|
||||
return []byte(s.Config.Session.SecretKey), nil
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
if err != nil && needLogin(c) {
|
||||
resp.NotAuth(c, fmt.Sprintf("Error with parse auth token: %v", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || !token.Valid && needLogin(c) {
|
||||
resp.NotAuth(c, "Token is invalid")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
expr := utils.IntValue(utils.InterfaceToString(claims["expired"]), 0)
|
||||
if expr > 0 && int64(expr) < time.Now().Unix() && needLogin(c) {
|
||||
resp.NotAuth(c, "Token is expired")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("users/%v", claims["user_id"])
|
||||
if isAdminApi {
|
||||
key = fmt.Sprintf("admin/%v", claims["user_id"])
|
||||
}
|
||||
if _, err := client.Get(context.Background(), key).Result(); err != nil && needLogin(c) {
|
||||
resp.NotAuth(c, "Token is not found in redis")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Set(types.LoginUserID, claims["user_id"])
|
||||
}
|
||||
}
|
||||
|
||||
func needLogin(c *gin.Context) bool {
|
||||
if c.Request.URL.Path == "/api/user/login" ||
|
||||
c.Request.URL.Path == "/api/user/logout" ||
|
||||
c.Request.URL.Path == "/api/user/resetPass" ||
|
||||
c.Request.URL.Path == "/api/admin/login" ||
|
||||
c.Request.URL.Path == "/api/admin/logout" ||
|
||||
c.Request.URL.Path == "/api/admin/login/captcha" ||
|
||||
c.Request.URL.Path == "/api/user/register" ||
|
||||
c.Request.URL.Path == "/api/chat/history" ||
|
||||
c.Request.URL.Path == "/api/chat/detail" ||
|
||||
c.Request.URL.Path == "/api/chat/list" ||
|
||||
c.Request.URL.Path == "/api/app/list" ||
|
||||
c.Request.URL.Path == "/api/app/type/list" ||
|
||||
c.Request.URL.Path == "/api/app/list/user" ||
|
||||
c.Request.URL.Path == "/api/model/list" ||
|
||||
c.Request.URL.Path == "/api/mj/imgWall" ||
|
||||
c.Request.URL.Path == "/api/mj/notify" ||
|
||||
c.Request.URL.Path == "/api/invite/hits" ||
|
||||
c.Request.URL.Path == "/api/sd/imgWall" ||
|
||||
c.Request.URL.Path == "/api/dall/imgWall" ||
|
||||
c.Request.URL.Path == "/api/product/list" ||
|
||||
c.Request.URL.Path == "/api/menu/list" ||
|
||||
c.Request.URL.Path == "/api/markMap/client" ||
|
||||
c.Request.URL.Path == "/api/payment/doPay" ||
|
||||
c.Request.URL.Path == "/api/payment/payWays" ||
|
||||
c.Request.URL.Path == "/api/suno/detail" ||
|
||||
c.Request.URL.Path == "/api/suno/play" ||
|
||||
c.Request.URL.Path == "/api/download" ||
|
||||
c.Request.URL.Path == "/api/dall/models" ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/test") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/payment/notify/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/user/clogin") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/config/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/function/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/static/") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 统一参数处理
|
||||
func parameterHandlerMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// GET 参数处理
|
||||
params := c.Request.URL.Query()
|
||||
for key, values := range params {
|
||||
for i, value := range values {
|
||||
params[key][i] = strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
// update get parameters
|
||||
c.Request.URL.RawQuery = params.Encode()
|
||||
// skip file upload requests
|
||||
contentType := c.Request.Header.Get("Content-Type")
|
||||
if strings.Contains(contentType, "multipart/form-data") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(contentType, "application/json") {
|
||||
// process POST JSON request body
|
||||
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 还原请求体
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
// 将请求体解析为 JSON
|
||||
var jsonData map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&jsonData); err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 对 JSON 数据中的字符串值去除两端空格
|
||||
trimJSONStrings(jsonData)
|
||||
// 更新请求体
|
||||
c.Request.Body = io.NopCloser(bytes.NewBufferString(utils.JsonEncode(jsonData)))
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// 递归对 JSON 数据中的字符串值去除两端空格
|
||||
func trimJSONStrings(data interface{}) {
|
||||
switch v := data.(type) {
|
||||
case map[string]interface{}:
|
||||
for key, value := range v {
|
||||
switch valueType := value.(type) {
|
||||
case string:
|
||||
v[key] = strings.TrimSpace(valueType)
|
||||
case map[string]interface{}, []interface{}:
|
||||
trimJSONStrings(value)
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
for i, value := range v {
|
||||
switch valueType := value.(type) {
|
||||
case string:
|
||||
v[i] = strings.TrimSpace(valueType)
|
||||
case map[string]interface{}, []interface{}:
|
||||
trimJSONStrings(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 静态资源中间件
|
||||
func staticResourceMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
url := c.Request.URL.String()
|
||||
// 拦截生成缩略图请求
|
||||
if strings.HasPrefix(url, "/static/") && strings.Contains(url, "?imageView2") {
|
||||
r := strings.SplitAfter(url, "imageView2")
|
||||
size := strings.Split(r[1], "/")
|
||||
if len(size) != 8 {
|
||||
c.String(http.StatusNotFound, "invalid thumb args")
|
||||
return
|
||||
}
|
||||
with := utils.IntValue(size[3], 0)
|
||||
height := utils.IntValue(size[5], 0)
|
||||
quality := utils.IntValue(size[7], 75)
|
||||
|
||||
// 打开图片文件
|
||||
filePath := strings.TrimLeft(c.Request.URL.Path, "/")
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
c.String(http.StatusNotFound, "Image not found")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 解码图片
|
||||
img, _, err := image.Decode(file)
|
||||
// for .webp image
|
||||
if err != nil {
|
||||
img, err = webp.Decode(file)
|
||||
}
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Error decoding image")
|
||||
return
|
||||
}
|
||||
|
||||
var newImg image.Image
|
||||
if height == 0 || with == 0 {
|
||||
// 固定宽度,高度自适应
|
||||
newImg = resize.Resize(uint(with), uint(height), img, resize.Lanczos3)
|
||||
} else {
|
||||
// 生成缩略图
|
||||
newImg = resize.Thumbnail(uint(with), uint(height), img, resize.Lanczos3)
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
err = jpeg.Encode(&buffer, newImg, &jpeg.Options{Quality: quality})
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 设置图片缓存有效期为一年 (365天)
|
||||
c.Header("Cache-Control", "max-age=31536000, public")
|
||||
// 直接输出图像数据流
|
||||
c.Data(http.StatusOK, "image/jpeg", buffer.Bytes())
|
||||
c.Abort() // 中断请求
|
||||
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
+107
-2
@@ -11,10 +11,12 @@ import (
|
||||
"bytes"
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"os"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
@@ -30,7 +32,6 @@ func NewDefaultConfig() *types.AppConfig {
|
||||
SecretKey: utils.RandString(64),
|
||||
MaxAge: 86400,
|
||||
},
|
||||
ApiConfig: types.ApiConfig{},
|
||||
OSS: types.OSSConfig{
|
||||
Active: "local",
|
||||
Local: types.LocalStorageConfig{
|
||||
@@ -38,7 +39,6 @@ func NewDefaultConfig() *types.AppConfig {
|
||||
BasePath: "./static/upload",
|
||||
},
|
||||
},
|
||||
AlipayConfig: types.AlipayConfig{Enabled: false, SandBox: false},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,3 +74,108 @@ func SaveConfig(config *types.AppConfig) error {
|
||||
|
||||
return os.WriteFile(config.Path, buf.Bytes(), 0644)
|
||||
}
|
||||
|
||||
func LoadSystemConfig(db *gorm.DB) *types.SystemConfig {
|
||||
// 加载系统配置
|
||||
var sysConfig model.Config
|
||||
var baseConfig types.BaseConfig
|
||||
db.Where("name", "system").First(&sysConfig)
|
||||
err := utils.JsonDecode(sysConfig.Value, &baseConfig)
|
||||
if err != nil {
|
||||
logger.Error("load system config error: ", err)
|
||||
}
|
||||
|
||||
// 加载许可证配置
|
||||
var license types.License
|
||||
sysConfig.Id = 0
|
||||
db.Where("name", types.ConfigKeyLicense).First(&sysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &license)
|
||||
if err != nil {
|
||||
logger.Error("load license config error: ", err)
|
||||
}
|
||||
|
||||
// 加载验证码配置
|
||||
var captchaConfig types.CaptchaConfig
|
||||
sysConfig.Id = 0
|
||||
db.Where("name", types.ConfigKeyCaptcha).First(&sysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &captchaConfig)
|
||||
if err != nil {
|
||||
logger.Error("load geek service config error: ", err)
|
||||
}
|
||||
|
||||
// 加载微信登录配置
|
||||
var wxLoginConfig types.WxLoginConfig
|
||||
sysConfig.Id = 0
|
||||
db.Where("name", types.ConfigKeyWxLogin).First(&sysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &wxLoginConfig)
|
||||
if err != nil {
|
||||
logger.Error("load wx login config error: ", err)
|
||||
}
|
||||
|
||||
// 加载短信配置
|
||||
var smsConfig types.SMSConfig
|
||||
sysConfig.Id = 0
|
||||
db.Where("name", types.ConfigKeySms).First(&sysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &smsConfig)
|
||||
if err != nil {
|
||||
logger.Error("load sms config error: ", err)
|
||||
}
|
||||
|
||||
// 加载 OSS 配置
|
||||
var ossConfig types.OSSConfig
|
||||
sysConfig.Id = 0
|
||||
db.Where("name", types.ConfigKeyOss).First(&sysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &ossConfig)
|
||||
if err != nil {
|
||||
logger.Error("load oss config error: ", err)
|
||||
}
|
||||
|
||||
// 加载 SMTP 配置
|
||||
var smtpConfig types.SmtpConfig
|
||||
sysConfig.Id = 0
|
||||
db.Where("name", types.ConfigKeySmtp).First(&sysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &smtpConfig)
|
||||
if err != nil {
|
||||
logger.Error("load smtp config error: ", err)
|
||||
}
|
||||
|
||||
// 加载支付配置
|
||||
var paymentConfig types.PaymentConfig
|
||||
sysConfig.Id = 0
|
||||
db.Where("name", types.ConfigKeyPayment).First(&sysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &paymentConfig)
|
||||
if err != nil {
|
||||
logger.Error("load payment config error: ", err)
|
||||
}
|
||||
|
||||
// 加载文本审查配置
|
||||
var moderationConfig types.ModerationConfig
|
||||
sysConfig.Id = 0
|
||||
db.Where("name", types.ConfigKeyModeration).First(&sysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &moderationConfig)
|
||||
if err != nil {
|
||||
logger.Error("load moderation config error: ", err)
|
||||
}
|
||||
|
||||
// 加载即梦AI配置
|
||||
var jimengConfig types.JimengConfig
|
||||
sysConfig.Id = 0
|
||||
db.Where("name", types.ConfigKeyJimeng).First(&sysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &jimengConfig)
|
||||
if err != nil {
|
||||
logger.Error("load jimeng config error: ", err)
|
||||
}
|
||||
|
||||
return &types.SystemConfig{
|
||||
Base: baseConfig,
|
||||
License: license,
|
||||
SMS: smsConfig,
|
||||
OSS: ossConfig,
|
||||
SMTP: smtpConfig,
|
||||
Payment: paymentConfig,
|
||||
Captcha: captchaConfig,
|
||||
WxLogin: wxLoginConfig,
|
||||
Moderation: moderationConfig,
|
||||
Jimeng: jimengConfig,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/golang-jwt/jwt"
|
||||
)
|
||||
|
||||
// 前端用户授权验证
|
||||
func UserAuthMiddleware(secretKey string, redis *redis.Client) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tokenString := c.GetHeader(types.UserAuthHeader)
|
||||
if tokenString == "" {
|
||||
resp.NotAuth(c, "无效的授权令牌")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("不支持的令牌签名方法: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(secretKey), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
resp.NotAuth(c, fmt.Sprintf("解析授权令牌失败: %v", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || !token.Valid {
|
||||
resp.NotAuth(c, "令牌无效")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
expr := utils.IntValue(utils.InterfaceToString(claims["expired"]), 0)
|
||||
if expr > 0 && int64(expr) < time.Now().Unix() {
|
||||
resp.NotAuth(c, "令牌过期")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("users/%v", claims["user_id"])
|
||||
if _, err := redis.Get(context.Background(), key).Result(); err != nil {
|
||||
resp.NotAuth(c, "当前用户已退出登录")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Set(types.LoginUserID, claims["user_id"])
|
||||
}
|
||||
}
|
||||
|
||||
// 管理后台用户授权验证
|
||||
func AdminAuthMiddleware(secretKey string, redis *redis.Client) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tokenString := c.GetHeader(types.AdminAuthHeader)
|
||||
if tokenString == "" {
|
||||
resp.NotAuth(c, "无效的授权令牌")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("不支持的令牌签名方法: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(secretKey), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
resp.NotAuth(c, fmt.Sprintf("解析授权令牌失败: %v", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || !token.Valid {
|
||||
resp.NotAuth(c, "令牌无效")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
expr := utils.IntValue(utils.InterfaceToString(claims["expired"]), 0)
|
||||
if expr > 0 && int64(expr) < time.Now().Unix() {
|
||||
resp.NotAuth(c, "令牌过期")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("admin/%v", claims["user_id"])
|
||||
if _, err := redis.Get(context.Background(), key).Result(); err != nil {
|
||||
resp.NotAuth(c, "当前用户已退出登录")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set(types.AdminUserID, claims["user_id"])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"geekai/utils"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 统一参数处理
|
||||
func ParameterHandlerMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// GET 参数处理
|
||||
params := c.Request.URL.Query()
|
||||
for key, values := range params {
|
||||
for i, value := range values {
|
||||
params[key][i] = strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
// update get parameters
|
||||
c.Request.URL.RawQuery = params.Encode()
|
||||
// skip file upload requests
|
||||
contentType := c.Request.Header.Get("Content-Type")
|
||||
if strings.Contains(contentType, "multipart/form-data") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(contentType, "application/json") {
|
||||
// process POST JSON request body
|
||||
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 还原请求体
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
// 将请求体解析为 JSON
|
||||
var jsonData map[string]any
|
||||
if err := c.ShouldBindJSON(&jsonData); err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 对 JSON 数据中的字符串值去除两端空格
|
||||
trimJSONStrings(jsonData)
|
||||
// 更新请求体
|
||||
c.Request.Body = io.NopCloser(bytes.NewBufferString(utils.JsonEncode(jsonData)))
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// 递归对 JSON 数据中的字符串值去除两端空格
|
||||
func trimJSONStrings(data any) {
|
||||
switch v := data.(type) {
|
||||
case map[string]any:
|
||||
for key, value := range v {
|
||||
switch valueType := value.(type) {
|
||||
case string:
|
||||
v[key] = strings.TrimSpace(valueType)
|
||||
case map[string]any, []any:
|
||||
trimJSONStrings(value)
|
||||
}
|
||||
}
|
||||
case []any:
|
||||
for i, value := range v {
|
||||
switch valueType := value.(type) {
|
||||
case string:
|
||||
v[i] = strings.TrimSpace(valueType)
|
||||
case map[string]any, []any:
|
||||
trimJSONStrings(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/utils"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
// RateLimitEvery 使用 Redis 做固定间隔限流:在 interval 内仅允许一次请求
|
||||
// Key 优先使用登录用户ID,若没有则退化为 route + IP
|
||||
func RateLimitEvery(redisClient *redis.Client, interval time.Duration) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
keyID := ""
|
||||
if userID, ok := c.Get(types.LoginUserID); ok {
|
||||
keyID = fmt.Sprintf("user:%s", utils.InterfaceToString(userID))
|
||||
} else {
|
||||
keyID = fmt.Sprintf("ip:%s", c.ClientIP())
|
||||
}
|
||||
|
||||
fullPath := c.FullPath()
|
||||
if fullPath == "" {
|
||||
fullPath = c.Request.URL.Path
|
||||
}
|
||||
key := fmt.Sprintf("rl:%s:%s", fullPath, keyID)
|
||||
|
||||
okSet, err := redisClient.SetNX(context.Background(), key, 1, interval).Result()
|
||||
if err != nil {
|
||||
// Redis 异常时放行,避免误伤可用性
|
||||
return
|
||||
}
|
||||
if !okSet {
|
||||
c.JSON(http.StatusTooManyRequests, types.BizVo{Code: types.Failed, Message: "请求过于频繁,请稍后重试"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"geekai/utils"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/nfnt/resize"
|
||||
"golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
// 静态资源中间件
|
||||
func StaticMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
url := c.Request.URL.String()
|
||||
// 拦截生成缩略图请求
|
||||
if strings.HasPrefix(url, "/static/") && strings.Contains(url, "?imageView2") {
|
||||
r := strings.SplitAfter(url, "imageView2")
|
||||
size := strings.Split(r[1], "/")
|
||||
if len(size) != 8 {
|
||||
c.String(http.StatusNotFound, "invalid thumb args")
|
||||
return
|
||||
}
|
||||
with := utils.IntValue(size[3], 0)
|
||||
height := utils.IntValue(size[5], 0)
|
||||
quality := utils.IntValue(size[7], 75)
|
||||
|
||||
// 打开图片文件
|
||||
filePath := strings.TrimLeft(c.Request.URL.Path, "/")
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
c.String(http.StatusNotFound, "Image not found")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 解码图片
|
||||
img, _, err := image.Decode(file)
|
||||
// for .webp image
|
||||
if err != nil {
|
||||
img, err = webp.Decode(file)
|
||||
}
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Error decoding image")
|
||||
return
|
||||
}
|
||||
|
||||
var newImg image.Image
|
||||
if height == 0 || with == 0 {
|
||||
// 固定宽度,高度自适应
|
||||
newImg = resize.Resize(uint(with), uint(height), img, resize.Lanczos3)
|
||||
} else {
|
||||
// 生成缩略图
|
||||
newImg = resize.Thumbnail(uint(with), uint(height), img, resize.Lanczos3)
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
err = jpeg.Encode(&buffer, newImg, &jpeg.Options{Quality: quality})
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 设置图片缓存有效期为一年 (365天)
|
||||
c.Header("Cache-Control", "max-age=31536000, public")
|
||||
// 直接输出图像数据流
|
||||
c.Data(http.StatusOK, "image/jpeg", buffer.Bytes())
|
||||
c.Abort() // 中断请求
|
||||
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
+2
-11
@@ -52,17 +52,6 @@ type Delta struct {
|
||||
} `json:"function_call,omitempty"`
|
||||
}
|
||||
|
||||
// ChatSession 聊天会话对象
|
||||
type ChatSession struct {
|
||||
UserId uint `json:"user_id"`
|
||||
ClientIP string `json:"client_ip"` // 客户端 IP
|
||||
ChatId string `json:"chat_id"` // 客户端聊天会话 ID, 多会话模式专用字段
|
||||
Model ChatModel `json:"model"` // GPT 模型
|
||||
Start int64 `json:"start"` // 开始请求时间戳
|
||||
Tools []int `json:"tools"` // 工具函数列表
|
||||
Stream bool `json:"stream"` // 是否采用流式输出
|
||||
}
|
||||
|
||||
type ChatModel struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -70,6 +59,8 @@ type ChatModel struct {
|
||||
Power int `json:"power"`
|
||||
MaxTokens int `json:"max_tokens"` // 最大响应长度
|
||||
MaxContext int `json:"max_context"` // 最大上下文长度
|
||||
Description string `json:"description"` //模型描述
|
||||
Category string `json:"category"` //模型类别
|
||||
Temperature float32 `json:"temperature"` // 模型温度
|
||||
KeyId int `json:"key_id"` // 绑定 API KEY
|
||||
}
|
||||
|
||||
+59
-102
@@ -17,87 +17,17 @@ type AppConfig struct {
|
||||
Session Session
|
||||
AdminSession Session
|
||||
ProxyURL string
|
||||
MysqlDns string // mysql 连接地址
|
||||
StaticDir string // 静态资源目录
|
||||
StaticUrl string // 静态资源 URL
|
||||
Redis RedisConfig // redis 连接信息
|
||||
ApiConfig ApiConfig // ChatPlus API authorization configs
|
||||
SMS SMSConfig // send mobile message config
|
||||
OSS OSSConfig // OSS config
|
||||
SmtpConfig SmtpConfig // 邮件发送配置
|
||||
XXLConfig XXLConfig
|
||||
AlipayConfig AlipayConfig // 支付宝支付渠道配置
|
||||
HuPiPayConfig HuPiPayConfig // 虎皮椒支付配置
|
||||
GeekPayConfig GeekPayConfig // GEEK 支付配置
|
||||
WechatPayConfig WechatPayConfig // 微信支付渠道配置
|
||||
TikaHost string // TiKa 服务器地址
|
||||
}
|
||||
|
||||
type SmtpConfig struct {
|
||||
UseTls bool // 是否使用 TLS 发送
|
||||
Host string
|
||||
Port int
|
||||
AppName string // 应用名称
|
||||
From string // 发件人邮箱地址
|
||||
Password string // 发件人邮箱密码
|
||||
}
|
||||
|
||||
type ApiConfig struct {
|
||||
ApiURL string
|
||||
AppId string
|
||||
Token string
|
||||
}
|
||||
|
||||
type AlipayConfig struct {
|
||||
Enabled bool // 是否启用该支付通道
|
||||
SandBox bool // 是否沙盒环境
|
||||
AppId string // 应用 ID
|
||||
UserId string // 支付宝用户 ID
|
||||
PrivateKey string // 用户私钥文件路径
|
||||
PublicKey string // 用户公钥文件路径
|
||||
AlipayPublicKey string // 支付宝公钥文件路径
|
||||
RootCert string // Root 秘钥路径
|
||||
NotifyURL string // 异步通知地址
|
||||
ReturnURL string // 同步通知地址
|
||||
}
|
||||
|
||||
type WechatPayConfig struct {
|
||||
Enabled bool // 是否启用该支付通道
|
||||
AppId string // 公众号的APPID,如:wxd678efh567hg6787
|
||||
MchId string // 直连商户的商户号,由微信支付生成并下发
|
||||
SerialNo string // 商户证书的证书序列号
|
||||
PrivateKey string // 用户私钥文件路径
|
||||
ApiV3Key string // API V3 秘钥
|
||||
NotifyURL string // 异步通知地址
|
||||
}
|
||||
|
||||
type HuPiPayConfig struct { //虎皮椒第四方支付配置
|
||||
Enabled bool // 是否启用该支付通道
|
||||
AppId string // App ID
|
||||
AppSecret string // app 密钥
|
||||
ApiURL string // 支付网关
|
||||
NotifyURL string // 异步通知地址
|
||||
ReturnURL string // 同步通知地址
|
||||
}
|
||||
|
||||
// GeekPayConfig GEEK支付配置
|
||||
type GeekPayConfig struct {
|
||||
Enabled bool
|
||||
AppId string // 商户 ID
|
||||
PrivateKey string // 私钥
|
||||
ApiURL string // API 网关
|
||||
NotifyURL string // 异步通知地址
|
||||
ReturnURL string // 同步通知地址
|
||||
Methods []string // 支付方式
|
||||
}
|
||||
|
||||
type XXLConfig struct { // XXL 任务调度配置
|
||||
Enabled bool
|
||||
ServerAddr string
|
||||
ExecutorIp string
|
||||
ExecutorPort string
|
||||
AccessToken string
|
||||
RegistryKey string
|
||||
MysqlDns string // mysql 连接地址
|
||||
StaticDir string // 静态资源目录
|
||||
StaticUrl string // 静态资源 URL
|
||||
Redis RedisConfig // redis 连接信息
|
||||
SMS SMSConfig // send mobile message config
|
||||
OSS OSSConfig // OSS config
|
||||
SmtpConfig SmtpConfig // 邮件发送配置
|
||||
AlipayConfig AlipayConfig // 支付宝支付渠道配置
|
||||
GeekPayConfig EpayConfig // GEEK 支付配置
|
||||
WechatPayConfig WxPayConfig // 微信支付渠道配置
|
||||
TikaHost string // TiKa 服务器地址
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
@@ -127,32 +57,28 @@ func (c RedisConfig) Url() string {
|
||||
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
||||
}
|
||||
|
||||
type SystemConfig struct {
|
||||
Title string `json:"title,omitempty"` // 网站标题
|
||||
Slogan string `json:"slogan,omitempty"` // 网站 slogan
|
||||
AdminTitle string `json:"admin_title,omitempty"` // 管理后台标题
|
||||
Logo string `json:"logo,omitempty"` // 圆形 Logo
|
||||
BarLogo string `json:"bar_logo,omitempty"` // 条形 Logo
|
||||
InitPower int `json:"init_power,omitempty"` // 新用户注册赠送算力值
|
||||
DailyPower int `json:"daily_power,omitempty"` // 每日签到赠送算力
|
||||
InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力值
|
||||
VipMonthPower int `json:"vip_month_power,omitempty"` // VIP 会员每月赠送的算力值
|
||||
type BaseConfig struct {
|
||||
Title string `json:"title,omitempty"` // 网站标题
|
||||
Slogan string `json:"slogan,omitempty"` // 网站 slogan
|
||||
AdminTitle string `json:"admin_title,omitempty"` // 管理后台标题
|
||||
Logo string `json:"logo,omitempty"` // 圆形 Logo
|
||||
BarLogo string `json:"bar_logo,omitempty"` // 条形 Logo
|
||||
|
||||
RegisterWays []string `json:"register_ways,omitempty"` // 注册方式:支持手机(mobile),邮箱注册(email),账号密码注册
|
||||
EnabledRegister bool `json:"enabled_register,omitempty"` // 是否开放注册
|
||||
|
||||
OrderPayTimeout int `json:"order_pay_timeout,omitempty"` //订单支付超时时间
|
||||
VipInfoText string `json:"vip_info_text,omitempty"` // 会员页面充值说明
|
||||
OrderPayTimeout int `json:"order_pay_timeout,omitempty"` //订单支付超时时间,单位:分钟
|
||||
|
||||
InitPower int `json:"init_power,omitempty"` // 新用户注册赠送算力值
|
||||
DailyPower int `json:"daily_power,omitempty"` // 每日签到赠送算力
|
||||
InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力值
|
||||
MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力
|
||||
MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力
|
||||
SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力
|
||||
DallPower int `json:"dall_power,omitempty"` // DALL-E-3 绘图消耗算力
|
||||
SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力
|
||||
LumaPower int `json:"luma_power,omitempty"` // Luma 生成视频消耗算力
|
||||
KeLingPowers map[string]int `json:"keling_powers,omitempty"` // 可灵生成视频消耗算力
|
||||
AdvanceVoicePower int `json:"advance_voice_power,omitempty"` // 高级语音对话消耗算力
|
||||
PromptPower int `json:"prompt_power,omitempty"` // 生成提示词消耗算力
|
||||
|
||||
WechatCardURL string `json:"wechat_card_url,omitempty"` // 微信客服地址
|
||||
|
||||
@@ -162,14 +88,45 @@ type SystemConfig struct {
|
||||
SdNegPrompt string `json:"sd_neg_prompt"` // SD 默认反向提示词
|
||||
MjMode string `json:"mj_mode"` // midjourney 默认的API模式,relax, fast, turbo
|
||||
|
||||
IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单
|
||||
Copyright string `json:"copyright"` // 版权信息
|
||||
ICP string `json:"icp"` // ICP 备案号
|
||||
MarkMapText string `json:"mark_map_text"` // 思维导入的默认文本
|
||||
IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单
|
||||
IndexPage string `json:"index_page"` // 首页显示的页面
|
||||
Copyright string `json:"copyright"` // 版权信息
|
||||
ICP string `json:"icp"` // ICP 备案号
|
||||
GaBeian string `json:"ga_beian"` // 公安备案号
|
||||
|
||||
EnabledVerify bool `json:"enabled_verify"` // 是否启用验证码
|
||||
EmailWhiteList []string `json:"email_white_list"` // 邮箱白名单列表
|
||||
TranslateModelId int `json:"translate_model_id"` // 用来做提示词翻译的大模型 id
|
||||
AssistantModelId int `json:"assistant_model_id"` // 用来做提示词,翻译的AI模型 id
|
||||
MaxFileSize int `json:"max_file_size"` // 最大文件大小,单位:MB
|
||||
|
||||
}
|
||||
|
||||
type SystemConfig struct {
|
||||
Base BaseConfig
|
||||
Payment PaymentConfig
|
||||
OSS OSSConfig
|
||||
SMS SMSConfig
|
||||
SMTP SmtpConfig
|
||||
Captcha CaptchaConfig
|
||||
WxLogin WxLoginConfig
|
||||
Jimeng JimengConfig
|
||||
License License
|
||||
Moderation ModerationConfig
|
||||
}
|
||||
|
||||
// 配置键名常量
|
||||
const (
|
||||
ConfigKeySystem = "system"
|
||||
ConfigKeyNotice = "notice"
|
||||
ConfigKeyAgreement = "agreement"
|
||||
ConfigKeyPrivacy = "privacy"
|
||||
ConfigKeyMarkMap = "mark_map"
|
||||
ConfigKeyCaptcha = "captcha"
|
||||
ConfigKeyWxLogin = "wx_login"
|
||||
ConfigKeyLicense = "license"
|
||||
ConfigKeySms = "sms"
|
||||
ConfigKeySmtp = "smtp"
|
||||
ConfigKeyOss = "oss"
|
||||
ConfigKeyPayment = "payment"
|
||||
ConfigKeyModeration = "moderation"
|
||||
ConfigKeyAI3D = "ai3d"
|
||||
ConfigKeyJimeng = "jimeng"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package types
|
||||
|
||||
import "os"
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
// GeekAI 增值服务
|
||||
var GeekAPIURL = "https://sapi.geekai.me"
|
||||
|
||||
func init() {
|
||||
if os.Getenv("GEEK_API_URL") != "" {
|
||||
GeekAPIURL = os.Getenv("GEEK_API_URL")
|
||||
}
|
||||
}
|
||||
|
||||
// CaptchaConfig 行为验证码配置
|
||||
type CaptchaConfig struct {
|
||||
ApiKey string `json:"api_key,omitempty"`
|
||||
Type string `json:"type,omitempty"` // 验证码类型, 可选值: "dot" 或 "slide"
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
}
|
||||
|
||||
// WxLoginConfig 微信登录配置
|
||||
type WxLoginConfig struct {
|
||||
ApiKey string `json:"api_key,omitempty"`
|
||||
NotifyURL string `json:"notify_url,omitempty"` // 登录成功回调 URL
|
||||
Enabled bool `json:"enabled,omitempty"` // 是否启用微信登录
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package types
|
||||
|
||||
// JimengConfig 即梦AI配置
|
||||
type JimengConfig struct {
|
||||
// 即梦AI的AccessKey和SecretKey
|
||||
AccessKey string `json:"access_key,omitempty"`
|
||||
SecretKey string `json:"secret_key,omitempty"`
|
||||
// 火山引擎大模型专用的验证方式
|
||||
ApiKey string `json:"api_key,omitempty"`
|
||||
// 算力配置
|
||||
Powers map[string]int `json:"powers,omitempty"`
|
||||
}
|
||||
|
||||
// JMTaskStatus 任务状态
|
||||
type JMTaskStatus string
|
||||
|
||||
const (
|
||||
JMTaskStatusInQueue = JMTaskStatus("in_queue") // 任务已提交
|
||||
JMTaskStatusGenerating = JMTaskStatus("generating") // 任务处理中
|
||||
JMTaskStatusDone = JMTaskStatus("done") // 处理完成
|
||||
JMTaskStatusNotFound = JMTaskStatus("not_found") // 任务未找到
|
||||
JMTaskStatusSuccess = JMTaskStatus("success") // 任务成功
|
||||
JMTaskStatusFailed = JMTaskStatus("failed") // 任务失败
|
||||
JMTaskStatusExpired = JMTaskStatus("expired") // 任务过期
|
||||
)
|
||||
|
||||
// JMTaskType 任务类型
|
||||
type JMTaskType string
|
||||
|
||||
const (
|
||||
JMTaskTypeImage = JMTaskType("image") // 文生图
|
||||
JMTaskTypeVideo = JMTaskType("video") // 图生图
|
||||
JMTaskTypeVirtualHuman = JMTaskType("virtual_human") // 图像编辑
|
||||
JMTaskTypeActionTransfer = JMTaskType("action_transfer") // 图像特效
|
||||
)
|
||||
|
||||
// JimengTaskRequest 即梦AI任务请求
|
||||
type JimengTaskRequest struct {
|
||||
TaskType JMTaskType `json:"type"` // 任务类型
|
||||
ReqKey string `json:"req_key"` // 请求Key
|
||||
Action string `json:"action"` // 请求Action
|
||||
Power int `json:"power"` // 消耗算力
|
||||
// 公共参数
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
ImageUrls []string `json:"image_urls,omitempty"`
|
||||
|
||||
// 图片生成参数
|
||||
Size string `json:"size,omitempty"`
|
||||
UsePreLLM bool `json:"use_pre_llm,omitempty"`
|
||||
Scale float64 `json:"scale,omitempty"`
|
||||
ForceSingle bool `json:"force_single,omitempty"`
|
||||
|
||||
// 视频生成参数
|
||||
Duration int `json:"duration,omitempty"` // 视频时长,单位:秒
|
||||
TemplateId string `json:"template_id,omitempty"` // 运镜模板ID
|
||||
AspectRatio string `json:"aspect_ratio,omitempty"`
|
||||
CameraStrength string `json:"camera_strength,omitempty"` // 运镜强度
|
||||
|
||||
// 数字人视频生成参数
|
||||
AudioURL string `json:"audio_url,omitempty"` // 音频URL
|
||||
RecognizeKey string `json:"recognize_key,omitempty"` // 识别主体请求Key
|
||||
|
||||
// 视频动作迁移参数
|
||||
VideoURL string `json:"video_url,omitempty"` // 动作视频URL
|
||||
}
|
||||
@@ -16,7 +16,7 @@ type MKey interface {
|
||||
string | int | uint
|
||||
}
|
||||
type MValue interface {
|
||||
*WsClient | *ChatSession | context.CancelFunc | []any
|
||||
*WsClient | context.CancelFunc | []any
|
||||
}
|
||||
type LMap[K MKey, T MValue] struct {
|
||||
lock sync.RWMutex
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package types
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
// 文本审查
|
||||
type ModerationConfig struct {
|
||||
Enable bool `json:"enable,omitempty"` // 是否启用文本审查
|
||||
Active string `json:"active,omitempty"`
|
||||
EnableGuide bool `json:"enable_guide,omitempty"` // 是否启用模型引导提示词
|
||||
GuidePrompt string `json:"guide_prompt,omitempty"` // 模型引导提示词
|
||||
Gitee ModerationGiteeConfig `json:"gitee,omitempty"`
|
||||
Baidu ModerationBaiduConfig `json:"baidu,omitempty"`
|
||||
Tencent ModerationTencentConfig `json:"tencent,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
ModerationGitee = "gitee"
|
||||
ModerationBaidu = "baidu"
|
||||
ModerationTencent = "tencent"
|
||||
)
|
||||
|
||||
// GiteeAI 文本审查配置
|
||||
type ModerationGiteeConfig struct {
|
||||
ApiKey string `json:"api_key,omitempty"`
|
||||
Model string `json:"model,omitempty"` // 文本审核模型
|
||||
}
|
||||
|
||||
// 百度文本审查配置
|
||||
type ModerationBaiduConfig struct {
|
||||
AccessKey string `json:"access_key,omitempty"`
|
||||
SecretKey string `json:"secret_key,omitempty"`
|
||||
}
|
||||
|
||||
// 腾讯云文本审查配置
|
||||
type ModerationTencentConfig struct {
|
||||
AccessKey string `json:"access_key,omitempty"`
|
||||
SecretKey string `json:"secret_key,omitempty"`
|
||||
}
|
||||
|
||||
type ModerationResult struct {
|
||||
Flagged bool `json:"flagged,omitempty"`
|
||||
Categories map[string]bool `json:"categories,omitempty"`
|
||||
CategoryScores map[string]float64 `json:"category_scores,omitempty"`
|
||||
}
|
||||
|
||||
var ModerationCategories = map[string]string{
|
||||
"politic": "内容涉及人物、事件或敏感的政治观点",
|
||||
"porn": "明确的色情内容",
|
||||
"insult": "具有侮辱、攻击性语言、人身攻击或冒犯性表达",
|
||||
"violence": "包含暴力、血腥、攻击行为或煽动暴力的言论",
|
||||
"illegal": "涉及违法活动的内容,如诈骗、赌博等",
|
||||
"terror": "宣扬恐怖主义、极端暴力或煽动恐怖行为的内容",
|
||||
"ad": "垃圾广告或未经许可的推广内容",
|
||||
"spam": "无意义重复内容或诱导性信息",
|
||||
"abuse": "人身攻击、恶意辱骂或侮辱性言论",
|
||||
"polity": "涉及国家政治、领导人或政策的违规讨论内容",
|
||||
}
|
||||
|
||||
// 敏感词来源
|
||||
const (
|
||||
ModerationSourceChat = "chat"
|
||||
ModerationSourceMJ = "mj"
|
||||
ModerationSourceDalle = "dalle"
|
||||
ModerationSourceSD = "sd"
|
||||
ModerationSourceSuno = "suno"
|
||||
ModerationSourceVideo = "video"
|
||||
ModerationSourceJiMeng = "jimeng"
|
||||
)
|
||||
+12
-16
@@ -11,29 +11,25 @@ type OrderStatus int
|
||||
|
||||
const (
|
||||
OrderNotPaid = OrderStatus(0)
|
||||
OrderScanned = OrderStatus(1) // 已扫码
|
||||
OrderPaidSuccess = OrderStatus(2)
|
||||
OrderPaidSuccess = OrderStatus(2) // 已支付
|
||||
OrderPaidFailed = OrderStatus(3) // 已关闭
|
||||
)
|
||||
|
||||
type OrderRemark struct {
|
||||
Days int `json:"days"` // 有效期
|
||||
Power int `json:"power"` // 增加算力点数
|
||||
Name string `json:"name"` // 产品名称
|
||||
Price float64 `json:"price"`
|
||||
Discount float64 `json:"discount"`
|
||||
Days int `json:"days"` // 有效期
|
||||
Power int `json:"power"` // 增加算力点数
|
||||
Name string `json:"name"` // 产品名称
|
||||
Price float64 `json:"price"`
|
||||
}
|
||||
|
||||
var PayMethods = map[string]string{
|
||||
// PayChannel 支付渠道
|
||||
var PayChannel = map[string]string{
|
||||
"alipay": "支付宝商号",
|
||||
"wechat": "微信商号",
|
||||
"hupi": "虎皮椒",
|
||||
"geek": "易支付",
|
||||
"wxpay": "微信商号",
|
||||
"epay": "易支付",
|
||||
}
|
||||
var PayNames = map[string]string{
|
||||
|
||||
var PayWays = map[string]string{
|
||||
"alipay": "支付宝",
|
||||
"wxpay": "微信支付",
|
||||
"qqpay": "QQ钱包",
|
||||
"jdpay": "京东支付",
|
||||
"douyin": "抖音支付",
|
||||
"paypal": "PayPal支付",
|
||||
}
|
||||
|
||||
+24
-26
@@ -8,41 +8,39 @@ package types
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
type OSSConfig struct {
|
||||
Active string
|
||||
Local LocalStorageConfig
|
||||
Minio MiniOssConfig
|
||||
QiNiu QiNiuOssConfig
|
||||
AliYun AliYunOssConfig
|
||||
Active string `json:"active,omitempty"`
|
||||
Local LocalStorageConfig `json:"local,omitempty"`
|
||||
Minio MiniOssConfig `json:"minio,omitempty"`
|
||||
QiNiu QiNiuOssConfig `json:"qiniu,omitempty"`
|
||||
AliYun AliYunOssConfig `json:"aliyun,omitempty"`
|
||||
}
|
||||
|
||||
type MiniOssConfig struct {
|
||||
Endpoint string
|
||||
AccessKey string
|
||||
AccessSecret string
|
||||
Bucket string
|
||||
SubDir string
|
||||
UseSSL bool
|
||||
Domain string
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
AccessKey string `json:"access_key,omitempty"`
|
||||
AccessSecret string `json:"access_secret,omitempty"`
|
||||
Bucket string `json:"bucket,omitempty"`
|
||||
UseSSL bool `json:"use_ssl,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
}
|
||||
|
||||
type QiNiuOssConfig struct {
|
||||
Zone string
|
||||
AccessKey string
|
||||
AccessSecret string
|
||||
Bucket string
|
||||
SubDir string
|
||||
Domain string
|
||||
Zone string `json:"zone,omitempty"`
|
||||
AccessKey string `json:"access_key,omitempty"`
|
||||
AccessSecret string `json:"access_secret,omitempty"`
|
||||
Bucket string `json:"bucket,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
}
|
||||
|
||||
type AliYunOssConfig struct {
|
||||
Endpoint string
|
||||
AccessKey string
|
||||
AccessSecret string
|
||||
Bucket string
|
||||
SubDir string
|
||||
Domain string
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
AccessKey string `json:"access_key,omitempty"`
|
||||
AccessSecret string `json:"access_secret,omitempty"`
|
||||
Bucket string `json:"bucket,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
}
|
||||
|
||||
type LocalStorageConfig struct {
|
||||
BasePath string
|
||||
BaseURL string
|
||||
BasePath string `json:"base_path,omitempty"`
|
||||
BaseURL string `json:"base_url,omitempty"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package types
|
||||
|
||||
type PaymentConfig struct {
|
||||
Alipay AlipayConfig `json:"alipay,omitempty"` // 支付宝支付渠道配置
|
||||
Epay EpayConfig `json:"epay,omitempty"` // 易支付配置
|
||||
WxPay WxPayConfig `json:"wxpay,omitempty"` // 微信支付渠道配置
|
||||
}
|
||||
|
||||
// AlipayConfig 支付宝支付配置
|
||||
type AlipayConfig struct {
|
||||
Enabled bool `json:"enabled,omitempty"` // 是否启用该支付通道
|
||||
SandBox bool `json:"sandbox,omitempty"` // 是否沙盒环境
|
||||
AppId string `json:"app_id,omitempty"` // 应用 ID
|
||||
PrivateKey string `json:"private_key,omitempty"` // 应用私钥
|
||||
AlipayPublicKey string `json:"alipay_public_key,omitempty"` // 支付宝公钥
|
||||
Domain string `json:"domain,omitempty"` // 支付回调域名
|
||||
}
|
||||
|
||||
func (c *AlipayConfig) Equal(other *AlipayConfig) bool {
|
||||
return c.AppId == other.AppId &&
|
||||
c.PrivateKey == other.PrivateKey &&
|
||||
c.AlipayPublicKey == other.AlipayPublicKey &&
|
||||
c.Domain == other.Domain
|
||||
}
|
||||
|
||||
// WxPayConfig 微信支付配置
|
||||
type WxPayConfig struct {
|
||||
Enabled bool `json:"enabled,omitempty"` // 是否启用该支付通道
|
||||
AppId string `json:"app_id,omitempty"` // 公众号的APPID,如:wxd678efh567hg6787
|
||||
MchId string `json:"mch_id,omitempty"` // 直连商户的商户号,由微信支付生成并下发
|
||||
SerialNo string `json:"serial_no,omitempty"` // 商户证书的证书序列号
|
||||
PrivateKey string `json:"private_key,omitempty"` // 商户证书私钥
|
||||
ApiV3Key string `json:"api_v3_key,omitempty"` // API V3 秘钥
|
||||
Domain string `json:"domain,omitempty"` // 支付回调域名
|
||||
}
|
||||
|
||||
func (c *WxPayConfig) Equal(other *WxPayConfig) bool {
|
||||
return c.AppId == other.AppId &&
|
||||
c.MchId == other.MchId &&
|
||||
c.SerialNo == other.SerialNo &&
|
||||
c.PrivateKey == other.PrivateKey &&
|
||||
c.ApiV3Key == other.ApiV3Key &&
|
||||
c.Domain == other.Domain
|
||||
}
|
||||
|
||||
// EpayConfig 易支付配置
|
||||
type EpayConfig struct {
|
||||
Enabled bool `json:"enabled,omitempty"` // 是否启用该支付通道
|
||||
AppId string `json:"app_id,omitempty"` // 商户 ID
|
||||
PrivateKey string `json:"private_key,omitempty"` // 私钥
|
||||
ApiURL string `json:"api_url,omitempty"` // z支付 API 网关
|
||||
Domain string `json:"domain,omitempty"` // 支付回调域名
|
||||
}
|
||||
|
||||
func (c *EpayConfig) Equal(other *EpayConfig) bool {
|
||||
return c.AppId == other.AppId &&
|
||||
c.PrivateKey == other.PrivateKey &&
|
||||
c.ApiURL == other.ApiURL &&
|
||||
c.Domain == other.Domain
|
||||
}
|
||||
@@ -8,6 +8,7 @@ package types
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
const LoginUserID = "LOGIN_USER_ID"
|
||||
const AdminUserID = "ADMIN_USER_ID"
|
||||
const LoginUserCache = "LOGIN_USER_CACHE"
|
||||
|
||||
const UserAuthHeader = "Authorization"
|
||||
|
||||
+11
-14
@@ -8,26 +8,23 @@ package types
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
type SMSConfig struct {
|
||||
Active string
|
||||
Ali SmsConfigAli
|
||||
Bao SmsConfigBao
|
||||
Active string `json:"active,omitempty"`
|
||||
Ali SmsConfigAli `json:"aliyun,omitempty"`
|
||||
Bao SmsConfigBao `json:"bao,omitempty"`
|
||||
}
|
||||
|
||||
// SmsConfigAli 阿里云短信平台配置
|
||||
type SmsConfigAli struct {
|
||||
AccessKey string
|
||||
AccessSecret string
|
||||
Product string
|
||||
Domain string
|
||||
Sign string // 短信签名
|
||||
CodeTempId string // 验证码短信模板 ID
|
||||
AccessKey string `json:"access_key,omitempty"`
|
||||
AccessSecret string `json:"access_secret,omitempty"`
|
||||
Sign string `json:"sign,omitempty"` // 短信签名
|
||||
CodeTempId string `json:"code_temp_id,omitempty"` // 验证码短信模板 ID
|
||||
}
|
||||
|
||||
// SmsConfigBao 短信宝平台配置
|
||||
type SmsConfigBao struct {
|
||||
Username string //短信宝平台注册的用户名
|
||||
Password string //短信宝平台注册的密码
|
||||
Domain string //域名
|
||||
Sign string // 短信签名
|
||||
CodeTemplate string // 验证码短信模板 匹配
|
||||
Username string `json:"username,omitempty"` //短信宝平台注册的用户名
|
||||
Password string `json:"password,omitempty"` //短信宝平台注册的密码
|
||||
Sign string `json:"sign,omitempty"` // 短信签名
|
||||
CodeTemplate string `json:"code_template,omitempty"` // 验证码短信模板 匹配
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package types
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
type SmtpConfig struct {
|
||||
UseTls bool `json:"use_tls,omitempty"` // 是否使用 TLS 发送
|
||||
Host string `json:"host,omitempty"` // 邮件服务器地址
|
||||
Port int `json:"port,omitempty"` // 邮件服务器端口
|
||||
AppName string `json:"app_name,omitempty"` // 应用名称
|
||||
From string `json:"from,omitempty"` // 发件人邮箱地址
|
||||
Password string `json:"password,omitempty"` // 发件人邮箱密码
|
||||
}
|
||||
|
||||
func (s *SmtpConfig) Equal(other *SmtpConfig) bool {
|
||||
return s.UseTls == other.UseTls &&
|
||||
s.Host == other.Host &&
|
||||
s.Port == other.Port &&
|
||||
s.AppName == other.AppName &&
|
||||
s.From == other.From &&
|
||||
s.Password == other.Password
|
||||
}
|
||||
+12
-11
@@ -70,17 +70,18 @@ type SdTaskParams struct {
|
||||
|
||||
// DallTask DALL-E task
|
||||
type DallTask struct {
|
||||
ModelId uint `json:"model_id"`
|
||||
ModelName string `json:"model_name"`
|
||||
Id uint `json:"id"`
|
||||
UserId uint `json:"user_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n"`
|
||||
Quality string `json:"quality"`
|
||||
Size string `json:"size"`
|
||||
Style string `json:"style"`
|
||||
Power int `json:"power"`
|
||||
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
|
||||
ModelId uint `json:"model_id"`
|
||||
ModelName string `json:"model_name"`
|
||||
Image []string `json:"image,omitempty"`
|
||||
Id uint `json:"id"`
|
||||
UserId uint `json:"user_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n"`
|
||||
Quality string `json:"quality"`
|
||||
Size string `json:"size"`
|
||||
Style string `json:"style"`
|
||||
Power int `json:"power"`
|
||||
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
|
||||
}
|
||||
|
||||
type SunoTask struct {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package types
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import "sync"
|
||||
|
||||
// UserLockManager 提供基于用户ID的TryLock功能,确保同一用户并发请求串行化
|
||||
type UserLockManager struct {
|
||||
mu sync.Mutex
|
||||
locks map[uint]bool
|
||||
}
|
||||
|
||||
func NewUserLockManager() *UserLockManager {
|
||||
return &UserLockManager{mu: sync.Mutex{}, locks: make(map[uint]bool)}
|
||||
}
|
||||
|
||||
// TryLock 尝试为指定用户加锁。若已被占用返回 false
|
||||
func (m *UserLockManager) TryLock(userId uint) bool {
|
||||
if userId == 0 {
|
||||
return true
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.locks[userId] {
|
||||
return false
|
||||
}
|
||||
m.locks[userId] = true
|
||||
return true
|
||||
}
|
||||
|
||||
// Unlock 释放指定用户的锁
|
||||
func (m *UserLockManager) Unlock(userId uint) {
|
||||
if userId == 0 {
|
||||
return
|
||||
}
|
||||
m.mu.Lock()
|
||||
delete(m.locks, userId)
|
||||
m.mu.Unlock()
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@ build_name: runner-build
|
||||
build_log: runner-build-errors.log
|
||||
valid_ext: .go, .tpl, .tmpl, .html
|
||||
no_rebuild_ext: .tpl, .tmpl, .html, .js, .vue
|
||||
ignored: assets, tmp, web, .git, .idea, test, data
|
||||
ignored: assets, tmp, web, .git, .idea, test, data, static
|
||||
build_delay: 600
|
||||
colors: 1
|
||||
log_color_main: cyan
|
||||
|
||||
+7
-12
@@ -18,22 +18,22 @@ require (
|
||||
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480
|
||||
github.com/qiniu/go-sdk/v7 v7.17.1
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/volcengine/volc-sdk-golang v1.0.23
|
||||
go.uber.org/zap v1.23.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gorm.io/driver/mysql v1.4.7
|
||||
)
|
||||
|
||||
require github.com/xxl-job/xxl-job-executor-go v1.2.0
|
||||
|
||||
require (
|
||||
github.com/go-pay/gopay v1.5.101
|
||||
github.com/go-rod/rod v0.116.2
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/google/go-tika v0.3.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
github.com/sashabaranov/go-openai v1.38.1
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/shopspring/decimal v1.3.1
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
github.com/volcengine/volcengine-go-sdk v1.1.34
|
||||
golang.org/x/image v0.15.0
|
||||
)
|
||||
|
||||
@@ -45,17 +45,13 @@ require (
|
||||
github.com/go-pay/util v0.0.2 // indirect
|
||||
github.com/go-pay/xlog v0.0.2 // indirect
|
||||
github.com/go-pay/xtime v0.0.2 // indirect
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.13 // indirect
|
||||
github.com/tklauser/numcpus v0.7.0 // indirect
|
||||
github.com/ysmood/fetchup v0.3.0 // indirect
|
||||
github.com/ysmood/goob v0.4.0 // indirect
|
||||
github.com/ysmood/got v0.40.0 // indirect
|
||||
github.com/ysmood/gson v0.7.3 // indirect
|
||||
github.com/ysmood/leakless v0.9.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -68,7 +64,6 @@ require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gaukas/godicttls v0.0.3 // indirect
|
||||
github.com/go-basic/ipv4 v1.0.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
@@ -78,7 +73,7 @@ require (
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
@@ -120,7 +115,7 @@ require (
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/fx v1.19.3
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.7.0 // indirect
|
||||
golang.org/x/crypto v0.23.0
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
gorm.io/gorm v1.25.1
|
||||
|
||||
+83
-24
@@ -1,3 +1,5 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
|
||||
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405 h1:cKNFQmeCQFN0WNfjScKoVrGi7vXxTVbkCvCqSrOf+P4=
|
||||
@@ -6,6 +8,7 @@ github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible h1:Sg/2xHwDrioHpxTN6WMiw
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
||||
@@ -13,11 +16,13 @@ github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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=
|
||||
@@ -28,6 +33,8 @@ github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0
|
||||
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
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/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
@@ -39,8 +46,6 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-basic/ipv4 v1.0.0 h1:gjyFAa1USC1hhXTkPOwBWDPfMcUaIM+tvo1XzV9EZxs=
|
||||
github.com/go-basic/ipv4 v1.0.0/go.mod h1:etLBnaxbidQfuqE6wgZQfs38nEWNmzALkxDZe4xY8Dg=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
@@ -73,8 +78,6 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
|
||||
github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
@@ -82,13 +85,33 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-tika v0.3.1 h1:l+jr10hDhZjcgxFRfcQChRLo1bPXQeLFluMyvDhXTTA=
|
||||
@@ -115,8 +138,11 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
@@ -127,6 +153,7 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
@@ -179,6 +206,7 @@ github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 h1:IFhPCcB0/H
|
||||
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480/go.mod h1:BijIqAP84FMYC4XbdJgjyMpiSjusU8x0Y0W9K2t0QtU=
|
||||
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk=
|
||||
github.com/qiniu/go-sdk/v7 v7.17.1 h1:UoQv7fBKtzAiD1qZPIvTy62Se48YLKxcCYP9nAwWMa0=
|
||||
github.com/qiniu/go-sdk/v7 v7.17.1/go.mod h1:nqoYCNo53ZlGA521RvRethvxUDvXKt4gtYXOwye868w=
|
||||
@@ -208,6 +236,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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=
|
||||
@@ -230,22 +259,10 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK
|
||||
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/xxl-job/xxl-job-executor-go v1.2.0 h1:MTl2DpwrK2+hNjRRks2k7vB3oy+3onqm9OaSarneeLQ=
|
||||
github.com/xxl-job/xxl-job-executor-go v1.2.0/go.mod h1:bUFhz/5Irp9zkdYk5MxhQcDDT6LlZrI8+rv5mHtQ1mo=
|
||||
github.com/ysmood/fetchup v0.3.0 h1:UhYz9xnLEVn2ukSuK3KCgcznWpHMdrmbsPpllcylyu8=
|
||||
github.com/ysmood/fetchup v0.3.0/go.mod h1:hbysoq65PXL0NQeNzUczNYIKpwpkwFL4LXMDEvIQq9A=
|
||||
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
|
||||
github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18=
|
||||
github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg=
|
||||
github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk=
|
||||
github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q=
|
||||
github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg=
|
||||
github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY=
|
||||
github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM=
|
||||
github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE=
|
||||
github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg=
|
||||
github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU=
|
||||
github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.23 h1:anOslb2Qp6ywnsbyq9jqR0ljuO63kg9PY+4OehIk5R8=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.23/go.mod h1:AfG/PZRUkHJ9inETvbjNifTDgut25Wbkm2QoYBTbvyU=
|
||||
github.com/volcengine/volcengine-go-sdk v1.1.34 h1:ha90JycCCTJNCse0UDziBgBsuX98ITOrkwYlDWcm7NI=
|
||||
github.com/volcengine/volcengine-go-sdk v1.1.34/go.mod h1:oxoVo+A17kvkwPkIeIHPVLjSw7EQAm+l/Vau1YGHN+A=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
@@ -260,8 +277,8 @@ go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
|
||||
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
|
||||
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
@@ -275,15 +292,23 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
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.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
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=
|
||||
@@ -294,12 +319,15 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -338,16 +366,41 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
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.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
|
||||
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
@@ -360,6 +413,10 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
@@ -369,4 +426,6 @@ gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8o
|
||||
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
|
||||
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
logger2 "geekai/logger"
|
||||
@@ -19,9 +20,10 @@ import (
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -29,7 +31,7 @@ import (
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
const SuperManagerID = 1
|
||||
const SuperUsername = "admin"
|
||||
|
||||
type ManagerHandler struct {
|
||||
handler.BaseHandler
|
||||
@@ -45,6 +47,26 @@ func NewAdminHandler(app *core.AppServer, db *gorm.DB, client *redis.Client, cap
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ManagerHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/")
|
||||
|
||||
// 公开接口,不需要授权
|
||||
group.POST("login", h.Login)
|
||||
group.GET("logout", h.Logout)
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("session", h.Session)
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.POST("enable", h.Enable)
|
||||
group.GET("remove", h.Remove)
|
||||
group.POST("resetPass", h.ResetPass)
|
||||
}
|
||||
}
|
||||
|
||||
// Login 登录
|
||||
func (h *ManagerHandler) Login(c *gin.Context) {
|
||||
var data struct {
|
||||
@@ -59,19 +81,6 @@ func (h *ManagerHandler) Login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if h.App.SysConfig.EnabledVerify {
|
||||
var check bool
|
||||
if data.X != 0 {
|
||||
check = h.captcha.SlideCheck(data)
|
||||
} else {
|
||||
check = h.captcha.Check(data)
|
||||
}
|
||||
if !check {
|
||||
resp.ERROR(c, "请先完人机验证")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var manager model.AdminUser
|
||||
res := h.DB.Model(&model.AdminUser{}).Where("username = ?", data.Username).First(&manager)
|
||||
if res.Error != nil {
|
||||
@@ -85,7 +94,7 @@ func (h *ManagerHandler) Login(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 超级管理员默认是ID:1
|
||||
if manager.Id != SuperManagerID && manager.Status == false {
|
||||
if manager.Username != SuperUsername && !manager.Status {
|
||||
resp.ERROR(c, "该用户已被禁止登录,请联系超级管理员")
|
||||
return
|
||||
}
|
||||
@@ -116,7 +125,7 @@ func (h *ManagerHandler) Login(c *gin.Context) {
|
||||
IsSuperAdmin bool `json:"is_super_admin"`
|
||||
Token string `json:"token"`
|
||||
}{
|
||||
IsSuperAdmin: manager.Id == 1,
|
||||
IsSuperAdmin: manager.Username == SuperUsername,
|
||||
Token: tokenString,
|
||||
}
|
||||
|
||||
@@ -135,16 +144,15 @@ func (h *ManagerHandler) Logout(c *gin.Context) {
|
||||
|
||||
// Session 会话检测
|
||||
func (h *ManagerHandler) Session(c *gin.Context) {
|
||||
id := h.GetLoginUserId(c)
|
||||
key := fmt.Sprintf("admin/%d", id)
|
||||
if _, err := h.redis.Get(context.Background(), key).Result(); err != nil {
|
||||
resp.NotAuth(c)
|
||||
id := h.GetAdminId(c)
|
||||
if id == 0 {
|
||||
resp.NotAuth(c, "当前用户已退出登录")
|
||||
return
|
||||
}
|
||||
var manager model.AdminUser
|
||||
res := h.DB.Where("id", id).First(&manager)
|
||||
if res.Error != nil {
|
||||
resp.NotAuth(c)
|
||||
err := h.DB.Where("id", id).First(&manager).Error
|
||||
if err != nil {
|
||||
resp.NotAuth(c, "当前用户已退出登录")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -219,12 +227,19 @@ func (h *ManagerHandler) Remove(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if id == SuperManagerID {
|
||||
var user model.AdminUser
|
||||
res := h.DB.Where("id", id).First(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if user.Username == SuperUsername {
|
||||
resp.ERROR(c, "超级管理员不能删除")
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Where("id", id).Delete(&model.AdminUser{})
|
||||
res = h.DB.Where("id", id).Delete(&model.AdminUser{})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
@@ -255,8 +270,14 @@ func (h *ManagerHandler) Enable(c *gin.Context) {
|
||||
|
||||
// ResetPass 重置密码
|
||||
func (h *ManagerHandler) ResetPass(c *gin.Context) {
|
||||
id := h.GetLoginUserId(c)
|
||||
if id != SuperManagerID {
|
||||
id := h.GetAdminId(c)
|
||||
var user model.AdminUser
|
||||
res := h.DB.Where("id", id).First(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
if user.Username != SuperUsername {
|
||||
resp.ERROR(c, "只有超级管理员能够进行该操作")
|
||||
return
|
||||
}
|
||||
@@ -270,13 +291,6 @@ func (h *ManagerHandler) ResetPass(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AdminUser
|
||||
res := h.DB.Where("id", data.Id).First(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
password := utils.GenPassword(data.Password, user.Salt)
|
||||
user.Password = password
|
||||
res = h.DB.Updates(&user)
|
||||
|
||||
@@ -10,6 +10,7 @@ package admin
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
@@ -30,6 +31,20 @@ func NewApiKeyHandler(app *core.AppServer, db *gorm.DB) *ApiKeyHandler {
|
||||
return &ApiKeyHandler{BaseHandler: handler.BaseHandler{DB: db, App: app}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ApiKeyHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/apikey/")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.POST("set", h.Set)
|
||||
group.GET("remove", h.Remove)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ApiKeyHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
|
||||
@@ -10,6 +10,7 @@ package admin
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
@@ -30,14 +31,29 @@ func NewChatAppHandler(app *core.AppServer, db *gorm.DB) *ChatAppHandler {
|
||||
return &ChatAppHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ChatAppHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/role/")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.POST("sort", h.Sort)
|
||||
group.POST("set", h.Set)
|
||||
group.GET("remove", h.Remove)
|
||||
}
|
||||
}
|
||||
|
||||
// Save 创建或者更新某个角色
|
||||
func (h *ChatAppHandler) Save(c *gin.Context) {
|
||||
var data vo.ChatRole
|
||||
var data vo.ChatApp
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
var role model.ChatRole
|
||||
var role model.ChatApp
|
||||
err := utils.CopyObject(data, &role)
|
||||
if err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
@@ -65,8 +81,8 @@ func (h *ChatAppHandler) Save(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *ChatAppHandler) List(c *gin.Context) {
|
||||
var items []model.ChatRole
|
||||
var roles = make([]vo.ChatRole, 0)
|
||||
var items []model.ChatApp
|
||||
var roles = make([]vo.ChatApp, 0)
|
||||
res := h.DB.Order("sort_num ASC").Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No data found")
|
||||
@@ -78,10 +94,10 @@ func (h *ChatAppHandler) List(c *gin.Context) {
|
||||
typeIds := make([]int, 0)
|
||||
for _, v := range items {
|
||||
if v.ModelId > 0 {
|
||||
modelIds = append(modelIds, v.ModelId)
|
||||
modelIds = append(modelIds, int(v.ModelId))
|
||||
}
|
||||
if v.Tid > 0 {
|
||||
typeIds = append(typeIds, v.Tid)
|
||||
typeIds = append(typeIds, int(v.Tid))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,14 +123,14 @@ func (h *ChatAppHandler) List(c *gin.Context) {
|
||||
}
|
||||
|
||||
for _, v := range items {
|
||||
var role vo.ChatRole
|
||||
var role vo.ChatApp
|
||||
err := utils.CopyObject(v, &role)
|
||||
if err == nil {
|
||||
role.Id = v.Id
|
||||
role.CreatedAt = v.CreatedAt.Unix()
|
||||
role.UpdatedAt = v.UpdatedAt.Unix()
|
||||
role.ModelName = modelNameMap[role.ModelId]
|
||||
role.TypeName = typeNameMap[role.Tid]
|
||||
role.ModelName = modelNameMap[int(role.ModelId)]
|
||||
role.TypeName = typeNameMap[int(role.Tid)]
|
||||
roles = append(roles, role)
|
||||
}
|
||||
}
|
||||
@@ -135,7 +151,7 @@ func (h *ChatAppHandler) Sort(c *gin.Context) {
|
||||
}
|
||||
|
||||
for index, id := range data.Ids {
|
||||
err := h.DB.Model(&model.ChatRole{}).Where("id = ?", id).Update("sort_num", data.Sorts[index]).Error
|
||||
err := h.DB.Model(&model.ChatApp{}).Where("id = ?", id).Update("sort_num", data.Sorts[index]).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
@@ -157,7 +173,7 @@ func (h *ChatAppHandler) Set(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err := h.DB.Model(&model.ChatRole{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error
|
||||
err := h.DB.Model(&model.ChatApp{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
@@ -172,9 +188,8 @@ func (h *ChatAppHandler) Remove(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
res := h.DB.Where("id", id).Delete(&model.ChatRole{})
|
||||
res := h.DB.Where("id", id).Delete(&model.ChatApp{})
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "删除失败!")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ package admin
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -20,6 +22,21 @@ func NewChatAppTypeHandler(app *core.AppServer, db *gorm.DB) *ChatAppTypeHandler
|
||||
return &ChatAppTypeHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ChatAppTypeHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/app/type/")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.GET("remove", h.Remove)
|
||||
group.POST("enable", h.Enable)
|
||||
group.POST("sort", h.Sort)
|
||||
}
|
||||
}
|
||||
|
||||
// Save 创建或更新App类型
|
||||
func (h *ChatAppTypeHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
|
||||
@@ -9,12 +9,14 @@ package admin
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -27,16 +29,31 @@ func NewChatHandler(app *core.AppServer, db *gorm.DB) *ChatHandler {
|
||||
return &ChatHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ChatHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/chat/")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("list", h.List)
|
||||
group.POST("message", h.Messages)
|
||||
group.GET("history", h.History)
|
||||
group.GET("remove", h.RemoveChat)
|
||||
group.GET("message/remove", h.RemoveMessage)
|
||||
}
|
||||
}
|
||||
|
||||
type chatItemVo struct {
|
||||
Username string `json:"username"`
|
||||
UserId uint `json:"user_id"`
|
||||
ChatId string `json:"chat_id"`
|
||||
Title string `json:"title"`
|
||||
Role vo.ChatRole `json:"role"`
|
||||
Model string `json:"model"`
|
||||
Token int `json:"token"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
MsgNum int `json:"msg_num"` // 消息数量
|
||||
Username string `json:"username"`
|
||||
UserId uint `json:"user_id"`
|
||||
ChatId string `json:"chat_id"`
|
||||
Title string `json:"title"`
|
||||
Role vo.ChatApp `json:"role"`
|
||||
Model string `json:"model"`
|
||||
Token int `json:"token"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
MsgNum int `json:"msg_num"` // 消息数量
|
||||
}
|
||||
|
||||
func (h *ChatHandler) List(c *gin.Context) {
|
||||
@@ -86,7 +103,7 @@ func (h *ChatHandler) List(c *gin.Context) {
|
||||
}
|
||||
var messages []model.ChatMessage
|
||||
var users []model.User
|
||||
var roles []model.ChatRole
|
||||
var roles []model.ChatApp
|
||||
h.DB.Where("chat_id IN ?", chatIds).Find(&messages)
|
||||
h.DB.Where("id IN ?", userIds).Find(&users)
|
||||
h.DB.Where("id IN ?", roleIds).Find(&roles)
|
||||
@@ -94,7 +111,7 @@ func (h *ChatHandler) List(c *gin.Context) {
|
||||
tokenMap := make(map[string]int)
|
||||
userMap := make(map[uint]string)
|
||||
msgMap := make(map[string]int)
|
||||
roleMap := make(map[uint]vo.ChatRole)
|
||||
roleMap := make(map[uint]vo.ChatApp)
|
||||
for _, msg := range messages {
|
||||
tokenMap[msg.ChatId] += msg.Tokens
|
||||
msgMap[msg.ChatId] += 1
|
||||
@@ -103,7 +120,7 @@ func (h *ChatHandler) List(c *gin.Context) {
|
||||
userMap[user.Id] = user.Username
|
||||
}
|
||||
for _, r := range roles {
|
||||
var roleVo vo.ChatRole
|
||||
var roleVo vo.ChatApp
|
||||
err := utils.CopyObject(r, &roleVo)
|
||||
if err != nil {
|
||||
continue
|
||||
@@ -189,7 +206,7 @@ func (h *ChatHandler) Messages(c *gin.Context) {
|
||||
}
|
||||
for _, item := range items {
|
||||
list = append(list, chatMessageVo{
|
||||
Id: item.Id,
|
||||
Id: uint(item.Id),
|
||||
UserId: item.UserId,
|
||||
Username: userMap[item.UserId],
|
||||
Content: item.Content,
|
||||
@@ -208,20 +225,28 @@ func (h *ChatHandler) Messages(c *gin.Context) {
|
||||
func (h *ChatHandler) History(c *gin.Context) {
|
||||
chatId := c.Query("chat_id") // 会话 ID
|
||||
var items []model.ChatMessage
|
||||
var messages = make([]vo.HistoryMessage, 0)
|
||||
var messages = make([]vo.ChatMessage, 0)
|
||||
res := h.DB.Where("chat_id = ?", chatId).Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No history message")
|
||||
return
|
||||
} else {
|
||||
for _, item := range items {
|
||||
var v vo.HistoryMessage
|
||||
var v vo.ChatMessage
|
||||
err := utils.CopyObject(item, &v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// 解析内容
|
||||
var content vo.MsgContent
|
||||
err = utils.JsonDecode(item.Content, &content)
|
||||
if err != nil {
|
||||
content.Text = item.Content
|
||||
}
|
||||
v.Content = content
|
||||
v.CreatedAt = item.CreatedAt.Unix()
|
||||
v.UpdatedAt = item.UpdatedAt.Unix()
|
||||
if err == nil {
|
||||
messages = append(messages, v)
|
||||
}
|
||||
messages = append(messages, v)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ package admin
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
@@ -28,6 +30,22 @@ func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
|
||||
return &ChatModelHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ChatModelHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/model/")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.POST("set", h.Set)
|
||||
group.POST("sort", h.Sort)
|
||||
group.GET("remove", h.Remove)
|
||||
group.POST("batch-remove", h.BatchRemove)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ChatModelHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
@@ -40,6 +58,8 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
|
||||
Power int `json:"power"`
|
||||
MaxTokens int `json:"max_tokens"` // 最大响应长度
|
||||
MaxContext int `json:"max_context"` // 最大上下文长度
|
||||
Desc string `json:"desc"` //模型描述
|
||||
Tag string `json:"tag"` //模型标签
|
||||
Temperature float32 `json:"temperature"` // 模型温度
|
||||
KeyId int `json:"key_id,omitempty"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
@@ -64,8 +84,10 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
|
||||
item.Power = data.Power
|
||||
item.MaxTokens = data.MaxTokens
|
||||
item.MaxContext = data.MaxContext
|
||||
item.Desc = data.Desc
|
||||
item.Tag = data.Tag
|
||||
item.Temperature = data.Temperature
|
||||
item.KeyId = data.KeyId
|
||||
item.KeyId = uint(data.KeyId)
|
||||
item.Type = data.Type
|
||||
item.Options = utils.JsonEncode(data.Options)
|
||||
var res *gorm.DB
|
||||
@@ -96,12 +118,16 @@ func (h *ChatModelHandler) List(c *gin.Context) {
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
enable := h.GetBool(c, "enable")
|
||||
name := h.GetTrim(c, "name")
|
||||
modelType := h.GetTrim(c, "type")
|
||||
if enable {
|
||||
session = session.Where("enabled", enable)
|
||||
}
|
||||
if name != "" {
|
||||
session = session.Where("name LIKE ?", name+"%")
|
||||
}
|
||||
if modelType != "" {
|
||||
session = session.Where("type", modelType)
|
||||
}
|
||||
var items []model.ChatModel
|
||||
var cms = make([]vo.ChatModel, 0)
|
||||
res := session.Order("sort_num ASC").Find(&items)
|
||||
@@ -113,7 +139,7 @@ func (h *ChatModelHandler) List(c *gin.Context) {
|
||||
// initialize key name
|
||||
keyIds := make([]int, 0)
|
||||
for _, v := range items {
|
||||
keyIds = append(keyIds, v.KeyId)
|
||||
keyIds = append(keyIds, int(v.KeyId))
|
||||
}
|
||||
var keys []model.ApiKey
|
||||
keyMap := make(map[uint]string)
|
||||
@@ -193,3 +219,33 @@ func (h *ChatModelHandler) Remove(c *gin.Context) {
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// BatchRemove 批量删除模型
|
||||
func (h *ChatModelHandler) BatchRemove(c *gin.Context) {
|
||||
var data struct {
|
||||
Ids []uint `json:"ids"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if len(data.Ids) == 0 {
|
||||
resp.ERROR(c, "请选择要删除的模型")
|
||||
return
|
||||
}
|
||||
|
||||
// 执行批量删除
|
||||
err := h.DB.Where("id IN ?", data.Ids).Delete(&model.ChatModel{}).Error
|
||||
if err != nil {
|
||||
logger.Error("批量删除模型失败:", err)
|
||||
resp.ERROR(c, "批量删除失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"message": fmt.Sprintf("成功删除 %d 个模型", len(data.Ids)),
|
||||
"deleted_count": len(data.Ids),
|
||||
})
|
||||
}
|
||||
|
||||
+354
-128
@@ -9,107 +9,397 @@ package admin
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service"
|
||||
"geekai/store"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/payment"
|
||||
"geekai/service/sms"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ConfigHandler struct {
|
||||
handler.BaseHandler
|
||||
levelDB *store.LevelDB
|
||||
licenseService *service.LicenseService
|
||||
licenseService *service.LicenseService
|
||||
sysConfig *types.SystemConfig
|
||||
alipayService *payment.AlipayService
|
||||
wxpayService *payment.WxPayService
|
||||
epayService *payment.EPayService
|
||||
smsManager *sms.SmsManager
|
||||
uploaderManager *oss.UploaderManager
|
||||
smtpService *service.SmtpService
|
||||
captchaService *service.CaptchaService
|
||||
wxLoginService *service.WxLoginService
|
||||
}
|
||||
|
||||
func NewConfigHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB, licenseService *service.LicenseService) *ConfigHandler {
|
||||
func NewConfigHandler(
|
||||
app *core.AppServer,
|
||||
db *gorm.DB,
|
||||
licenseService *service.LicenseService,
|
||||
sysConfig *types.SystemConfig,
|
||||
alipayService *payment.AlipayService,
|
||||
wxpayService *payment.WxPayService,
|
||||
epayService *payment.EPayService,
|
||||
smsManager *sms.SmsManager,
|
||||
uploaderManager *oss.UploaderManager,
|
||||
smtpService *service.SmtpService,
|
||||
captchaService *service.CaptchaService,
|
||||
wxLoginService *service.WxLoginService,
|
||||
) *ConfigHandler {
|
||||
return &ConfigHandler{
|
||||
BaseHandler: handler.BaseHandler{App: app, DB: db},
|
||||
levelDB: levelDB,
|
||||
licenseService: licenseService,
|
||||
BaseHandler: handler.BaseHandler{App: app, DB: db},
|
||||
licenseService: licenseService,
|
||||
sysConfig: sysConfig,
|
||||
alipayService: alipayService,
|
||||
wxpayService: wxpayService,
|
||||
epayService: epayService,
|
||||
smsManager: smsManager,
|
||||
uploaderManager: uploaderManager,
|
||||
smtpService: smtpService,
|
||||
captchaService: captchaService,
|
||||
wxLoginService: wxLoginService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) Update(c *gin.Context) {
|
||||
var data struct {
|
||||
Key string `json:"key"`
|
||||
Config struct {
|
||||
types.SystemConfig
|
||||
Content string `json:"content,omitempty"`
|
||||
Updated bool `json:"updated,omitempty"`
|
||||
} `json:"config"`
|
||||
ConfigBak types.SystemConfig `json:"config_bak,omitempty"`
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ConfigHandler) RegisterRoutes() {
|
||||
rg := h.App.Engine.Group("/api/admin/config")
|
||||
|
||||
// 需要管理员登录的接口
|
||||
rg.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
rg.POST("update/base", h.UpdateBase)
|
||||
rg.POST("update/power", h.UpdatePower)
|
||||
rg.POST("update/notice", h.UpdateNotice)
|
||||
rg.POST("update/agreement", h.UpdateAgreement)
|
||||
rg.POST("update/privacy", h.UpdatePrivacy)
|
||||
rg.POST("update/mark_map", h.UpdateMarkMap)
|
||||
rg.POST("update/captcha", h.UpdateCaptcha)
|
||||
rg.POST("update/wx_login", h.UpdateWxLogin)
|
||||
rg.POST("update/payment", h.UpdatePayment)
|
||||
rg.POST("update/sms", h.UpdateSms)
|
||||
rg.POST("update/oss", h.UpdateOss)
|
||||
rg.POST("update/smtp", h.UpdateStmp)
|
||||
rg.GET("get", h.Get)
|
||||
rg.POST("license/active", h.Active)
|
||||
rg.GET("license/get", h.GetLicense)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateBase 更新基础配置
|
||||
func (h *ConfigHandler) UpdateBase(c *gin.Context) {
|
||||
var data types.BaseConfig
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
logger.Errorf("Update config failed: %v", err)
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// ONLY authorized user can change the copyright
|
||||
if (data.Key == "system" && data.Config.Copyright != data.ConfigBak.Copyright) && !h.licenseService.GetLicense().Configs.DeCopy {
|
||||
resp.ERROR(c, "您无权修改版权信息,请先联系作者获取授权")
|
||||
// 未授权的话不允许修改版权
|
||||
license := h.licenseService.GetLicense()
|
||||
if !license.IsActive && data.Copyright != h.sysConfig.Base.Copyright {
|
||||
resp.ERROR(c, "未授权系统不允许修改版权信息")
|
||||
return
|
||||
}
|
||||
|
||||
// 如果要启用图形验证码功能,则检查是否配置了 API 服务
|
||||
if data.Config.EnabledVerify && h.App.Config.ApiConfig.AppId == "" {
|
||||
resp.ERROR(c, "启用验证码服务需要先配置 GeekAI 官方 API 服务 AppId 和 Token")
|
||||
// 未授权的话不允许修改 Logo
|
||||
if !license.IsActive && data.Logo != h.sysConfig.Base.Logo {
|
||||
resp.ERROR(c, "未授权系统不允许修改 Logo")
|
||||
return
|
||||
}
|
||||
|
||||
value := utils.JsonEncode(&data.Config)
|
||||
config := model.Config{Key: data.Key, Config: value}
|
||||
res := h.DB.FirstOrCreate(&config, model.Config{Key: data.Key})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
err := h.Update(types.ConfigKeySystem, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if config.Id > 0 {
|
||||
config.Config = value
|
||||
res := h.DB.Updates(&config)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
h.sysConfig.Base = data
|
||||
|
||||
// update config cache for AppServer
|
||||
var cfg model.Config
|
||||
h.DB.Where("marker", data.Key).First(&cfg)
|
||||
var err error
|
||||
if data.Key == "system" {
|
||||
err = utils.JsonDecode(cfg.Config, &h.App.SysConfig)
|
||||
}
|
||||
if err != nil {
|
||||
resp.ERROR(c, "Failed to update config cache: "+err.Error())
|
||||
return
|
||||
}
|
||||
logger.Infof("Update AppServer's config successfully: %v", config.Config)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, config)
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// Get 获取指定的系统配置
|
||||
func (h *ConfigHandler) Get(c *gin.Context) {
|
||||
key := c.Query("key")
|
||||
// UpdatePower 更新系统配置
|
||||
func (h *ConfigHandler) UpdatePower(c *gin.Context) {
|
||||
var data struct {
|
||||
InitPower int `json:"init_power,omitempty"` // 新用户注册赠送算力值
|
||||
DailyPower int `json:"daily_power,omitempty"` // 每日签到赠送算力
|
||||
InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力值
|
||||
MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力
|
||||
MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力
|
||||
SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力
|
||||
SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力
|
||||
LumaPower int `json:"luma_power,omitempty"` // Luma 生成视频消耗算力
|
||||
KeLingPowers map[string]int `json:"keling_powers,omitempty"` // 可灵生成视频消耗算力
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
h.sysConfig.Base.InitPower = data.InitPower
|
||||
h.sysConfig.Base.DailyPower = data.DailyPower
|
||||
h.sysConfig.Base.InvitePower = data.InvitePower
|
||||
h.sysConfig.Base.MjPower = data.MjPower
|
||||
h.sysConfig.Base.MjActionPower = data.MjActionPower
|
||||
h.sysConfig.Base.SdPower = data.SdPower
|
||||
h.sysConfig.Base.SunoPower = data.SunoPower
|
||||
h.sysConfig.Base.LumaPower = data.LumaPower
|
||||
h.sysConfig.Base.KeLingPowers = data.KeLingPowers
|
||||
|
||||
err := h.Update(types.ConfigKeySystem, h.sysConfig.Base)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, h.sysConfig.Base)
|
||||
}
|
||||
|
||||
// UpdateNotice 更新公告配置
|
||||
func (h *ConfigHandler) UpdateNotice(c *gin.Context) {
|
||||
var data struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeyNotice, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// UpdateAgreement 更新用户协议配置
|
||||
func (h *ConfigHandler) UpdateAgreement(c *gin.Context) {
|
||||
var data struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeyAgreement, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// UpdatePrivacy 更新隐私政策配置
|
||||
func (h *ConfigHandler) UpdatePrivacy(c *gin.Context) {
|
||||
var data struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeyPrivacy, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// UpdateMarkMap 更新思维导图配置
|
||||
func (h *ConfigHandler) UpdateMarkMap(c *gin.Context) {
|
||||
var data struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeyMarkMap, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// UpdateCaptcha 更新行为验证码配置
|
||||
func (h *ConfigHandler) UpdateCaptcha(c *gin.Context) {
|
||||
var data types.CaptchaConfig
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeyCaptcha, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
h.captchaService.UpdateConfig(data)
|
||||
resp.SUCCESS(c, data)
|
||||
|
||||
}
|
||||
|
||||
// UpdatePayment 更新支付配置
|
||||
func (h *ConfigHandler) UpdatePayment(c *gin.Context) {
|
||||
var data types.PaymentConfig
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeyPayment, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 如果启用状态发生改变,则需要更新支付服务配置
|
||||
if data.WxPay.Enabled {
|
||||
err = h.wxpayService.UpdateConfig(&data.WxPay)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
if data.Epay.Enabled {
|
||||
h.epayService.UpdateConfig(&data.Epay)
|
||||
}
|
||||
if data.Alipay.Enabled {
|
||||
err = h.alipayService.UpdateConfig(&data.Alipay)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
h.sysConfig.Payment = data
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// UpdateSms 更新短信配置
|
||||
func (h *ConfigHandler) UpdateSms(c *gin.Context) {
|
||||
var data types.SMSConfig
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeySms, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 更新服务配置
|
||||
h.smsManager.UpdateConfig(data)
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// UpdateOss 更新 Oss 配置
|
||||
func (h *ConfigHandler) UpdateOss(c *gin.Context) {
|
||||
var data types.OSSConfig
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeyOss, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 更新服务配置
|
||||
h.uploaderManager.UpdateConfig(data)
|
||||
h.sysConfig.OSS = data
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// UpdateStmp 更新 Stmp 配置
|
||||
func (h *ConfigHandler) UpdateStmp(c *gin.Context) {
|
||||
var data types.SmtpConfig
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeySmtp, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 更新服务配置
|
||||
h.smtpService.UpdateConfig(&data)
|
||||
h.sysConfig.SMTP = data
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// UpdateWxLogin 更新微信登录配置
|
||||
func (h *ConfigHandler) UpdateWxLogin(c *gin.Context) {
|
||||
var data types.WxLoginConfig
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
err := h.Update(types.ConfigKeyWxLogin, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.wxLoginService.UpdateConfig(data)
|
||||
h.sysConfig.WxLogin = data
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// Update 更新系统配置
|
||||
func (h *ConfigHandler) Update(name string, value any) error {
|
||||
var config model.Config
|
||||
res := h.DB.Where("marker", key).First(&config)
|
||||
err := h.DB.Where("name", name).First(&config).Error
|
||||
if err != nil { // 不存在则创建
|
||||
config.Name = name
|
||||
config.Value = utils.JsonEncode(value)
|
||||
return h.DB.Create(&config).Error
|
||||
} else { // 存在则更新
|
||||
config.Value = utils.JsonEncode(value)
|
||||
return h.DB.Updates(&config).Error
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Get 获取指定名称的系统配置
|
||||
func (h *ConfigHandler) Get(c *gin.Context) {
|
||||
name := c.Query("key")
|
||||
var config model.Config
|
||||
res := h.DB.Where("name", name).First(&config)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var value map[string]interface{}
|
||||
err := utils.JsonDecode(config.Config, &value)
|
||||
var value map[string]any
|
||||
err := utils.JsonDecode(config.Value, &value)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
@@ -127,19 +417,21 @@ func (h *ConfigHandler) Active(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
info, err := host.Info()
|
||||
|
||||
err := h.licenseService.ActiveLicense(data.License)
|
||||
license := h.licenseService.GetLicense()
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = h.licenseService.ActiveLicense(data.License, info.HostID)
|
||||
if err != nil {
|
||||
if err := h.Update(types.ConfigKeyLicense, license); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 更新系统配置
|
||||
h.sysConfig.License = *license
|
||||
|
||||
resp.SUCCESS(c)
|
||||
resp.SUCCESS(c, license.MachineId)
|
||||
|
||||
}
|
||||
|
||||
@@ -148,69 +440,3 @@ func (h *ConfigHandler) GetLicense(c *gin.Context) {
|
||||
license := h.licenseService.GetLicense()
|
||||
resp.SUCCESS(c, license)
|
||||
}
|
||||
|
||||
// FixData 修复数据
|
||||
func (h *ConfigHandler) FixData(c *gin.Context) {
|
||||
resp.ERROR(c, "当前升级版本没有数据需要修正!")
|
||||
//var fixed bool
|
||||
//version := "data_fix_4.1.4"
|
||||
//err := h.levelDB.Get(version, &fixed)
|
||||
//if err == nil || fixed {
|
||||
// resp.ERROR(c, "当前版本数据修复已完成,请不要重复执行操作")
|
||||
// return
|
||||
//}
|
||||
//tx := h.DB.Begin()
|
||||
//var users []model.User
|
||||
//err = tx.Find(&users).Error
|
||||
//if err != nil {
|
||||
// resp.ERROR(c, err.Error())
|
||||
// return
|
||||
//}
|
||||
//for _, user := range users {
|
||||
// if user.Email != "" || user.Mobile != "" {
|
||||
// continue
|
||||
// }
|
||||
// if utils.IsValidEmail(user.Username) {
|
||||
// user.Email = user.Username
|
||||
// } else if utils.IsValidMobile(user.Username) {
|
||||
// user.Mobile = user.Username
|
||||
// }
|
||||
// err = tx.Save(&user).Error
|
||||
// if err != nil {
|
||||
// resp.ERROR(c, err.Error())
|
||||
// tx.Rollback()
|
||||
// return
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//var orders []model.Order
|
||||
//err = h.DB.Find(&orders).Error
|
||||
//if err != nil {
|
||||
// resp.ERROR(c, err.Error())
|
||||
// return
|
||||
//}
|
||||
//for _, order := range orders {
|
||||
// if order.PayWay == "支付宝" {
|
||||
// order.PayWay = "alipay"
|
||||
// order.PayType = "alipay"
|
||||
// } else if order.PayWay == "微信支付" {
|
||||
// order.PayWay = "wechat"
|
||||
// order.PayType = "wxpay"
|
||||
// } else if order.PayWay == "hupi" {
|
||||
// order.PayType = "wxpay"
|
||||
// }
|
||||
// err = tx.Save(&order).Error
|
||||
// if err != nil {
|
||||
// resp.ERROR(c, err.Error())
|
||||
// tx.Rollback()
|
||||
// return
|
||||
// }
|
||||
//}
|
||||
//tx.Commit()
|
||||
//err = h.levelDB.Put(version, true)
|
||||
//if err != nil {
|
||||
// resp.ERROR(c, err.Error())
|
||||
// return
|
||||
//}
|
||||
//resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
@@ -13,10 +13,11 @@ import (
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DashboardHandler struct {
|
||||
@@ -27,46 +28,161 @@ func NewDashboardHandler(app *core.AppServer, db *gorm.DB) *DashboardHandler {
|
||||
return &DashboardHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *DashboardHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/dashboard/")
|
||||
group.GET("stats", h.Stats)
|
||||
}
|
||||
|
||||
// statsVo 增加 recentOrders、recentUsers 字段
|
||||
// 最近订单
|
||||
type OrderBrief struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
Amount float64 `json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// 最近用户
|
||||
type UserBrief struct {
|
||||
Nickname string `json:"nickname"`
|
||||
Avatar string `json:"avatar"`
|
||||
LastActive time.Time `json:"last_active"`
|
||||
}
|
||||
|
||||
type statsVo struct {
|
||||
Users int64 `json:"users"`
|
||||
Chats int64 `json:"chats"`
|
||||
Tokens int `json:"tokens"`
|
||||
Income float64 `json:"income"`
|
||||
Chart map[string]map[string]float64 `json:"chart"`
|
||||
Users int64 `json:"users"`
|
||||
Chats int64 `json:"chats"`
|
||||
Tokens int `json:"tokens"`
|
||||
Income float64 `json:"income"`
|
||||
Chart map[string]map[string]float64 `json:"chart"`
|
||||
TodayUsers int64 `json:"todayUsers"`
|
||||
TodayChats int64 `json:"todayChats"`
|
||||
TodayTokens int `json:"todayTokens"`
|
||||
TodayIncome float64 `json:"todayIncome"`
|
||||
TodayOrders int64 `json:"todayOrders"`
|
||||
TodayImageJobs int64 `json:"todayImageJobs"`
|
||||
TodayVideoJobs int64 `json:"todayVideoJobs"`
|
||||
TodayMusicJobs int64 `json:"todayMusicJobs"`
|
||||
Orders int64 `json:"orders"`
|
||||
ImageJobs int64 `json:"imageJobs"`
|
||||
VideoJobs int64 `json:"videoJobs"`
|
||||
MusicJobs int64 `json:"musicJobs"`
|
||||
RecentOrders []OrderBrief `json:"recentOrders"`
|
||||
RecentUsers []UserBrief `json:"recentUsers"`
|
||||
}
|
||||
|
||||
func (h *DashboardHandler) Stats(c *gin.Context) {
|
||||
stats := statsVo{}
|
||||
// new users statistic
|
||||
var userCount int64
|
||||
now := time.Now()
|
||||
zeroTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
res := h.DB.Model(&model.User{}).Where("created_at > ?", zeroTime).Count(&userCount)
|
||||
if res.Error == nil {
|
||||
stats.Users = userCount
|
||||
|
||||
// 总用户数
|
||||
h.DB.Model(&model.User{}).Count(&stats.Users)
|
||||
|
||||
// 今日新增用户
|
||||
h.DB.Model(&model.User{}).Where("created_at > ?", zeroTime).Count(&stats.TodayUsers)
|
||||
|
||||
// 总对话数
|
||||
h.DB.Model(&model.ChatItem{}).Count(&stats.Chats)
|
||||
|
||||
// 今日新增对话
|
||||
h.DB.Model(&model.ChatItem{}).Where("created_at > ?", zeroTime).Count(&stats.TodayChats)
|
||||
|
||||
// 总算力消耗
|
||||
var powerLogs []model.PowerLog
|
||||
h.DB.Where("mark = ?", types.PowerSub).Find(&powerLogs)
|
||||
for _, item := range powerLogs {
|
||||
stats.Tokens += item.Amount
|
||||
}
|
||||
|
||||
// new chats statistic
|
||||
var chatCount int64
|
||||
res = h.DB.Model(&model.ChatItem{}).Where("created_at > ?", zeroTime).Count(&chatCount)
|
||||
if res.Error == nil {
|
||||
stats.Chats = chatCount
|
||||
// 今日算力消耗
|
||||
var todayPowerLogs []model.PowerLog
|
||||
h.DB.Where("mark = ?", types.PowerSub).Where("created_at > ?", zeroTime).Find(&todayPowerLogs)
|
||||
for _, item := range todayPowerLogs {
|
||||
stats.TodayTokens += item.Amount
|
||||
}
|
||||
|
||||
// tokens took stats
|
||||
var historyMessages []model.ChatMessage
|
||||
res = h.DB.Where("created_at > ?", zeroTime).Find(&historyMessages)
|
||||
for _, item := range historyMessages {
|
||||
stats.Tokens += item.Tokens
|
||||
}
|
||||
|
||||
// 订单收入
|
||||
var orders []model.Order
|
||||
res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Find(&orders)
|
||||
for _, item := range orders {
|
||||
// 总收入
|
||||
var allOrders []model.Order
|
||||
h.DB.Where("status = ?", types.OrderPaidSuccess).Find(&allOrders)
|
||||
for _, item := range allOrders {
|
||||
stats.Income += item.Amount
|
||||
}
|
||||
|
||||
// 今日收入
|
||||
var todayOrders []model.Order
|
||||
h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Find(&todayOrders)
|
||||
for _, item := range todayOrders {
|
||||
stats.TodayIncome += item.Amount
|
||||
}
|
||||
|
||||
// 订单总数
|
||||
h.DB.Model(&model.Order{}).Where("status = ?", types.OrderPaidSuccess).Count(&stats.Orders)
|
||||
|
||||
// 今日订单数
|
||||
h.DB.Model(&model.Order{}).Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Count(&stats.TodayOrders)
|
||||
|
||||
// 图片生成任务统计
|
||||
var mjJobs, sdJobs, dallJobs, jimengImageJobs int64
|
||||
h.DB.Model(&model.MidJourneyJob{}).Count(&mjJobs)
|
||||
h.DB.Model(&model.SdJob{}).Count(&sdJobs)
|
||||
h.DB.Model(&model.DallJob{}).Count(&dallJobs)
|
||||
h.DB.Model(&model.JimengJob{}).Where("type IN ?", []string{"text_to_image", "image_to_image", "image_edit", "image_effects"}).Count(&jimengImageJobs)
|
||||
stats.ImageJobs = mjJobs + sdJobs + dallJobs + jimengImageJobs
|
||||
|
||||
logger.Info("stats.ImageJobs", stats.ImageJobs)
|
||||
|
||||
// 今日图片生成任务统计
|
||||
var todayMjJobs, todaySdJobs, todayDallJobs, todayJimengImageJobs int64
|
||||
h.DB.Model(&model.MidJourneyJob{}).Where("created_at > ?", zeroTime).Count(&todayMjJobs)
|
||||
h.DB.Model(&model.SdJob{}).Where("created_at > ?", zeroTime).Count(&todaySdJobs)
|
||||
h.DB.Model(&model.DallJob{}).Where("created_at > ?", zeroTime).Count(&todayDallJobs)
|
||||
h.DB.Model(&model.JimengJob{}).Where("type IN ?", []string{"text_to_image", "image_to_image", "image_edit", "image_effects"}).Where("created_at > ?", zeroTime).Count(&todayJimengImageJobs)
|
||||
stats.TodayImageJobs = todayMjJobs + todaySdJobs + todayDallJobs + todayJimengImageJobs
|
||||
|
||||
// 视频生成任务统计
|
||||
var videoJobs, jimengVideoJobs int64
|
||||
h.DB.Model(&model.VideoJob{}).Count(&videoJobs)
|
||||
h.DB.Model(&model.JimengJob{}).Where("type IN ?", []string{"text_to_video", "image_to_video"}).Count(&jimengVideoJobs)
|
||||
stats.VideoJobs = videoJobs + jimengVideoJobs
|
||||
|
||||
// 今日视频生成任务统计
|
||||
var todayVideoJobs, todayJimengVideoJobs int64
|
||||
h.DB.Model(&model.VideoJob{}).Where("created_at > ?", zeroTime).Count(&todayVideoJobs)
|
||||
h.DB.Model(&model.JimengJob{}).Where("type IN ?", []string{"text_to_video", "image_to_video"}).Where("created_at > ?", zeroTime).Count(&todayJimengVideoJobs)
|
||||
stats.TodayVideoJobs = todayVideoJobs + todayJimengVideoJobs
|
||||
|
||||
// 音乐生成任务统计
|
||||
h.DB.Model(&model.SunoJob{}).Count(&stats.MusicJobs)
|
||||
|
||||
// 今日音乐生成任务统计
|
||||
h.DB.Model(&model.SunoJob{}).Where("created_at > ?", zeroTime).Count(&stats.TodayMusicJobs)
|
||||
|
||||
// recentOrders: 最近10条已支付订单
|
||||
var orderList []model.Order
|
||||
h.DB.Model(&model.Order{}).Where("status = ?", types.OrderPaidSuccess).Order("created_at desc").Limit(10).Find(&orderList)
|
||||
for _, o := range orderList {
|
||||
stats.RecentOrders = append(stats.RecentOrders, OrderBrief{
|
||||
OrderNo: o.OrderNo,
|
||||
Amount: o.Amount,
|
||||
CreatedAt: o.CreatedAt,
|
||||
})
|
||||
}
|
||||
// recentUsers: 最近10个注册用户
|
||||
var userList []model.User
|
||||
h.DB.Model(&model.User{}).Order("created_at desc").Limit(10).Find(&userList)
|
||||
for _, u := range userList {
|
||||
lastActive := u.UpdatedAt
|
||||
if lastActive.IsZero() {
|
||||
lastActive = u.CreatedAt
|
||||
}
|
||||
stats.RecentUsers = append(stats.RecentUsers, UserBrief{
|
||||
Nickname: u.Nickname,
|
||||
Avatar: u.Avatar,
|
||||
LastActive: lastActive,
|
||||
})
|
||||
}
|
||||
|
||||
// 统计7天的订单的图表
|
||||
startDate := now.Add(-7 * 24 * time.Hour).Format("2006-01-02")
|
||||
var statsChart = make(map[string]map[string]float64)
|
||||
@@ -81,23 +197,29 @@ func (h *DashboardHandler) Stats(c *gin.Context) {
|
||||
|
||||
// 统计用户7天增加的曲线
|
||||
var users []model.User
|
||||
res = h.DB.Model(&model.User{}).Where("created_at > ?", startDate).Find(&users)
|
||||
if res.Error == nil {
|
||||
err := h.DB.Model(&model.User{}).Where("created_at > ?", startDate).Find(&users).Error
|
||||
if err == nil {
|
||||
for _, item := range users {
|
||||
userStatistic[item.CreatedAt.Format("2006-01-02")] += 1
|
||||
}
|
||||
}
|
||||
|
||||
// 统计7天Token 消耗
|
||||
res = h.DB.Where("created_at > ?", startDate).Find(&historyMessages)
|
||||
for _, item := range historyMessages {
|
||||
historyMessagesStatistic[item.CreatedAt.Format("2006-01-02")] += float64(item.Tokens)
|
||||
// 统计7天算力消耗
|
||||
var chartPowerLogs []model.PowerLog
|
||||
err = h.DB.Where("mark = ?", types.PowerSub).Where("created_at > ?", startDate).Find(&chartPowerLogs).Error
|
||||
if err == nil {
|
||||
for _, item := range chartPowerLogs {
|
||||
historyMessagesStatistic[item.CreatedAt.Format("2006-01-02")] += float64(item.Amount)
|
||||
}
|
||||
}
|
||||
|
||||
// 统计最近7天的订单
|
||||
res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", startDate).Find(&orders)
|
||||
for _, item := range orders {
|
||||
incomeStatistic[item.CreatedAt.Format("2006-01-02")], _ = decimal.NewFromFloat(incomeStatistic[item.CreatedAt.Format("2006-01-02")]).Add(decimal.NewFromFloat(item.Amount)).Float64()
|
||||
var orders []model.Order
|
||||
err = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", startDate).Find(&orders).Error
|
||||
if err == nil {
|
||||
for _, item := range orders {
|
||||
incomeStatistic[item.CreatedAt.Format("2006-01-02")], _ = decimal.NewFromFloat(incomeStatistic[item.CreatedAt.Format("2006-01-02")]).Add(decimal.NewFromFloat(item.Amount)).Float64()
|
||||
}
|
||||
}
|
||||
|
||||
statsChart["users"] = userStatistic
|
||||
|
||||
@@ -9,6 +9,7 @@ package admin
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
@@ -30,6 +31,21 @@ func NewFunctionHandler(app *core.AppServer, db *gorm.DB) *FunctionHandler {
|
||||
return &FunctionHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *FunctionHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/function/")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.POST("set", h.Set)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("token", h.GenToken)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *FunctionHandler) Save(c *gin.Context) {
|
||||
var data vo.Function
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
@@ -119,7 +135,6 @@ func (h *FunctionHandler) GenToken(c *gin.Context) {
|
||||
})
|
||||
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
||||
if err != nil {
|
||||
logger.Error("error with generate token", err)
|
||||
resp.ERROR(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ package admin
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -32,6 +34,20 @@ func NewImageHandler(app *core.AppServer, db *gorm.DB, userService *service.User
|
||||
return &ImageHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, userService: userService, uploader: manager}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ImageHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/image/")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("list/mj", h.MjList)
|
||||
group.POST("list/sd", h.SdList)
|
||||
group.POST("list/dall", h.DallList)
|
||||
group.GET("remove", h.Remove)
|
||||
}
|
||||
}
|
||||
|
||||
type imageQuery struct {
|
||||
Prompt string `json:"prompt"`
|
||||
Username string `json:"username"`
|
||||
@@ -189,11 +205,10 @@ func (h *ImageHandler) Remove(c *gin.Context) {
|
||||
tx.Delete(&job)
|
||||
md = "mid-journey"
|
||||
power = job.Power
|
||||
userId = job.UserId
|
||||
userId = int(job.UserId)
|
||||
remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
imgURL = job.ImgURL
|
||||
break
|
||||
case "sd":
|
||||
var job model.SdJob
|
||||
if res := h.DB.Where("id", id).First(&job); res.Error != nil {
|
||||
@@ -205,11 +220,10 @@ func (h *ImageHandler) Remove(c *gin.Context) {
|
||||
tx.Delete(&job)
|
||||
md = "stable-diffusion"
|
||||
power = job.Power
|
||||
userId = job.UserId
|
||||
userId = int(job.UserId)
|
||||
remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
imgURL = job.ImgURL
|
||||
break
|
||||
case "dall":
|
||||
var job model.DallJob
|
||||
if res := h.DB.Where("id", id).First(&job); res.Error != nil {
|
||||
@@ -225,14 +239,13 @@ func (h *ImageHandler) Remove(c *gin.Context) {
|
||||
remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
imgURL = job.ImgURL
|
||||
break
|
||||
default:
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if progress != 100 {
|
||||
err := h.userService.IncreasePower(userId, power, model.PowerLog{
|
||||
err := h.userService.IncreasePower(uint(userId), power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: md,
|
||||
Remark: remark,
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service"
|
||||
"geekai/service/jimeng"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AdminJimengHandler 管理后台即梦AI处理器
|
||||
type AdminJimengHandler struct {
|
||||
handler.BaseHandler
|
||||
jimengClient *jimeng.Client
|
||||
userService *service.UserService
|
||||
uploader *oss.UploaderManager
|
||||
}
|
||||
|
||||
// NewAdminJimengHandler 创建管理后台即梦AI处理器
|
||||
func NewAdminJimengHandler(app *core.AppServer, db *gorm.DB, jimengClient *jimeng.Client, userService *service.UserService, uploader *oss.UploaderManager) *AdminJimengHandler {
|
||||
return &AdminJimengHandler{
|
||||
BaseHandler: handler.BaseHandler{App: app, DB: db},
|
||||
jimengClient: jimengClient,
|
||||
userService: userService,
|
||||
uploader: uploader,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册即梦AI管理后台路由
|
||||
func (h *AdminJimengHandler) RegisterRoutes() {
|
||||
rg := h.App.Engine.Group("/api/admin/jimeng/")
|
||||
rg.GET("/jobs", h.Jobs)
|
||||
rg.GET("/jobs/:id", h.JobDetail)
|
||||
rg.POST("/jobs/remove", h.BatchRemove)
|
||||
rg.GET("/stats", h.Stats)
|
||||
rg.POST("/config/update", h.UpdateConfig)
|
||||
}
|
||||
|
||||
// Jobs 获取任务列表
|
||||
func (h *AdminJimengHandler) Jobs(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
taskType := h.GetTrim(c, "type")
|
||||
status := h.GetTrim(c, "status")
|
||||
|
||||
var tasks []model.JimengJob
|
||||
var total int64
|
||||
|
||||
session := h.DB.Model(&model.JimengJob{})
|
||||
|
||||
// 构建查询条件
|
||||
if userId > 0 {
|
||||
session = session.Where("user_id = ?", userId)
|
||||
}
|
||||
if taskType != "" {
|
||||
session = session.Where("type = ?", taskType)
|
||||
}
|
||||
if status != "" {
|
||||
session = session.Where("status = ?", status)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
err := session.Count(&total).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "获取任务数量失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
offset := (page - 1) * pageSize
|
||||
err = session.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&tasks).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "获取任务列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"jobs": tasks,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// JobDetail 获取任务详情
|
||||
func (h *AdminJimengHandler) JobDetail(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
jobId, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var job model.JimengJob
|
||||
err = h.DB.Where("id = ?", jobId).First(&job).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "任务不存在")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, job)
|
||||
}
|
||||
|
||||
// BatchRemove 批量删除任务
|
||||
func (h *AdminJimengHandler) BatchRemove(c *gin.Context) {
|
||||
var req struct {
|
||||
JobIds []uint `json:"job_ids" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var deletedCount int64 = 0
|
||||
for _, jobId := range req.JobIds {
|
||||
var job model.JimengJob
|
||||
err := h.DB.Where("id = ?", jobId).First(&job).Error
|
||||
if err != nil {
|
||||
continue // 跳过不存在的
|
||||
}
|
||||
tx := h.DB.Begin()
|
||||
if job.Status != types.JMTaskStatusSuccess && job.Power > 0 {
|
||||
remark := fmt.Sprintf("任务未成功,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
err = h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: "jimeng",
|
||||
Remark: remark,
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
continue
|
||||
}
|
||||
}
|
||||
err = tx.Where("id = ?", jobId).Delete(&model.JimengJob{}).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
continue
|
||||
}
|
||||
tx.Commit()
|
||||
deletedCount++
|
||||
if job.ImgURL != "" {
|
||||
err = h.uploader.GetUploadHandler().Delete(job.ImgURL)
|
||||
if err != nil {
|
||||
logger.Error("remove image failed: ", err)
|
||||
}
|
||||
}
|
||||
if job.VideoURL != "" {
|
||||
err = h.uploader.GetUploadHandler().Delete(job.VideoURL)
|
||||
if err != nil {
|
||||
logger.Error("remove video failed: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"message": "批量删除成功",
|
||||
"deleted_count": deletedCount,
|
||||
})
|
||||
}
|
||||
|
||||
// Stats 获取统计信息
|
||||
func (h *AdminJimengHandler) Stats(c *gin.Context) {
|
||||
type StatResult struct {
|
||||
Status types.JMTaskStatus `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
var stats []StatResult
|
||||
err := h.DB.Model(&model.JimengJob{}).
|
||||
Select("status, COUNT(*) as count").
|
||||
Group("status").
|
||||
Find(&stats).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "获取统计信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 整理统计数据
|
||||
result := gin.H{
|
||||
"totalTasks": int64(0),
|
||||
"completedTasks": int64(0),
|
||||
"processingTasks": int64(0),
|
||||
"failedTasks": int64(0),
|
||||
"pendingTasks": int64(0),
|
||||
}
|
||||
|
||||
for _, stat := range stats {
|
||||
result["totalTasks"] = result["totalTasks"].(int64) + stat.Count
|
||||
switch stat.Status {
|
||||
case types.JMTaskStatusInQueue:
|
||||
result["pendingTasks"] = stat.Count
|
||||
case types.JMTaskStatusSuccess:
|
||||
result["completedTasks"] = stat.Count
|
||||
case types.JMTaskStatusGenerating:
|
||||
result["processingTasks"] = stat.Count
|
||||
case types.JMTaskStatusFailed:
|
||||
result["failedTasks"] = stat.Count
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, result)
|
||||
}
|
||||
|
||||
// UpdateConfig 更新即梦AI配置
|
||||
func (h *AdminJimengHandler) UpdateConfig(c *gin.Context) {
|
||||
var req types.JimengConfig
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if req.AccessKey == "" {
|
||||
resp.ERROR(c, "AccessKey不能为空")
|
||||
return
|
||||
}
|
||||
if req.SecretKey == "" {
|
||||
resp.ERROR(c, "SecretKey不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证算力配置
|
||||
if len(req.Powers) == 0 {
|
||||
resp.ERROR(c, "请至少配置一个模型的积分")
|
||||
return
|
||||
}
|
||||
for key, val := range req.Powers {
|
||||
if val <= 0 {
|
||||
resp.ERROR(c, fmt.Sprintf("模型 %s 的积分必须大于0", key))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
tx := h.DB.Begin()
|
||||
value := utils.JsonEncode(&req)
|
||||
var exist model.Config
|
||||
tx.Where("name", types.ConfigKeyJimeng).First(&exist)
|
||||
|
||||
if exist.Id > 0 {
|
||||
exist.Value = value
|
||||
err := tx.Updates(&exist).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "更新配置失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
exist.Name = types.ConfigKeyJimeng
|
||||
exist.Value = value
|
||||
err := tx.Create(&exist).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "创建配置失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 更新服务中的客户端配置
|
||||
err := h.jimengClient.UpdateConfig(req)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
tx.Commit()
|
||||
h.App.SysConfig.Jimeng = req
|
||||
|
||||
resp.SUCCESS(c, gin.H{"message": "配置更新成功"})
|
||||
}
|
||||
@@ -10,6 +10,7 @@ package admin
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service"
|
||||
@@ -33,6 +34,19 @@ func NewMediaHandler(app *core.AppServer, db *gorm.DB, userService *service.User
|
||||
return &MediaHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, userService: userService, uploader: manager}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *MediaHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/media/")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("suno", h.SunoList)
|
||||
group.POST("videos", h.Videos)
|
||||
group.GET("remove", h.Remove)
|
||||
}
|
||||
}
|
||||
|
||||
type mediaQuery struct {
|
||||
Type string `json:"type"` // 任务类型 luma, keling
|
||||
Prompt string `json:"prompt"`
|
||||
@@ -150,11 +164,10 @@ func (h *MediaHandler) Remove(c *gin.Context) {
|
||||
tx.Delete(&job)
|
||||
md = "suno"
|
||||
power = job.Power
|
||||
userId = job.UserId
|
||||
userId = int(job.UserId)
|
||||
remark = fmt.Sprintf("SUNO 任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
fileURL = job.AudioURL
|
||||
break
|
||||
case "luma":
|
||||
case "keling":
|
||||
var job model.VideoJob
|
||||
@@ -167,21 +180,20 @@ func (h *MediaHandler) Remove(c *gin.Context) {
|
||||
tx.Delete(&job)
|
||||
md = job.Type
|
||||
power = job.Power
|
||||
userId = job.UserId
|
||||
userId = int(job.UserId)
|
||||
remark = fmt.Sprintf("LUMA 任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
fileURL = job.VideoURL
|
||||
if fileURL == "" {
|
||||
fileURL = job.WaterURL
|
||||
}
|
||||
break
|
||||
default:
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if progress != 100 {
|
||||
err := h.userService.IncreasePower(userId, power, model.PowerLog{
|
||||
err := h.userService.IncreasePower(uint(userId), power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: md,
|
||||
Remark: remark,
|
||||
|
||||
@@ -27,6 +27,16 @@ func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler {
|
||||
return &MenuHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *MenuHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/menu/")
|
||||
group.POST("save", h.Save)
|
||||
group.GET("list", h.List)
|
||||
group.POST("enable", h.Enable)
|
||||
group.POST("sort", h.Sort)
|
||||
group.GET("remove", h.Remove)
|
||||
}
|
||||
|
||||
func (h *MenuHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service/moderation"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ModerationHandler struct {
|
||||
handler.BaseHandler
|
||||
sysConfig *types.SystemConfig
|
||||
moderationManager *moderation.ServiceManager
|
||||
}
|
||||
|
||||
func NewModerationHandler(app *core.AppServer, db *gorm.DB, sysConfig *types.SystemConfig, moderationManager *moderation.ServiceManager) *ModerationHandler {
|
||||
return &ModerationHandler{BaseHandler: handler.BaseHandler{DB: db, App: app}, sysConfig: sysConfig, moderationManager: moderationManager}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ModerationHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/moderation/")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.POST("batch-remove", h.BatchRemove)
|
||||
group.GET("source-list", h.GetSourceList)
|
||||
group.POST("config", h.UpdateModeration)
|
||||
group.POST("test", h.TestModeration)
|
||||
}
|
||||
}
|
||||
|
||||
// List 获取文本审核记录列表
|
||||
func (h *ModerationHandler) List(c *gin.Context) {
|
||||
var data struct {
|
||||
Username string `json:"username"`
|
||||
Source string `json:"source"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
|
||||
// 构建查询条件
|
||||
if data.Username != "" {
|
||||
// 通过用户名查找用户ID
|
||||
var user model.User
|
||||
if err := h.DB.Where("username LIKE ?", "%"+data.Username+"%").First(&user).Error; err == nil {
|
||||
session = session.Where("user_id", user.Id)
|
||||
}
|
||||
}
|
||||
|
||||
if data.Source != "" {
|
||||
session = session.Where("source", data.Source)
|
||||
}
|
||||
|
||||
if data.StartDate != "" && data.EndDate != "" {
|
||||
startTime := data.StartDate + " 00:00:00"
|
||||
endTime := data.EndDate + " 23:59:59"
|
||||
session = session.Where("created_at >= ? AND created_at <= ?", startTime, endTime)
|
||||
}
|
||||
|
||||
// 统计总数
|
||||
var total int64
|
||||
session.Model(&model.Moderation{}).Count(&total)
|
||||
|
||||
// 分页
|
||||
page := data.Page
|
||||
pageSize := data.PageSize
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
session = session.Offset(offset).Limit(pageSize)
|
||||
|
||||
// 查询数据
|
||||
var items []model.Moderation
|
||||
err := session.Order("id DESC").Find(&items).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
userIds := make([]uint, 0)
|
||||
for _, item := range items {
|
||||
userIds = append(userIds, item.UserId)
|
||||
}
|
||||
|
||||
var users []model.User
|
||||
if len(userIds) > 0 {
|
||||
h.DB.Where("id IN ?", userIds).Find(&users)
|
||||
}
|
||||
|
||||
userMap := make(map[uint]string)
|
||||
for _, user := range users {
|
||||
userMap[user.Id] = user.Username
|
||||
}
|
||||
|
||||
// 转换为响应数据
|
||||
list := make([]map[string]any, 0)
|
||||
for _, item := range items {
|
||||
var moderation types.ModerationResult
|
||||
err := utils.JsonDecode(item.Result, &moderation)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var result []string
|
||||
for value, label := range types.ModerationCategories {
|
||||
if moderation.Categories[value] {
|
||||
result = append(result, label)
|
||||
}
|
||||
}
|
||||
list = append(list, map[string]any{
|
||||
"id": item.Id,
|
||||
"user_id": item.UserId,
|
||||
"username": userMap[item.UserId],
|
||||
"source": item.Source,
|
||||
"input": item.Input,
|
||||
"output": item.Output,
|
||||
"result": result,
|
||||
"created_at": item.CreatedAt.Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, map[string]any{
|
||||
"items": list,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *ModerationHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
if id <= 0 {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.DB.Where("id", id).Delete(&model.Moderation{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// BatchRemove 批量删除文本审核记录
|
||||
func (h *ModerationHandler) BatchRemove(c *gin.Context) {
|
||||
var data struct {
|
||||
Ids []uint `json:"ids"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if len(data.Ids) == 0 {
|
||||
resp.ERROR(c, "请选择要删除的记录")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.DB.Where("id IN ?", data.Ids).Delete(&model.Moderation{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// 获取 source 列表
|
||||
func (h *ModerationHandler) GetSourceList(c *gin.Context) {
|
||||
sources := []gin.H{
|
||||
{
|
||||
"id": types.ModerationSourceChat,
|
||||
"name": "AI对话",
|
||||
},
|
||||
{
|
||||
"id": types.ModerationSourceMJ,
|
||||
"name": "Midjourney 绘图",
|
||||
},
|
||||
{
|
||||
"id": types.ModerationSourceDalle,
|
||||
"name": "Dalle 绘图",
|
||||
},
|
||||
{
|
||||
"id": types.ModerationSourceSD,
|
||||
"name": "StableDiffusion 绘图",
|
||||
},
|
||||
{
|
||||
"id": types.ModerationSourceSuno,
|
||||
"name": "Suno 音乐",
|
||||
},
|
||||
{
|
||||
"id": types.ModerationSourceVideo,
|
||||
"name": "视频生成",
|
||||
},
|
||||
{
|
||||
"id": types.ModerationSourceJiMeng,
|
||||
"name": "即梦AI",
|
||||
},
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, sources)
|
||||
}
|
||||
|
||||
// UpdateModeration 更新文本审查配置
|
||||
func (h *ModerationHandler) UpdateModeration(c *gin.Context) {
|
||||
var data types.ModerationConfig
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var config model.Config
|
||||
err := h.DB.Where("name", types.ConfigKeyModeration).First(&config).Error
|
||||
if err != nil {
|
||||
config.Name = types.ConfigKeyModeration
|
||||
config.Value = utils.JsonEncode(data)
|
||||
err = h.DB.Create(&config).Error
|
||||
} else {
|
||||
config.Value = utils.JsonEncode(data)
|
||||
err = h.DB.Updates(&config).Error
|
||||
}
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.moderationManager.UpdateConfig(data)
|
||||
h.sysConfig.Moderation = data
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// 测试结果类型,用于前端显示
|
||||
type ModerationTestResult struct {
|
||||
IsAbnormal bool `json:"isAbnormal"`
|
||||
Details []ModerationTestDetail `json:"details"`
|
||||
}
|
||||
|
||||
type ModerationTestDetail struct {
|
||||
Category string `json:"category"`
|
||||
Description string `json:"description"`
|
||||
Confidence string `json:"confidence"`
|
||||
IsCategory bool `json:"isCategory"`
|
||||
}
|
||||
|
||||
// TestModeration 测试文本审查服务
|
||||
func (h *ModerationHandler) TestModeration(c *gin.Context) {
|
||||
var data struct {
|
||||
Text string `json:"text"`
|
||||
Service string `json:"service"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if data.Text == "" {
|
||||
resp.ERROR(c, "测试文本不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否启用了文本审查
|
||||
if !h.sysConfig.Moderation.Enable {
|
||||
resp.ERROR(c, "文本审查服务未启用")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前激活的审核服务
|
||||
service := h.moderationManager.GetService()
|
||||
// 执行文本审核
|
||||
result, err := service.Moderate(data.Text)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "审核服务调用失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为前端需要的格式
|
||||
testResult := ModerationTestResult{
|
||||
IsAbnormal: result.Flagged,
|
||||
Details: make([]ModerationTestDetail, 0),
|
||||
}
|
||||
|
||||
// 构建详细信息
|
||||
for category, description := range types.ModerationCategories {
|
||||
score := result.CategoryScores[category]
|
||||
isCategory := result.Categories[category]
|
||||
|
||||
testResult.Details = append(testResult.Details, ModerationTestDetail{
|
||||
Category: category,
|
||||
Description: description,
|
||||
Confidence: fmt.Sprintf("%.2f", score),
|
||||
IsCategory: isCategory,
|
||||
})
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, testResult)
|
||||
}
|
||||
@@ -29,6 +29,14 @@ func NewOrderHandler(app *core.AppServer, db *gorm.DB) *OrderHandler {
|
||||
return &OrderHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *OrderHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/order/")
|
||||
group.POST("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("clear", h.Clear)
|
||||
}
|
||||
|
||||
func (h *OrderHandler) List(c *gin.Context) {
|
||||
var data struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
@@ -68,16 +76,16 @@ func (h *OrderHandler) List(c *gin.Context) {
|
||||
order.Id = item.Id
|
||||
order.CreatedAt = item.CreatedAt.Unix()
|
||||
order.UpdatedAt = item.UpdatedAt.Unix()
|
||||
payMethod, ok := types.PayMethods[item.PayWay]
|
||||
payChannel, ok := types.PayChannel[item.Channel]
|
||||
if !ok {
|
||||
payMethod = item.PayWay
|
||||
payChannel = item.Channel
|
||||
}
|
||||
payName, ok := types.PayNames[item.PayType]
|
||||
payWays, ok := types.PayWays[item.PayWay]
|
||||
if !ok {
|
||||
payName = item.PayWay
|
||||
payWays = item.PayWay
|
||||
}
|
||||
order.PayMethod = payMethod
|
||||
order.PayName = payName
|
||||
order.ChannelName = payChannel
|
||||
order.PayName = payWays
|
||||
list = append(list, order)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
@@ -121,8 +129,8 @@ func (h *OrderHandler) Clear(c *gin.Context) {
|
||||
}
|
||||
deleteIds := make([]uint, 0)
|
||||
for _, order := range orders {
|
||||
// 只删除 15 分钟内的未支付订单
|
||||
if time.Now().After(order.CreatedAt.Add(time.Minute * 15)) {
|
||||
// 只删除超时的未支付订单
|
||||
if time.Now().After(order.CreatedAt.Add(time.Minute * time.Duration(h.App.SysConfig.Base.OrderPayTimeout))) {
|
||||
deleteIds = append(deleteIds, order.Id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,12 @@ func NewPowerLogHandler(app *core.AppServer, db *gorm.DB) *PowerLogHandler {
|
||||
return &PowerLogHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *PowerLogHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/powerLog/")
|
||||
group.POST("list", h.List)
|
||||
}
|
||||
|
||||
func (h *PowerLogHandler) List(c *gin.Context) {
|
||||
var data struct {
|
||||
Username string `json:"username"`
|
||||
|
||||
@@ -15,9 +15,10 @@ import (
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ProductHandler struct {
|
||||
@@ -28,14 +29,22 @@ func NewProductHandler(app *core.AppServer, db *gorm.DB) *ProductHandler {
|
||||
return &ProductHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ProductHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/product/")
|
||||
group.POST("save", h.Save)
|
||||
group.GET("list", h.List)
|
||||
group.POST("enable", h.Enable)
|
||||
group.POST("sort", h.Sort)
|
||||
group.GET("remove", h.Remove)
|
||||
}
|
||||
|
||||
func (h *ProductHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Price float64 `json:"price"`
|
||||
Discount float64 `json:"discount"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Days int `json:"days"`
|
||||
Power int `json:"power"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
@@ -45,12 +54,10 @@ func (h *ProductHandler) Save(c *gin.Context) {
|
||||
}
|
||||
|
||||
item := model.Product{
|
||||
Name: data.Name,
|
||||
Price: data.Price,
|
||||
Discount: data.Discount,
|
||||
Days: data.Days,
|
||||
Power: data.Power,
|
||||
Enabled: data.Enabled}
|
||||
Name: data.Name,
|
||||
Price: data.Price,
|
||||
Power: data.Power,
|
||||
Enabled: data.Enabled}
|
||||
item.Id = data.Id
|
||||
if item.Id > 0 {
|
||||
item.CreatedAt = time.Unix(data.CreatedAt, 0)
|
||||
|
||||
@@ -29,6 +29,16 @@ func NewRedeemHandler(app *core.AppServer, db *gorm.DB) *RedeemHandler {
|
||||
return &RedeemHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *RedeemHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/redeem/")
|
||||
group.GET("list", h.List)
|
||||
group.POST("create", h.Create)
|
||||
group.POST("set", h.Set)
|
||||
group.GET("remove", h.Remove)
|
||||
group.POST("export", h.Export)
|
||||
}
|
||||
|
||||
func (h *RedeemHandler) List(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
@@ -106,8 +116,8 @@ func (h *RedeemHandler) Export(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 设置响应头,告诉浏览器这是一个附件,需要下载
|
||||
c.Header("Content-Disposition", "attachment; filename=output.csv")
|
||||
c.Header("Content-Type", "text/csv")
|
||||
c.Header("Prompt-Disposition", "attachment; filename=output.csv")
|
||||
c.Header("Prompt-Type", "text/csv")
|
||||
|
||||
// 创建一个 CSV writer
|
||||
writer := csv.NewWriter(c.Writer)
|
||||
|
||||
@@ -9,6 +9,7 @@ package admin
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/handler"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
@@ -28,6 +29,17 @@ func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderMan
|
||||
return &UploadHandler{BaseHandler: handler.BaseHandler{DB: db, App: app}, uploaderManager: manager}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *UploadHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/upload")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("", h.Upload)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *UploadHandler) Upload(c *gin.Context) {
|
||||
// 判断文件大小
|
||||
f, err := c.FormFile("file")
|
||||
@@ -36,7 +48,7 @@ func (h *UploadHandler) Upload(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if h.App.SysConfig.MaxFileSize > 0 && f.Size > int64(h.App.SysConfig.MaxFileSize)*1024*1024 {
|
||||
if h.App.SysConfig.Base.MaxFileSize > 0 && f.Size > int64(h.App.SysConfig.Base.MaxFileSize)*1024*1024 {
|
||||
resp.ERROR(c, "文件大小超过限制")
|
||||
return
|
||||
}
|
||||
@@ -49,7 +61,7 @@ func (h *UploadHandler) Upload(c *gin.Context) {
|
||||
|
||||
userId := 0
|
||||
res := h.DB.Create(&model.File{
|
||||
UserId: userId,
|
||||
UserId: uint(userId),
|
||||
Name: file.Name,
|
||||
ObjKey: file.ObjKey,
|
||||
URL: file.URL,
|
||||
|
||||
@@ -10,6 +10,7 @@ package admin
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service"
|
||||
@@ -19,10 +20,9 @@ import (
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -36,6 +36,22 @@ func NewUserHandler(app *core.AppServer, db *gorm.DB, licenseService *service.Li
|
||||
return &UserHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, licenseService: licenseService, redis: redisCli}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *UserHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/user/")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("loginLog", h.LoginLog)
|
||||
group.GET("genLoginLink", h.GenLoginLink)
|
||||
group.POST("resetPass", h.ResetPass)
|
||||
}
|
||||
}
|
||||
|
||||
// List 用户列表
|
||||
func (h *UserHandler) List(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 1)
|
||||
@@ -178,6 +194,7 @@ func (h *UserHandler) Save(c *gin.Context) {
|
||||
Power: data.Power,
|
||||
Status: true,
|
||||
ChatRoles: utils.JsonEncode(data.ChatRoles),
|
||||
ChatConfig: "{}",
|
||||
ChatModels: utils.JsonEncode(data.ChatModels),
|
||||
ExpiredTime: utils.Str2stamp(data.ExpiredTime),
|
||||
}
|
||||
@@ -353,4 +370,4 @@ func (h *UserHandler) GenLoginLink(c *gin.Context) {
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, tokenString)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,10 @@ import (
|
||||
logger2 "geekai/logger"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -69,6 +70,14 @@ func (h *BaseHandler) GetLoginUserId(c *gin.Context) uint {
|
||||
return uint(utils.IntValue(utils.InterfaceToString(userId), 0))
|
||||
}
|
||||
|
||||
func (h *BaseHandler) GetAdminId(c *gin.Context) uint {
|
||||
userId, ok := c.Get(types.AdminUserID)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return uint(utils.IntValue(utils.InterfaceToString(userId), 0))
|
||||
}
|
||||
|
||||
func (h *BaseHandler) IsLogin(c *gin.Context) bool {
|
||||
return h.GetLoginUserId(c) > 0
|
||||
}
|
||||
|
||||
@@ -8,23 +8,45 @@ package handler
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 今日头条函数实现
|
||||
|
||||
type CaptchaHandler struct {
|
||||
App *core.AppServer
|
||||
service *service.CaptchaService
|
||||
}
|
||||
|
||||
func NewCaptchaHandler(s *service.CaptchaService) *CaptchaHandler {
|
||||
return &CaptchaHandler{service: s}
|
||||
func NewCaptchaHandler(app *core.AppServer, s *service.CaptchaService, sysConfig *types.SystemConfig) *CaptchaHandler {
|
||||
return &CaptchaHandler{App: app, service: s}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *CaptchaHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/captcha/")
|
||||
|
||||
// 无需授权的接口
|
||||
group.GET("get", h.Get)
|
||||
group.POST("check", h.Check)
|
||||
group.GET("slide/get", h.SlideGet)
|
||||
group.POST("slide/check", h.SlideCheck)
|
||||
group.GET("config", h.GetConfig)
|
||||
}
|
||||
|
||||
func (h *CaptchaHandler) GetConfig(c *gin.Context) {
|
||||
resp.SUCCESS(c, gin.H{"enabled": h.service.GetConfig().Enabled, "type": h.service.GetConfig().Type})
|
||||
}
|
||||
|
||||
func (h *CaptchaHandler) Get(c *gin.Context) {
|
||||
if !h.service.GetConfig().Enabled {
|
||||
resp.ERROR(c, "验证码服务未启用")
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.service.Get()
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
@@ -36,6 +58,11 @@ func (h *CaptchaHandler) Get(c *gin.Context) {
|
||||
|
||||
// Check verify the captcha data
|
||||
func (h *CaptchaHandler) Check(c *gin.Context) {
|
||||
if !h.service.GetConfig().Enabled {
|
||||
resp.ERROR(c, "验证码服务未启用")
|
||||
return
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Key string `json:"key"`
|
||||
Dots string `json:"dots"`
|
||||
@@ -55,6 +82,11 @@ func (h *CaptchaHandler) Check(c *gin.Context) {
|
||||
|
||||
// SlideGet 获取滑动验证图片
|
||||
func (h *CaptchaHandler) SlideGet(c *gin.Context) {
|
||||
if !h.service.GetConfig().Enabled {
|
||||
resp.ERROR(c, "验证码服务未启用")
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.service.SlideGet()
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
@@ -66,6 +98,11 @@ func (h *CaptchaHandler) SlideGet(c *gin.Context) {
|
||||
|
||||
// SlideCheck 滑动验证结果校验
|
||||
func (h *CaptchaHandler) SlideCheck(c *gin.Context) {
|
||||
if !h.service.GetConfig().Enabled {
|
||||
resp.ERROR(c, "验证码服务未启用")
|
||||
return
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Key string `json:"key"`
|
||||
X int `json:"x"`
|
||||
|
||||
@@ -9,6 +9,7 @@ package handler
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
@@ -19,18 +20,31 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ChatRoleHandler struct {
|
||||
type ChatAppHandler struct {
|
||||
BaseHandler
|
||||
}
|
||||
|
||||
func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler {
|
||||
return &ChatRoleHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
func NewChatAppHandler(app *core.AppServer, db *gorm.DB) *ChatAppHandler {
|
||||
return &ChatAppHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ChatAppHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/app/")
|
||||
group.GET("list", h.List)
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("list/user", h.ListByUser)
|
||||
group.POST("update", h.UpdateApp)
|
||||
}
|
||||
}
|
||||
|
||||
// List 获取用户聊天应用列表
|
||||
func (h *ChatRoleHandler) List(c *gin.Context) {
|
||||
func (h *ChatAppHandler) List(c *gin.Context) {
|
||||
tid := h.GetInt(c, "tid", 0)
|
||||
var roles []model.ChatRole
|
||||
var roles []model.ChatApp
|
||||
session := h.DB.Where("enable", true)
|
||||
if tid > 0 {
|
||||
session = session.Where("tid", tid)
|
||||
@@ -41,9 +55,9 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var roleVos = make([]vo.ChatRole, 0)
|
||||
var roleVos = make([]vo.ChatApp, 0)
|
||||
for _, r := range roles {
|
||||
var v vo.ChatRole
|
||||
var v vo.ChatApp
|
||||
err := utils.CopyObject(r, &v)
|
||||
if err == nil {
|
||||
v.Id = r.Id
|
||||
@@ -54,20 +68,22 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
|
||||
}
|
||||
|
||||
// ListByUser 获取用户添加的角色列表
|
||||
func (h *ChatRoleHandler) ListByUser(c *gin.Context) {
|
||||
func (h *ChatAppHandler) ListByUser(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
var roles []model.ChatRole
|
||||
var roles []model.ChatApp
|
||||
session := h.DB.Where("enable", true)
|
||||
// 如果用户没登录,则获取所有角色
|
||||
if userId > 0 {
|
||||
var user model.User
|
||||
h.DB.First(&user, userId)
|
||||
var roleKeys []string
|
||||
err := utils.JsonDecode(user.ChatRoles, &roleKeys)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "角色解析失败!")
|
||||
return
|
||||
if user.ChatRoles != "" {
|
||||
err := utils.JsonDecode(user.ChatRoles, &roleKeys)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "角色解析失败!")
|
||||
return
|
||||
}
|
||||
}
|
||||
// 保证用户至少有一个角色可用
|
||||
if len(roleKeys) > 0 {
|
||||
@@ -84,9 +100,9 @@ func (h *ChatRoleHandler) ListByUser(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var roleVos = make([]vo.ChatRole, 0)
|
||||
var roleVos = make([]vo.ChatApp, 0)
|
||||
for _, r := range roles {
|
||||
var v vo.ChatRole
|
||||
var v vo.ChatApp
|
||||
err := utils.CopyObject(r, &v)
|
||||
if err == nil {
|
||||
v.Id = r.Id
|
||||
@@ -96,8 +112,8 @@ func (h *ChatRoleHandler) ListByUser(c *gin.Context) {
|
||||
resp.SUCCESS(c, roleVos)
|
||||
}
|
||||
|
||||
// UpdateRole 更新用户聊天角色
|
||||
func (h *ChatRoleHandler) UpdateRole(c *gin.Context) {
|
||||
// UpdateApp 更新用户聊天应用
|
||||
func (h *ChatAppHandler) UpdateApp(c *gin.Context) {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
@@ -19,6 +19,12 @@ func NewChatAppTypeHandler(app *core.AppServer, db *gorm.DB) *ChatAppTypeHandler
|
||||
return &ChatAppTypeHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ChatAppTypeHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/app/type/")
|
||||
group.GET("list", h.List)
|
||||
}
|
||||
|
||||
// List 获取App类型列表
|
||||
func (h *ChatAppTypeHandler) List(c *gin.Context) {
|
||||
var items []model.AppType
|
||||
|
||||
+375
-162
@@ -14,18 +14,20 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/moderation"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
@@ -36,39 +38,157 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
ChatEventStart = "start"
|
||||
ChatEventEnd = "end"
|
||||
ChatEventComplete = "complete"
|
||||
ChatEventError = "error"
|
||||
ChatEventMessageDelta = "message_delta"
|
||||
ChatEventTitle = "title"
|
||||
)
|
||||
|
||||
type ChatInput struct {
|
||||
UserId uint `json:"user_id"`
|
||||
RoleId uint `json:"role_id"`
|
||||
ModelId uint `json:"model_id"`
|
||||
ChatId string `json:"chat_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
Tools []uint `json:"tools"`
|
||||
Stream bool `json:"stream"`
|
||||
Files []vo.File `json:"files"`
|
||||
ChatModel model.ChatModel `json:"chat_model,omitempty"`
|
||||
ChatRole model.ChatApp `json:"chat_role,omitempty"`
|
||||
LastMsgId uint `json:"last_msg_id,omitempty"` // 最后的消息ID,用于重新生成答案的时候过滤上下文
|
||||
}
|
||||
|
||||
type ChatHandler struct {
|
||||
BaseHandler
|
||||
redis *redis.Client
|
||||
uploadManager *oss.UploaderManager
|
||||
licenseService *service.LicenseService
|
||||
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
|
||||
ChatContexts *types.LMap[string, []any] // 聊天上下文 Map [chatId] => []Message
|
||||
userService *service.UserService
|
||||
redis *redis.Client
|
||||
uploadManager *oss.UploaderManager
|
||||
licenseService *service.LicenseService
|
||||
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
|
||||
userService *service.UserService
|
||||
moderationManager *moderation.ServiceManager
|
||||
userLocks *types.UserLockManager
|
||||
}
|
||||
|
||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService, userService *service.UserService) *ChatHandler {
|
||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService, userService *service.UserService, moderationManager *moderation.ServiceManager) *ChatHandler {
|
||||
return &ChatHandler{
|
||||
BaseHandler: BaseHandler{App: app, DB: db},
|
||||
redis: redis,
|
||||
uploadManager: manager,
|
||||
licenseService: licenseService,
|
||||
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
|
||||
ChatContexts: types.NewLMap[string, []any](),
|
||||
userService: userService,
|
||||
BaseHandler: BaseHandler{App: app, DB: db},
|
||||
redis: redis,
|
||||
uploadManager: manager,
|
||||
licenseService: licenseService,
|
||||
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
|
||||
userService: userService,
|
||||
moderationManager: moderationManager,
|
||||
userLocks: types.NewUserLockManager(),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSession, role model.ChatRole, prompt string, ws *types.WsClient) error {
|
||||
if !h.App.Debug {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Error("Recover message from error: ", r)
|
||||
}
|
||||
}()
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ChatHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/chat/")
|
||||
|
||||
// 聊天接口不需要授权(已在authConfig中配置)
|
||||
group.Any("message", h.Chat)
|
||||
|
||||
// 其他接口需要用户授权
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("list", h.List)
|
||||
group.GET("detail", h.Detail)
|
||||
group.POST("update", h.Update)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("history", h.History)
|
||||
group.GET("clear", h.Clear)
|
||||
group.POST("tokens", h.Tokens)
|
||||
group.GET("stop", h.StopGenerate)
|
||||
group.POST("tts", h.TextToSpeech)
|
||||
}
|
||||
}
|
||||
|
||||
// Chat 处理聊天请求
|
||||
func (h *ChatHandler) Chat(c *gin.Context) {
|
||||
// 设置SSE响应头
|
||||
c.Header("Prompt-Type", "text/event-stream")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("Connection", "keep-alive")
|
||||
c.Header("X-Accel-Buffering", "no")
|
||||
|
||||
var input ChatInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
pushMessage(c, ChatEventError, types.InvalidArgs)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 用户级并发锁,确保同一用户同时只有一个对话请求
|
||||
if !h.userLocks.TryLock(input.UserId) {
|
||||
pushMessage(c, ChatEventError, "您有一个对话请求正在进行中,请稍后再试或先停止当前生成!")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
defer h.userLocks.Unlock(input.UserId)
|
||||
|
||||
ctx, cancel := context.WithCancel(c.Request.Context())
|
||||
defer cancel()
|
||||
|
||||
// 这里做个全局的异常处理,防止整个请求异常,导致 SSE 连接断开
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Errorf("chat handler error: %v", err)
|
||||
pushMessage(c, ChatEventError, err)
|
||||
c.Abort()
|
||||
}
|
||||
}()
|
||||
|
||||
// 使用旧的聊天数据覆盖模型和角色ID
|
||||
var chat model.ChatItem
|
||||
h.DB.Where("chat_id", input.ChatId).First(&chat)
|
||||
if chat.Id > 0 {
|
||||
input.ModelId = chat.ModelId
|
||||
input.RoleId = chat.RoleId
|
||||
}
|
||||
|
||||
// 验证聊天角色
|
||||
var chatRole model.ChatApp
|
||||
err := h.DB.First(&chatRole, input.RoleId).Error
|
||||
if err != nil || !chatRole.Enable {
|
||||
pushMessage(c, ChatEventError, "当前聊天角色不存在或者未启用,请更换角色之后再发起对话!")
|
||||
return
|
||||
}
|
||||
input.ChatRole = chatRole
|
||||
|
||||
// 获取模型信息
|
||||
var chatModel model.ChatModel
|
||||
err = h.DB.Where("id", input.ModelId).First(&chatModel).Error
|
||||
if err != nil || !chatModel.Enabled {
|
||||
pushMessage(c, ChatEventError, "当前AI模型暂未启用,请更换模型后再发起对话!")
|
||||
return
|
||||
}
|
||||
input.ChatModel = chatModel
|
||||
|
||||
// 发送消息
|
||||
err = h.sendMessage(ctx, input, c)
|
||||
if err != nil {
|
||||
pushMessage(c, ChatEventError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
pushMessage(c, ChatEventEnd, "对话完成")
|
||||
}
|
||||
|
||||
func pushMessage(c *gin.Context, msgType string, content interface{}) {
|
||||
c.SSEvent("message", map[string]interface{}{
|
||||
"type": msgType,
|
||||
"body": content,
|
||||
})
|
||||
c.Writer.Flush()
|
||||
}
|
||||
|
||||
func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.Context) error {
|
||||
var user model.User
|
||||
res := h.DB.Model(&model.User{}).First(&user, session.UserId)
|
||||
res := h.DB.Model(&model.User{}).First(&user, input.UserId)
|
||||
if res.Error != nil {
|
||||
return errors.New("未授权用户,您正在进行非法操作!")
|
||||
}
|
||||
@@ -79,12 +199,12 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
return errors.New("User 对象转换失败," + err.Error())
|
||||
}
|
||||
|
||||
if userVo.Status == false {
|
||||
if !userVo.Status {
|
||||
return errors.New("您的账号已经被禁用,如果疑问,请联系管理员!")
|
||||
}
|
||||
|
||||
if userVo.Power < session.Model.Power {
|
||||
return fmt.Errorf("您当前剩余算力 %d 已不足以支付当前模型的单次对话需要消耗的算力 %d,[立即购买](/member)。", userVo.Power, session.Model.Power)
|
||||
if userVo.Power < input.ChatModel.Power {
|
||||
return fmt.Errorf("您的算力不足,请购买算力。")
|
||||
}
|
||||
|
||||
if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() {
|
||||
@@ -92,30 +212,29 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
}
|
||||
|
||||
// 检查 prompt 长度是否超过了当前模型允许的最大上下文长度
|
||||
promptTokens, _ := utils.CalcTokens(prompt, session.Model.Value)
|
||||
if promptTokens > session.Model.MaxContext {
|
||||
promptTokens, _ := utils.CalcTokens(input.Prompt, input.ChatModel.Value)
|
||||
if promptTokens > input.ChatModel.MaxContext {
|
||||
|
||||
return errors.New("对话内容超出了当前模型允许的最大上下文长度!")
|
||||
}
|
||||
|
||||
var req = types.ApiRequest{
|
||||
Model: session.Model.Value,
|
||||
Stream: session.Stream,
|
||||
Temperature: session.Model.Temperature,
|
||||
Model: input.ChatModel.Value,
|
||||
Stream: input.Stream,
|
||||
Temperature: input.ChatModel.Temperature,
|
||||
}
|
||||
// 兼容 OpenAI 模型
|
||||
if strings.HasPrefix(session.Model.Value, "o1-") ||
|
||||
strings.HasPrefix(session.Model.Value, "o3-") ||
|
||||
strings.HasPrefix(session.Model.Value, "gpt") {
|
||||
req.MaxCompletionTokens = session.Model.MaxTokens
|
||||
session.Start = time.Now().Unix()
|
||||
if strings.HasPrefix(input.ChatModel.Value, "o1-") ||
|
||||
strings.HasPrefix(input.ChatModel.Value, "o3-") ||
|
||||
strings.HasPrefix(input.ChatModel.Value, "gpt") {
|
||||
req.MaxCompletionTokens = input.ChatModel.MaxTokens
|
||||
} else {
|
||||
req.MaxTokens = session.Model.MaxTokens
|
||||
req.MaxTokens = input.ChatModel.MaxTokens
|
||||
}
|
||||
|
||||
if len(session.Tools) > 0 && !strings.HasPrefix(session.Model.Value, "o1-") {
|
||||
if len(input.Tools) > 0 && !strings.HasPrefix(input.ChatModel.Value, "o1-") {
|
||||
var items []model.Function
|
||||
res = h.DB.Where("enabled", true).Where("id IN ?", session.Tools).Find(&items)
|
||||
res = h.DB.Where("enabled", true).Where("id IN ?", input.Tools).Find(&items)
|
||||
if res.Error == nil {
|
||||
var tools = make([]types.Tool, 0)
|
||||
for _, v := range items {
|
||||
@@ -146,25 +265,34 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
}
|
||||
|
||||
// 加载聊天上下文
|
||||
chatCtx := make([]interface{}, 0)
|
||||
messages := make([]interface{}, 0)
|
||||
if h.App.SysConfig.EnableContext {
|
||||
if h.ChatContexts.Has(session.ChatId) {
|
||||
messages = h.ChatContexts.Get(session.ChatId)
|
||||
} else {
|
||||
_ = utils.JsonDecode(role.Context, &messages)
|
||||
if h.App.SysConfig.ContextDeep > 0 {
|
||||
var historyMessages []model.ChatMessage
|
||||
res := h.DB.Where("chat_id = ? and use_context = 1", session.ChatId).Limit(h.App.SysConfig.ContextDeep).Order("id DESC").Find(&historyMessages)
|
||||
if res.Error == nil {
|
||||
for i := len(historyMessages) - 1; i >= 0; i-- {
|
||||
msg := historyMessages[i]
|
||||
ms := types.Message{Role: "user", Content: msg.Content}
|
||||
if msg.Type == types.ReplyMsg {
|
||||
ms.Role = "assistant"
|
||||
}
|
||||
chatCtx = append(chatCtx, ms)
|
||||
chatCtx := make([]any, 0)
|
||||
messages := make([]any, 0)
|
||||
if h.App.SysConfig.Base.EnableContext {
|
||||
_ = utils.JsonDecode(input.ChatRole.Context, &messages)
|
||||
if h.App.SysConfig.Base.ContextDeep > 0 {
|
||||
var historyMessages []model.ChatMessage
|
||||
dbSession := h.DB.Session(&gorm.Session{}).Where("chat_id", input.ChatId)
|
||||
if input.LastMsgId > 0 { // 重新生成和编辑逻辑
|
||||
var lastMessage model.ChatMessage
|
||||
err = dbSession.Where("id < ?", input.LastMsgId).Where("type", types.ReplyMsg).Order("id DESC").First(&lastMessage).Error
|
||||
if err != nil {
|
||||
input.LastMsgId = 0
|
||||
} else {
|
||||
input.LastMsgId = lastMessage.Id
|
||||
}
|
||||
dbSession = dbSession.Where("id < ?", input.LastMsgId)
|
||||
// 删除对应的聊天记录
|
||||
h.DB.Debug().Where("chat_id", input.ChatId).Where("id > ?", input.LastMsgId).Delete(&model.ChatMessage{})
|
||||
}
|
||||
err = dbSession.Limit(h.App.SysConfig.Base.ContextDeep).Order("id DESC").Find(&historyMessages).Error
|
||||
if err == nil {
|
||||
for i := len(historyMessages) - 1; i >= 0; i-- {
|
||||
msg := historyMessages[i]
|
||||
ms := types.Message{Role: "user", Content: msg.Content}
|
||||
if msg.Type == types.ReplyMsg {
|
||||
ms.Role = "assistant"
|
||||
}
|
||||
chatCtx = append(chatCtx, ms)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,90 +307,124 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
v := messages[i]
|
||||
tks, _ = utils.CalcTokens(utils.JsonEncode(v), req.Model)
|
||||
// 上下文 token 超出了模型的最大上下文长度
|
||||
if tokens+tks >= session.Model.MaxContext {
|
||||
if tokens+tks >= input.ChatModel.MaxContext {
|
||||
break
|
||||
}
|
||||
|
||||
// 上下文的深度超出了模型的最大上下文深度
|
||||
if len(chatCtx) >= h.App.SysConfig.ContextDeep {
|
||||
if len(chatCtx) >= h.App.SysConfig.Base.ContextDeep {
|
||||
break
|
||||
}
|
||||
|
||||
tokens += tks
|
||||
chatCtx = append(chatCtx, v)
|
||||
}
|
||||
|
||||
logger.Debugf("聊天上下文:%+v", chatCtx)
|
||||
}
|
||||
reqMgs := make([]interface{}, 0)
|
||||
reqMgs := make([]any, 0)
|
||||
|
||||
// 添加引导提示词,防止模型生成违规内容
|
||||
if h.App.SysConfig.Moderation.EnableGuide {
|
||||
reqMgs = append(reqMgs, map[string]any{
|
||||
"role": "system",
|
||||
"content": h.App.SysConfig.Moderation.GuidePrompt,
|
||||
})
|
||||
}
|
||||
|
||||
for i := len(chatCtx) - 1; i >= 0; i-- {
|
||||
reqMgs = append(reqMgs, chatCtx[i])
|
||||
}
|
||||
|
||||
fullPrompt := prompt
|
||||
text := prompt
|
||||
// extract files in prompt
|
||||
files := utils.ExtractFileURLs(prompt)
|
||||
logger.Debugf("detected FILES: %+v", files)
|
||||
// 如果不是逆向模型,则提取文件内容
|
||||
if len(files) > 0 && !(session.Model.Value == "gpt-4-all" ||
|
||||
strings.HasPrefix(session.Model.Value, "gpt-4-gizmo") ||
|
||||
strings.HasSuffix(session.Model.Value, "claude-3")) {
|
||||
contents := make([]string, 0)
|
||||
var file model.File
|
||||
for _, v := range files {
|
||||
h.DB.Where("url = ?", v).First(&file)
|
||||
content, err := utils.ReadFileContent(v, h.App.Config.TikaHost)
|
||||
if err != nil {
|
||||
logger.Error("error with read file: ", err)
|
||||
} else {
|
||||
contents = append(contents, fmt.Sprintf("%s 文件内容:%s", file.Name, content))
|
||||
}
|
||||
text = strings.Replace(text, v, "", 1)
|
||||
}
|
||||
if len(contents) > 0 {
|
||||
fullPrompt = fmt.Sprintf("请根据提供的文件内容信息回答问题(其中Excel 已转成 HTML):\n\n %s\n\n 问题:%s", strings.Join(contents, "\n"), text)
|
||||
}
|
||||
|
||||
tokens, _ := utils.CalcTokens(fullPrompt, req.Model)
|
||||
if tokens > session.Model.MaxContext {
|
||||
return fmt.Errorf("文件的长度超出模型允许的最大上下文长度,请减少文件内容数量或文件大小。")
|
||||
}
|
||||
}
|
||||
logger.Debug("最终Prompt:", fullPrompt)
|
||||
|
||||
// extract images from prompt
|
||||
imgURLs := utils.ExtractImgURLs(prompt)
|
||||
logger.Debugf("detected IMG: %+v", imgURLs)
|
||||
var content interface{}
|
||||
if len(imgURLs) > 0 {
|
||||
data := make([]interface{}, 0)
|
||||
for _, v := range imgURLs {
|
||||
text = strings.Replace(text, v, "", 1)
|
||||
data = append(data, gin.H{
|
||||
fileContents := make([]string, 0) // 文件内容
|
||||
var finalPrompt = input.Prompt
|
||||
imgList := make([]any, 0)
|
||||
for _, file := range input.Files {
|
||||
logger.Debugf("detected file: %+v", file.URL)
|
||||
// 处理图片
|
||||
if isImageURL(file.URL) {
|
||||
imgList = append(imgList, gin.H{
|
||||
"type": "image_url",
|
||||
"image_url": gin.H{
|
||||
"url": v,
|
||||
"url": file.URL,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// 处理文件,提取文件内容
|
||||
content, err := utils.ReadFileContent(file.URL, h.App.Config.TikaHost)
|
||||
if err != nil {
|
||||
logger.Error("error with read file: ", err)
|
||||
continue
|
||||
} else {
|
||||
fileContents = append(fileContents, fmt.Sprintf("%s 文件内容:%s", file.Name, content))
|
||||
logger.Debugf("fileContents: %s", fileContents)
|
||||
}
|
||||
}
|
||||
data = append(data, gin.H{
|
||||
"type": "text",
|
||||
"text": strings.TrimSpace(text),
|
||||
})
|
||||
content = data
|
||||
} else {
|
||||
content = fullPrompt
|
||||
}
|
||||
req.Messages = append(reqMgs, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": content,
|
||||
})
|
||||
|
||||
logger.Debugf("%+v", req.Messages)
|
||||
if len(fileContents) > 0 {
|
||||
finalPrompt = fmt.Sprintf("请根据提供的文件内容信息回答问题(其中Excel 已转成 HTML):\n\n %s\n\n 问题:%s", strings.Join(fileContents, "\n"), input.Prompt)
|
||||
tokens, _ := utils.CalcTokens(finalPrompt, req.Model)
|
||||
if tokens > input.ChatModel.MaxContext {
|
||||
return fmt.Errorf("文件的长度超出模型允许的最大上下文长度,请减少文件内容数量或文件大小。")
|
||||
}
|
||||
} else {
|
||||
finalPrompt = input.Prompt
|
||||
}
|
||||
|
||||
return h.sendOpenAiMessage(req, userVo, ctx, session, role, prompt, ws)
|
||||
if len(imgList) > 0 {
|
||||
imgList = append(imgList, map[string]any{
|
||||
"type": "text",
|
||||
"text": input.Prompt,
|
||||
})
|
||||
req.Messages = append(reqMgs, map[string]any{
|
||||
"role": "user",
|
||||
"content": imgList,
|
||||
})
|
||||
} else {
|
||||
req.Messages = append(reqMgs, map[string]any{
|
||||
"role": "user",
|
||||
"content": finalPrompt,
|
||||
})
|
||||
}
|
||||
|
||||
return h.sendOpenAiMessage(req, userVo, ctx, input, c)
|
||||
}
|
||||
|
||||
// 判断一个 URL 是否图片链接
|
||||
func isImageURL(url string) bool {
|
||||
// 检查是否是有效的URL
|
||||
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查文件扩展名
|
||||
ext := strings.ToLower(path.Ext(url))
|
||||
validImageExts := map[string]bool{
|
||||
".jpg": true,
|
||||
".jpeg": true,
|
||||
".png": true,
|
||||
".gif": true,
|
||||
".bmp": true,
|
||||
".webp": true,
|
||||
".svg": true,
|
||||
".ico": true,
|
||||
}
|
||||
|
||||
if !validImageExts[ext] {
|
||||
return false
|
||||
}
|
||||
|
||||
// 发送HEAD请求检查Content-Type
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
resp, err := client.Head(url)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
return strings.HasPrefix(contentType, "image/")
|
||||
}
|
||||
|
||||
// Tokens 统计 token 数量
|
||||
@@ -331,15 +493,14 @@ func (h *ChatHandler) StopGenerate(c *gin.Context) {
|
||||
|
||||
// 发送请求到 OpenAI 服务器
|
||||
// useOwnApiKey: 是否使用了用户自己的 API KEY
|
||||
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, session *types.ChatSession, apiKey *model.ApiKey) (*http.Response, error) {
|
||||
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, input ChatInput, apiKey *model.ApiKey) (*http.Response, error) {
|
||||
// if the chat model bind a KEY, use it directly
|
||||
if session.Model.KeyId > 0 {
|
||||
h.DB.Where("id", session.Model.KeyId).Find(apiKey)
|
||||
}
|
||||
// use the last unused key
|
||||
if apiKey.Id == 0 {
|
||||
if input.ChatModel.KeyId > 0 {
|
||||
h.DB.Where("id", input.ChatModel.KeyId).Where("enabled", true).Find(apiKey)
|
||||
} else { // use the last unused key
|
||||
h.DB.Where("type", "chat").Where("enabled", true).Order("last_used_at ASC").First(apiKey)
|
||||
}
|
||||
|
||||
if apiKey.Id == 0 {
|
||||
return nil, errors.New("no available key, please import key")
|
||||
}
|
||||
@@ -389,16 +550,16 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, sessi
|
||||
}
|
||||
|
||||
// 扣减用户算力
|
||||
func (h *ChatHandler) subUserPower(userVo vo.User, session *types.ChatSession, promptTokens int, replyTokens int) {
|
||||
func (h *ChatHandler) subUserPower(userVo vo.User, input ChatInput, promptTokens int, replyTokens int) {
|
||||
power := 1
|
||||
if session.Model.Power > 0 {
|
||||
power = session.Model.Power
|
||||
if input.ChatModel.Power > 0 {
|
||||
power = input.ChatModel.Power
|
||||
}
|
||||
|
||||
err := h.userService.DecreasePower(int(userVo.Id), power, model.PowerLog{
|
||||
err := h.userService.DecreasePower(userVo.Id, power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: session.Model.Value,
|
||||
Remark: fmt.Sprintf("模型名称:%s, 提问长度:%d,回复长度:%d", session.Model.Name, promptTokens, replyTokens),
|
||||
Model: input.ChatModel.Value,
|
||||
Remark: fmt.Sprintf("模型名称:%s, 提问长度:%d,回复长度:%d", input.ChatModel.Name, promptTokens, replyTokens),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
@@ -406,22 +567,43 @@ func (h *ChatHandler) subUserPower(userVo vo.User, session *types.ChatSession, p
|
||||
}
|
||||
|
||||
func (h *ChatHandler) saveChatHistory(
|
||||
c *gin.Context,
|
||||
req types.ApiRequest,
|
||||
usage Usage,
|
||||
message types.Message,
|
||||
session *types.ChatSession,
|
||||
role model.ChatRole,
|
||||
input ChatInput,
|
||||
userVo vo.User,
|
||||
promptCreatedAt time.Time,
|
||||
replyCreatedAt time.Time) {
|
||||
|
||||
// 更新上下文消息
|
||||
if h.App.SysConfig.EnableContext {
|
||||
chatCtx := req.Messages // 提问消息
|
||||
chatCtx = append(chatCtx, message) // 回复消息
|
||||
h.ChatContexts.Put(session.ChatId, chatCtx)
|
||||
// 文本审核
|
||||
if h.App.SysConfig.Moderation.Enable {
|
||||
moderationResult, err := h.moderationManager.GetService().Moderate(usage.Content)
|
||||
if err != nil {
|
||||
logger.Error("failed to moderate content: ", err)
|
||||
}
|
||||
logger.Debugf("moderationResult: %+v", moderationResult)
|
||||
if moderationResult.Flagged {
|
||||
// 记录违规内容
|
||||
moderation := model.Moderation{
|
||||
UserId: userVo.Id,
|
||||
Source: types.ModerationSourceChat,
|
||||
Input: usage.Prompt,
|
||||
Output: usage.Content,
|
||||
Result: utils.JsonEncode(moderationResult),
|
||||
}
|
||||
err = h.DB.Create(&moderation).Error
|
||||
if err != nil {
|
||||
logger.Error("failed to save moderation: ", err)
|
||||
}
|
||||
pushMessage(c, ChatEventError, "很抱歉,内容触发敏感词预警,AI 无法回答!!!")
|
||||
// 更新用户算力
|
||||
if input.ChatModel.Power > 0 {
|
||||
h.subUserPower(userVo, input, 0, 0)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 追加聊天记录
|
||||
// for prompt
|
||||
var promptTokens, replyTokens, totalTokens int
|
||||
@@ -432,12 +614,15 @@ func (h *ChatHandler) saveChatHistory(
|
||||
}
|
||||
|
||||
historyUserMsg := model.ChatMessage{
|
||||
UserId: userVo.Id,
|
||||
ChatId: session.ChatId,
|
||||
RoleId: role.Id,
|
||||
Type: types.PromptMsg,
|
||||
Icon: userVo.Avatar,
|
||||
Content: template.HTMLEscapeString(usage.Prompt),
|
||||
UserId: userVo.Id,
|
||||
ChatId: input.ChatId,
|
||||
RoleId: input.RoleId,
|
||||
Type: types.PromptMsg,
|
||||
Icon: userVo.Avatar,
|
||||
Content: utils.JsonEncode(vo.MsgContent{
|
||||
Text: usage.Prompt,
|
||||
Files: input.Files,
|
||||
}),
|
||||
Tokens: promptTokens,
|
||||
TotalTokens: promptTokens,
|
||||
UseContext: true,
|
||||
@@ -460,12 +645,15 @@ func (h *ChatHandler) saveChatHistory(
|
||||
totalTokens = replyTokens + getTotalTokens(req)
|
||||
}
|
||||
historyReplyMsg := model.ChatMessage{
|
||||
UserId: userVo.Id,
|
||||
ChatId: session.ChatId,
|
||||
RoleId: role.Id,
|
||||
Type: types.ReplyMsg,
|
||||
Icon: role.Icon,
|
||||
Content: usage.Content,
|
||||
UserId: userVo.Id,
|
||||
ChatId: input.ChatId,
|
||||
RoleId: input.RoleId,
|
||||
Type: types.ReplyMsg,
|
||||
Icon: input.ChatRole.Icon,
|
||||
Content: utils.JsonEncode(vo.MsgContent{
|
||||
Text: message.Content,
|
||||
Files: input.Files,
|
||||
}),
|
||||
Tokens: replyTokens,
|
||||
TotalTokens: totalTokens,
|
||||
UseContext: true,
|
||||
@@ -478,18 +666,34 @@ func (h *ChatHandler) saveChatHistory(
|
||||
logger.Error("failed to save reply history message: ", err)
|
||||
}
|
||||
|
||||
// 发送完整聊天记录给前端
|
||||
var messageVo vo.ChatMessage
|
||||
err = utils.CopyObject(historyReplyMsg, &messageVo)
|
||||
if err == nil {
|
||||
// 解析内容
|
||||
var content vo.MsgContent
|
||||
err = utils.JsonDecode(historyReplyMsg.Content, &content)
|
||||
if err != nil {
|
||||
content.Text = historyReplyMsg.Content
|
||||
}
|
||||
messageVo.Content = content
|
||||
messageVo.CreatedAt = historyReplyMsg.CreatedAt.Unix()
|
||||
messageVo.UpdatedAt = historyReplyMsg.UpdatedAt.Unix()
|
||||
pushMessage(c, ChatEventComplete, messageVo)
|
||||
}
|
||||
|
||||
// 更新用户算力
|
||||
if session.Model.Power > 0 {
|
||||
h.subUserPower(userVo, session, promptTokens, replyTokens)
|
||||
if input.ChatModel.Power > 0 {
|
||||
h.subUserPower(userVo, input, promptTokens, replyTokens)
|
||||
}
|
||||
// 保存当前会话
|
||||
var chatItem model.ChatItem
|
||||
err = h.DB.Where("chat_id = ?", session.ChatId).First(&chatItem).Error
|
||||
err = h.DB.Where("chat_id = ?", input.ChatId).First(&chatItem).Error
|
||||
if err != nil {
|
||||
chatItem.ChatId = session.ChatId
|
||||
chatItem.ChatId = input.ChatId
|
||||
chatItem.UserId = userVo.Id
|
||||
chatItem.RoleId = role.Id
|
||||
chatItem.ModelId = session.Model.Id
|
||||
chatItem.RoleId = input.RoleId
|
||||
chatItem.ModelId = input.ModelId
|
||||
if utf8.RuneCountInString(usage.Prompt) > 30 {
|
||||
chatItem.Title = string([]rune(usage.Prompt)[:30]) + "..."
|
||||
} else {
|
||||
@@ -503,7 +707,7 @@ func (h *ChatHandler) saveChatHistory(
|
||||
}
|
||||
}
|
||||
|
||||
// 文本生成语音
|
||||
// TextToSpeech 文本生成语音
|
||||
func (h *ChatHandler) TextToSpeech(c *gin.Context) {
|
||||
var data struct {
|
||||
ModelId int `json:"model_id"`
|
||||
@@ -517,13 +721,19 @@ func (h *ChatHandler) TextToSpeech(c *gin.Context) {
|
||||
textHash := utils.Sha256(fmt.Sprintf("%d/%s", data.ModelId, data.Text))
|
||||
audioFile := fmt.Sprintf("%s/audio", h.App.Config.StaticDir)
|
||||
if _, err := os.Stat(audioFile); err != nil {
|
||||
os.MkdirAll(audioFile, 0755)
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(audioFile, 0755); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
audioFile = fmt.Sprintf("%s/%s.mp3", audioFile, textHash)
|
||||
if _, err := os.Stat(audioFile); err == nil {
|
||||
// 设置响应头
|
||||
c.Header("Content-Type", "audio/mpeg")
|
||||
c.Header("Content-Disposition", "attachment; filename=speech.mp3")
|
||||
c.Header("Prompt-Type", "audio/mpeg")
|
||||
c.Header("Prompt-Disposition", "attachment; filename=speech.mp3")
|
||||
c.File(audioFile)
|
||||
return
|
||||
}
|
||||
@@ -587,9 +797,12 @@ func (h *ChatHandler) TextToSpeech(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
c.Header("Content-Type", "audio/mpeg")
|
||||
c.Header("Content-Disposition", "attachment; filename=speech.mp3")
|
||||
c.Header("Prompt-Type", "audio/mpeg")
|
||||
c.Header("Prompt-Disposition", "attachment; filename=speech.mp3")
|
||||
|
||||
// 直接写入完整的音频数据到响应
|
||||
c.Writer.Write(audioBytes)
|
||||
_, err = c.Writer.Write(audioBytes)
|
||||
if err != nil {
|
||||
logger.Error("写入音频数据到响应失败:", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
// List 获取会话列表
|
||||
func (h *ChatHandler) List(c *gin.Context) {
|
||||
logger.Info(h.GetLoginUserId(c))
|
||||
if !h.IsLogin(c) {
|
||||
resp.SUCCESS(c)
|
||||
return
|
||||
@@ -28,7 +29,7 @@ func (h *ChatHandler) List(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
var items = make([]vo.ChatItem, 0)
|
||||
var chats []model.ChatItem
|
||||
h.DB.Where("user_id", userId).Order("id DESC").Find(&chats)
|
||||
h.DB.Debug().Where("user_id", userId).Order("id DESC").Find(&chats)
|
||||
if len(chats) == 0 {
|
||||
resp.SUCCESS(c, items)
|
||||
return
|
||||
@@ -41,9 +42,9 @@ func (h *ChatHandler) List(c *gin.Context) {
|
||||
modelValues = append(modelValues, chat.Model)
|
||||
}
|
||||
|
||||
var roles []model.ChatRole
|
||||
var roles []model.ChatApp
|
||||
var models []model.ChatModel
|
||||
roleMap := make(map[uint]model.ChatRole)
|
||||
roleMap := make(map[uint]model.ChatApp)
|
||||
modelMap := make(map[string]model.ChatModel)
|
||||
h.DB.Where("id IN ?", roleIds).Find(&roles)
|
||||
h.DB.Where("value IN ?", modelValues).Find(&models)
|
||||
@@ -104,8 +105,6 @@ func (h *ChatHandler) Clear(c *gin.Context) {
|
||||
var chatIds = make([]string, 0)
|
||||
for _, chat := range chats {
|
||||
chatIds = append(chatIds, chat.ChatId)
|
||||
// 清空会话上下文
|
||||
h.ChatContexts.Delete(chat.ChatId)
|
||||
}
|
||||
err = h.DB.Transaction(func(tx *gorm.DB) error {
|
||||
res := h.DB.Where("user_id =?", user.Id).Delete(&model.ChatItem{})
|
||||
@@ -133,20 +132,28 @@ func (h *ChatHandler) Clear(c *gin.Context) {
|
||||
func (h *ChatHandler) History(c *gin.Context) {
|
||||
chatId := c.Query("chat_id") // 会话 ID
|
||||
var items []model.ChatMessage
|
||||
var messages = make([]vo.HistoryMessage, 0)
|
||||
var messages = make([]vo.ChatMessage, 0)
|
||||
res := h.DB.Where("chat_id = ?", chatId).Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No history message")
|
||||
return
|
||||
} else {
|
||||
for _, item := range items {
|
||||
var v vo.HistoryMessage
|
||||
var v vo.ChatMessage
|
||||
err := utils.CopyObject(item, &v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// 解析内容
|
||||
var content vo.MsgContent
|
||||
err = utils.JsonDecode(item.Content, &content)
|
||||
if err != nil {
|
||||
content.Text = item.Content
|
||||
}
|
||||
v.Content = content
|
||||
v.CreatedAt = item.CreatedAt.Unix()
|
||||
v.UpdatedAt = item.UpdatedAt.Unix()
|
||||
if err == nil {
|
||||
messages = append(messages, v)
|
||||
}
|
||||
messages = append(messages, v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,10 +186,6 @@ func (h *ChatHandler) Remove(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 是否要删除 MidJourney 绘画记录和图片文件?
|
||||
|
||||
// 清空会话上下文
|
||||
h.ChatContexts.Delete(chatId)
|
||||
resp.SUCCESS(c, types.OkMsg)
|
||||
}
|
||||
|
||||
@@ -202,7 +205,7 @@ func (h *ChatHandler) Detail(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 填充角色名称
|
||||
var role model.ChatRole
|
||||
var role model.ChatApp
|
||||
res = h.DB.Where("id", chatItem.RoleId).First(&role)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Role not found")
|
||||
|
||||
@@ -26,6 +26,12 @@ func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
|
||||
return &ChatModelHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ChatModelHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/model/")
|
||||
group.GET("list", h.List)
|
||||
}
|
||||
|
||||
// List 模型列表
|
||||
func (h *ChatModelHandler) List(c *gin.Context) {
|
||||
var items []model.ChatModel
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
req2 "github.com/imroc/req/v3"
|
||||
)
|
||||
|
||||
@@ -55,18 +56,16 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
req types.ApiRequest,
|
||||
userVo vo.User,
|
||||
ctx context.Context,
|
||||
session *types.ChatSession,
|
||||
role model.ChatRole,
|
||||
prompt string,
|
||||
ws *types.WsClient) error {
|
||||
input ChatInput,
|
||||
c *gin.Context) error {
|
||||
promptCreatedAt := time.Now() // 记录提问时间
|
||||
start := time.Now()
|
||||
var apiKey = model.ApiKey{}
|
||||
response, err := h.doRequest(ctx, req, session, &apiKey)
|
||||
response, err := h.doRequest(ctx, req, input, &apiKey)
|
||||
logger.Info("HTTP请求完成,耗时:", time.Since(start))
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
return fmt.Errorf("用户取消了请求:%s", prompt)
|
||||
return fmt.Errorf("用户取消了请求:%s", input.Prompt)
|
||||
} else if strings.Contains(err.Error(), "no available key") {
|
||||
return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||
}
|
||||
@@ -112,7 +111,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
}
|
||||
|
||||
if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 {
|
||||
utils.SendChunkMsg(ws, "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。")
|
||||
pushMessage(c, "text", "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。")
|
||||
break
|
||||
}
|
||||
|
||||
@@ -140,7 +139,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
if res.Error == nil {
|
||||
toolCall = true
|
||||
callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label)
|
||||
utils.SendChunkMsg(ws, callMsg)
|
||||
pushMessage(c, "text", callMsg)
|
||||
contents = append(contents, callMsg)
|
||||
}
|
||||
continue
|
||||
@@ -163,7 +162,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
reasoning = true
|
||||
}
|
||||
|
||||
utils.SendChunkMsg(ws, reasoningContent)
|
||||
pushMessage(c, "text", reasoningContent)
|
||||
contents = append(contents, reasoningContent)
|
||||
} else if responseBody.Choices[0].Delta.Content != "" {
|
||||
finalContent := responseBody.Choices[0].Delta.Content
|
||||
@@ -172,14 +171,14 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
reasoning = false
|
||||
}
|
||||
contents = append(contents, utils.InterfaceToString(finalContent))
|
||||
utils.SendChunkMsg(ws, finalContent)
|
||||
pushMessage(c, "text", finalContent)
|
||||
}
|
||||
}
|
||||
} // end for
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
logger.Info("用户取消了请求:", prompt)
|
||||
logger.Info("用户取消了请求:", input.Prompt)
|
||||
} else {
|
||||
logger.Error("信息读取出错:", err)
|
||||
}
|
||||
@@ -214,20 +213,20 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
errMsg = utils.InterfaceToString(apiRes.Data)
|
||||
contents = append(contents, errMsg)
|
||||
}
|
||||
utils.SendChunkMsg(ws, errMsg)
|
||||
pushMessage(c, "text", errMsg)
|
||||
}
|
||||
|
||||
// 消息发送成功
|
||||
if len(contents) > 0 {
|
||||
usage := Usage{
|
||||
Prompt: prompt,
|
||||
Prompt: input.Prompt,
|
||||
Content: strings.Join(contents, ""),
|
||||
PromptTokens: 0,
|
||||
CompletionTokens: 0,
|
||||
TotalTokens: 0,
|
||||
}
|
||||
message.Content = usage.Content
|
||||
h.saveChatHistory(req, usage, message, session, role, userVo, promptCreatedAt, replyCreatedAt)
|
||||
h.saveChatHistory(c, req, usage, message, input, userVo, promptCreatedAt, replyCreatedAt)
|
||||
}
|
||||
} else { // 非流式输出
|
||||
var respVo OpenAIResVo
|
||||
@@ -240,13 +239,10 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
return fmt.Errorf("解析响应失败:%v", body)
|
||||
}
|
||||
content := respVo.Choices[0].Message.Content
|
||||
if strings.HasPrefix(req.Model, "o1-") {
|
||||
content = fmt.Sprintf("AI思考结束,耗时:%d 秒。\n%s", time.Now().Unix()-session.Start, respVo.Choices[0].Message.Content)
|
||||
}
|
||||
utils.SendChunkMsg(ws, content)
|
||||
respVo.Usage.Prompt = prompt
|
||||
pushMessage(c, "text", content)
|
||||
respVo.Usage.Prompt = input.Prompt
|
||||
respVo.Usage.Content = content
|
||||
h.saveChatHistory(req, respVo.Usage, respVo.Choices[0].Message, session, role, userVo, promptCreatedAt, time.Now())
|
||||
h.saveChatHistory(c, req, respVo.Usage, respVo.Choices[0].Message, input, userVo, promptCreatedAt, time.Now())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -27,18 +27,27 @@ func NewConfigHandler(app *core.AppServer, db *gorm.DB, licenseService *service.
|
||||
return &ConfigHandler{BaseHandler: BaseHandler{App: app, DB: db}, licenseService: licenseService}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ConfigHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/config/")
|
||||
|
||||
// 无需授权的接口
|
||||
group.GET("get", h.Get)
|
||||
group.GET("license", h.License)
|
||||
}
|
||||
|
||||
// Get 获取指定的系统配置
|
||||
func (h *ConfigHandler) Get(c *gin.Context) {
|
||||
key := c.Query("key")
|
||||
var config model.Config
|
||||
res := h.DB.Where("marker", key).First(&config)
|
||||
res := h.DB.Where("name", key).First(&config)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var value map[string]interface{}
|
||||
err := utils.JsonDecode(config.Config, &value)
|
||||
var value map[string]any
|
||||
err := utils.JsonDecode(config.Value, &value)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
|
||||
@@ -10,9 +10,11 @@ package handler
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/dalle"
|
||||
"geekai/service/moderation"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
@@ -25,16 +27,18 @@ import (
|
||||
|
||||
type DallJobHandler struct {
|
||||
BaseHandler
|
||||
dallService *dalle.Service
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
dallService *dalle.Service
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
moderationManager *moderation.ServiceManager
|
||||
}
|
||||
|
||||
func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service, manager *oss.UploaderManager, userService *service.UserService) *DallJobHandler {
|
||||
func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service, manager *oss.UploaderManager, userService *service.UserService, moderationManager *moderation.ServiceManager) *DallJobHandler {
|
||||
return &DallJobHandler{
|
||||
dallService: service,
|
||||
uploader: manager,
|
||||
userService: userService,
|
||||
dallService: service,
|
||||
uploader: manager,
|
||||
userService: userService,
|
||||
moderationManager: moderationManager,
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
@@ -42,6 +46,24 @@ func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *DallJobHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/dall/")
|
||||
|
||||
// 公开接口,不需要授权
|
||||
group.GET("imgWall", h.ImgWall)
|
||||
group.GET("models", h.GetModels)
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("image", h.Image)
|
||||
group.GET("jobs", h.JobList)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
}
|
||||
}
|
||||
|
||||
// Image 创建一个绘画任务
|
||||
func (h *DallJobHandler) Image(c *gin.Context) {
|
||||
var data types.DallTask
|
||||
@@ -50,6 +72,29 @@ func (h *DallJobHandler) Image(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 文本审核
|
||||
if h.App.SysConfig.Moderation.Enable {
|
||||
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
|
||||
if err != nil {
|
||||
logger.Error("failed to moderate content: ", err)
|
||||
}
|
||||
if moderationResult.Flagged {
|
||||
// 记录违规内容
|
||||
moderation := model.Moderation{
|
||||
UserId: h.GetLoginUserId(c),
|
||||
Source: types.ModerationSourceDalle,
|
||||
Input: data.Prompt,
|
||||
Result: utils.JsonEncode(moderationResult),
|
||||
}
|
||||
err = h.DB.Create(&moderation).Error
|
||||
if err != nil {
|
||||
logger.Error("failed to save moderation: ", err)
|
||||
}
|
||||
resp.ERROR(c, "当前创作内容包含敏感词,提示词未通过文本审核,请重新输入!")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var chatModel model.ChatModel
|
||||
if res := h.DB.Where("id = ?", data.ModelId).First(&chatModel); res.Error != nil {
|
||||
resp.ERROR(c, "模型不存在")
|
||||
@@ -73,11 +118,12 @@ func (h *DallJobHandler) Image(c *gin.Context) {
|
||||
UserId: uint(userId),
|
||||
ModelId: chatModel.Id,
|
||||
ModelName: chatModel.Value,
|
||||
Image: data.Image,
|
||||
Prompt: data.Prompt,
|
||||
Quality: data.Quality,
|
||||
Size: data.Size,
|
||||
Style: data.Style,
|
||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
||||
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
||||
Power: chatModel.Power,
|
||||
}
|
||||
job := model.DallJob{
|
||||
@@ -96,7 +142,7 @@ func (h *DallJobHandler) Image(c *gin.Context) {
|
||||
h.dallService.PushTask(task)
|
||||
|
||||
// 扣减算力
|
||||
err = h.userService.DecreasePower(int(user.Id), chatModel.Power, model.PowerLog{
|
||||
err = h.userService.DecreasePower(user.Id, chatModel.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: chatModel.Value,
|
||||
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(task.Prompt, 10)),
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/crawler"
|
||||
"geekai/service/dalle"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
@@ -31,7 +30,6 @@ import (
|
||||
|
||||
type FunctionHandler struct {
|
||||
BaseHandler
|
||||
config types.ApiConfig
|
||||
uploadManager *oss.UploaderManager
|
||||
dallService *dalle.Service
|
||||
userService *service.UserService
|
||||
@@ -49,13 +47,23 @@ func NewFunctionHandler(
|
||||
App: server,
|
||||
DB: db,
|
||||
},
|
||||
config: config.ApiConfig,
|
||||
uploadManager: manager,
|
||||
dallService: dallService,
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *FunctionHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/function/")
|
||||
group.GET("list", h.List)
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.POST("weibo", h.WeiBo)
|
||||
group.POST("zaobao", h.ZaoBao)
|
||||
group.POST("dalle3", h.Dall3)
|
||||
}
|
||||
|
||||
type resVo struct {
|
||||
Code types.BizCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
@@ -107,16 +115,10 @@ func (h *FunctionHandler) WeiBo(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if h.config.Token == "" {
|
||||
resp.ERROR(c, "无效的 API Token")
|
||||
return
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/weibo/fetch", h.config.ApiURL)
|
||||
url := fmt.Sprintf("%s/api/weibo/fetch", types.GeekAPIURL)
|
||||
var res resVo
|
||||
r, err := req.C().R().
|
||||
SetHeader("AppId", h.config.AppId).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.config.Token)).
|
||||
SetHeader("Authorization", "Bearer geekai-plus").
|
||||
SetSuccessResult(&res).Get(url)
|
||||
if err != nil {
|
||||
resp.ERROR(c, fmt.Sprintf("%v", err))
|
||||
@@ -146,16 +148,10 @@ func (h *FunctionHandler) ZaoBao(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if h.config.Token == "" {
|
||||
resp.ERROR(c, "无效的 API Token")
|
||||
return
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/zaobao/fetch", h.config.ApiURL)
|
||||
url := fmt.Sprintf("%s/api/zaobao/fetch", types.GeekAPIURL)
|
||||
var res resVo
|
||||
r, err := req.C().R().
|
||||
SetHeader("AppId", h.config.AppId).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.config.Token)).
|
||||
SetHeader("Authorization", "Bearer geekai-plus").
|
||||
SetSuccessResult(&res).Get(url)
|
||||
if err != nil {
|
||||
resp.ERROR(c, fmt.Sprintf("%v", err))
|
||||
@@ -193,16 +189,23 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var chatModel model.ChatModel
|
||||
res := h.DB.Where("type = ?", "img").Where("enabled", true).First(&chatModel)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "没有找到可用的AI绘图模型!")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debugf("绘画参数:%+v", params)
|
||||
var user model.User
|
||||
res := h.DB.Where("id = ?", params["user_id"]).First(&user)
|
||||
res = h.DB.Where("id = ?", params["user_id"]).First(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "当前用户不存在!")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Power < h.App.SysConfig.DallPower {
|
||||
resp.ERROR(c, "创建 DALL-E 绘图任务失败,算力不足")
|
||||
if user.Power < chatModel.Power {
|
||||
resp.ERROR(c, "创建绘图任务失败,算力不足")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -211,24 +214,24 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
|
||||
task := types.DallTask{
|
||||
UserId: user.Id,
|
||||
Prompt: prompt,
|
||||
ModelId: 0,
|
||||
ModelName: "dall-e-3",
|
||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
||||
ModelId: chatModel.Id,
|
||||
ModelName: chatModel.Value,
|
||||
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
||||
N: 1,
|
||||
Quality: "standard",
|
||||
Size: "1024x1024",
|
||||
Style: "vivid",
|
||||
Power: h.App.SysConfig.DallPower,
|
||||
Power: chatModel.Power,
|
||||
}
|
||||
job := model.DallJob{
|
||||
UserId: user.Id,
|
||||
Prompt: prompt,
|
||||
Power: h.App.SysConfig.DallPower,
|
||||
Power: chatModel.Power,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
}
|
||||
err := h.DB.Create(&job).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "创建 DALL-E 绘图任务失败:"+err.Error())
|
||||
resp.ERROR(c, "创建绘图任务失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -240,7 +243,7 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 扣减算力
|
||||
err = h.userService.DecreasePower(int(user.Id), job.Power, model.PowerLog{
|
||||
err = h.userService.DecreasePower(user.Id, job.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: task.ModelName,
|
||||
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(job.Prompt, 10)),
|
||||
@@ -253,76 +256,6 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
|
||||
resp.SUCCESS(c, content)
|
||||
}
|
||||
|
||||
// 实现一个联网搜索的函数工具,采用爬虫实现
|
||||
func (h *FunctionHandler) WebSearch(c *gin.Context) {
|
||||
if err := h.checkAuth(c); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var params map[string]interface{}
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// 从参数中获取搜索关键词
|
||||
keyword, ok := params["keyword"].(string)
|
||||
if !ok || keyword == "" {
|
||||
resp.ERROR(c, "搜索关键词不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 从参数中获取最大页数,默认为1页
|
||||
maxPages := 1
|
||||
if pages, ok := params["max_pages"].(float64); ok {
|
||||
maxPages = int(pages)
|
||||
}
|
||||
|
||||
// 获取用户ID
|
||||
userID, ok := params["user_id"].(float64)
|
||||
if !ok {
|
||||
resp.ERROR(c, "用户ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 查询用户信息
|
||||
var user model.User
|
||||
res := h.DB.Where("id = ?", int(userID)).First(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户算力是否足够
|
||||
searchPower := 1 // 每次搜索消耗1点算力
|
||||
if user.Power < searchPower {
|
||||
resp.ERROR(c, "算力不足,无法执行网络搜索")
|
||||
return
|
||||
}
|
||||
|
||||
// 执行网络搜索
|
||||
searchResults, err := crawler.SearchWeb(keyword, maxPages)
|
||||
if err != nil {
|
||||
resp.ERROR(c, fmt.Sprintf("搜索失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 扣减用户算力
|
||||
err = h.userService.DecreasePower(int(user.Id), searchPower, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "web_search",
|
||||
Remark: fmt.Sprintf("网络搜索:%s", utils.CutWords(keyword, 10)),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, "扣减算力失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 返回搜索结果
|
||||
resp.SUCCESS(c, searchResults)
|
||||
}
|
||||
|
||||
// List 获取所有的工具函数列表
|
||||
func (h *FunctionHandler) List(c *gin.Context) {
|
||||
var items []model.Function
|
||||
|
||||
+134
-14
@@ -8,14 +8,18 @@ package handler
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// InviteHandler 用户邀请
|
||||
@@ -27,6 +31,23 @@ func NewInviteHandler(app *core.AppServer, db *gorm.DB) *InviteHandler {
|
||||
return &InviteHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *InviteHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/invite/")
|
||||
|
||||
// 公开接口,不需要授权
|
||||
group.GET("hits", h.Hits)
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("code", h.Code)
|
||||
group.GET("list", h.List)
|
||||
group.GET("stats", h.Stats)
|
||||
group.GET("rules", h.Rules)
|
||||
}
|
||||
}
|
||||
|
||||
// Code 获取当前用户邀请码
|
||||
func (h *InviteHandler) Code(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
@@ -65,21 +86,34 @@ func (h *InviteHandler) List(c *gin.Context) {
|
||||
var total int64
|
||||
session.Model(&model.InviteLog{}).Count(&total)
|
||||
var items []model.InviteLog
|
||||
var list = make([]vo.InviteLog, 0)
|
||||
offset := (page - 1) * pageSize
|
||||
res := session.Order("id DESC").Offset(offset).Limit(pageSize).Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var v vo.InviteLog
|
||||
err := utils.CopyObject(item, &v)
|
||||
if err == nil {
|
||||
v.Id = item.Id
|
||||
v.CreatedAt = item.CreatedAt.Unix()
|
||||
list = append(list, v)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
err := session.Order("id DESC").Offset(offset).Limit(pageSize).Find(&items).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
userIds := make([]uint, 0)
|
||||
for _, item := range items {
|
||||
userIds = append(userIds, item.UserId)
|
||||
}
|
||||
userMap := make(map[uint]model.User)
|
||||
var users []model.User
|
||||
h.DB.Model(&model.User{}).Where("id IN (?)", userIds).Find(&users)
|
||||
for _, user := range users {
|
||||
userMap[user.Id] = user
|
||||
}
|
||||
|
||||
var list = make([]vo.InviteLog, 0)
|
||||
for _, item := range items {
|
||||
var v vo.InviteLog
|
||||
err := utils.CopyObject(item, &v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
v.CreatedAt = item.CreatedAt.Unix()
|
||||
v.Avatar = userMap[item.UserId].Avatar
|
||||
list = append(list, v)
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, list))
|
||||
}
|
||||
@@ -90,3 +124,89 @@ func (h *InviteHandler) Hits(c *gin.Context) {
|
||||
h.DB.Model(&model.InviteCode{}).Where("code = ?", code).UpdateColumn("hits", gorm.Expr("hits + ?", 1))
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Stats 获取邀请统计
|
||||
func (h *InviteHandler) Stats(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
|
||||
// 获取邀请码
|
||||
var inviteCode model.InviteCode
|
||||
res := h.DB.Where("user_id = ?", userId).First(&inviteCode)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "邀请码不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 统计累计邀请数
|
||||
var totalInvite int64
|
||||
h.DB.Model(&model.InviteLog{}).Where("inviter_id = ?", userId).Count(&totalInvite)
|
||||
|
||||
// 统计今日邀请数
|
||||
today := time.Now().Format("2006-01-02")
|
||||
var todayInvite int64
|
||||
h.DB.Model(&model.InviteLog{}).Where("inviter_id = ? AND DATE(created_at) = ?", userId, today).Count(&todayInvite)
|
||||
|
||||
// 获取系统配置中的邀请奖励
|
||||
var config model.Config
|
||||
var invitePower int = 200 // 默认值
|
||||
if h.DB.Where("name = ?", "system").First(&config).Error == nil {
|
||||
var configMap map[string]any
|
||||
if utils.JsonDecode(config.Value, &configMap) == nil {
|
||||
if power, ok := configMap["invite_power"].(float64); ok {
|
||||
invitePower = int(power)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算获得奖励总数
|
||||
rewardTotal := int(totalInvite) * invitePower
|
||||
|
||||
// 构建邀请链接
|
||||
inviteLink := fmt.Sprintf("%s/register?invite=%s", h.App.Config.StaticUrl, inviteCode.Code)
|
||||
|
||||
stats := vo.InviteStats{
|
||||
InviteCount: int(totalInvite),
|
||||
RewardTotal: rewardTotal,
|
||||
TodayInvite: int(todayInvite),
|
||||
InviteCode: inviteCode.Code,
|
||||
InviteLink: inviteLink,
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, stats)
|
||||
}
|
||||
|
||||
// Rules 获取奖励规则
|
||||
func (h *InviteHandler) Rules(c *gin.Context) {
|
||||
// 获取系统配置中的邀请奖励
|
||||
var config model.Config
|
||||
var invitePower int = 200 // 默认值
|
||||
if h.DB.Where("name = ?", "system").First(&config).Error == nil {
|
||||
var configMap map[string]interface{}
|
||||
if utils.JsonDecode(config.Value, &configMap) == nil {
|
||||
if power, ok := configMap["invite_power"].(float64); ok {
|
||||
invitePower = int(power)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rules := []vo.RewardRule{
|
||||
{
|
||||
Id: 1,
|
||||
Title: "好友注册",
|
||||
Desc: "好友通过邀请链接成功注册",
|
||||
Icon: "icon-user-fill",
|
||||
Color: "#1989fa",
|
||||
Reward: invitePower,
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
Title: "好友首次充值",
|
||||
Desc: "好友首次充值任意金额",
|
||||
Icon: "icon-money",
|
||||
Color: "#07c160",
|
||||
Reward: invitePower * 2, // 假设首次充值奖励是注册奖励的2倍
|
||||
},
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, rules)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/jimeng"
|
||||
"geekai/service/moderation"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// JimengHandler 即梦AI处理器
|
||||
type JimengHandler struct {
|
||||
BaseHandler
|
||||
jimengService *jimeng.Service
|
||||
userService *service.UserService
|
||||
moderationManager *moderation.ServiceManager
|
||||
}
|
||||
|
||||
// NewJimengHandler 创建即梦AI处理器
|
||||
func NewJimengHandler(app *core.AppServer, jimengService *jimeng.Service, db *gorm.DB, userService *service.UserService, moderationManager *moderation.ServiceManager) *JimengHandler {
|
||||
return &JimengHandler{
|
||||
BaseHandler: BaseHandler{App: app, DB: db},
|
||||
jimengService: jimengService,
|
||||
userService: userService,
|
||||
moderationManager: moderationManager,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由,新增统一任务接口
|
||||
func (h *JimengHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/jimeng/")
|
||||
group.GET("power-config", h.GetPowerConfig)
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("task", h.CreateTask)
|
||||
group.POST("jobs", h.Jobs)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("retry", h.Retry)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTask 统一任务创建接口
|
||||
func (h *JimengHandler) CreateTask(c *gin.Context) {
|
||||
var req types.JimengTaskRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// 文本审核
|
||||
if h.App.SysConfig.Moderation.Enable && req.Prompt != "" {
|
||||
moderationResult, err := h.moderationManager.GetService().Moderate(req.Prompt)
|
||||
if err != nil {
|
||||
logger.Error("failed to moderate content: ", err)
|
||||
}
|
||||
if moderationResult.Flagged {
|
||||
// 记录违规内容
|
||||
moderation := model.Moderation{
|
||||
UserId: h.GetLoginUserId(c),
|
||||
Source: types.ModerationSourceJiMeng,
|
||||
Input: req.Prompt,
|
||||
Result: utils.JsonEncode(moderationResult),
|
||||
}
|
||||
err = h.DB.Create(&moderation).Error
|
||||
if err != nil {
|
||||
logger.Error("failed to save moderation: ", err)
|
||||
}
|
||||
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if req.Prompt == "" && len(req.ImageUrls) == 0 {
|
||||
resp.ERROR(c, "提示词和图片不能同时为空")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取算力消耗
|
||||
powerCost, err := h.getTaskPower(req)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "计算任务消耗积分失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if user.Power < powerCost {
|
||||
resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost))
|
||||
return
|
||||
}
|
||||
req.Power = powerCost
|
||||
|
||||
job, err := h.jimengService.CreateTask(user.Id, &req)
|
||||
if err != nil {
|
||||
logger.Errorf("create jimeng task failed: %v", err)
|
||||
resp.ERROR(c, "创建任务失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.userService.DecreasePower(user.Id, powerCost, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: job.ReqKey,
|
||||
Remark: h.getTaskRemark(req, job.Id),
|
||||
})
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *JimengHandler) getTaskRemark(req types.JimengTaskRequest, jobId uint) string {
|
||||
remark := fmt.Sprintf("即梦任务%s,任务ID:%d", req.ReqKey, jobId)
|
||||
perUnit, ok := h.App.SysConfig.Jimeng.Powers[req.ReqKey]
|
||||
if !ok || perUnit <= 0 {
|
||||
return remark // Fallback if power not found or invalid
|
||||
}
|
||||
switch req.TaskType {
|
||||
case types.JMTaskTypeImage:
|
||||
remark = fmt.Sprintf("即梦图片生成,任务ID:%d,%d积分/张", jobId, perUnit)
|
||||
case types.JMTaskTypeVideo:
|
||||
seconds := 0
|
||||
if perUnit > 0 {
|
||||
seconds = req.Power / perUnit
|
||||
}
|
||||
remark = fmt.Sprintf("即梦视频生成,任务ID:%d,%d积分/秒, %d秒", jobId, perUnit, seconds)
|
||||
case types.JMTaskTypeVirtualHuman:
|
||||
seconds := 0
|
||||
if perUnit > 0 {
|
||||
seconds = req.Power / perUnit
|
||||
}
|
||||
remark = fmt.Sprintf("即梦数字人视频生成,任务ID:%d,%d积分/秒, %d秒", jobId, perUnit, seconds)
|
||||
case types.JMTaskTypeActionTransfer:
|
||||
seconds := 0
|
||||
if perUnit > 0 {
|
||||
seconds = req.Power / perUnit
|
||||
}
|
||||
remark = fmt.Sprintf("即梦视频动作迁移,任务ID:%d,%d积分/秒, %d秒", jobId, perUnit, seconds)
|
||||
}
|
||||
return remark
|
||||
}
|
||||
|
||||
// Jobs 获取任务列表
|
||||
func (h *JimengHandler) Jobs(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
|
||||
var req struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Filter string `json:"filter"`
|
||||
Ids []uint `json:"ids"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var jobs []model.JimengJob
|
||||
var total int64
|
||||
query := h.DB.Model(&model.JimengJob{}).Where("user_id = ?", userId)
|
||||
|
||||
switch req.Filter {
|
||||
case "image":
|
||||
query = query.Where("type = ?", types.JMTaskTypeImage)
|
||||
case "video":
|
||||
query = query.Where("type = ?", types.JMTaskTypeVideo)
|
||||
case "virtual_human":
|
||||
query = query.Where("type = ?", types.JMTaskTypeVirtualHuman)
|
||||
case "action_transfer":
|
||||
query = query.Where("type = ?", types.JMTaskTypeActionTransfer)
|
||||
}
|
||||
|
||||
if len(req.Ids) > 0 {
|
||||
query = query.Where("id IN (?)", req.Ids)
|
||||
}
|
||||
|
||||
// 统计总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
if err := query.Order("updated_at DESC").Offset(offset).Limit(req.PageSize).Find(&jobs).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 填充 VO
|
||||
var jobVos []vo.JimengJob
|
||||
for _, job := range jobs {
|
||||
var jobVo vo.JimengJob
|
||||
err := utils.CopyObject(job, &jobVo)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
jobVo.CreatedAt = job.CreatedAt.Unix()
|
||||
jobVos = append(jobVos, jobVo)
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, req.Page, req.PageSize, jobVos))
|
||||
}
|
||||
|
||||
// Remove 删除任务
|
||||
func (h *JimengHandler) Remove(c *gin.Context) {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
jobId := h.GetInt(c, "id", 0)
|
||||
if jobId == 0 {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取任务,判断状态
|
||||
job, err := h.jimengService.GetJob(uint(jobId))
|
||||
if err != nil {
|
||||
resp.ERROR(c, "任务不存在")
|
||||
return
|
||||
}
|
||||
if job.UserId != user.Id {
|
||||
resp.ERROR(c, "无权限操作")
|
||||
return
|
||||
}
|
||||
|
||||
// 正在运行中的任务不能删除
|
||||
if job.Status == types.JMTaskStatusGenerating || job.Status == types.JMTaskStatusInQueue {
|
||||
resp.ERROR(c, "正在运行中的任务不能删除,否则无法退回算力")
|
||||
return
|
||||
}
|
||||
|
||||
tx := h.DB.Begin()
|
||||
if err := tx.Where("id = ? AND user_id = ?", jobId, user.Id).Delete(&model.JimengJob{}).Error; err != nil {
|
||||
logger.Errorf("delete jimeng job failed: %v", err)
|
||||
resp.ERROR(c, "删除任务失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 失败任务删除后退回算力
|
||||
if job.Status == types.JMTaskStatusFailed {
|
||||
logger.Infof("delete jimeng job failed, refund power: %d", job.Power)
|
||||
err = h.userService.IncreasePower(user.Id, job.Power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: job.ReqKey,
|
||||
Remark: fmt.Sprintf("删除任务,退回%d算力", job.Power),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, "退回算力失败")
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
resp.SUCCESS(c, gin.H{})
|
||||
}
|
||||
|
||||
// Retry 重试任务
|
||||
func (h *JimengHandler) Retry(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
|
||||
jobId := h.GetInt(c, "id", 0)
|
||||
if jobId == 0 {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查任务是否存在且属于当前用户
|
||||
job, err := h.jimengService.GetJob(uint(jobId))
|
||||
if err != nil {
|
||||
resp.ERROR(c, "任务不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if job.UserId != userId {
|
||||
resp.ERROR(c, "无权限操作")
|
||||
return
|
||||
}
|
||||
|
||||
// 只有失败的任务才能重试
|
||||
if job.Status != types.JMTaskStatusFailed {
|
||||
resp.ERROR(c, "只有失败的任务才能重试")
|
||||
return
|
||||
}
|
||||
|
||||
// 重置任务状态
|
||||
if err := h.jimengService.UpdateJobStatus(uint(jobId), types.JMTaskStatusInQueue, ""); err != nil {
|
||||
logger.Errorf("reset job status failed: %v", err)
|
||||
resp.ERROR(c, "重置任务状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 重新推送到队列
|
||||
if err := h.jimengService.PushTaskToQueue(uint(jobId)); err != nil {
|
||||
logger.Errorf("push retry task to queue failed: %v", err)
|
||||
resp.ERROR(c, "推送重试任务失败")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{"message": "重试任务已提交"})
|
||||
}
|
||||
|
||||
func (h *JimengHandler) getTaskPower(req types.JimengTaskRequest) (int, error) {
|
||||
logger.Debugf("getTaskPower req: %+v", req)
|
||||
config := h.App.SysConfig.Jimeng
|
||||
basePower, ok := config.Powers[req.ReqKey]
|
||||
if !ok || basePower <= 0 {
|
||||
return 0, errors.New("未配置模型积分或配置不合法")
|
||||
}
|
||||
switch req.TaskType {
|
||||
case types.JMTaskTypeImage:
|
||||
return basePower, nil
|
||||
case types.JMTaskTypeVideo:
|
||||
if req.Duration == 0 {
|
||||
return 0, errors.New("视频时长不能为0")
|
||||
}
|
||||
return basePower * req.Duration, nil
|
||||
case types.JMTaskTypeVirtualHuman:
|
||||
if req.AudioURL == "" {
|
||||
return 0, errors.New("音频URL不能为空")
|
||||
}
|
||||
audioDuration, err := utils.AudioDurationFromURL(req.AudioURL)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
seconds := int(audioDuration.Seconds())
|
||||
if seconds <= 0 {
|
||||
return 0, errors.New("音频时长无效")
|
||||
}
|
||||
return basePower * seconds, nil
|
||||
case types.JMTaskTypeActionTransfer:
|
||||
if req.VideoURL == "" {
|
||||
return 0, errors.New("视频URL不能为空")
|
||||
}
|
||||
videoDuration, err := utils.VideoDurationMP4FromURL(req.VideoURL)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
seconds := int(videoDuration.Seconds())
|
||||
if seconds <= 0 {
|
||||
return 0, errors.New("视频时长无效")
|
||||
}
|
||||
return basePower * seconds, nil
|
||||
default:
|
||||
return 0, errors.New("任务类型不支持")
|
||||
}
|
||||
}
|
||||
|
||||
// GetPowerConfig 获取即梦各任务类型算力消耗配置
|
||||
func (h *JimengHandler) GetPowerConfig(c *gin.Context) {
|
||||
config := h.App.SysConfig.Jimeng
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"powers": config.Powers,
|
||||
})
|
||||
}
|
||||
@@ -10,11 +10,13 @@ package handler
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -34,6 +36,17 @@ func NewMarkMapHandler(app *core.AppServer, db *gorm.DB, userService *service.Us
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *MarkMapHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/markMap/")
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("gen", h.Generate)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成思维导图
|
||||
func (h *MarkMapHandler) Generate(c *gin.Context) {
|
||||
var data struct {
|
||||
@@ -95,7 +108,7 @@ func (h *MarkMapHandler) Generate(c *gin.Context) {
|
||||
|
||||
// 扣减算力
|
||||
if chatModel.Power > 0 {
|
||||
err = h.userService.DecreasePower(int(userId), chatModel.Power, model.PowerLog{
|
||||
err = h.userService.DecreasePower(userId, chatModel.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: chatModel.Value,
|
||||
Remark: fmt.Sprintf("AI绘制思维导图,模型名称:%s, ", chatModel.Value),
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -25,6 +26,12 @@ func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler {
|
||||
return &MenuHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *MenuHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/menu/")
|
||||
group.GET("list", h.List)
|
||||
}
|
||||
|
||||
// List 数据列表
|
||||
func (h *MenuHandler) List(c *gin.Context) {
|
||||
index := h.GetBool(c, "index")
|
||||
@@ -33,7 +40,7 @@ func (h *MenuHandler) List(c *gin.Context) {
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
session = session.Where("enabled", true)
|
||||
if index {
|
||||
session = session.Where("id IN ?", h.App.SysConfig.IndexNavs)
|
||||
session = session.Where("id IN ?", h.App.SysConfig.Base.IndexNavs)
|
||||
}
|
||||
res := session.Order("sort_num ASC").Find(&items)
|
||||
if res.Error == nil {
|
||||
|
||||
+67
-21
@@ -10,9 +10,11 @@ package handler
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/mj"
|
||||
"geekai/service/moderation"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
@@ -27,18 +29,20 @@ import (
|
||||
|
||||
type MidJourneyHandler struct {
|
||||
BaseHandler
|
||||
mjService *mj.Service
|
||||
snowflake *service.Snowflake
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
mjService *mj.Service
|
||||
snowflake *service.Snowflake
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
moderationManager *moderation.ServiceManager
|
||||
}
|
||||
|
||||
func NewMidJourneyHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, service *mj.Service, manager *oss.UploaderManager, userService *service.UserService) *MidJourneyHandler {
|
||||
func NewMidJourneyHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, service *mj.Service, manager *oss.UploaderManager, userService *service.UserService, moderationManager *moderation.ServiceManager) *MidJourneyHandler {
|
||||
return &MidJourneyHandler{
|
||||
snowflake: snowflake,
|
||||
mjService: service,
|
||||
uploader: manager,
|
||||
userService: userService,
|
||||
snowflake: snowflake,
|
||||
mjService: service,
|
||||
uploader: manager,
|
||||
userService: userService,
|
||||
moderationManager: moderationManager,
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
@@ -46,6 +50,25 @@ func NewMidJourneyHandler(app *core.AppServer, db *gorm.DB, snowflake *service.S
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *MidJourneyHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/mj/")
|
||||
|
||||
// 公开接口,不需要授权
|
||||
group.GET("imgWall", h.ImgWall)
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("image", h.Image)
|
||||
group.POST("upscale", h.Upscale)
|
||||
group.POST("variation", h.Variation)
|
||||
group.GET("jobs", h.JobList)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *MidJourneyHandler) preCheck(c *gin.Context) bool {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
@@ -53,7 +76,7 @@ func (h *MidJourneyHandler) preCheck(c *gin.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if user.Power < h.App.SysConfig.MjPower {
|
||||
if user.Power < h.App.SysConfig.Base.MjPower {
|
||||
resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
|
||||
return false
|
||||
}
|
||||
@@ -90,6 +113,29 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 文本审核
|
||||
if h.App.SysConfig.Moderation.Enable {
|
||||
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
|
||||
if err != nil {
|
||||
logger.Error("failed to moderate content: ", err)
|
||||
}
|
||||
if moderationResult.Flagged {
|
||||
// 记录违规内容
|
||||
moderation := model.Moderation{
|
||||
UserId: h.GetLoginUserId(c),
|
||||
Source: types.ModerationSourceMJ,
|
||||
Input: data.Prompt,
|
||||
Result: utils.JsonEncode(moderationResult),
|
||||
}
|
||||
err = h.DB.Create(&moderation).Error
|
||||
if err != nil {
|
||||
logger.Error("failed to save moderation: ", err)
|
||||
}
|
||||
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var params = ""
|
||||
if data.Rate != "" && !strings.Contains(params, "--ar") {
|
||||
params += " --ar " + data.Rate
|
||||
@@ -159,17 +205,17 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
|
||||
Params: params,
|
||||
UserId: userId,
|
||||
ImgArr: data.ImgArr,
|
||||
Mode: h.App.SysConfig.MjMode,
|
||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
||||
Mode: h.App.SysConfig.Base.MjMode,
|
||||
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
||||
}
|
||||
job := model.MidJourneyJob{
|
||||
Type: data.TaskType,
|
||||
UserId: userId,
|
||||
UserId: uint(userId),
|
||||
TaskId: taskId,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
Progress: 0,
|
||||
Prompt: fmt.Sprintf("%s %s", data.Prompt, params),
|
||||
Power: h.App.SysConfig.MjPower,
|
||||
Power: h.App.SysConfig.Base.MjPower,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
opt := "绘图"
|
||||
@@ -232,15 +278,15 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) {
|
||||
Index: data.Index,
|
||||
MessageId: data.MessageId,
|
||||
MessageHash: data.MessageHash,
|
||||
Mode: h.App.SysConfig.MjMode,
|
||||
Mode: h.App.SysConfig.Base.MjMode,
|
||||
}
|
||||
job := model.MidJourneyJob{
|
||||
Type: types.TaskUpscale.String(),
|
||||
UserId: userId,
|
||||
UserId: uint(userId),
|
||||
TaskId: taskId,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
Progress: 0,
|
||||
Power: h.App.SysConfig.MjActionPower,
|
||||
Power: h.App.SysConfig.Base.MjActionPower,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 {
|
||||
@@ -287,16 +333,16 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
|
||||
ChannelId: data.ChannelId,
|
||||
MessageId: data.MessageId,
|
||||
MessageHash: data.MessageHash,
|
||||
Mode: h.App.SysConfig.MjMode,
|
||||
Mode: h.App.SysConfig.Base.MjMode,
|
||||
}
|
||||
job := model.MidJourneyJob{
|
||||
Type: types.TaskVariation.String(),
|
||||
ChannelId: data.ChannelId,
|
||||
UserId: userId,
|
||||
UserId: uint(userId),
|
||||
TaskId: taskId,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
Progress: 0,
|
||||
Power: h.App.SysConfig.MjActionPower,
|
||||
Power: h.App.SysConfig.Base.MjActionPower,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 {
|
||||
@@ -422,7 +468,7 @@ func (h *MidJourneyHandler) Publish(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享
|
||||
err := h.DB.Model(&model.MidJourneyJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action).Error
|
||||
err := h.DB.Model(&model.MidJourneyJob{Id: uint(id), UserId: uint(userId)}).UpdateColumn("publish", action).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
|
||||
@@ -9,17 +9,19 @@ package handler
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type NetHandler struct {
|
||||
@@ -31,6 +33,22 @@ func NewNetHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManage
|
||||
return &NetHandler{BaseHandler: BaseHandler{App: app, DB: db}, uploaderManager: manager}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *NetHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/upload")
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("", h.Upload)
|
||||
group.POST("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
}
|
||||
|
||||
// 公开接口,不需要授权
|
||||
h.App.Engine.GET("/api/download", h.Download)
|
||||
}
|
||||
|
||||
func (h *NetHandler) Upload(c *gin.Context) {
|
||||
file, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
|
||||
if err != nil {
|
||||
@@ -46,7 +64,7 @@ func (h *NetHandler) Upload(c *gin.Context) {
|
||||
|
||||
userId := h.GetLoginUserId(c)
|
||||
res := h.DB.Create(&model.File{
|
||||
UserId: int(userId),
|
||||
UserId: uint(userId),
|
||||
Name: file.Name,
|
||||
ObjKey: file.ObjKey,
|
||||
URL: file.URL,
|
||||
@@ -143,7 +161,15 @@ func (h *NetHandler) Download(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
// 使用http.Get下载文件
|
||||
r, err := http.Get(fileUrl)
|
||||
req, err := http.NewRequest("GET", fileUrl, nil)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 模拟浏览器 UA
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
|
||||
client := &http.Client{}
|
||||
r, err := client.Do(req)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
@@ -156,6 +182,5 @@ func (h *NetHandler) Download(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.Status(http.StatusOK)
|
||||
// 将下载的文件内容写入响应
|
||||
_, _ = io.Copy(c.Writer, r.Body)
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ package handler
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -28,6 +28,18 @@ func NewOrderHandler(app *core.AppServer, db *gorm.DB) *OrderHandler {
|
||||
return &OrderHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *OrderHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/order/")
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("list", h.List)
|
||||
group.GET("query", h.Query)
|
||||
}
|
||||
}
|
||||
|
||||
// List 订单列表
|
||||
func (h *OrderHandler) List(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 1)
|
||||
@@ -48,20 +60,21 @@ func (h *OrderHandler) List(c *gin.Context) {
|
||||
order.Id = item.Id
|
||||
order.CreatedAt = item.CreatedAt.Unix()
|
||||
order.UpdatedAt = item.UpdatedAt.Unix()
|
||||
payMethod, ok := types.PayMethods[item.PayWay]
|
||||
payChannel, ok := types.PayChannel[item.Channel]
|
||||
if !ok {
|
||||
payMethod = item.PayWay
|
||||
payChannel = item.PayWay
|
||||
}
|
||||
payName, ok := types.PayNames[item.PayType]
|
||||
payWays, ok := types.PayWays[item.PayWay]
|
||||
if !ok {
|
||||
payName = item.PayWay
|
||||
payWays = item.PayWay
|
||||
}
|
||||
order.PayMethod = payMethod
|
||||
order.PayName = payName
|
||||
order.ChannelName = payChannel
|
||||
order.PayName = payWays
|
||||
list = append(list, order)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, list))
|
||||
@@ -82,17 +95,8 @@ func (h *OrderHandler) Query(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
counter := 0
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
var item model.Order
|
||||
h.DB.Where("order_no = ?", orderNo).First(&item)
|
||||
if counter >= 15 || item.Status == types.OrderPaidSuccess || item.Status != order.Status {
|
||||
order.Status = item.Status
|
||||
break
|
||||
}
|
||||
counter++
|
||||
}
|
||||
var item model.Order
|
||||
h.DB.Where("order_no = ?", orderNo).First(&item)
|
||||
|
||||
resp.SUCCESS(c, gin.H{"status": order.Status})
|
||||
}
|
||||
|
||||
+274
-241
@@ -11,6 +11,7 @@ import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/payment"
|
||||
@@ -33,52 +34,148 @@ type PayWay struct {
|
||||
// PaymentHandler 支付服务回调 handler
|
||||
type PaymentHandler struct {
|
||||
BaseHandler
|
||||
alipayService *payment.AlipayService
|
||||
huPiPayService *payment.HuPiPayService
|
||||
geekPayService *payment.GeekPayService
|
||||
wechatPayService *payment.WechatPayService
|
||||
snowflake *service.Snowflake
|
||||
userService *service.UserService
|
||||
fs embed.FS
|
||||
lock sync.Mutex
|
||||
signKey string // 用来签名的随机秘钥
|
||||
alipayService *payment.AlipayService
|
||||
epayService *payment.EPayService
|
||||
wxpayService *payment.WxPayService
|
||||
snowflake *service.Snowflake
|
||||
userService *service.UserService
|
||||
fs embed.FS
|
||||
lock sync.Mutex
|
||||
config *types.PaymentConfig
|
||||
}
|
||||
|
||||
func NewPaymentHandler(
|
||||
server *core.AppServer,
|
||||
alipayService *payment.AlipayService,
|
||||
huPiPayService *payment.HuPiPayService,
|
||||
geekPayService *payment.GeekPayService,
|
||||
wechatPayService *payment.WechatPayService,
|
||||
geekPayService *payment.EPayService,
|
||||
wxpayService *payment.WxPayService,
|
||||
db *gorm.DB,
|
||||
userService *service.UserService,
|
||||
snowflake *service.Snowflake,
|
||||
fs embed.FS) *PaymentHandler {
|
||||
fs embed.FS,
|
||||
sysConfig *types.SystemConfig) *PaymentHandler {
|
||||
return &PaymentHandler{
|
||||
alipayService: alipayService,
|
||||
huPiPayService: huPiPayService,
|
||||
geekPayService: geekPayService,
|
||||
wechatPayService: wechatPayService,
|
||||
snowflake: snowflake,
|
||||
userService: userService,
|
||||
fs: fs,
|
||||
lock: sync.Mutex{},
|
||||
alipayService: alipayService,
|
||||
epayService: geekPayService,
|
||||
wxpayService: wxpayService,
|
||||
snowflake: snowflake,
|
||||
userService: userService,
|
||||
fs: fs,
|
||||
lock: sync.Mutex{},
|
||||
BaseHandler: BaseHandler{
|
||||
App: server,
|
||||
DB: db,
|
||||
},
|
||||
signKey: utils.RandString(32),
|
||||
config: &sysConfig.Payment,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) Pay(c *gin.Context) {
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *PaymentHandler) RegisterRoutes() {
|
||||
rg := h.App.Engine.Group("/api/payment/")
|
||||
|
||||
// 支付回调接口(公开)
|
||||
rg.POST("notify/alipay", h.AlipayNotify)
|
||||
rg.GET("notify/epay", h.EPayNotify)
|
||||
rg.POST("notify/wxpay", h.WxpayNotify)
|
||||
|
||||
// 需要用户登录的接口
|
||||
rg.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
rg.POST("create", h.CreateOrder)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) StartSyncOrders() {
|
||||
go func() {
|
||||
for {
|
||||
err := h.SyncOrders()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
time.Sleep(time.Second * 5)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// SyncOrders 同步订单状态
|
||||
func (h *PaymentHandler) SyncOrders() error {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Errorf("同步订单状态发生异常: %v", err)
|
||||
}
|
||||
}()
|
||||
var orders []model.Order
|
||||
err := h.DB.Where("status", types.OrderNotPaid).Where("checked", false).Find(&orders).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, order := range orders {
|
||||
time.Sleep(time.Second * 1)
|
||||
//超时15分钟的订单,直接标记为已关闭
|
||||
if time.Now().After(order.CreatedAt.Add(time.Minute * 5)) {
|
||||
h.DB.Model(&model.Order{}).Where("id", order.Id).Update("checked", true)
|
||||
logger.Errorf("订单超时:%v", order)
|
||||
continue
|
||||
}
|
||||
// 查询订单状态
|
||||
var res payment.OrderInfo
|
||||
switch order.Channel {
|
||||
case payment.PayChannelEpay:
|
||||
res, err = h.epayService.Query(order.OrderNo)
|
||||
if err != nil {
|
||||
logger.Errorf("error with query order info: %v", err)
|
||||
continue
|
||||
}
|
||||
// 微信支付
|
||||
case payment.PayChannelWX:
|
||||
res, err = h.wxpayService.Query(order.OrderNo)
|
||||
logger.Debugf("微信支付订单状态:%+v", res)
|
||||
if err != nil {
|
||||
logger.Errorf("error with query order info: %v", err)
|
||||
continue
|
||||
}
|
||||
case payment.PayChannelAL:
|
||||
res, err = h.alipayService.Query(order.OrderNo)
|
||||
if err != nil {
|
||||
logger.Errorf("error with query order info: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 订单已关闭
|
||||
if res.Closed() {
|
||||
h.DB.Model(&model.Order{}).Where("id", order.Id).Updates(map[string]any{
|
||||
"checked": true,
|
||||
"status": types.OrderPaidFailed,
|
||||
})
|
||||
logger.Errorf("订单已关闭:%v", order)
|
||||
continue
|
||||
}
|
||||
|
||||
// 订单未支付,不处理,继续轮询
|
||||
if !res.Success() {
|
||||
continue
|
||||
}
|
||||
|
||||
// 订单支付成功
|
||||
err = h.paySuccess(res)
|
||||
if err != nil {
|
||||
logger.Errorf("error with deal order: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) CreateOrder(c *gin.Context) {
|
||||
var data struct {
|
||||
PayWay string `json:"pay_way"`
|
||||
PayType string `json:"pay_type"`
|
||||
ProductId int `json:"product_id"`
|
||||
UserId int `json:"user_id"`
|
||||
Device string `json:"device"`
|
||||
Host string `json:"host"`
|
||||
PayWay string `json:"pay_way,omitempty"` // 支付方式:支付宝,微信
|
||||
Pid int `json:"pid,omitempty"`
|
||||
Device string `json:"device,omitempty"`
|
||||
Domain string `json:"domain,omitempty"` // 支付回调域名
|
||||
Channel string `json:"channel,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
@@ -86,7 +183,7 @@ func (h *PaymentHandler) Pay(c *gin.Context) {
|
||||
}
|
||||
|
||||
var product model.Product
|
||||
err := h.DB.Where("id", data.ProductId).First(&product).Error
|
||||
err := h.DB.Where("id", data.Pid).First(&product).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "Product not found")
|
||||
return
|
||||
@@ -97,136 +194,118 @@ func (h *PaymentHandler) Pay(c *gin.Context) {
|
||||
resp.ERROR(c, "error with generate trade no: "+err.Error())
|
||||
return
|
||||
}
|
||||
userId := h.GetLoginUserId(c)
|
||||
var user model.User
|
||||
err = h.DB.Where("id", data.UserId).First(&user).Error
|
||||
err = h.DB.Where("id", userId).First(&user).Error
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
amount := product.Discount
|
||||
var payURL, returnURL, notifyURL string
|
||||
amount := product.Price
|
||||
var payURL, notifyURL string
|
||||
switch data.PayWay {
|
||||
case "alipay":
|
||||
if h.App.Config.AlipayConfig.NotifyURL != "" { // 用于本地调试支付
|
||||
notifyURL = h.App.Config.AlipayConfig.NotifyURL
|
||||
} else {
|
||||
notifyURL = fmt.Sprintf("%s/api/payment/notify/alipay", data.Host)
|
||||
}
|
||||
if h.App.Config.AlipayConfig.ReturnURL != "" { // 用于本地调试支付
|
||||
returnURL = h.App.Config.AlipayConfig.ReturnURL
|
||||
} else {
|
||||
returnURL = fmt.Sprintf("%s/payReturn", data.Host)
|
||||
}
|
||||
money := fmt.Sprintf("%.2f", amount)
|
||||
if data.Device == "wechat" {
|
||||
payURL, err = h.alipayService.PayMobile(payment.AlipayParams{
|
||||
case "wxpay":
|
||||
logger.Debugf("微信支付,%+v", data)
|
||||
data.Channel = payment.PayChannelWX
|
||||
// 优先使用微信官方支付
|
||||
if h.config.WxPay.Enabled {
|
||||
data.Channel = "wxpay"
|
||||
if h.config.WxPay.Domain != "" {
|
||||
data.Domain = h.config.WxPay.Domain
|
||||
}
|
||||
notifyURL = fmt.Sprintf("%s/api/payment/notify/wxpay", data.Domain)
|
||||
payURL, err = h.wxpayService.Pay(payment.PayRequest{
|
||||
OutTradeNo: orderNo,
|
||||
Subject: product.Name,
|
||||
TotalFee: money,
|
||||
ReturnURL: returnURL,
|
||||
NotifyURL: notifyURL,
|
||||
})
|
||||
} else {
|
||||
payURL, err = h.alipayService.PayPC(payment.AlipayParams{
|
||||
OutTradeNo: orderNo,
|
||||
Subject: product.Name,
|
||||
TotalFee: money,
|
||||
ReturnURL: returnURL,
|
||||
NotifyURL: notifyURL,
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate pay url: "+err.Error())
|
||||
return
|
||||
}
|
||||
break
|
||||
case "wechat":
|
||||
if h.App.Config.WechatPayConfig.NotifyURL != "" {
|
||||
notifyURL = h.App.Config.WechatPayConfig.NotifyURL
|
||||
} else {
|
||||
notifyURL = fmt.Sprintf("%s/api/payment/notify/wechat", data.Host)
|
||||
}
|
||||
if data.Device == "wechat" {
|
||||
payURL, err = h.wechatPayService.PayUrlH5(payment.WechatPayParams{
|
||||
OutTradeNo: orderNo,
|
||||
TotalFee: int(amount * 100),
|
||||
TotalFee: fmt.Sprintf("%d", int(amount*100)),
|
||||
Subject: product.Name,
|
||||
NotifyURL: notifyURL,
|
||||
ClientIP: c.ClientIP(),
|
||||
Device: data.Device,
|
||||
PayWay: payment.PayWayWX,
|
||||
})
|
||||
} else {
|
||||
payURL, err = h.wechatPayService.PayUrlNative(payment.WechatPayParams{
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
} else if h.config.Epay.Enabled { // 聚合支付
|
||||
logger.Debugf("聚合支付%+v", data)
|
||||
data.Channel = payment.PayChannelEpay
|
||||
if h.config.Epay.Domain != "" {
|
||||
data.Domain = h.config.Epay.Domain
|
||||
}
|
||||
notifyURL = fmt.Sprintf("%s/api/payment/notify/epay", data.Domain)
|
||||
params := payment.PayRequest{
|
||||
OutTradeNo: orderNo,
|
||||
TotalFee: int(amount * 100),
|
||||
Subject: product.Name,
|
||||
TotalFee: fmt.Sprintf("%f", amount),
|
||||
ClientIP: c.ClientIP(),
|
||||
Device: data.Device,
|
||||
PayWay: payment.PayWayWX,
|
||||
NotifyURL: notifyURL,
|
||||
}
|
||||
|
||||
r, err := h.epayService.Pay(params)
|
||||
logger.Debugf("请求支付结果,%+v", r)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
} else {
|
||||
payURL = r
|
||||
}
|
||||
} else {
|
||||
resp.ERROR(c, "系统没有配置可用的支付渠道!")
|
||||
return
|
||||
}
|
||||
case "alipay":
|
||||
if h.config.Alipay.Enabled {
|
||||
logger.Debugf("支付宝,%+v", data)
|
||||
data.Channel = payment.PayChannelAL
|
||||
if h.config.Alipay.Domain != "" { // 用于本地调试支付
|
||||
data.Domain = h.config.Alipay.Domain
|
||||
}
|
||||
notifyURL = fmt.Sprintf("%s/api/payment/notify/alipay", data.Domain)
|
||||
money := fmt.Sprintf("%.2f", amount)
|
||||
payURL, err = h.alipayService.Pay(payment.PayRequest{
|
||||
Device: data.Device,
|
||||
OutTradeNo: orderNo,
|
||||
Subject: product.Name,
|
||||
TotalFee: money,
|
||||
NotifyURL: notifyURL,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
break
|
||||
case "hupi":
|
||||
if h.App.Config.HuPiPayConfig.NotifyURL != "" {
|
||||
notifyURL = h.App.Config.HuPiPayConfig.NotifyURL
|
||||
} else {
|
||||
notifyURL = fmt.Sprintf("%s/api/payment/notify/hupi", data.Host)
|
||||
}
|
||||
if h.App.Config.HuPiPayConfig.ReturnURL != "" {
|
||||
returnURL = h.App.Config.HuPiPayConfig.ReturnURL
|
||||
} else {
|
||||
returnURL = fmt.Sprintf("%s/payReturn", data.Host)
|
||||
}
|
||||
r, err := h.huPiPayService.Pay(payment.HuPiPayParams{
|
||||
Version: "1.1",
|
||||
TradeOrderId: orderNo,
|
||||
TotalFee: fmt.Sprintf("%f", amount),
|
||||
Title: product.Name,
|
||||
NotifyURL: notifyURL,
|
||||
ReturnURL: returnURL,
|
||||
WapName: "GeekAI助手",
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
payURL = r.URL
|
||||
break
|
||||
case "geek":
|
||||
if h.App.Config.GeekPayConfig.NotifyURL != "" {
|
||||
notifyURL = h.App.Config.GeekPayConfig.NotifyURL
|
||||
} else {
|
||||
notifyURL = fmt.Sprintf("%s/api/payment/notify/geek", data.Host)
|
||||
}
|
||||
if h.App.Config.GeekPayConfig.ReturnURL != "" {
|
||||
data.Host = utils.GetBaseURL(h.App.Config.GeekPayConfig.ReturnURL)
|
||||
}
|
||||
if data.Device == "wechat" { // 微信客户端打开,调回手机端用户中心页面
|
||||
returnURL = fmt.Sprintf("%s/mobile/profile", data.Host)
|
||||
} else {
|
||||
returnURL = fmt.Sprintf("%s/payReturn", data.Host)
|
||||
}
|
||||
params := payment.GeekPayParams{
|
||||
OutTradeNo: orderNo,
|
||||
Method: "web",
|
||||
Name: product.Name,
|
||||
Money: fmt.Sprintf("%f", amount),
|
||||
ClientIP: c.ClientIP(),
|
||||
Device: data.Device,
|
||||
Type: data.PayType,
|
||||
ReturnURL: returnURL,
|
||||
NotifyURL: notifyURL,
|
||||
}
|
||||
|
||||
res, err := h.geekPayService.Pay(params)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate pay url: "+err.Error())
|
||||
return
|
||||
}
|
||||
} else if h.config.Epay.Enabled { // 聚合支付
|
||||
logger.Debugf("聚合支付,%+v", data)
|
||||
data.Channel = payment.PayChannelEpay
|
||||
if h.config.Epay.Domain != "" {
|
||||
data.Domain = h.config.Epay.Domain
|
||||
}
|
||||
notifyURL = fmt.Sprintf("%s/api/payment/notify/epay", data.Domain)
|
||||
params := payment.PayRequest{
|
||||
OutTradeNo: orderNo,
|
||||
Subject: product.Name,
|
||||
TotalFee: fmt.Sprintf("%f", amount),
|
||||
ClientIP: c.ClientIP(),
|
||||
Device: data.Device,
|
||||
PayWay: data.PayWay,
|
||||
NotifyURL: notifyURL,
|
||||
}
|
||||
|
||||
r, err := h.epayService.Pay(params)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
} else {
|
||||
payURL = r
|
||||
}
|
||||
} else {
|
||||
resp.ERROR(c, "系统没有配置可用的支付渠道!")
|
||||
return
|
||||
}
|
||||
payURL = res.PayURL
|
||||
default:
|
||||
resp.ERROR(c, "不支持的支付渠道")
|
||||
return
|
||||
@@ -234,43 +313,40 @@ func (h *PaymentHandler) Pay(c *gin.Context) {
|
||||
|
||||
// 创建订单
|
||||
remark := types.OrderRemark{
|
||||
Days: product.Days,
|
||||
Power: product.Power,
|
||||
Name: product.Name,
|
||||
Price: product.Price,
|
||||
Discount: product.Discount,
|
||||
Power: product.Power,
|
||||
Name: product.Name,
|
||||
Price: product.Price,
|
||||
}
|
||||
order := model.Order{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
ProductId: product.Id,
|
||||
OrderNo: orderNo,
|
||||
Subject: product.Name,
|
||||
Amount: amount,
|
||||
Status: types.OrderNotPaid,
|
||||
PayWay: data.PayWay,
|
||||
PayType: data.PayType,
|
||||
Remark: utils.JsonEncode(remark),
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
OrderNo: orderNo,
|
||||
Subject: product.Name,
|
||||
Amount: amount,
|
||||
Status: types.OrderNotPaid,
|
||||
PayWay: data.PayWay,
|
||||
Channel: data.Channel,
|
||||
Remark: utils.JsonEncode(remark),
|
||||
}
|
||||
err = h.DB.Create(&order).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with create order: "+err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c, payURL)
|
||||
resp.SUCCESS(c, gin.H{"pay_url": payURL, "order_no": orderNo})
|
||||
}
|
||||
|
||||
// 异步通知回调公共逻辑
|
||||
func (h *PaymentHandler) notify(orderNo string, tradeNo string) error {
|
||||
// 支付成功处理
|
||||
func (h *PaymentHandler) paySuccess(info payment.OrderInfo) error {
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
|
||||
var order model.Order
|
||||
err := h.DB.Where("order_no = ?", orderNo).First(&order).Error
|
||||
err := h.DB.Where("order_no", info.OutTradeNo).First(&order).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with fetch order: %v", err)
|
||||
}
|
||||
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
|
||||
// 已支付订单,直接返回
|
||||
if order.Status == types.OrderPaidSuccess {
|
||||
return nil
|
||||
@@ -289,20 +365,22 @@ func (h *PaymentHandler) notify(orderNo string, tradeNo string) error {
|
||||
}
|
||||
|
||||
// 增加用户算力
|
||||
err = h.userService.IncreasePower(int(order.UserId), remark.Power, model.PowerLog{
|
||||
Type: types.PowerRecharge,
|
||||
Model: order.PayWay,
|
||||
Remark: fmt.Sprintf("充值算力,金额:%f,订单号:%s", order.Amount, order.OrderNo),
|
||||
err = h.userService.IncreasePower(order.UserId, remark.Power, model.PowerLog{
|
||||
Type: types.PowerRecharge,
|
||||
Model: order.Subject,
|
||||
Remark: fmt.Sprintf("充值算力,金额:%f,订单号:%s", order.Amount, order.OrderNo),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新订单状态
|
||||
order.PayTime = time.Now().Unix()
|
||||
order.PayTime = utils.Str2stamp(info.PayTime)
|
||||
order.Status = types.OrderPaidSuccess
|
||||
order.TradeNo = tradeNo
|
||||
err = h.DB.Updates(&order).Error
|
||||
order.TradeNo = info.TradeId
|
||||
order.Checked = true
|
||||
err = h.DB.Debug().Updates(&order).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with update order info: %v", err)
|
||||
}
|
||||
@@ -317,54 +395,6 @@ func (h *PaymentHandler) notify(orderNo string, tradeNo string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPayWays 获取支付方式
|
||||
func (h *PaymentHandler) GetPayWays(c *gin.Context) {
|
||||
payWays := make([]gin.H, 0)
|
||||
if h.App.Config.AlipayConfig.Enabled {
|
||||
payWays = append(payWays, gin.H{"pay_way": "alipay", "pay_type": "alipay"})
|
||||
}
|
||||
if h.App.Config.HuPiPayConfig.Enabled {
|
||||
payWays = append(payWays, gin.H{"pay_way": "hupi", "pay_type": "wxpay"})
|
||||
}
|
||||
if h.App.Config.GeekPayConfig.Enabled {
|
||||
for _, v := range h.App.Config.GeekPayConfig.Methods {
|
||||
payWays = append(payWays, gin.H{"pay_way": "geek", "pay_type": v})
|
||||
}
|
||||
}
|
||||
if h.App.Config.WechatPayConfig.Enabled {
|
||||
payWays = append(payWays, gin.H{"pay_way": "wechat", "pay_type": "wxpay"})
|
||||
}
|
||||
resp.SUCCESS(c, payWays)
|
||||
}
|
||||
|
||||
// HuPiPayNotify 虎皮椒支付异步回调
|
||||
func (h *PaymentHandler) HuPiPayNotify(c *gin.Context) {
|
||||
err := c.Request.ParseForm()
|
||||
if err != nil {
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
orderNo := c.Request.Form.Get("trade_order_id")
|
||||
tradeNo := c.Request.Form.Get("open_order_id")
|
||||
logger.Infof("收到虎皮椒订单支付回调,%+v", c.Request.Form)
|
||||
|
||||
if err = h.huPiPayService.Check(orderNo); err != nil {
|
||||
logger.Error("订单校验失败:", err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.notify(orderNo, tradeNo)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// AlipayNotify 支付宝支付回调
|
||||
func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
|
||||
err := c.Request.ParseForm()
|
||||
@@ -373,16 +403,15 @@ func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
result := h.alipayService.TradeVerify(c.Request)
|
||||
logger.Infof("收到支付宝商号订单支付回调:%+v", result)
|
||||
if !result.Success() {
|
||||
logger.Error("订单校验失败:", result.Message)
|
||||
orderInfo, err := h.alipayService.Query(c.Request.Form.Get("out_trade_no"))
|
||||
logger.Infof("收到支付宝商号订单支付回调:%+v", orderInfo)
|
||||
if !orderInfo.Success() {
|
||||
logger.Errorf("订单校验失败:%v", err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
tradeNo := c.Request.Form.Get("trade_no")
|
||||
err = h.notify(result.OutTradeNo, tradeNo)
|
||||
err = h.paySuccess(orderInfo)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
@@ -392,28 +421,35 @@ func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
|
||||
c.String(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// GeekPayNotify 支付异步回调
|
||||
func (h *PaymentHandler) GeekPayNotify(c *gin.Context) {
|
||||
// EPayNotify 易支付支付异步回调
|
||||
func (h *PaymentHandler) EPayNotify(c *gin.Context) {
|
||||
var params = make(map[string]string)
|
||||
for k := range c.Request.URL.Query() {
|
||||
params[k] = c.Query(k)
|
||||
}
|
||||
|
||||
logger.Infof("收到GeekPay订单支付回调:%+v", params)
|
||||
// 检查支付状态
|
||||
logger.Infof("收到易支付订单支付回调:%+v", params)
|
||||
// 检查支付状态, 如果未支付,则返回成功
|
||||
if params["trade_status"] != "TRADE_SUCCESS" {
|
||||
c.String(http.StatusOK, "success")
|
||||
return
|
||||
}
|
||||
|
||||
sign := h.geekPayService.Sign(params)
|
||||
sign := h.epayService.Sign(params)
|
||||
if sign != c.Query("sign") {
|
||||
logger.Errorf("签名验证失败, %s, %s", sign, c.Query("sign"))
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
// 查询订单状态
|
||||
order, err := h.epayService.Query(params["out_trade_no"])
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.notify(params["out_trade_no"], params["trade_no"])
|
||||
err = h.paySuccess(order)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
@@ -423,26 +459,23 @@ func (h *PaymentHandler) GeekPayNotify(c *gin.Context) {
|
||||
c.String(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// WechatPayNotify 微信商户支付异步回调
|
||||
func (h *PaymentHandler) WechatPayNotify(c *gin.Context) {
|
||||
// WxpayNotify 微信商户支付异步回调
|
||||
func (h *PaymentHandler) WxpayNotify(c *gin.Context) {
|
||||
err := c.Request.ParseForm()
|
||||
if err != nil {
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
result := h.wechatPayService.TradeVerify(c.Request)
|
||||
logger.Infof("收到微信商号订单支付回调:%+v", result)
|
||||
if !result.Success() {
|
||||
logger.Error("订单校验失败:", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": "FAIL",
|
||||
"message": err.Error(),
|
||||
})
|
||||
orderInfo, err := h.wxpayService.TradeVerify(c.Request)
|
||||
logger.Infof("收到微信商号订单支付回调:%+v", orderInfo)
|
||||
if err != nil {
|
||||
logger.Errorf("订单校验失败:%v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": "FAIL"})
|
||||
return
|
||||
}
|
||||
|
||||
err = h.notify(result.OutTradeNo, result.TradeId)
|
||||
err = h.paySuccess(orderInfo)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
|
||||
@@ -9,11 +9,13 @@ package handler
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -27,6 +29,18 @@ func NewPowerLogHandler(app *core.AppServer, db *gorm.DB) *PowerLogHandler {
|
||||
return &PowerLogHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *PowerLogHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/powerLog/")
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("list", h.List)
|
||||
group.GET("stats", h.Stats)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PowerLogHandler) List(c *gin.Context) {
|
||||
var data struct {
|
||||
Model string `json:"model"`
|
||||
@@ -72,3 +86,45 @@ func (h *PowerLogHandler) List(c *gin.Context) {
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
|
||||
}
|
||||
|
||||
// Stats 获取用户算力统计
|
||||
func (h *PowerLogHandler) Stats(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
if userId == 0 {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户信息(包含余额)
|
||||
var user model.User
|
||||
if err := h.DB.Where("id", userId).First(&user).Error; err != nil {
|
||||
resp.ERROR(c, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 计算总消费(所有支出记录)
|
||||
var totalConsume int64
|
||||
h.DB.Model(&model.PowerLog{}).
|
||||
Where("user_id", userId).
|
||||
Where("mark", types.PowerSub).
|
||||
Select("COALESCE(SUM(amount), 0)").
|
||||
Scan(&totalConsume)
|
||||
|
||||
// 计算今日消费
|
||||
today := time.Now().Format("2006-01-02")
|
||||
var todayConsume int64
|
||||
h.DB.Model(&model.PowerLog{}).
|
||||
Where("user_id", userId).
|
||||
Where("mark", types.PowerSub).
|
||||
Where("DATE(created_at) = ?", today).
|
||||
Select("COALESCE(SUM(amount), 0)").
|
||||
Scan(&todayConsume)
|
||||
|
||||
stats := map[string]interface{}{
|
||||
"total": totalConsume,
|
||||
"today": todayConsume,
|
||||
"balance": user.Power,
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, stats)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -25,6 +26,12 @@ func NewProductHandler(app *core.AppServer, db *gorm.DB) *ProductHandler {
|
||||
return &ProductHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ProductHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/product/")
|
||||
group.GET("list", h.List)
|
||||
}
|
||||
|
||||
// List 模型列表
|
||||
func (h *ProductHandler) List(c *gin.Context) {
|
||||
var items []model.Product
|
||||
|
||||
@@ -10,12 +10,14 @@ package handler
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -39,6 +41,20 @@ func NewPromptHandler(app *core.AppServer, db *gorm.DB, userService *service.Use
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *PromptHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/prompt/")
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis)).Use(middleware.RateLimitEvery(h.App.Redis, 30*time.Second))
|
||||
{
|
||||
group.POST("lyric", h.Lyric)
|
||||
group.POST("image", h.Image)
|
||||
group.POST("video", h.Video)
|
||||
group.POST("meta", h.MetaPrompt)
|
||||
}
|
||||
}
|
||||
|
||||
// Lyric 生成歌词
|
||||
func (h *PromptHandler) Lyric(c *gin.Context) {
|
||||
var data struct {
|
||||
@@ -48,25 +64,12 @@ func (h *PromptHandler) Lyric(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.LyricPromptTemplate, data.Prompt), h.App.SysConfig.TranslateModelId)
|
||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.LyricPromptTemplate, data.Prompt), h.App.SysConfig.Base.AssistantModelId)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if h.App.SysConfig.PromptPower > 0 {
|
||||
userId := h.GetLoginUserId(c)
|
||||
err = h.userService.DecreasePower(int(userId), h.App.SysConfig.PromptPower, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: h.getPromptModel(),
|
||||
Remark: "生成歌词",
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, content)
|
||||
}
|
||||
|
||||
@@ -79,23 +82,12 @@ func (h *PromptHandler) Image(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.ImagePromptOptimizeTemplate, data.Prompt), h.App.SysConfig.TranslateModelId)
|
||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.ImagePromptOptimizeTemplate, data.Prompt), h.App.SysConfig.Base.AssistantModelId)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
if h.App.SysConfig.PromptPower > 0 {
|
||||
userId := h.GetLoginUserId(c)
|
||||
err = h.userService.DecreasePower(int(userId), h.App.SysConfig.PromptPower, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: h.getPromptModel(),
|
||||
Remark: "生成绘画提示词",
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, strings.Trim(content, `"`))
|
||||
}
|
||||
|
||||
@@ -108,25 +100,12 @@ func (h *PromptHandler) Video(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.VideoPromptTemplate, data.Prompt), h.App.SysConfig.TranslateModelId)
|
||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.VideoPromptTemplate, data.Prompt), h.App.SysConfig.Base.AssistantModelId)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if h.App.SysConfig.PromptPower > 0 {
|
||||
userId := h.GetLoginUserId(c)
|
||||
err = h.userService.DecreasePower(int(userId), h.App.SysConfig.PromptPower, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: h.getPromptModel(),
|
||||
Remark: "生成视频脚本",
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, strings.Trim(content, `"`))
|
||||
}
|
||||
|
||||
@@ -158,9 +137,9 @@ func (h *PromptHandler) MetaPrompt(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *PromptHandler) getPromptModel() string {
|
||||
if h.App.SysConfig.TranslateModelId > 0 {
|
||||
if h.App.SysConfig.Base.AssistantModelId > 0 {
|
||||
var chatModel model.ChatModel
|
||||
h.DB.Where("id", h.App.SysConfig.TranslateModelId).First(&chatModel)
|
||||
h.DB.Where("id", h.App.SysConfig.Base.AssistantModelId).First(&chatModel)
|
||||
return chatModel.Value
|
||||
}
|
||||
return "gpt-4o"
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
@@ -39,6 +40,18 @@ func NewRealtimeHandler(server *core.AppServer, db *gorm.DB, userService *servic
|
||||
return &RealtimeHandler{BaseHandler: BaseHandler{App: server, DB: db}, userService: userService}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *RealtimeHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/realtime/")
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.Any("", h.Connection)
|
||||
group.POST("voice", h.VoiceChat)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RealtimeHandler) Connection(c *gin.Context) {
|
||||
// 获取客户端请求中指定的子协议
|
||||
clientProtocols := c.GetHeader("Sec-WebSocket-Protocol")
|
||||
@@ -154,7 +167,7 @@ func (h *RealtimeHandler) VoiceChat(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if user.Power < h.App.SysConfig.AdvanceVoicePower {
|
||||
if user.Power < h.App.SysConfig.Base.AdvanceVoicePower {
|
||||
resp.ERROR(c, "当前用户算力不足,无法使用该功能")
|
||||
return
|
||||
}
|
||||
@@ -198,7 +211,7 @@ func (h *RealtimeHandler) VoiceChat(c *gin.Context) {
|
||||
h.DB.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
|
||||
// 扣减算力
|
||||
err = h.userService.DecreasePower(int(userId), h.App.SysConfig.AdvanceVoicePower, model.PowerLog{
|
||||
err = h.userService.DecreasePower(userId, h.App.SysConfig.Base.AdvanceVoicePower, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "advanced-voice",
|
||||
Remark: "实时语音通话",
|
||||
|
||||
@@ -10,14 +10,16 @@ package handler
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type RedeemHandler struct {
|
||||
@@ -30,6 +32,17 @@ func NewRedeemHandler(app *core.AppServer, db *gorm.DB, userService *service.Use
|
||||
return &RedeemHandler{BaseHandler: BaseHandler{App: app, DB: db}, userService: userService}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *RedeemHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/redeem/")
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("verify", h.Verify)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RedeemHandler) Verify(c *gin.Context) {
|
||||
var data struct {
|
||||
Code string `json:"code"`
|
||||
@@ -61,7 +74,7 @@ func (h *RedeemHandler) Verify(c *gin.Context) {
|
||||
}
|
||||
|
||||
tx := h.DB.Begin()
|
||||
err := h.userService.IncreasePower(int(userId), item.Power, model.PowerLog{
|
||||
err := h.userService.IncreasePower(userId, item.Power, model.PowerLog{
|
||||
Type: types.PowerRedeem,
|
||||
Model: "兑换码",
|
||||
Remark: fmt.Sprintf("兑换码核销,算力:%d,兑换码:%s...", item.Power, item.Code[:10]),
|
||||
|
||||
+62
-17
@@ -10,8 +10,10 @@ package handler
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/moderation"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/sd"
|
||||
"geekai/store"
|
||||
@@ -28,12 +30,13 @@ import (
|
||||
|
||||
type SdJobHandler struct {
|
||||
BaseHandler
|
||||
redis *redis.Client
|
||||
sdService *sd.Service
|
||||
uploader *oss.UploaderManager
|
||||
snowflake *service.Snowflake
|
||||
leveldb *store.LevelDB
|
||||
userService *service.UserService
|
||||
redis *redis.Client
|
||||
sdService *sd.Service
|
||||
uploader *oss.UploaderManager
|
||||
snowflake *service.Snowflake
|
||||
leveldb *store.LevelDB
|
||||
userService *service.UserService
|
||||
moderationManager *moderation.ServiceManager
|
||||
}
|
||||
|
||||
func NewSdJobHandler(app *core.AppServer,
|
||||
@@ -42,13 +45,15 @@ func NewSdJobHandler(app *core.AppServer,
|
||||
manager *oss.UploaderManager,
|
||||
snowflake *service.Snowflake,
|
||||
userService *service.UserService,
|
||||
levelDB *store.LevelDB) *SdJobHandler {
|
||||
levelDB *store.LevelDB,
|
||||
moderationManager *moderation.ServiceManager) *SdJobHandler {
|
||||
return &SdJobHandler{
|
||||
sdService: service,
|
||||
uploader: manager,
|
||||
snowflake: snowflake,
|
||||
leveldb: levelDB,
|
||||
userService: userService,
|
||||
sdService: service,
|
||||
uploader: manager,
|
||||
snowflake: snowflake,
|
||||
leveldb: levelDB,
|
||||
userService: userService,
|
||||
moderationManager: moderationManager,
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
@@ -56,6 +61,23 @@ func NewSdJobHandler(app *core.AppServer,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *SdJobHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/sd/")
|
||||
|
||||
// 公开接口,不需要授权
|
||||
group.GET("imgWall", h.ImgWall)
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("image", h.Image)
|
||||
group.GET("jobs", h.JobList)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *SdJobHandler) preCheck(c *gin.Context) bool {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
@@ -63,7 +85,7 @@ func (h *SdJobHandler) preCheck(c *gin.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if user.Power < h.App.SysConfig.SdPower {
|
||||
if user.Power < h.App.SysConfig.Base.SdPower {
|
||||
resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
|
||||
return false
|
||||
}
|
||||
@@ -84,6 +106,29 @@ func (h *SdJobHandler) Image(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if h.App.SysConfig.Moderation.Enable {
|
||||
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
|
||||
if err != nil {
|
||||
logger.Error("failed to moderate content: ", err)
|
||||
}
|
||||
if moderationResult.Flagged {
|
||||
// 记录违规内容
|
||||
moderation := model.Moderation{
|
||||
UserId: h.GetLoginUserId(c),
|
||||
Source: types.ModerationSourceSD,
|
||||
Input: data.Prompt,
|
||||
Result: utils.JsonEncode(moderationResult),
|
||||
}
|
||||
err = h.DB.Create(&moderation).Error
|
||||
if err != nil {
|
||||
logger.Error("failed to save moderation: ", err)
|
||||
}
|
||||
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if data.Width <= 0 {
|
||||
data.Width = 512
|
||||
}
|
||||
@@ -131,18 +176,18 @@ func (h *SdJobHandler) Image(c *gin.Context) {
|
||||
HdSteps: data.HdSteps,
|
||||
},
|
||||
UserId: userId,
|
||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
||||
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
||||
}
|
||||
|
||||
job := model.SdJob{
|
||||
UserId: userId,
|
||||
UserId: uint(userId),
|
||||
Type: types.TaskImage.String(),
|
||||
TaskId: taskId,
|
||||
Params: utils.JsonEncode(task.Params),
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
Prompt: data.Prompt,
|
||||
Progress: 0,
|
||||
Power: h.App.SysConfig.SdPower,
|
||||
Power: h.App.SysConfig.Base.SdPower,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
res := h.DB.Create(&job)
|
||||
@@ -273,7 +318,7 @@ func (h *SdJobHandler) Publish(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享
|
||||
|
||||
err := h.DB.Model(&model.SdJob{Id: uint(id), UserId: int(userId)}).UpdateColumn("publish", action).Error
|
||||
err := h.DB.Model(&model.SdJob{Id: uint(id), UserId: uint(userId)}).UpdateColumn("publish", action).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
|
||||
+24
-17
@@ -24,24 +24,31 @@ const CodeStorePrefix = "/verify/codes/"
|
||||
|
||||
type SmsHandler struct {
|
||||
BaseHandler
|
||||
redis *redis.Client
|
||||
sms *sms.ServiceManager
|
||||
smtp *service.SmtpService
|
||||
captcha *service.CaptchaService
|
||||
redis *redis.Client
|
||||
sms *sms.SmsManager
|
||||
smtp *service.SmtpService
|
||||
captchaService *service.CaptchaService
|
||||
}
|
||||
|
||||
func NewSmsHandler(
|
||||
app *core.AppServer,
|
||||
client *redis.Client,
|
||||
sms *sms.ServiceManager,
|
||||
sms *sms.SmsManager,
|
||||
smtp *service.SmtpService,
|
||||
captcha *service.CaptchaService) *SmsHandler {
|
||||
return &SmsHandler{
|
||||
redis: client,
|
||||
sms: sms,
|
||||
captcha: captcha,
|
||||
smtp: smtp,
|
||||
BaseHandler: BaseHandler{App: app}}
|
||||
redis: client,
|
||||
sms: sms,
|
||||
captchaService: captcha,
|
||||
smtp: smtp,
|
||||
BaseHandler: BaseHandler{App: app}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *SmsHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/sms/")
|
||||
// 无需授权的接口
|
||||
group.POST("code", h.SendCode)
|
||||
}
|
||||
|
||||
// SendCode 发送验证码
|
||||
@@ -56,12 +63,12 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
if h.App.SysConfig.EnabledVerify {
|
||||
if h.captchaService.GetConfig().Enabled {
|
||||
var check bool
|
||||
if data.X != 0 {
|
||||
check = h.captcha.SlideCheck(data)
|
||||
check = h.captchaService.SlideCheck(data)
|
||||
} else {
|
||||
check = h.captcha.Check(data)
|
||||
check = h.captchaService.Check(data)
|
||||
}
|
||||
if !check {
|
||||
resp.ERROR(c, "请先完人机验证")
|
||||
@@ -72,14 +79,14 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
|
||||
code := utils.RandomNumber(6)
|
||||
var err error
|
||||
if strings.Contains(data.Receiver, "@") { // email
|
||||
if !utils.Contains(h.App.SysConfig.RegisterWays, "email") {
|
||||
if !utils.Contains(h.App.SysConfig.Base.RegisterWays, "email") {
|
||||
resp.ERROR(c, "系统已禁用邮箱注册!")
|
||||
return
|
||||
}
|
||||
// 检查邮箱后缀是否在白名单
|
||||
if len(h.App.SysConfig.EmailWhiteList) > 0 {
|
||||
if len(h.App.SysConfig.Base.EmailWhiteList) > 0 {
|
||||
inWhiteList := false
|
||||
for _, suffix := range h.App.SysConfig.EmailWhiteList {
|
||||
for _, suffix := range h.App.SysConfig.Base.EmailWhiteList {
|
||||
if strings.HasSuffix(data.Receiver, suffix) {
|
||||
inWhiteList = true
|
||||
break
|
||||
@@ -92,7 +99,7 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
|
||||
}
|
||||
err = h.smtp.SendVerifyCode(data.Receiver, code)
|
||||
} else {
|
||||
if !utils.Contains(h.App.SysConfig.RegisterWays, "mobile") {
|
||||
if !utils.Contains(h.App.SysConfig.Base.RegisterWays, "mobile") {
|
||||
resp.ERROR(c, "系统已禁用手机号注册!")
|
||||
return
|
||||
}
|
||||
|
||||
+56
-10
@@ -10,8 +10,10 @@ package handler
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/moderation"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/suno"
|
||||
"geekai/store/model"
|
||||
@@ -26,20 +28,41 @@ import (
|
||||
|
||||
type SunoHandler struct {
|
||||
BaseHandler
|
||||
sunoService *suno.Service
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
sunoService *suno.Service
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
moderationManager *moderation.ServiceManager
|
||||
}
|
||||
|
||||
func NewSunoHandler(app *core.AppServer, db *gorm.DB, service *suno.Service, uploader *oss.UploaderManager, userService *service.UserService) *SunoHandler {
|
||||
func NewSunoHandler(app *core.AppServer, db *gorm.DB, service *suno.Service, uploader *oss.UploaderManager, userService *service.UserService, moderationManager *moderation.ServiceManager) *SunoHandler {
|
||||
return &SunoHandler{
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
},
|
||||
sunoService: service,
|
||||
uploader: uploader,
|
||||
userService: userService,
|
||||
sunoService: service,
|
||||
uploader: uploader,
|
||||
userService: userService,
|
||||
moderationManager: moderationManager,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *SunoHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/suno/")
|
||||
|
||||
// 公开接口,不需要授权
|
||||
group.GET("play", h.Play)
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("create", h.Create)
|
||||
group.GET("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
group.POST("update", h.Update)
|
||||
group.GET("detail", h.Detail)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,13 +87,36 @@ func (h *SunoHandler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if h.App.SysConfig.Moderation.Enable {
|
||||
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
|
||||
if err != nil {
|
||||
logger.Error("failed to moderate content: ", err)
|
||||
}
|
||||
if moderationResult.Flagged {
|
||||
// 记录违规内容
|
||||
moderation := model.Moderation{
|
||||
UserId: h.GetLoginUserId(c),
|
||||
Source: types.ModerationSourceSuno,
|
||||
Input: data.Prompt,
|
||||
Result: utils.JsonEncode(moderationResult),
|
||||
}
|
||||
err = h.DB.Create(&moderation).Error
|
||||
if err != nil {
|
||||
logger.Error("failed to save moderation: ", err)
|
||||
}
|
||||
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
if user.Power < h.App.SysConfig.SunoPower {
|
||||
if user.Power < h.App.SysConfig.Base.SunoPower {
|
||||
resp.ERROR(c, "您的算力不足,请充值后再试!")
|
||||
return
|
||||
}
|
||||
@@ -107,7 +153,7 @@ func (h *SunoHandler) Create(c *gin.Context) {
|
||||
|
||||
// 插入数据库
|
||||
job := model.SunoJob{
|
||||
UserId: task.UserId,
|
||||
UserId: uint(task.UserId),
|
||||
Prompt: data.Prompt,
|
||||
Instrumental: data.Instrumental,
|
||||
ModelName: data.Model,
|
||||
@@ -118,7 +164,7 @@ func (h *SunoHandler) Create(c *gin.Context) {
|
||||
RefSongId: data.RefSongId,
|
||||
RefTaskId: data.RefTaskId,
|
||||
ExtendSecs: data.ExtendSecs,
|
||||
Power: h.App.SysConfig.SunoPower,
|
||||
Power: h.App.SysConfig.Base.SunoPower,
|
||||
SongId: utils.RandString(32),
|
||||
}
|
||||
if data.Lyrics != "" {
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/service"
|
||||
"geekai/service/payment"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type TestHandler struct {
|
||||
App *core.AppServer
|
||||
db *gorm.DB
|
||||
snowflake *service.Snowflake
|
||||
js *payment.GeekPayService
|
||||
js *payment.EPayService
|
||||
}
|
||||
|
||||
func NewTestHandler(db *gorm.DB, snowflake *service.Snowflake, js *payment.GeekPayService) *TestHandler {
|
||||
return &TestHandler{db: db, snowflake: snowflake, js: js}
|
||||
func NewTestHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, js *payment.EPayService) *TestHandler {
|
||||
return &TestHandler{App: app, db: db, snowflake: snowflake, js: js}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *TestHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/test/")
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.Any("sse", h.PostTest, h.SseTest)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *TestHandler) SseTest(c *gin.Context) {
|
||||
|
||||
+221
-235
@@ -8,8 +8,10 @@ package handler
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store"
|
||||
@@ -20,8 +22,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
@@ -36,8 +36,10 @@ type UserHandler struct {
|
||||
redis *redis.Client
|
||||
levelDB *store.LevelDB
|
||||
licenseService *service.LicenseService
|
||||
captcha *service.CaptchaService
|
||||
captchaService *service.CaptchaService
|
||||
userService *service.UserService
|
||||
wxLoginService *service.WxLoginService
|
||||
ipSearcher *xdb.Searcher
|
||||
}
|
||||
|
||||
func NewUserHandler(
|
||||
@@ -48,15 +50,45 @@ func NewUserHandler(
|
||||
levelDB *store.LevelDB,
|
||||
captcha *service.CaptchaService,
|
||||
userService *service.UserService,
|
||||
wxLoginService *service.WxLoginService,
|
||||
ipSearcher *xdb.Searcher,
|
||||
licenseService *service.LicenseService) *UserHandler {
|
||||
return &UserHandler{
|
||||
BaseHandler: BaseHandler{DB: db, App: app},
|
||||
searcher: searcher,
|
||||
redis: client,
|
||||
levelDB: levelDB,
|
||||
captcha: captcha,
|
||||
captchaService: captcha,
|
||||
licenseService: licenseService,
|
||||
userService: userService,
|
||||
wxLoginService: wxLoginService,
|
||||
ipSearcher: ipSearcher,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *UserHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/user/")
|
||||
|
||||
// 公开接口,不需要授权
|
||||
group.POST("register", h.Register)
|
||||
group.POST("login", h.Login)
|
||||
group.POST("resetPass", h.ResetPass)
|
||||
group.GET("login/qrcode", h.GetWxLoginQRCode)
|
||||
group.POST("login/callback", h.WxLoginCallback)
|
||||
group.GET("login/status", h.GetWxLoginState)
|
||||
group.GET("logout", h.Logout)
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("session", h.Session)
|
||||
group.GET("profile", h.Profile)
|
||||
group.POST("profile/update", h.ProfileUpdate)
|
||||
group.POST("password", h.UpdatePass)
|
||||
group.POST("bind/mobile", h.BindMobile)
|
||||
group.POST("bind/email", h.BindEmail)
|
||||
group.GET("signin", h.SignIn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,12 +112,13 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if h.App.SysConfig.EnabledVerify && data.RegWay == "username" {
|
||||
// 人机验证
|
||||
if h.captchaService.GetConfig().Enabled {
|
||||
var check bool
|
||||
if data.X != 0 {
|
||||
check = h.captcha.SlideCheck(data)
|
||||
check = h.captchaService.SlideCheck(data)
|
||||
} else {
|
||||
check = h.captcha.Check(data)
|
||||
check = h.captchaService.Check(data)
|
||||
}
|
||||
if !check {
|
||||
resp.ERROR(c, "请先完人机验证")
|
||||
@@ -125,28 +158,8 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 验证邀请码
|
||||
inviteCode := model.InviteCode{}
|
||||
if data.InviteCode != "" {
|
||||
res := h.DB.Where("code = ?", data.InviteCode).First(&inviteCode)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "无效的邀请码")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
salt := utils.RandString(8)
|
||||
user := model.User{
|
||||
Username: data.Username,
|
||||
Password: utils.GenPassword(data.Password, salt),
|
||||
Avatar: "/images/avatar/user.png",
|
||||
Salt: salt,
|
||||
Status: true,
|
||||
ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
|
||||
Power: h.App.SysConfig.InitPower,
|
||||
}
|
||||
|
||||
// check if the username is existing
|
||||
user := model.User{Username: data.Username, Password: data.Password}
|
||||
var item model.User
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if data.Mobile != "" {
|
||||
@@ -166,73 +179,19 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 被邀请人也获得赠送算力
|
||||
if data.InviteCode != "" {
|
||||
user.Power += h.App.SysConfig.InvitePower
|
||||
}
|
||||
if h.licenseService.GetLicense().Configs.DeCopy {
|
||||
user.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
|
||||
} else {
|
||||
user.Nickname = fmt.Sprintf("极客学长@%d", utils.RandomNumber(6))
|
||||
}
|
||||
|
||||
tx := h.DB.Begin()
|
||||
if err := tx.Create(&user).Error; err != nil {
|
||||
user, err := h.createNewUser(user, data.InviteCode)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 记录邀请关系
|
||||
if data.InviteCode != "" {
|
||||
// 增加邀请数量
|
||||
h.DB.Model(&model.InviteCode{}).Where("code = ?", data.InviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
|
||||
if h.App.SysConfig.InvitePower > 0 {
|
||||
err := h.userService.IncreasePower(int(inviteCode.UserId), h.App.SysConfig.InvitePower, model.PowerLog{
|
||||
Type: types.PowerInvite,
|
||||
Model: "Invite",
|
||||
Remark: fmt.Sprintf("邀请用户注册奖励,金额:%d,邀请码:%s,新用户:%s", h.App.SysConfig.InvitePower, inviteCode.Code, user.Username),
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 添加邀请记录
|
||||
err := tx.Create(&model.InviteLog{
|
||||
InviterId: inviteCode.UserId,
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
InviteCode: inviteCode.Code,
|
||||
Remark: fmt.Sprintf("奖励 %d 算力", h.App.SysConfig.InvitePower),
|
||||
}).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
_ = h.redis.Del(c, key) // 注册成功,删除短信验证码
|
||||
// 自动登录创建 token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": user.Id,
|
||||
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
|
||||
})
|
||||
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
||||
token, err := h.doLogin(&user, c.ClientIP())
|
||||
if err != nil {
|
||||
resp.ERROR(c, "Failed to generate token, "+err.Error())
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 保存到 redis
|
||||
key = fmt.Sprintf("users/%d", user.Id)
|
||||
if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil {
|
||||
resp.ERROR(c, "error with save token: "+err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c, gin.H{"token": tokenString, "user_id": user.Id, "username": user.Username})
|
||||
|
||||
resp.SUCCESS(c, gin.H{"token": token, "user_id": user.Id, "username": user.Username})
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
@@ -248,15 +207,12 @@ func (h *UserHandler) Login(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
verifyKey := fmt.Sprintf("users/verify/%s", data.Username)
|
||||
needVerify, err := h.redis.Get(c, verifyKey).Bool()
|
||||
|
||||
if h.App.SysConfig.EnabledVerify && needVerify {
|
||||
if h.captchaService.GetConfig().Enabled {
|
||||
var check bool
|
||||
if data.X != 0 {
|
||||
check = h.captcha.SlideCheck(data)
|
||||
check = h.captchaService.SlideCheck(data)
|
||||
} else {
|
||||
check = h.captcha.Check(data)
|
||||
check = h.captchaService.Check(data)
|
||||
}
|
||||
if !check {
|
||||
resp.ERROR(c, "请先完人机验证")
|
||||
@@ -267,54 +223,28 @@ func (h *UserHandler) Login(c *gin.Context) {
|
||||
var user model.User
|
||||
res := h.DB.Where("username = ?", data.Username).First(&user)
|
||||
if res.Error != nil {
|
||||
h.redis.Set(c, verifyKey, true, 0)
|
||||
resp.ERROR(c, "用户名不存在")
|
||||
return
|
||||
}
|
||||
|
||||
password := utils.GenPassword(data.Password, user.Salt)
|
||||
if password != user.Password {
|
||||
h.redis.Set(c, verifyKey, true, 0)
|
||||
resp.ERROR(c, "用户名或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Status == false {
|
||||
if !user.Status {
|
||||
resp.ERROR(c, "该用户已被禁止登录,请联系管理员")
|
||||
return
|
||||
}
|
||||
|
||||
// 更新最后登录时间和IP
|
||||
user.LastLoginIp = c.ClientIP()
|
||||
user.LastLoginAt = time.Now().Unix()
|
||||
h.DB.Model(&user).Updates(user)
|
||||
|
||||
h.DB.Create(&model.UserLoginLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
LoginIp: c.ClientIP(),
|
||||
LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()),
|
||||
})
|
||||
|
||||
// 创建 token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": user.Id,
|
||||
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
|
||||
})
|
||||
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
||||
token, err := h.doLogin(&user, c.ClientIP())
|
||||
if err != nil {
|
||||
resp.ERROR(c, "Failed to generate token, "+err.Error())
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 保存到 redis
|
||||
sessionKey := fmt.Sprintf("users/%d", user.Id)
|
||||
if _, err = h.redis.Set(c, sessionKey, tokenString, 0).Result(); err != nil {
|
||||
resp.ERROR(c, "error with save token: "+err.Error())
|
||||
return
|
||||
}
|
||||
// 移除登录行为验证码
|
||||
h.redis.Del(c, verifyKey)
|
||||
resp.SUCCESS(c, gin.H{"token": tokenString, "user_id": user.Id, "username": user.Username})
|
||||
|
||||
resp.SUCCESS(c, gin.H{"token": token, "user_id": user.Id, "username": user.Username})
|
||||
}
|
||||
|
||||
// Logout 注 销
|
||||
@@ -326,134 +256,165 @@ func (h *UserHandler) Logout(c *gin.Context) {
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// CLogin 第三方登录请求二维码
|
||||
func (h *UserHandler) CLogin(c *gin.Context) {
|
||||
returnURL := h.GetTrim(c, "return_url")
|
||||
var res types.BizVo
|
||||
apiURL := fmt.Sprintf("%s/api/clogin/request", h.App.Config.ApiConfig.ApiURL)
|
||||
r, err := req.C().R().SetBody(gin.H{"login_type": "wx", "return_url": returnURL}).
|
||||
SetHeader("AppId", h.App.Config.ApiConfig.AppId).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.App.Config.ApiConfig.Token)).
|
||||
SetSuccessResult(&res).
|
||||
Post(apiURL)
|
||||
// GetWxLoginQRCode 获取微信登录二维码URL
|
||||
func (h *UserHandler) GetWxLoginQRCode(c *gin.Context) {
|
||||
if !h.wxLoginService.GetConfig().Enabled {
|
||||
resp.ERROR(c, "微信登录功能未启用")
|
||||
return
|
||||
}
|
||||
|
||||
if h.wxLoginService.GetConfig().ApiKey == "" {
|
||||
resp.ERROR(c, "微信登录服务令牌未配置")
|
||||
return
|
||||
}
|
||||
|
||||
state := utils.RandString(32)
|
||||
qrCodeURL, err := h.wxLoginService.GetLoginQrCodeUrl(state)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
if r.IsErrorState() {
|
||||
resp.ERROR(c, "error with login http status: "+r.Status)
|
||||
return
|
||||
}
|
||||
|
||||
if res.Code != types.Success {
|
||||
resp.ERROR(c, "error with http response: "+res.Message)
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, res.Data)
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"url": qrCodeURL,
|
||||
"state": state,
|
||||
})
|
||||
}
|
||||
|
||||
// CLoginCallback 第三方登录回调
|
||||
func (h *UserHandler) CLoginCallback(c *gin.Context) {
|
||||
loginType := c.Query("login_type")
|
||||
code := c.Query("code")
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
action := c.Query("action")
|
||||
// 查询微信登录状态
|
||||
func (h *UserHandler) GetWxLoginState(c *gin.Context) {
|
||||
state := c.Query("state")
|
||||
if state == "" {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var res types.BizVo
|
||||
apiURL := fmt.Sprintf("%s/api/clogin/info", h.App.Config.ApiConfig.ApiURL)
|
||||
r, err := req.C().R().SetBody(gin.H{"login_type": loginType, "code": code}).
|
||||
SetHeader("AppId", h.App.Config.ApiConfig.AppId).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.App.Config.ApiConfig.Token)).
|
||||
SetSuccessResult(&res).
|
||||
Post(apiURL)
|
||||
status, err := h.wxLoginService.GetLoginStatus(state)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
if r.IsErrorState() {
|
||||
resp.ERROR(c, "error with login http status: "+r.Status)
|
||||
|
||||
if status.Status != service.LoginStatusSuccess {
|
||||
resp.SUCCESS(c, status)
|
||||
return
|
||||
}
|
||||
|
||||
if res.Code != types.Success {
|
||||
resp.ERROR(c, "error with http response: "+res.Message)
|
||||
return
|
||||
}
|
||||
|
||||
// login successfully
|
||||
data := res.Data.(map[string]interface{})
|
||||
// 登录成功
|
||||
var user model.User
|
||||
if action == "bind" && userId > 0 {
|
||||
err = h.DB.Where("openid", data["openid"]).First(&user).Error
|
||||
if err == nil {
|
||||
resp.ERROR(c, "该微信已经绑定其他账号,请先解绑")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.DB.Where("id", userId).First(&user).Error
|
||||
h.DB.Where("openid = ?", status.OpenID).First(&user)
|
||||
if user.Id == 0 {
|
||||
// 创建新用户
|
||||
user, err = h.createNewUser(model.User{OpenId: status.OpenID}, "")
|
||||
if err != nil {
|
||||
resp.ERROR(c, "绑定用户不存在")
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = h.DB.Model(&user).UpdateColumn("openid", data["openid"]).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "更新用户信息失败,"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{"token": ""})
|
||||
token, err := h.doLogin(&user, c.ClientIP())
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
session := gin.H{}
|
||||
tx := h.DB.Where("openid", data["openid"]).First(&user)
|
||||
if tx.Error != nil {
|
||||
// create new user
|
||||
var totalUser int64
|
||||
h.DB.Model(&model.User{}).Count(&totalUser)
|
||||
if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum {
|
||||
resp.ERROR(c, "当前注册用户数已达上限,请请升级 License")
|
||||
return
|
||||
}
|
||||
status.Status = service.LoginStatusExpired
|
||||
h.wxLoginService.SetLoginStatus(state, *status)
|
||||
|
||||
salt := utils.RandString(8)
|
||||
password := fmt.Sprintf("%d", utils.RandomNumber(8))
|
||||
user = model.User{
|
||||
Username: fmt.Sprintf("%s@%d", loginType, utils.RandomNumber(10)),
|
||||
Password: utils.GenPassword(password, salt),
|
||||
Avatar: fmt.Sprintf("%s", data["avatar"]),
|
||||
Salt: salt,
|
||||
Status: true,
|
||||
ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
|
||||
Power: h.App.SysConfig.InitPower,
|
||||
OpenId: fmt.Sprintf("%s", data["openid"]),
|
||||
Nickname: fmt.Sprintf("%s", data["nickname"]),
|
||||
}
|
||||
status.Status = service.LoginStatusSuccess
|
||||
status.Token = token
|
||||
resp.SUCCESS(c, status)
|
||||
}
|
||||
|
||||
tx = h.DB.Create(&user)
|
||||
if tx.Error != nil {
|
||||
resp.ERROR(c, "保存数据失败")
|
||||
logger.Error(tx.Error)
|
||||
return
|
||||
// createNewUser 创建新用户
|
||||
func (h *UserHandler) createNewUser(user model.User, inviteCode string) (model.User, error) {
|
||||
if user.OpenId != "" {
|
||||
user.Platform = "wechat"
|
||||
user.Nickname = fmt.Sprintf("微信用户@%d", utils.RandomNumber(6))
|
||||
user.Username = fmt.Sprintf("wx@%d", utils.RandomNumber(8))
|
||||
user.Password = "geekai123"
|
||||
} else {
|
||||
user.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
|
||||
if user.Username == "" || user.Password == "" {
|
||||
return user, fmt.Errorf("用户名或密码不能为空")
|
||||
}
|
||||
session["username"] = user.Username
|
||||
session["password"] = password
|
||||
} else { // login directly
|
||||
// 更新最后登录时间和IP
|
||||
user.LastLoginIp = c.ClientIP()
|
||||
user.LastLoginAt = time.Now().Unix()
|
||||
h.DB.Model(&user).Updates(user)
|
||||
|
||||
h.DB.Create(&model.UserLoginLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
LoginIp: c.ClientIP(),
|
||||
LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()),
|
||||
})
|
||||
}
|
||||
|
||||
salt := utils.RandString(8)
|
||||
user.Salt = salt
|
||||
user.Password = utils.GenPassword(user.Password, salt)
|
||||
user.Avatar = "/images/avatar/user.png"
|
||||
user.Status = true
|
||||
user.ChatRoles = utils.JsonEncode([]string{"gpt"})
|
||||
user.ChatConfig = "{}"
|
||||
user.ChatModels = "{}"
|
||||
user.Power = h.App.SysConfig.Base.InitPower
|
||||
|
||||
// 创建用户
|
||||
tx := h.DB.Begin()
|
||||
if err := tx.Create(&user).Error; err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
// 记录邀请关系
|
||||
if inviteCode != "" {
|
||||
inviteCode := model.InviteCode{}
|
||||
err := h.DB.Where("code = ?", inviteCode).First(&inviteCode).Error
|
||||
if err != nil {
|
||||
return user, fmt.Errorf("无效的邀请码")
|
||||
}
|
||||
|
||||
// 增加邀请数量
|
||||
h.DB.Model(&model.InviteCode{}).Where("code = ?", inviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
|
||||
if h.App.SysConfig.Base.InvitePower > 0 {
|
||||
err := h.userService.IncreasePower(inviteCode.UserId, h.App.SysConfig.Base.InvitePower, model.PowerLog{
|
||||
Type: types.PowerInvite,
|
||||
Model: "Invite",
|
||||
Remark: fmt.Sprintf("邀请用户注册奖励,金额:%d,邀请码:%s,新用户:%s", h.App.SysConfig.Base.InvitePower, inviteCode.Code, user.Username),
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return user, err
|
||||
}
|
||||
|
||||
// 添加邀请记录
|
||||
err = tx.Create(&model.InviteLog{
|
||||
InviterId: inviteCode.UserId,
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
InviteCode: inviteCode.Code,
|
||||
Remark: fmt.Sprintf("奖励 %d 算力", h.App.SysConfig.Base.InvitePower),
|
||||
}).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return user, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// doLogin 执行登录操作
|
||||
func (h *UserHandler) doLogin(user *model.User, ip string) (string, error) {
|
||||
// 更新最后登录时间和IP
|
||||
user.LastLoginIp = ip
|
||||
user.LastLoginAt = time.Now().Unix()
|
||||
err := h.DB.Model(user).Updates(user).Error
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to update user: %v", err)
|
||||
}
|
||||
|
||||
// 记录登录日志
|
||||
h.DB.Create(&model.UserLoginLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
LoginIp: ip,
|
||||
LoginAddress: utils.Ip2Region(h.ipSearcher, ip),
|
||||
})
|
||||
|
||||
// 创建 token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": user.Id,
|
||||
@@ -461,17 +422,42 @@ func (h *UserHandler) CLoginCallback(c *gin.Context) {
|
||||
})
|
||||
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
||||
if err != nil {
|
||||
resp.ERROR(c, "Failed to generate token, "+err.Error())
|
||||
return
|
||||
return "", fmt.Errorf("failed to generate token: %v", err)
|
||||
}
|
||||
|
||||
// 保存到 redis
|
||||
key := fmt.Sprintf("users/%d", user.Id)
|
||||
if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil {
|
||||
resp.ERROR(c, "error with save token: "+err.Error())
|
||||
sessionKey := fmt.Sprintf("users/%d", user.Id)
|
||||
if _, err = h.redis.Set(context.Background(), sessionKey, tokenString, 0).Result(); err != nil {
|
||||
return "", fmt.Errorf("error with save token: %v", err)
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// WxLoginCallback 微信登录回调处理
|
||||
func (h *UserHandler) WxLoginCallback(c *gin.Context) {
|
||||
var data struct {
|
||||
OpenID string `json:"openid"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
session["token"] = tokenString
|
||||
resp.SUCCESS(c, session)
|
||||
|
||||
if data.OpenID == "" || data.State == "" {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 设置登录状态
|
||||
status := service.LoginStatus{
|
||||
Status: service.LoginStatusSuccess,
|
||||
OpenID: data.OpenID,
|
||||
}
|
||||
h.wxLoginService.SetLoginStatus(data.State, status)
|
||||
|
||||
resp.SUCCESS(c, status)
|
||||
}
|
||||
|
||||
// Session 获取/验证会话
|
||||
@@ -735,11 +721,11 @@ func (h *UserHandler) SignIn(c *gin.Context) {
|
||||
|
||||
// 签到
|
||||
h.levelDB.Put(key, true)
|
||||
if h.App.SysConfig.DailyPower > 0 {
|
||||
h.userService.IncreasePower(int(userId), h.App.SysConfig.DailyPower, model.PowerLog{
|
||||
if h.App.SysConfig.Base.DailyPower > 0 {
|
||||
h.userService.IncreasePower(userId, h.App.SysConfig.Base.DailyPower, model.PowerLog{
|
||||
Type: types.PowerSignIn,
|
||||
Model: "SignIn",
|
||||
Remark: fmt.Sprintf("每日签到奖励,金额:%d", h.App.SysConfig.DailyPower),
|
||||
Remark: fmt.Sprintf("每日签到奖励,金额:%d", h.App.SysConfig.Base.DailyPower),
|
||||
})
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
|
||||
@@ -10,8 +10,10 @@ package handler
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/moderation"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/video"
|
||||
"geekai/store/model"
|
||||
@@ -26,20 +28,37 @@ import (
|
||||
|
||||
type VideoHandler struct {
|
||||
BaseHandler
|
||||
videoService *video.Service
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
videoService *video.Service
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
moderationManager *moderation.ServiceManager
|
||||
}
|
||||
|
||||
func NewVideoHandler(app *core.AppServer, db *gorm.DB, service *video.Service, uploader *oss.UploaderManager, userService *service.UserService) *VideoHandler {
|
||||
func NewVideoHandler(app *core.AppServer, db *gorm.DB, service *video.Service, uploader *oss.UploaderManager, userService *service.UserService, moderationManager *moderation.ServiceManager) *VideoHandler {
|
||||
return &VideoHandler{
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
},
|
||||
videoService: service,
|
||||
uploader: uploader,
|
||||
userService: userService,
|
||||
videoService: service,
|
||||
uploader: uploader,
|
||||
userService: userService,
|
||||
moderationManager: moderationManager,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *VideoHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/video/")
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("luma/create", h.LumaCreate)
|
||||
group.POST("keling/create", h.KeLingCreate)
|
||||
group.GET("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,13 +81,36 @@ func (h *VideoHandler) LumaCreate(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if h.App.SysConfig.Moderation.Enable {
|
||||
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
|
||||
if err != nil {
|
||||
logger.Error("failed to moderate content: ", err)
|
||||
}
|
||||
if moderationResult.Flagged {
|
||||
// 记录违规内容
|
||||
moderation := model.Moderation{
|
||||
UserId: h.GetLoginUserId(c),
|
||||
Source: types.ModerationSourceVideo,
|
||||
Input: data.Prompt,
|
||||
Result: utils.JsonEncode(moderationResult),
|
||||
}
|
||||
err = h.DB.Create(&moderation).Error
|
||||
if err != nil {
|
||||
logger.Error("failed to save moderation: ", err)
|
||||
}
|
||||
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
if user.Power < h.App.SysConfig.LumaPower {
|
||||
if user.Power < h.App.SysConfig.Base.LumaPower {
|
||||
resp.ERROR(c, "您的算力不足,请充值后再试!")
|
||||
return
|
||||
}
|
||||
@@ -85,14 +127,14 @@ func (h *VideoHandler) LumaCreate(c *gin.Context) {
|
||||
Type: types.VideoLuma,
|
||||
Prompt: data.Prompt,
|
||||
Params: params,
|
||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
||||
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
||||
}
|
||||
// 插入数据库
|
||||
job := model.VideoJob{
|
||||
UserId: userId,
|
||||
UserId: uint(userId),
|
||||
Type: types.VideoLuma,
|
||||
Prompt: data.Prompt,
|
||||
Power: h.App.SysConfig.LumaPower,
|
||||
Power: h.App.SysConfig.Base.LumaPower,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
}
|
||||
tx := h.DB.Create(&job)
|
||||
@@ -147,7 +189,7 @@ func (h *VideoHandler) KeLingCreate(c *gin.Context) {
|
||||
|
||||
// 计算当前任务所需算力
|
||||
key := fmt.Sprintf("%s_%s_%s", data.Model, data.Mode, data.Duration)
|
||||
power := h.App.SysConfig.KeLingPowers[key]
|
||||
power := h.App.SysConfig.Base.KeLingPowers[key]
|
||||
if power == 0 {
|
||||
resp.ERROR(c, "当前模型暂不支持")
|
||||
return
|
||||
@@ -181,12 +223,12 @@ func (h *VideoHandler) KeLingCreate(c *gin.Context) {
|
||||
Type: types.VideoKeLing,
|
||||
Prompt: data.Prompt,
|
||||
Params: params,
|
||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
||||
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
||||
Channel: data.Channel,
|
||||
}
|
||||
// 插入数据库
|
||||
job := model.VideoJob{
|
||||
UserId: userId,
|
||||
UserId: uint(userId),
|
||||
Type: types.VideoKeLing,
|
||||
Prompt: data.Prompt,
|
||||
Power: power,
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"context"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Websocket 连接处理 handler
|
||||
|
||||
type WebsocketHandler struct {
|
||||
BaseHandler
|
||||
wsService *service.WebsocketService
|
||||
chatHandler *ChatHandler
|
||||
}
|
||||
|
||||
func NewWebsocketHandler(app *core.AppServer, s *service.WebsocketService, db *gorm.DB, chatHandler *ChatHandler) *WebsocketHandler {
|
||||
return &WebsocketHandler{
|
||||
BaseHandler: BaseHandler{App: app, DB: db},
|
||||
chatHandler: chatHandler,
|
||||
wsService: s,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WebsocketHandler) Client(c *gin.Context) {
|
||||
clientProtocols := c.GetHeader("Sec-WebSocket-Protocol")
|
||||
ws, err := (&websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
Subprotocols: strings.Split(clientProtocols, ","),
|
||||
}).Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
clientId := c.Query("client_id")
|
||||
client := types.NewWsClient(ws, clientId)
|
||||
userId := h.GetLoginUserId(c)
|
||||
if userId == 0 {
|
||||
_ = client.Send([]byte("Invalid user_id"))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
var user model.User
|
||||
if err := h.DB.Where("id", userId).First(&user).Error; err != nil {
|
||||
_ = client.Send([]byte("Invalid user_id"))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
h.wsService.Clients.Put(clientId, client)
|
||||
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
|
||||
go func() {
|
||||
for {
|
||||
_, msg, err := client.Receive()
|
||||
if err != nil {
|
||||
logger.Debugf("close connection: %s", client.Conn.RemoteAddr())
|
||||
client.Close()
|
||||
h.wsService.Clients.Delete(clientId)
|
||||
break
|
||||
}
|
||||
|
||||
var message types.InputMessage
|
||||
err = utils.JsonDecode(string(msg), &message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debugf("Receive a message:%+v", message)
|
||||
if message.Type == types.MsgTypePing {
|
||||
utils.SendChannelMsg(client, types.ChPing, "pong")
|
||||
continue
|
||||
}
|
||||
|
||||
// 当前只处理聊天消息,其他消息全部丢弃
|
||||
var chatMessage types.ChatMessage
|
||||
err = utils.JsonDecode(utils.JsonEncode(message.Body), &chatMessage)
|
||||
if err != nil || message.Channel != types.ChChat {
|
||||
logger.Warnf("invalid message body:%+v", message.Body)
|
||||
continue
|
||||
}
|
||||
var chatRole model.ChatRole
|
||||
err = h.DB.First(&chatRole, chatMessage.RoleId).Error
|
||||
if err != nil || !chatRole.Enable {
|
||||
utils.SendAndFlush(client, "当前聊天角色不存在或者未启用,请更换角色之后再发起对话!!!")
|
||||
continue
|
||||
}
|
||||
// if the role bind a model_id, use role's bind model_id
|
||||
if chatRole.ModelId > 0 {
|
||||
chatMessage.RoleId = chatRole.ModelId
|
||||
}
|
||||
// get model info
|
||||
var chatModel model.ChatModel
|
||||
err = h.DB.Where("id", chatMessage.ModelId).First(&chatModel).Error
|
||||
if err != nil || chatModel.Enabled == false {
|
||||
utils.SendAndFlush(client, "当前AI模型暂未启用,请更换模型后再发起对话!!!")
|
||||
continue
|
||||
}
|
||||
|
||||
session := &types.ChatSession{
|
||||
ClientIP: c.ClientIP(),
|
||||
UserId: userId,
|
||||
}
|
||||
|
||||
// use old chat data override the chat model and role ID
|
||||
var chat model.ChatItem
|
||||
h.DB.Where("chat_id", chatMessage.ChatId).First(&chat)
|
||||
if chat.Id > 0 {
|
||||
chatModel.Id = chat.ModelId
|
||||
chatMessage.RoleId = int(chat.RoleId)
|
||||
}
|
||||
|
||||
session.ChatId = chatMessage.ChatId
|
||||
session.Tools = chatMessage.Tools
|
||||
session.Stream = chatMessage.Stream
|
||||
// 复制模型数据
|
||||
err = utils.CopyObject(chatModel, &session.Model)
|
||||
if err != nil {
|
||||
logger.Error(err, chatModel)
|
||||
}
|
||||
session.Model.Id = chatModel.Id
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
h.chatHandler.ReqCancelFunc.Put(clientId, cancel)
|
||||
err = h.chatHandler.sendMessage(ctx, session, chatRole, chatMessage.Content, client)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
utils.SendAndFlush(client, err.Error())
|
||||
} else {
|
||||
utils.SendMsg(client, types.ReplyMessage{Channel: types.ChChat, Type: types.MsgTypeEnd})
|
||||
logger.Infof("回答完毕: %v", message.Body)
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -8,11 +8,12 @@ package logger
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var logger *zap.Logger
|
||||
@@ -23,7 +24,7 @@ func GetLogger() *zap.SugaredLogger {
|
||||
return sugarLogger
|
||||
}
|
||||
|
||||
logLevel := zap.NewAtomicLevelAt(getLogLevel(os.Getenv("LOG_LEVEL")))
|
||||
logLevel := zap.NewAtomicLevelAt(getLogLevel(os.Getenv("GEEKAI_LOG_LEVEL")))
|
||||
encoder := getEncoder()
|
||||
writerSyncer := getLogWriter()
|
||||
fileCore := zapcore.NewCore(encoder, writerSyncer, logLevel)
|
||||
|
||||
+131
-258
@@ -17,7 +17,9 @@ import (
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service"
|
||||
"geekai/service/dalle"
|
||||
"geekai/service/jimeng"
|
||||
"geekai/service/mj"
|
||||
"geekai/service/moderation"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/payment"
|
||||
"geekai/service/sd"
|
||||
@@ -29,7 +31,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"runtime/debug"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -70,15 +72,16 @@ func main() {
|
||||
if configFile == "" {
|
||||
configFile = "config.toml"
|
||||
}
|
||||
debug, _ := strconv.ParseBool(os.Getenv("APP_DEBUG"))
|
||||
logger.Info("Loading config file: ", configFile)
|
||||
if !debug {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Error("Panic Error:", err)
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Error("Panic Error:", err)
|
||||
// 打印堆栈信息
|
||||
if os.Getenv("GEEKAI_DEBUG") == "true" {
|
||||
debug.PrintStack()
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
app := fx.New(
|
||||
// 初始化配置应用配置
|
||||
@@ -88,16 +91,16 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
config.Path = configFile
|
||||
if debug {
|
||||
_ = core.SaveConfig(config)
|
||||
}
|
||||
return config
|
||||
}),
|
||||
// 创建应用服务
|
||||
fx.Provide(core.NewServer),
|
||||
// 初始化
|
||||
fx.Invoke(func(s *core.AppServer, client *redis.Client) {
|
||||
s.Init(debug, client)
|
||||
s.Init(client)
|
||||
}),
|
||||
fx.Provide(func(db *gorm.DB) *types.SystemConfig {
|
||||
return core.LoadSystemConfig(db)
|
||||
}),
|
||||
|
||||
// 初始化数据库
|
||||
@@ -125,7 +128,7 @@ func main() {
|
||||
}),
|
||||
|
||||
// 创建控制器
|
||||
fx.Provide(handler.NewChatRoleHandler),
|
||||
fx.Provide(handler.NewChatAppHandler),
|
||||
fx.Provide(handler.NewUserHandler),
|
||||
fx.Provide(handler.NewChatHandler),
|
||||
fx.Provide(handler.NewNetHandler),
|
||||
@@ -140,7 +143,14 @@ func main() {
|
||||
fx.Provide(handler.NewProductHandler),
|
||||
fx.Provide(handler.NewConfigHandler),
|
||||
fx.Provide(handler.NewPowerLogHandler),
|
||||
fx.Provide(handler.NewJimengHandler),
|
||||
|
||||
fx.Provide(service.NewMigrationService),
|
||||
fx.Invoke(func(migrationService *service.MigrationService) {
|
||||
migrationService.StartMigrate()
|
||||
}),
|
||||
|
||||
// 管理后台控制器
|
||||
fx.Provide(admin.NewConfigHandler),
|
||||
fx.Provide(admin.NewAdminHandler),
|
||||
fx.Provide(admin.NewApiKeyHandler),
|
||||
@@ -151,21 +161,8 @@ func main() {
|
||||
fx.Provide(admin.NewChatModelHandler),
|
||||
fx.Provide(admin.NewProductHandler),
|
||||
fx.Provide(admin.NewOrderHandler),
|
||||
fx.Provide(admin.NewChatHandler),
|
||||
fx.Provide(admin.NewPowerLogHandler),
|
||||
|
||||
// 创建服务
|
||||
fx.Provide(sms.NewSendServiceManager),
|
||||
fx.Provide(func(config *types.AppConfig) *service.CaptchaService {
|
||||
return service.NewCaptchaService(config.ApiConfig)
|
||||
}),
|
||||
fx.Provide(oss.NewUploaderManager),
|
||||
fx.Provide(dalle.NewService),
|
||||
fx.Invoke(func(s *dalle.Service) {
|
||||
s.Run()
|
||||
s.DownloadImages()
|
||||
s.CheckTaskStatus()
|
||||
}),
|
||||
fx.Provide(admin.NewAdminJimengHandler),
|
||||
|
||||
// 邮件服务
|
||||
fx.Provide(service.NewSmtpService),
|
||||
@@ -175,6 +172,14 @@ func main() {
|
||||
licenseService.SyncLicense()
|
||||
}),
|
||||
|
||||
// Dalle 服务
|
||||
fx.Provide(dalle.NewService),
|
||||
fx.Invoke(func(s *dalle.Service) {
|
||||
s.Run()
|
||||
s.DownloadImages()
|
||||
s.CheckTaskStatus()
|
||||
}),
|
||||
|
||||
// MidJourney service pool
|
||||
fx.Provide(mj.NewService),
|
||||
fx.Provide(mj.NewClient),
|
||||
@@ -203,329 +208,205 @@ func main() {
|
||||
s.SyncTaskProgress()
|
||||
s.DownloadFiles()
|
||||
}),
|
||||
fx.Provide(service.NewUserService),
|
||||
fx.Provide(payment.NewAlipayService),
|
||||
fx.Provide(payment.NewHuPiPay),
|
||||
fx.Provide(payment.NewJPayService),
|
||||
fx.Provide(payment.NewWechatService),
|
||||
|
||||
// 即梦AI 服务
|
||||
fx.Provide(jimeng.NewClient),
|
||||
fx.Provide(jimeng.NewService),
|
||||
fx.Invoke(func(service *jimeng.Service) {
|
||||
service.Start()
|
||||
}),
|
||||
|
||||
fx.Provide(service.NewSnowflake),
|
||||
fx.Provide(service.NewXXLJobExecutor),
|
||||
fx.Invoke(func(exec *service.XXLJobExecutor, config *types.AppConfig) {
|
||||
if config.XXLConfig.Enabled {
|
||||
go func() {
|
||||
log.Fatal(exec.Run())
|
||||
}()
|
||||
}
|
||||
|
||||
// 创建短信服务
|
||||
fx.Provide(sms.NewAliYunSmsService),
|
||||
fx.Provide(sms.NewBaoSmsService),
|
||||
fx.Provide(sms.NewSmsManager),
|
||||
fx.Provide(func(config *types.SystemConfig) *service.CaptchaService {
|
||||
return service.NewCaptchaService(config.Captcha)
|
||||
}),
|
||||
fx.Provide(func(config *types.SystemConfig, client *redis.Client) *service.WxLoginService {
|
||||
return service.NewWxLoginService(config.WxLogin, client)
|
||||
}),
|
||||
|
||||
// 支付服务
|
||||
fx.Provide(payment.NewAlipayService),
|
||||
fx.Provide(payment.NewEPayService),
|
||||
fx.Provide(payment.NewWxpayService),
|
||||
|
||||
// 文件上传服务
|
||||
fx.Provide(oss.NewLocalStorage),
|
||||
fx.Provide(oss.NewMiniOss),
|
||||
fx.Provide(oss.NewQiNiuOss),
|
||||
fx.Provide(oss.NewAliYunOss),
|
||||
fx.Provide(oss.NewUploaderManager),
|
||||
|
||||
// 用户服务
|
||||
fx.Provide(service.NewUserService),
|
||||
|
||||
// 文本审查服务
|
||||
fx.Provide(moderation.NewGiteeAIModeration),
|
||||
fx.Provide(moderation.NewBaiduAIModeration),
|
||||
fx.Provide(moderation.NewTencentAIModeration),
|
||||
fx.Provide(moderation.NewServiceManager),
|
||||
fx.Provide(admin.NewModerationHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ModerationHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
|
||||
// 注册路由
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) {
|
||||
group := s.Engine.Group("/api/app/")
|
||||
group.GET("list", h.List)
|
||||
group.GET("list/user", h.ListByUser)
|
||||
group.POST("update", h.UpdateRole)
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatAppHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.UserHandler) {
|
||||
group := s.Engine.Group("/api/user/")
|
||||
group.POST("register", h.Register)
|
||||
group.POST("login", h.Login)
|
||||
group.GET("logout", h.Logout)
|
||||
group.GET("session", h.Session)
|
||||
group.GET("profile", h.Profile)
|
||||
group.POST("profile/update", h.ProfileUpdate)
|
||||
group.POST("password", h.UpdatePass)
|
||||
group.POST("bind/mobile", h.BindMobile)
|
||||
group.POST("bind/email", h.BindEmail)
|
||||
group.POST("resetPass", h.ResetPass)
|
||||
group.GET("clogin", h.CLogin)
|
||||
group.GET("clogin/callback", h.CLoginCallback)
|
||||
group.GET("signin", h.SignIn)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatHandler) {
|
||||
group := s.Engine.Group("/api/chat/")
|
||||
group.GET("list", h.List)
|
||||
group.GET("detail", h.Detail)
|
||||
group.POST("update", h.Update)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("history", h.History)
|
||||
group.GET("clear", h.Clear)
|
||||
group.POST("tokens", h.Tokens)
|
||||
group.GET("stop", h.StopGenerate)
|
||||
group.POST("tts", h.TextToSpeech)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.NetHandler) {
|
||||
s.Engine.POST("/api/upload", h.Upload)
|
||||
s.Engine.POST("/api/upload/list", h.List)
|
||||
s.Engine.GET("/api/upload/remove", h.Remove)
|
||||
s.Engine.GET("/api/download", h.Download)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) {
|
||||
group := s.Engine.Group("/api/sms/")
|
||||
group.POST("code", h.SendCode)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) {
|
||||
group := s.Engine.Group("/api/captcha/")
|
||||
group.GET("get", h.Get)
|
||||
group.POST("check", h.Check)
|
||||
group.GET("slide/get", h.SlideGet)
|
||||
group.POST("slide/check", h.SlideCheck)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.RedeemHandler) {
|
||||
group := s.Engine.Group("/api/redeem/")
|
||||
group.POST("verify", h.Verify)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) {
|
||||
group := s.Engine.Group("/api/mj/")
|
||||
group.POST("image", h.Image)
|
||||
group.POST("upscale", h.Upscale)
|
||||
group.POST("variation", h.Variation)
|
||||
group.GET("jobs", h.JobList)
|
||||
group.GET("imgWall", h.ImgWall)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.SdJobHandler) {
|
||||
group := s.Engine.Group("/api/sd")
|
||||
group.POST("image", h.Image)
|
||||
group.GET("jobs", h.JobList)
|
||||
group.GET("imgWall", h.ImgWall)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ConfigHandler) {
|
||||
group := s.Engine.Group("/api/config/")
|
||||
group.GET("get", h.Get)
|
||||
group.GET("license", h.License)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
|
||||
// 管理后台控制器
|
||||
// 管理后台路由注册
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ConfigHandler) {
|
||||
group := s.Engine.Group("/api/admin/config")
|
||||
group.POST("update", h.Update)
|
||||
group.GET("get", h.Get)
|
||||
group.POST("active", h.Active)
|
||||
group.GET("fixData", h.FixData)
|
||||
group.GET("license", h.GetLicense)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ManagerHandler) {
|
||||
group := s.Engine.Group("/api/admin/")
|
||||
group.POST("login", h.Login)
|
||||
group.GET("logout", h.Logout)
|
||||
group.GET("session", h.Session)
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.POST("enable", h.Enable)
|
||||
group.GET("remove", h.Remove)
|
||||
group.POST("resetPass", h.ResetPass)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ApiKeyHandler) {
|
||||
group := s.Engine.Group("/api/admin/apikey/")
|
||||
group.POST("save", h.Save)
|
||||
group.GET("list", h.List)
|
||||
group.POST("set", h.Set)
|
||||
group.GET("remove", h.Remove)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.UserHandler) {
|
||||
group := s.Engine.Group("/api/admin/user/")
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("loginLog", h.LoginLog)
|
||||
group.GET("genLoginLink", h.GenLoginLink)
|
||||
group.POST("resetPass", h.ResetPass)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatAppHandler) {
|
||||
group := s.Engine.Group("/api/admin/role/")
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.POST("sort", h.Sort)
|
||||
group.POST("set", h.Set)
|
||||
group.GET("remove", h.Remove)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.RedeemHandler) {
|
||||
group := s.Engine.Group("/api/admin/redeem/")
|
||||
group.GET("list", h.List)
|
||||
group.POST("create", h.Create)
|
||||
group.POST("set", h.Set)
|
||||
group.GET("remove", h.Remove)
|
||||
group.POST("export", h.Export)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) {
|
||||
group := s.Engine.Group("/api/admin/dashboard/")
|
||||
group.GET("stats", h.Stats)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatModelHandler) {
|
||||
group := s.Engine.Group("/api/model/")
|
||||
group.GET("list", h.List)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatModelHandler) {
|
||||
group := s.Engine.Group("/api/admin/model/")
|
||||
group.POST("save", h.Save)
|
||||
group.GET("list", h.List)
|
||||
group.POST("set", h.Set)
|
||||
group.POST("sort", h.Sort)
|
||||
group.GET("remove", h.Remove)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.PaymentHandler) {
|
||||
group := s.Engine.Group("/api/payment/")
|
||||
group.POST("doPay", h.Pay)
|
||||
group.GET("payWays", h.GetPayWays)
|
||||
group.POST("notify/alipay", h.AlipayNotify)
|
||||
group.GET("notify/geek", h.GeekPayNotify)
|
||||
group.POST("notify/wechat", h.WechatPayNotify)
|
||||
group.POST("notify/hupi", h.HuPiPayNotify)
|
||||
h.RegisterRoutes()
|
||||
h.StartSyncOrders()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ProductHandler) {
|
||||
group := s.Engine.Group("/api/admin/product/")
|
||||
group.POST("save", h.Save)
|
||||
group.GET("list", h.List)
|
||||
group.POST("enable", h.Enable)
|
||||
group.POST("sort", h.Sort)
|
||||
group.GET("remove", h.Remove)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.OrderHandler) {
|
||||
group := s.Engine.Group("/api/admin/order/")
|
||||
group.POST("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("clear", h.Clear)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.OrderHandler) {
|
||||
group := s.Engine.Group("/api/order/")
|
||||
group.GET("list", h.List)
|
||||
group.GET("query", h.Query)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ProductHandler) {
|
||||
group := s.Engine.Group("/api/product/")
|
||||
group.GET("list", h.List)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
|
||||
fx.Provide(handler.NewInviteHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.InviteHandler) {
|
||||
group := s.Engine.Group("/api/invite/")
|
||||
group.GET("code", h.Code)
|
||||
group.GET("list", h.List)
|
||||
group.GET("hits", h.Hits)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
|
||||
fx.Provide(admin.NewFunctionHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.FunctionHandler) {
|
||||
group := s.Engine.Group("/api/admin/function/")
|
||||
group.POST("save", h.Save)
|
||||
group.POST("set", h.Set)
|
||||
group.GET("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("token", h.GenToken)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
|
||||
fx.Provide(admin.NewUploadHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.UploadHandler) {
|
||||
s.Engine.POST("/api/admin/upload", h.Upload)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
|
||||
fx.Provide(handler.NewFunctionHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.FunctionHandler) {
|
||||
group := s.Engine.Group("/api/function/")
|
||||
group.POST("weibo", h.WeiBo)
|
||||
group.POST("zaobao", h.ZaoBao)
|
||||
group.POST("dalle3", h.Dall3)
|
||||
group.POST("websearch", h.WebSearch)
|
||||
group.GET("list", h.List)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(admin.NewChatHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatHandler) {
|
||||
group := s.Engine.Group("/api/admin/chat/")
|
||||
group.POST("list", h.List)
|
||||
group.POST("message", h.Messages)
|
||||
group.GET("history", h.History)
|
||||
group.GET("remove", h.RemoveChat)
|
||||
group.GET("message/remove", h.RemoveMessage)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.PowerLogHandler) {
|
||||
group := s.Engine.Group("/api/powerLog/")
|
||||
group.POST("list", h.List)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.PowerLogHandler) {
|
||||
group := s.Engine.Group("/api/admin/powerLog/")
|
||||
group.POST("list", h.List)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(admin.NewMenuHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.MenuHandler) {
|
||||
group := s.Engine.Group("/api/admin/menu/")
|
||||
group.POST("save", h.Save)
|
||||
group.GET("list", h.List)
|
||||
group.POST("enable", h.Enable)
|
||||
group.POST("sort", h.Sort)
|
||||
group.GET("remove", h.Remove)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewMenuHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.MenuHandler) {
|
||||
group := s.Engine.Group("/api/menu/")
|
||||
group.GET("list", h.List)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewMarkMapHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.MarkMapHandler) {
|
||||
s.Engine.POST("/api/markMap/gen", h.Generate)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewDallJobHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.DallJobHandler) {
|
||||
group := s.Engine.Group("/api/dall")
|
||||
group.POST("image", h.Image)
|
||||
group.GET("jobs", h.JobList)
|
||||
group.GET("imgWall", h.ImgWall)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
group.GET("models", h.GetModels)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewSunoHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.SunoHandler) {
|
||||
group := s.Engine.Group("/api/suno")
|
||||
group.POST("create", h.Create)
|
||||
group.GET("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
group.POST("update", h.Update)
|
||||
group.GET("detail", h.Detail)
|
||||
group.GET("play", h.Play)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewVideoHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.VideoHandler) {
|
||||
group := s.Engine.Group("/api/video")
|
||||
group.POST("luma/create", h.LumaCreate)
|
||||
group.POST("keling/create", h.KeLingCreate)
|
||||
group.GET("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
|
||||
// 即梦AI 路由
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.JimengHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.AdminJimengHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(admin.NewChatAppTypeHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatAppTypeHandler) {
|
||||
group := s.Engine.Group("/api/admin/app/type")
|
||||
group.POST("save", h.Save)
|
||||
group.GET("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.POST("enable", h.Enable)
|
||||
group.POST("sort", h.Sort)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewChatAppTypeHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatAppTypeHandler) {
|
||||
group := s.Engine.Group("/api/app/type")
|
||||
group.GET("list", h.List)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewTestHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.TestHandler) {
|
||||
group := s.Engine.Group("/api/test")
|
||||
group.Any("sse", h.PostTest, h.SseTest)
|
||||
}),
|
||||
fx.Provide(service.NewWebsocketService),
|
||||
fx.Provide(handler.NewWebsocketHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.WebsocketHandler) {
|
||||
s.Engine.Any("/api/ws", h.Client)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewPromptHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.PromptHandler) {
|
||||
group := s.Engine.Group("/api/prompt")
|
||||
group.POST("/lyric", h.Lyric)
|
||||
group.POST("/image", h.Image)
|
||||
group.POST("/video", h.Video)
|
||||
group.POST("/meta", h.MetaPrompt)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
|
||||
go func() {
|
||||
@@ -550,23 +431,15 @@ func main() {
|
||||
}),
|
||||
fx.Provide(admin.NewImageHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ImageHandler) {
|
||||
group := s.Engine.Group("/api/admin/image")
|
||||
group.POST("/list/mj", h.MjList)
|
||||
group.POST("/list/sd", h.SdList)
|
||||
group.POST("/list/dall", h.DallList)
|
||||
group.GET("/remove", h.Remove)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(admin.NewMediaHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.MediaHandler) {
|
||||
group := s.Engine.Group("/api/admin/media")
|
||||
group.POST("/suno", h.SunoList)
|
||||
group.POST("/videos", h.Videos)
|
||||
group.GET("/remove", h.Remove)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewRealtimeHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.RealtimeHandler) {
|
||||
s.Engine.Any("/api/realtime", h.Connection)
|
||||
s.Engine.POST("/api/realtime/voice", h.VoiceChat)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
)
|
||||
// 启动应用程序
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDszCCApugAwIBAgIQICMRB0rBU2/rZJbfJGMYIzANBgkqhkiG9w0BAQsFADCBkTELMAkGA1UE
|
||||
BhMCQ04xGzAZBgNVBAoMEkFudCBGaW5hbmNpYWwgdGVzdDElMCMGA1UECwwcQ2VydGlmaWNhdGlv
|
||||
biBBdXRob3JpdHkgdGVzdDE+MDwGA1UEAww1QW50IEZpbmFuY2lhbCBDZXJ0aWZpY2F0aW9uIEF1
|
||||
dGhvcml0eSBDbGFzcyAyIFIxIHRlc3QwHhcNMjMxMTA3MDYzNTQxWhcNMjQxMTA2MDYzNTQxWjCB
|
||||
hDELMAkGA1UEBhMCQ04xHzAdBgNVBAoMFm1ib25meTkwMTVAc2FuZGJveC5jb20xDzANBgNVBAsM
|
||||
BkFsaXBheTFDMEEGA1UEAww65pSv5LuY5a6dKOS4reWbvSnnvZHnu5zmioDmnK/mnInpmZDlhazl
|
||||
j7gtMjA4ODcyMTAyMDc1MDU4MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKsoKcw5
|
||||
sxaiyV7mpWzDtnQ1K518eQLP0+dJlZAf06aBep/Aj9DIqrba/k7DHt8dKQvILMLAMpN1+2IRxbaO
|
||||
yxMa/laj3lZ1eHrB6F077O3D62oHcE3noZtXL0N1zZAxpmkNmYIHeLZS2oLMS4ANu47O/wpDC7BV
|
||||
HjdpZugtdPJ4mxdCpM9GDdLs7W4s5QI4PUPK4skFNMFoKI+0cYP/9ju87UP//IHC/K510GWNl+Gn
|
||||
Cvgag3AmiIB0utJNsGhxm6zT1T9tUWjW9iz/BxBKiPatsCX9VpPQzGnW7ZonRQtiZSokIlP2IPvl
|
||||
H5DcwpWUz3/LUY0SmKxnKOEYeOOqCW8CAwEAAaMSMBAwDgYDVR0PAQH/BAQDAgTwMA0GCSqGSIb3
|
||||
DQEBCwUAA4IBAQAtgxF2EzjOndEFxBUD9tFwcSt6XKGggOp52oft1pvynPg4ALTLafOtfEPDrFBH
|
||||
PwpYrSu9s9C8NJtaA2HrlCfBjIuwEFTXiN+HPvS0SwSPKt9AXEiTcOF8vDcGamEen8QI4fo5Jia7
|
||||
2VRKkerkww5/+FzSaVO7ZUKuL80M1QJStmAZc8kPPwdYOTTW2bGf8BcmSDL6SPElBkt7tCCRd4sn
|
||||
+jq4cZ0yb2i77rBZCwHcTvfTqIBblPwLv4uGvg3+83BxIB5w6Kqp06bKEAPmobFY5IVHa+ON0/qi
|
||||
BXxXr+WQ3piKRVQEN64+PTAjSc67Ix1umvpLl3Ko6Ry7NJmpDcUn
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDszCCApugAwIBAgIQIBkIGbgVxq210KxLJ+YA/TANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UE
|
||||
BhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxJTAjBgNVBAsMHENlcnRpZmljYXRpb24gQXV0
|
||||
aG9yaXR5IHRlc3QxNjA0BgNVBAMMLUFudCBGaW5hbmNpYWwgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
|
||||
dHkgUjEgdGVzdDAeFw0xOTA4MTkxMTE2MDBaFw0yNDA4MDExMTE2MDBaMIGRMQswCQYDVQQGEwJD
|
||||
TjEbMBkGA1UECgwSQW50IEZpbmFuY2lhbCB0ZXN0MSUwIwYDVQQLDBxDZXJ0aWZpY2F0aW9uIEF1
|
||||
dGhvcml0eSB0ZXN0MT4wPAYDVQQDDDVBbnQgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y
|
||||
aXR5IENsYXNzIDIgUjEgdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMh4FKYO
|
||||
ZyRQHD6eFbPKZeSAnrfjfU7xmS9Yoozuu+iuqZlb6Z0SPLUqqTZAFZejOcmr07ln/pwZxluqplxC
|
||||
5+B48End4nclDMlT5HPrDr3W0frs6Xsa2ZNcyil/iKNB5MbGll8LRAxntsKvZZj6vUTMb705gYgm
|
||||
VUMILwi/ZxKTQqBtkT/kQQ5y6nOZsj7XI5rYdz6qqOROrpvS/d7iypdHOMIM9Iz9DlL1mrCykbBi
|
||||
t25y+gTeXmuisHUwqaRpwtCGK4BayCqxRGbNipe6W73EK9lBrrzNtTr9NaysesT/v+l25JHCL9tG
|
||||
wpNr1oWFzk4IHVOg0ORiQ6SUgxZUTYcCAwEAAaMSMBAwDgYDVR0PAQH/BAQDAgTwMA0GCSqGSIb3
|
||||
DQEBCwUAA4IBAQBWThEoIaQoBX2YeRY/I8gu6TYnFXtyuCljANnXnM38ft+ikhE5mMNgKmJYLHvT
|
||||
yWWWgwHoSAWEuml7EGbE/2AK2h3k0MdfiWLzdmpPCRG/RJHk6UB1pMHPilI+c0MVu16OPpKbg5Vf
|
||||
LTv7dsAB40AzKsvyYw88/Ezi1osTXo6QQwda7uefvudirtb8FcQM9R66cJxl3kt1FXbpYwheIm/p
|
||||
j1mq64swCoIYu4NrsUYtn6CV542DTQMI5QdXkn+PzUUly8F6kDp+KpMNd0avfWNL5+O++z+F5Szy
|
||||
1CPta1D7EQ/eYmMP+mOQ35oifWIoFCpN6qQVBS/Hob1J/UUyg7BW
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,88 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBszCCAVegAwIBAgIIaeL+wBcKxnswDAYIKoEcz1UBg3UFADAuMQswCQYDVQQG
|
||||
EwJDTjEOMAwGA1UECgwFTlJDQUMxDzANBgNVBAMMBlJPT1RDQTAeFw0xMjA3MTQw
|
||||
MzExNTlaFw00MjA3MDcwMzExNTlaMC4xCzAJBgNVBAYTAkNOMQ4wDAYDVQQKDAVO
|
||||
UkNBQzEPMA0GA1UEAwwGUk9PVENBMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE
|
||||
MPCca6pmgcchsTf2UnBeL9rtp4nw+itk1Kzrmbnqo05lUwkwlWK+4OIrtFdAqnRT
|
||||
V7Q9v1htkv42TsIutzd126NdMFswHwYDVR0jBBgwFoAUTDKxl9kzG8SmBcHG5Yti
|
||||
W/CXdlgwDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFEwysZfZ
|
||||
MxvEpgXBxuWLYlvwl3ZYMAwGCCqBHM9VAYN1BQADSAAwRQIgG1bSLeOXp3oB8H7b
|
||||
53W+CKOPl2PknmWEq/lMhtn25HkCIQDaHDgWxWFtnCrBjH16/W3Ezn7/U/Vjo5xI
|
||||
pDoiVhsLwg==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIF0zCCA7ugAwIBAgIIH8+hjWpIDREwDQYJKoZIhvcNAQELBQAwejELMAkGA1UE
|
||||
BhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxIDAeBgNVBAsMF0NlcnRpZmlj
|
||||
YXRpb24gQXV0aG9yaXR5MTEwLwYDVQQDDChBbnQgRmluYW5jaWFsIENlcnRpZmlj
|
||||
YXRpb24gQXV0aG9yaXR5IFIxMB4XDTE4MDMyMTEzNDg0MFoXDTM4MDIyODEzNDg0
|
||||
MFowejELMAkGA1UEBhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxIDAeBgNV
|
||||
BAsMF0NlcnRpZmljYXRpb24gQXV0aG9yaXR5MTEwLwYDVQQDDChBbnQgRmluYW5j
|
||||
aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFIxMIICIjANBgkqhkiG9w0BAQEF
|
||||
AAOCAg8AMIICCgKCAgEAtytTRcBNuur5h8xuxnlKJetT65cHGemGi8oD+beHFPTk
|
||||
rUTlFt9Xn7fAVGo6QSsPb9uGLpUFGEdGmbsQ2q9cV4P89qkH04VzIPwT7AywJdt2
|
||||
xAvMs+MgHFJzOYfL1QkdOOVO7NwKxH8IvlQgFabWomWk2Ei9WfUyxFjVO1LVh0Bp
|
||||
dRBeWLMkdudx0tl3+21t1apnReFNQ5nfX29xeSxIhesaMHDZFViO/DXDNW2BcTs6
|
||||
vSWKyJ4YIIIzStumD8K1xMsoaZBMDxg4itjWFaKRgNuPiIn4kjDY3kC66Sl/6yTl
|
||||
YUz8AybbEsICZzssdZh7jcNb1VRfk79lgAprm/Ktl+mgrU1gaMGP1OE25JCbqli1
|
||||
Pbw/BpPynyP9+XulE+2mxFwTYhKAwpDIDKuYsFUXuo8t261pCovI1CXFzAQM2w7H
|
||||
DtA2nOXSW6q0jGDJ5+WauH+K8ZSvA6x4sFo4u0KNCx0ROTBpLif6GTngqo3sj+98
|
||||
SZiMNLFMQoQkjkdN5Q5g9N6CFZPVZ6QpO0JcIc7S1le/g9z5iBKnifrKxy0TQjtG
|
||||
PsDwc8ubPnRm/F82RReCoyNyx63indpgFfhN7+KxUIQ9cOwwTvemmor0A+ZQamRe
|
||||
9LMuiEfEaWUDK+6O0Gl8lO571uI5onYdN1VIgOmwFbe+D8TcuzVjIZ/zvHrAGUcC
|
||||
AwEAAaNdMFswCwYDVR0PBAQDAgEGMAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFF90
|
||||
tATATwda6uWx2yKjh0GynOEBMB8GA1UdIwQYMBaAFF90tATATwda6uWx2yKjh0Gy
|
||||
nOEBMA0GCSqGSIb3DQEBCwUAA4ICAQCVYaOtqOLIpsrEikE5lb+UARNSFJg6tpkf
|
||||
tJ2U8QF/DejemEHx5IClQu6ajxjtu0Aie4/3UnIXop8nH/Q57l+Wyt9T7N2WPiNq
|
||||
JSlYKYbJpPF8LXbuKYG3BTFTdOVFIeRe2NUyYh/xs6bXGr4WKTXb3qBmzR02FSy3
|
||||
IODQw5Q6zpXj8prYqFHYsOvGCEc1CwJaSaYwRhTkFedJUxiyhyB5GQwoFfExCVHW
|
||||
05ZFCAVYFldCJvUzfzrWubN6wX0DD2dwultgmldOn/W/n8at52mpPNvIdbZb2F41
|
||||
T0YZeoWnCJrYXjq/32oc1cmifIHqySnyMnavi75DxPCdZsCOpSAT4j4lAQRGsfgI
|
||||
kkLPGQieMfNNkMCKh7qjwdXAVtdqhf0RVtFILH3OyEodlk1HYXqX5iE5wlaKzDop
|
||||
PKwf2Q3BErq1xChYGGVS+dEvyXc/2nIBlt7uLWKp4XFjqekKbaGaLJdjYP5b2s7N
|
||||
1dM0MXQ/f8XoXKBkJNzEiM3hfsU6DOREgMc1DIsFKxfuMwX3EkVQM1If8ghb6x5Y
|
||||
jXayv+NLbidOSzk4vl5QwngO/JYFMkoc6i9LNwEaEtR9PhnrdubxmrtM+RjfBm02
|
||||
77q3dSWFESFQ4QxYWew4pHE0DpWbWy/iMIKQ6UZ5RLvB8GEcgt8ON7BBJeMc+Dyi
|
||||
kT9qhqn+lw==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICiDCCAgygAwIBAgIIQX76UsB/30owDAYIKoZIzj0EAwMFADB6MQswCQYDVQQG
|
||||
EwJDTjEWMBQGA1UECgwNQW50IEZpbmFuY2lhbDEgMB4GA1UECwwXQ2VydGlmaWNh
|
||||
dGlvbiBBdXRob3JpdHkxMTAvBgNVBAMMKEFudCBGaW5hbmNpYWwgQ2VydGlmaWNh
|
||||
dGlvbiBBdXRob3JpdHkgRTEwHhcNMTkwNDI4MTYyMDQ0WhcNNDkwNDIwMTYyMDQ0
|
||||
WjB6MQswCQYDVQQGEwJDTjEWMBQGA1UECgwNQW50IEZpbmFuY2lhbDEgMB4GA1UE
|
||||
CwwXQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxMTAvBgNVBAMMKEFudCBGaW5hbmNp
|
||||
YWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgRTEwdjAQBgcqhkjOPQIBBgUrgQQA
|
||||
IgNiAASCCRa94QI0vR5Up9Yr9HEupz6hSoyjySYqo7v837KnmjveUIUNiuC9pWAU
|
||||
WP3jwLX3HkzeiNdeg22a0IZPoSUCpasufiLAnfXh6NInLiWBrjLJXDSGaY7vaokt
|
||||
rpZvAdmjXTBbMAsGA1UdDwQEAwIBBjAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBRZ
|
||||
4ZTgDpksHL2qcpkFkxD2zVd16TAfBgNVHSMEGDAWgBRZ4ZTgDpksHL2qcpkFkxD2
|
||||
zVd16TAMBggqhkjOPQQDAwUAA2gAMGUCMQD4IoqT2hTUn0jt7oXLdMJ8q4vLp6sg
|
||||
wHfPiOr9gxreb+e6Oidwd2LDnC4OUqCWiF8CMAzwKs4SnDJYcMLf2vpkbuVE4dTH
|
||||
Rglz+HGcTLWsFs4KxLsq7MuU+vJTBUeDJeDjdA==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDxTCCAq2gAwIBAgIUEMdk6dVgOEIS2cCP0Q43P90Ps5YwDQYJKoZIhvcNAQEF
|
||||
BQAwajELMAkGA1UEBhMCQ04xEzARBgNVBAoMCmlUcnVzQ2hpbmExHDAaBgNVBAsM
|
||||
E0NoaW5hIFRydXN0IE5ldHdvcmsxKDAmBgNVBAMMH2lUcnVzQ2hpbmEgQ2xhc3Mg
|
||||
MiBSb290IENBIC0gRzMwHhcNMTMwNDE4MDkzNjU2WhcNMzMwNDE4MDkzNjU2WjBq
|
||||
MQswCQYDVQQGEwJDTjETMBEGA1UECgwKaVRydXNDaGluYTEcMBoGA1UECwwTQ2hp
|
||||
bmEgVHJ1c3QgTmV0d29yazEoMCYGA1UEAwwfaVRydXNDaGluYSBDbGFzcyAyIFJv
|
||||
b3QgQ0EgLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOPPShpV
|
||||
nJbMqqCw6Bz1kehnoPst9pkr0V9idOwU2oyS47/HjJXk9Rd5a9xfwkPO88trUpz5
|
||||
4GmmwspDXjVFu9L0eFaRuH3KMha1Ak01citbF7cQLJlS7XI+tpkTGHEY5pt3EsQg
|
||||
wykfZl/A1jrnSkspMS997r2Gim54cwz+mTMgDRhZsKK/lbOeBPpWtcFizjXYCqhw
|
||||
WktvQfZBYi6o4sHCshnOswi4yV1p+LuFcQ2ciYdWvULh1eZhLxHbGXyznYHi0dGN
|
||||
z+I9H8aXxqAQfHVhbdHNzi77hCxFjOy+hHrGsyzjrd2swVQ2iUWP8BfEQqGLqM1g
|
||||
KgWKYfcTGdbPB1MCAwEAAaNjMGEwHQYDVR0OBBYEFG/oAMxTVe7y0+408CTAK8hA
|
||||
uTyRMB8GA1UdIwQYMBaAFG/oAMxTVe7y0+408CTAK8hAuTyRMA8GA1UdEwEB/wQF
|
||||
MAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQBLnUTfW7hp
|
||||
emMbuUGCk7RBswzOT83bDM6824EkUnf+X0iKS95SUNGeeSWK2o/3ALJo5hi7GZr3
|
||||
U8eLaWAcYizfO99UXMRBPw5PRR+gXGEronGUugLpxsjuynoLQu8GQAeysSXKbN1I
|
||||
UugDo9u8igJORYA+5ms0s5sCUySqbQ2R5z/GoceyI9LdxIVa1RjVX8pYOj8JFwtn
|
||||
DJN3ftSFvNMYwRuILKuqUYSHc2GPYiHVflDh5nDymCMOQFcFG3WsEuB+EYQPFgIU
|
||||
1DHmdZcz7Llx8UOZXX2JupWCYzK1XhJb+r4hK5ncf/w8qGtYlmyJpxk3hr1TfUJX
|
||||
Yf4Zr0fJsGuv
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,19 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDmTCCAoGgAwIBAgIQICMRB2LW76yahgdg3IFNPDANBgkqhkiG9w0BAQsFADCBkTELMAkGA1UE
|
||||
BhMCQ04xGzAZBgNVBAoMEkFudCBGaW5hbmNpYWwgdGVzdDElMCMGA1UECwwcQ2VydGlmaWNhdGlv
|
||||
biBBdXRob3JpdHkgdGVzdDE+MDwGA1UEAww1QW50IEZpbmFuY2lhbCBDZXJ0aWZpY2F0aW9uIEF1
|
||||
dGhvcml0eSBDbGFzcyAyIFIxIHRlc3QwHhcNMjMxMTA3MDU0NjE5WhcNMjQxMTExMDU0NjE5WjBr
|
||||
MQswCQYDVQQGEwJDTjEfMB0GA1UECgwWbWJvbmZ5OTAxNUBzYW5kYm94LmNvbTEPMA0GA1UECwwG
|
||||
QWxpcGF5MSowKAYDVQQDDCEyMDg4NzIxMDIwNzUwNTgxLTkwMjEwMDAxMzE2NTgwMjMwggEiMA0G
|
||||
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxihQPf1Q+g9ArgM46shVqL5sbRha/df95D1PsWyEq
|
||||
ANmWmG4zZ+ksYDVQrc4KzhSRoi56sm/7TDFYTmM6bW99e/nKW58WxyZB4ie5qA3F4n17psPyDqb8
|
||||
IokcQmCphSFDaXQD6AoXoLNtTM0vAI2cWxAgebZ/vsrdj5Ntjt+Rp3NYMCk1i5xovHcfILzLEGbX
|
||||
QXoT9fo5AhHotTWa6xHVLPUGY9qwLzQxHzBmvy5ZMfnOfJkm/mDisTSqAUB59F3dzU/1ARVkEZ1w
|
||||
Mgb4XohWBw6iurQfbMnH2mIomAAwwZVFv+sXDbL9yMbSMo/SjVsTQprn0Q0EnwLo7nmmOM6HAgMB
|
||||
AAGjEjAQMA4GA1UdDwEB/wQEAwIE8DANBgkqhkiG9w0BAQsFAAOCAQEAn3Y4/C1h9R6ONsBqX3/q
|
||||
XfHX7yX1FM0Y1x48X3/Yxk6HivAkTukhhhVYVKJsbrbzRqHDp9vhAP/FR6o6pAevaYMmLov0VMXU
|
||||
7oAuetgkaYEYkDuNen5/Hpdhqi2vTtdT+q9w8zHJd6MDQ0aoHgIxpLKw5vof2R1N4fwSgNXMiXE5
|
||||
kmllKQMem/+on2p+Sj80/2asxryHIGlH87qPzkffv+kIOkZthbTApTFLLjdVri2QHGe8/cc4xy01
|
||||
/9iR3IUzNahotT41lJ4bMevBY7XMAS3n5ekyABN/9ZRJqhWdXgmFCRN/u56qd6lDgu7R2M2QUoyc
|
||||
LuW5DfgRItKlmUB7sw==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1 +0,0 @@
|
||||
MIIEpQIBAAKCAQEAsYoUD39UPoPQK4DOOrIVai+bG0YWv3X/eQ9T7FshKgDZlphuM2fpLGA1UK3OCs4UkaIuerJv+0wxWE5jOm1vfXv5ylufFscmQeInuagNxeJ9e6bD8g6m/CKJHEJgqYUhQ2l0A+gKF6CzbUzNLwCNnFsQIHm2f77K3Y+TbY7fkadzWDApNYucaLx3HyC8yxBm10F6E/X6OQIR6LU1musR1Sz1BmPasC80MR8wZr8uWTH5znyZJv5g4rE0qgFAefRd3c1P9QEVZBGdcDIG+F6IVgcOorq0H2zJx9piKJgAMMGVRb/rFw2y/cjG0jKP0o1bE0Ka59ENBJ8C6O55pjjOhwIDAQABAoIBAFetNfz1R7hbxjlFshMAkVzQR8wvT9qbvl+dtzdZRcaFhu89NecDIP7+QDYor0FcxoGpU0TazDyRQyk2BQD8vHt+9zv9BVLtZLJSqoWgPbUFBi1DjS8EF2ka8RVYnn35NhUhhd7L//ftL88Bh673mfembQ9srDjoEy1Z01feoABAnCMkNFl986DmEwnarvEufXSDIgeN4ioMxha4NvfIPuI0zpVdV1O9sv+SGC+VEWZBtN3GNsaf4zS/f8FVGvTiU/Abz0gSw/iwSPHclDWQDTN3yFHf/tfqlzh0mH0WfhnuOBFWXzK+R7fbnM+asI9ttvzRcfpzgRGXdPcNcOv/6cECgYEA3DVqpi1k8MYfJixju6SG5gfyhM4VFksFmCMaNPgtatDMBKLMTgV/Ej6LXREojcy29uZl83F09pVlpd41eG39ULIPktixA/BqErQ2UaWh6kOxifycpu22Jh0r09hax6UgVrcBrrnCJEjcFsuJlrZvXQSzc3PBxjWy5gjabS5h9iECgYEAzmVAIh2frF01Y95zsLueAhhZwCtPanm6kf7ivR4r1plIX3b2sNRhWGmEHFgaCE6Braa0ogQ73Hd26kw4ZW+D6QMGC/zjCBEzDLLf++SjdVUHiY5AR4WHqXzq1jdAlsVyo9R661oAOp3lhiJVGLNXkHyEfEVPHsaxJh4osYSbX6cCgYEAx32Qx0i6eDFTyLZQB46uMrgiaVN04QRH5iJuvGvUYT8UhGKjaU8rZfDJOh+wOH2rhxMEaz1uc3C2bERY9mfWI4Ob/jFWc7YZsiYWS3Mcsuhubw4tMECLUg39RWZsHw8ls8kIuixIh6yFzhTH6YQOcRswIrhMZG8DScfdcSmiz2ECgYEAkWP1t5KSpkLKl11etcKUXfl1T8+yk9jIOowIgRw92WAFAWq2AH67TCKYM7dEL1HOO9tRJ0hAOt/U3ttuZtYVYBEHM26jJ02mXm2rJrA7DS4mrxmL4lYH6LbcXqZxU0Qnq4zEQgIWYzRTORf6Rfof1uJAGaJhR9bDd4yLMfGt2cUCgYEAo216Y61xOHUTA4AF1eekk+r+uOcQgQDvLXfs9FkDdJLk0mPG48/+eIYpPFnANJ/riF/DWOp8WGEe2IzA9yUFexzDbNQK8ha9kGcxaSAyiCwzjZ/t9/+hScDSV8kNqWSRSisu/YOFleEHbokT6mbLZ+gdqES8mUUanaEBzRQYGxo=
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.7 KiB |
@@ -8,35 +8,38 @@ package service
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"github.com/imroc/req/v3"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
)
|
||||
|
||||
type CaptchaService struct {
|
||||
config types.ApiConfig
|
||||
config types.CaptchaConfig
|
||||
client *req.Client
|
||||
}
|
||||
|
||||
func NewCaptchaService(config types.ApiConfig) *CaptchaService {
|
||||
func NewCaptchaService(captchaConfig types.CaptchaConfig) *CaptchaService {
|
||||
return &CaptchaService{
|
||||
config: config,
|
||||
config: captchaConfig,
|
||||
client: req.C().SetTimeout(10 * time.Second),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CaptchaService) UpdateConfig(config types.CaptchaConfig) {
|
||||
s.config = config
|
||||
}
|
||||
|
||||
func (s *CaptchaService) GetConfig() types.CaptchaConfig {
|
||||
return s.config
|
||||
}
|
||||
|
||||
func (s *CaptchaService) Get() (interface{}, error) {
|
||||
if s.config.Token == "" {
|
||||
return nil, errors.New("无效的 API Token")
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/captcha/get", s.config.ApiURL)
|
||||
url := fmt.Sprintf("%s/api/captcha/get", types.GeekAPIURL)
|
||||
var res types.BizVo
|
||||
r, err := s.client.R().
|
||||
SetHeader("AppId", s.config.AppId).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.ApiKey)).
|
||||
SetSuccessResult(&res).Get(url)
|
||||
if err != nil || r.IsErrorState() {
|
||||
return nil, fmt.Errorf("请求 API 失败:%v", err)
|
||||
@@ -49,12 +52,11 @@ func (s *CaptchaService) Get() (interface{}, error) {
|
||||
return res.Data, nil
|
||||
}
|
||||
|
||||
func (s *CaptchaService) Check(data interface{}) bool {
|
||||
url := fmt.Sprintf("%s/api/captcha/check", s.config.ApiURL)
|
||||
func (s *CaptchaService) Check(data any) bool {
|
||||
url := fmt.Sprintf("%s/api/captcha/check", types.GeekAPIURL)
|
||||
var res types.BizVo
|
||||
r, err := s.client.R().
|
||||
SetHeader("AppId", s.config.AppId).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.ApiKey)).
|
||||
SetBodyJsonMarshal(data).
|
||||
SetSuccessResult(&res).Post(url)
|
||||
if err != nil || r.IsErrorState() {
|
||||
@@ -68,16 +70,11 @@ func (s *CaptchaService) Check(data interface{}) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *CaptchaService) SlideGet() (interface{}, error) {
|
||||
if s.config.Token == "" {
|
||||
return nil, errors.New("无效的 API Token")
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/captcha/slide/get", s.config.ApiURL)
|
||||
func (s *CaptchaService) SlideGet() (any, error) {
|
||||
url := fmt.Sprintf("%s/api/captcha/slide/get", types.GeekAPIURL)
|
||||
var res types.BizVo
|
||||
r, err := s.client.R().
|
||||
SetHeader("AppId", s.config.AppId).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.ApiKey)).
|
||||
SetSuccessResult(&res).Get(url)
|
||||
if err != nil || r.IsErrorState() {
|
||||
return nil, fmt.Errorf("请求 API 失败:%v", err)
|
||||
@@ -90,12 +87,11 @@ func (s *CaptchaService) SlideGet() (interface{}, error) {
|
||||
return res.Data, nil
|
||||
}
|
||||
|
||||
func (s *CaptchaService) SlideCheck(data interface{}) bool {
|
||||
url := fmt.Sprintf("%s/api/captcha/slide/check", s.config.ApiURL)
|
||||
func (s *CaptchaService) SlideCheck(data any) bool {
|
||||
url := fmt.Sprintf("%s/api/captcha/slide/check", types.GeekAPIURL)
|
||||
var res types.BizVo
|
||||
r, err := s.client.R().
|
||||
SetHeader("AppId", s.config.AppId).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.ApiKey)).
|
||||
SetBodyJsonMarshal(data).
|
||||
SetSuccessResult(&res).Post(url)
|
||||
if err != nil || r.IsErrorState() {
|
||||
|
||||
@@ -1,333 +0,0 @@
|
||||
package crawler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/logger"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
"github.com/go-rod/rod/lib/launcher"
|
||||
"github.com/go-rod/rod/lib/proto"
|
||||
)
|
||||
|
||||
// Service 网络爬虫服务
|
||||
type Service struct {
|
||||
browser *rod.Browser
|
||||
}
|
||||
|
||||
// NewService 创建一个新的爬虫服务
|
||||
func NewService() (*Service, error) {
|
||||
// 启动浏览器
|
||||
path, _ := launcher.LookPath()
|
||||
u := launcher.New().Bin(path).
|
||||
Headless(true). // 无头模式
|
||||
Set("disable-web-security", ""). // 禁用网络安全限制
|
||||
Set("disable-gpu", ""). // 禁用 GPU 加速
|
||||
Set("no-sandbox", ""). // 禁用沙箱模式
|
||||
Set("disable-setuid-sandbox", "").// 禁用 setuid 沙箱
|
||||
MustLaunch()
|
||||
|
||||
browser := rod.New().ControlURL(u).MustConnect()
|
||||
|
||||
return &Service{
|
||||
browser: browser,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SearchResult 搜索结果
|
||||
type SearchResult struct {
|
||||
Title string `json:"title"` // 标题
|
||||
URL string `json:"url"` // 链接
|
||||
Content string `json:"content"` // 内容摘要
|
||||
}
|
||||
|
||||
// WebSearch 网络搜索
|
||||
func (s *Service) WebSearch(keyword string, maxPages int) ([]SearchResult, error) {
|
||||
if keyword == "" {
|
||||
return nil, errors.New("搜索关键词不能为空")
|
||||
}
|
||||
|
||||
if maxPages <= 0 {
|
||||
maxPages = 1
|
||||
}
|
||||
if maxPages > 10 {
|
||||
maxPages = 10 // 最多搜索 10 页
|
||||
}
|
||||
|
||||
results := make([]SearchResult, 0)
|
||||
|
||||
// 使用百度搜索
|
||||
searchURL := fmt.Sprintf("https://www.baidu.com/s?wd=%s", url.QueryEscape(keyword))
|
||||
|
||||
// 设置页面超时
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 创建页面
|
||||
page := s.browser.MustPage()
|
||||
defer page.MustClose()
|
||||
|
||||
// 设置视口大小
|
||||
err := page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{
|
||||
Width: 1280,
|
||||
Height: 800,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("设置视口失败: %v", err)
|
||||
}
|
||||
|
||||
// 导航到搜索页面
|
||||
err = page.Context(ctx).Navigate(searchURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("导航到搜索页面失败: %v", err)
|
||||
}
|
||||
|
||||
// 等待搜索结果加载完成
|
||||
err = page.WaitLoad()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("等待页面加载完成失败: %v", err)
|
||||
}
|
||||
|
||||
// 分析当前页面的搜索结果
|
||||
for i := 0; i < maxPages; i++ {
|
||||
if i > 0 {
|
||||
// 点击下一页按钮
|
||||
nextPage, err := page.Element("a.n")
|
||||
if err != nil || nextPage == nil {
|
||||
break // 没有下一页
|
||||
}
|
||||
|
||||
err = nextPage.Click(proto.InputMouseButtonLeft, 1)
|
||||
if err != nil {
|
||||
break // 点击下一页失败
|
||||
}
|
||||
|
||||
// 等待新页面加载
|
||||
err = page.WaitLoad()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 提取搜索结果
|
||||
resultElements, err := page.Elements(".result, .c-container")
|
||||
if err != nil || resultElements == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, result := range resultElements {
|
||||
// 获取标题
|
||||
titleElement, err := result.Element("h3, .t")
|
||||
if err != nil || titleElement == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
title, err := titleElement.Text()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取 URL
|
||||
linkElement, err := titleElement.Element("a")
|
||||
if err != nil || linkElement == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
href, err := linkElement.Attribute("href")
|
||||
if err != nil || href == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取内容摘要 - 尝试多个可能的选择器
|
||||
var contentElement *rod.Element
|
||||
var content string
|
||||
|
||||
// 尝试多个可能的选择器来适应不同版本的百度搜索结果
|
||||
selectors := []string{".content-right_8Zs40", ".c-abstract", ".content_LJ0WN", ".content"}
|
||||
for _, selector := range selectors {
|
||||
contentElement, err = result.Element(selector)
|
||||
if err == nil && contentElement != nil {
|
||||
content, _ = contentElement.Text()
|
||||
if content != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有选择器都失败,尝试直接从结果块中提取文本
|
||||
if content == "" {
|
||||
// 获取结果元素的所有文本
|
||||
fullText, err := result.Text()
|
||||
if err == nil && fullText != "" {
|
||||
// 简单处理:从全文中移除标题,剩下的可能是摘要
|
||||
fullText = strings.Replace(fullText, title, "", 1)
|
||||
// 清理文本
|
||||
content = strings.TrimSpace(fullText)
|
||||
// 限制内容长度
|
||||
if len(content) > 200 {
|
||||
content = content[:200] + "..."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加到结果集
|
||||
results = append(results, SearchResult{
|
||||
Title: title,
|
||||
URL: *href,
|
||||
Content: content,
|
||||
})
|
||||
|
||||
// 限制结果数量,每页最多 10 条
|
||||
if len(results) >= 10*maxPages {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取真实 URL(百度搜索结果中的 URL 是短链接,需要跳转获取真实 URL)
|
||||
for i, result := range results {
|
||||
realURL, err := s.getRedirectURL(result.URL)
|
||||
if err == nil && realURL != "" {
|
||||
results[i].URL = realURL
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// 获取真实 URL
|
||||
func (s *Service) getRedirectURL(shortURL string) (string, error) {
|
||||
// 创建页面
|
||||
page, err := s.browser.Page(proto.TargetCreateTarget{URL: ""})
|
||||
if err != nil {
|
||||
return shortURL, err // 返回原始URL
|
||||
}
|
||||
defer func() {
|
||||
_ = page.Close()
|
||||
}()
|
||||
|
||||
// 导航到短链接
|
||||
err = page.Navigate(shortURL)
|
||||
if err != nil {
|
||||
return shortURL, err // 返回原始URL
|
||||
}
|
||||
|
||||
// 等待重定向完成
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// 获取当前 URL
|
||||
info, err := page.Info()
|
||||
if err != nil {
|
||||
return shortURL, err // 返回原始URL
|
||||
}
|
||||
|
||||
return info.URL, nil
|
||||
}
|
||||
|
||||
// Close 关闭浏览器
|
||||
func (s *Service) Close() error {
|
||||
if s.browser != nil {
|
||||
err := s.browser.Close()
|
||||
s.browser = nil
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchWeb 封装的搜索方法
|
||||
func SearchWeb(keyword string, maxPages int) (string, error) {
|
||||
// 添加panic恢复机制
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log := logger.GetLogger()
|
||||
log.Errorf("爬虫服务崩溃: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
service, err := NewService()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建爬虫服务失败: %v", err)
|
||||
}
|
||||
defer service.Close()
|
||||
|
||||
// 设置超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 使用goroutine和通道来处理超时
|
||||
resultChan := make(chan []SearchResult, 1)
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
results, err := service.WebSearch(keyword, maxPages)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
resultChan <- results
|
||||
}()
|
||||
|
||||
// 等待结果或超时
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", fmt.Errorf("搜索超时: %v", ctx.Err())
|
||||
case err := <-errChan:
|
||||
return "", fmt.Errorf("搜索失败: %v", err)
|
||||
case results := <-resultChan:
|
||||
if len(results) == 0 {
|
||||
return "未找到关于 \"" + keyword + "\" 的相关搜索结果", nil
|
||||
}
|
||||
|
||||
// 格式化结果
|
||||
var builder strings.Builder
|
||||
builder.WriteString(fmt.Sprintf("为您找到关于 \"%s\" 的 %d 条搜索结果:\n\n", keyword, len(results)))
|
||||
|
||||
for i, result := range results {
|
||||
// // 尝试打开链接获取实际内容
|
||||
// page := service.browser.MustPage()
|
||||
// defer page.MustClose()
|
||||
|
||||
// // 设置页面超时
|
||||
// pageCtx, pageCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
// defer pageCancel()
|
||||
|
||||
// // 导航到目标页面
|
||||
// err := page.Context(pageCtx).Navigate(result.URL)
|
||||
// if err == nil {
|
||||
// // 等待页面加载
|
||||
// _ = page.WaitLoad()
|
||||
|
||||
// // 获取页面标题
|
||||
// title, err := page.Eval("() => document.title")
|
||||
// if err == nil && title.Value.String() != "" {
|
||||
// result.Title = title.Value.String()
|
||||
// }
|
||||
|
||||
// // 获取页面主要内容
|
||||
// if content, err := page.Element("body"); err == nil {
|
||||
// if text, err := content.Text(); err == nil {
|
||||
// // 清理并截取内容
|
||||
// text = strings.TrimSpace(text)
|
||||
// if len(text) > 200 {
|
||||
// text = text[:200] + "..."
|
||||
// }
|
||||
// result.Content = text
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
builder.WriteString(fmt.Sprintf("%d. **%s**\n", i+1, result.Title))
|
||||
builder.WriteString(fmt.Sprintf(" 链接: %s\n", result.URL))
|
||||
if result.Content != "" {
|
||||
builder.WriteString(fmt.Sprintf(" 摘要: %s\n", result.Content))
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
@@ -50,7 +50,9 @@ func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Clien
|
||||
// PushTask push a new mj task in to task queue
|
||||
func (s *Service) PushTask(task types.DallTask) {
|
||||
logger.Infof("add a new DALL-E task to the task list: %+v", task)
|
||||
s.taskQueue.RPush(task)
|
||||
if err := s.taskQueue.RPush(task); err != nil {
|
||||
logger.Errorf("push dall-e task to queue failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Run() {
|
||||
@@ -93,12 +95,14 @@ func (s *Service) Run() {
|
||||
}
|
||||
|
||||
type imgReq struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n,omitempty"`
|
||||
Size string `json:"size,omitempty"`
|
||||
Quality string `json:"quality,omitempty"`
|
||||
Style string `json:"style,omitempty"`
|
||||
Model string `json:"model"`
|
||||
Image []string `json:"image,omitempty"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n,omitempty"`
|
||||
Size string `json:"size,omitempty"`
|
||||
Quality string `json:"quality,omitempty"`
|
||||
Style string `json:"style,omitempty"`
|
||||
ResponseFormat string `json:"response_format,omitempty"`
|
||||
}
|
||||
|
||||
type imgRes struct {
|
||||
@@ -121,18 +125,13 @@ type ErrRes struct {
|
||||
|
||||
func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
logger.Debugf("绘画参数:%+v", task)
|
||||
prompt := task.Prompt
|
||||
// translate prompt
|
||||
if utils.HasChinese(prompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, prompt), task.TranslateModelId)
|
||||
if err == nil {
|
||||
prompt = content
|
||||
logger.Debugf("重写后提示词:%s", prompt)
|
||||
}
|
||||
}
|
||||
|
||||
var chatModel model.ChatModel
|
||||
s.db.Where("id = ?", task.ModelId).First(&chatModel)
|
||||
if task.ModelId > 0 {
|
||||
s.db.Where("id", task.ModelId).First(&chatModel)
|
||||
} else {
|
||||
s.db.Where("value", task.ModelName).First(&chatModel)
|
||||
}
|
||||
|
||||
// get image generation API KEY
|
||||
var apiKey model.ApiKey
|
||||
@@ -155,12 +154,17 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
apiURL := fmt.Sprintf("%s/v1/images/generations", apiKey.ApiURL)
|
||||
reqBody := imgReq{
|
||||
Model: chatModel.Value,
|
||||
Prompt: prompt,
|
||||
Prompt: task.Prompt,
|
||||
N: 1,
|
||||
Size: task.Size,
|
||||
Style: task.Style,
|
||||
Quality: task.Quality,
|
||||
}
|
||||
// 图片编辑
|
||||
if len(task.Image) > 0 {
|
||||
reqBody.Prompt = fmt.Sprintf("%s, %s", strings.Join(task.Image, " "), task.Prompt)
|
||||
}
|
||||
|
||||
logger.Infof("Channel:%s, API KEY:%s, BODY: %+v", apiURL, apiKey.Value, reqBody)
|
||||
r, err := s.httpClient.R().SetHeader("Body-Type", "application/json").
|
||||
SetHeader("Authorization", "Bearer "+apiKey.Value).
|
||||
@@ -178,15 +182,12 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
return "", fmt.Errorf("error with send request, status: %s, %+v", r.Status, errRes.Error)
|
||||
}
|
||||
|
||||
all, _ := io.ReadAll(r.Body)
|
||||
logger.Debugf("response: %+v", string(all))
|
||||
|
||||
// update the api key last use time
|
||||
s.db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
var imgURL string
|
||||
var data = map[string]interface{}{
|
||||
"progress": 100,
|
||||
"prompt": prompt,
|
||||
"prompt": task.Prompt,
|
||||
}
|
||||
// 如果返回的是base64,则需要上传到oss
|
||||
if res.Data[0].B64Json != "" {
|
||||
@@ -208,11 +209,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
|
||||
var content string
|
||||
if sync {
|
||||
imgURL, err := s.downloadImage(task.Id, res.Data[0].Url)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with download image: %v", err)
|
||||
}
|
||||
content = fmt.Sprintf("```\n%s\n```\n下面是我为你创作的图片:\n\n\n", prompt, imgURL)
|
||||
content = fmt.Sprintf("```\n%s\n```\n下面是我为你创作的图片:\n\n\n", task.Prompt, imgURL)
|
||||
}
|
||||
|
||||
return content, nil
|
||||
@@ -242,7 +239,7 @@ func (s *Service) CheckTaskStatus() {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
err = s.userService.IncreasePower(int(job.UserId), job.Power, model.PowerLog{
|
||||
err = s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: task.ModelName,
|
||||
Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg),
|
||||
@@ -291,7 +288,7 @@ func (s *Service) DownloadImages() {
|
||||
|
||||
func (s *Service) downloadImage(jobId uint, orgURL string) (string, error) {
|
||||
// sava image
|
||||
imgURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(orgURL, false)
|
||||
imgURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(orgURL, ".png", false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
package jimeng
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/volcengine/volc-sdk-golang/base"
|
||||
"github.com/volcengine/volc-sdk-golang/service/visual"
|
||||
"github.com/volcengine/volcengine-go-sdk/service/arkruntime"
|
||||
"github.com/volcengine/volcengine-go-sdk/service/arkruntime/model"
|
||||
"github.com/volcengine/volcengine-go-sdk/volcengine"
|
||||
)
|
||||
|
||||
// Client 即梦API客户端
|
||||
type Client struct {
|
||||
visual *visual.Visual
|
||||
config types.JimengConfig
|
||||
}
|
||||
|
||||
// NewClient 创建即梦API客户端
|
||||
func NewClient(sysConfig *types.SystemConfig) *Client {
|
||||
|
||||
client := &Client{}
|
||||
client.UpdateConfig(sysConfig.Jimeng)
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *Client) UpdateConfig(config types.JimengConfig) error {
|
||||
// 使用官方SDK的visual实例
|
||||
visualInstance := visual.NewInstance()
|
||||
visualInstance.Client.SetAccessKey(config.AccessKey)
|
||||
visualInstance.Client.SetSecretKey(config.SecretKey)
|
||||
|
||||
// 添加即梦AI专有的API配置
|
||||
jimengApis := map[string]*base.ApiInfo{
|
||||
"CVSync2AsyncSubmitTask": {
|
||||
Method: http.MethodPost,
|
||||
Path: "/",
|
||||
Query: url.Values{
|
||||
"Action": []string{"CVSync2AsyncSubmitTask"},
|
||||
"Version": []string{"2022-08-31"},
|
||||
},
|
||||
},
|
||||
"CVSync2AsyncGetResult": {
|
||||
Method: http.MethodPost,
|
||||
Path: "/",
|
||||
Query: url.Values{
|
||||
"Action": []string{"CVSync2AsyncGetResult"},
|
||||
"Version": []string{"2022-08-31"},
|
||||
},
|
||||
},
|
||||
"CVSubmitTask": {
|
||||
Method: http.MethodPost,
|
||||
Path: "/",
|
||||
Query: url.Values{
|
||||
"Action": []string{"CVSubmitTask"},
|
||||
"Version": []string{"2022-08-31"},
|
||||
},
|
||||
},
|
||||
"CVGetResult": {
|
||||
Method: http.MethodPost,
|
||||
Path: "/",
|
||||
Query: url.Values{
|
||||
"Action": []string{"CVGetResult"},
|
||||
"Version": []string{"2022-08-31"},
|
||||
},
|
||||
},
|
||||
"CVProcess": {
|
||||
Method: http.MethodPost,
|
||||
Path: "/",
|
||||
Query: url.Values{
|
||||
"Action": []string{"CVProcess"},
|
||||
"Version": []string{"2022-08-31"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 将即梦API添加到现有的ApiInfoList中
|
||||
for name, info := range jimengApis {
|
||||
visualInstance.Client.ApiInfoList[name] = info
|
||||
}
|
||||
|
||||
c.config = config
|
||||
c.visual = visualInstance
|
||||
|
||||
return c.testConnection()
|
||||
}
|
||||
|
||||
// GetErrorMessage 根据错误代码获取对应的错误信息
|
||||
func GetErrorMessage(code int) string {
|
||||
if message, exists := errorCodeMessages[code]; exists {
|
||||
return message
|
||||
}
|
||||
return fmt.Sprintf("未知错误代码: %d", code)
|
||||
}
|
||||
|
||||
// HandleResponseError 处理响应错误,根据错误代码返回详细的错误信息
|
||||
func HandleResponseError(code int, message string) error {
|
||||
if code == ECSuccess {
|
||||
return nil
|
||||
}
|
||||
return errors.New(GetErrorMessage(code))
|
||||
}
|
||||
|
||||
// testConnection 测试即梦AI连接
|
||||
func (c *Client) testConnection() error {
|
||||
|
||||
// 使用一个简单的查询任务来测试连接
|
||||
testReq := &QueryTaskRequest{
|
||||
ReqKey: "test_connection",
|
||||
TaskId: "test_task_id_12345",
|
||||
}
|
||||
|
||||
_, err := c.QueryTask(testReq, ASyncActionGetResult)
|
||||
// 即使任务不存在,只要不是认证错误就说明连接正常
|
||||
if err != nil {
|
||||
// 检查是否是认证错误
|
||||
if strings.Contains(err.Error(), "InvalidAccessKey") {
|
||||
return fmt.Errorf("认证失败,请检查AccessKey和SecretKey是否正确")
|
||||
}
|
||||
// 其他错误(如任务不存在)说明连接正常
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SubmitTask 提交异步任务
|
||||
func (c *Client) SubmitTask(req map[string]any) (*SubmitTaskResponse, error) {
|
||||
// 直接将请求转为map[string]interface{}
|
||||
reqBodyBytes, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal request failed: %w", err)
|
||||
}
|
||||
|
||||
// 直接使用序列化后的字节
|
||||
jsonBody := reqBodyBytes
|
||||
action := ASyncActionSubmit
|
||||
if v, ok := req["action"]; ok {
|
||||
action = v.(string)
|
||||
delete(req, "action")
|
||||
}
|
||||
|
||||
// 调用SDK的JSON方法
|
||||
respBody, statusCode, err := c.visual.Client.Json(action, nil, string(jsonBody))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("submit task failed (status: %d): %w", statusCode, err)
|
||||
}
|
||||
|
||||
logger.Infof("Jimeng SubmitTask Response: %s", string(respBody))
|
||||
|
||||
// 解析响应
|
||||
var result SubmitTaskResponse
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal response failed: %w", err)
|
||||
}
|
||||
|
||||
// 检查响应错误代码
|
||||
if err := HandleResponseError(result.Code, result.Message); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// 识别数字人主体
|
||||
func (c *Client) AvatarRecognition(imgUrl string, reqKey string) error {
|
||||
params := map[string]any{
|
||||
"image_url": imgUrl,
|
||||
"req_key": reqKey,
|
||||
}
|
||||
reqBodyBytes, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal request failed: %w", err)
|
||||
}
|
||||
// 调用SDK的JSON方法
|
||||
respBody, statusCode, err := c.visual.Client.Json(SyncActionSubmit, nil, string(reqBodyBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("submit task failed (status: %d): %w", statusCode, err)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var result SubmitTaskResponse
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return fmt.Errorf("unmarshal response failed: %w", err)
|
||||
}
|
||||
|
||||
// 检查响应错误代码
|
||||
if err := HandleResponseError(result.Code, result.Message); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 等待任务完成
|
||||
for {
|
||||
resp, err := c.QueryTask(&QueryTaskRequest{
|
||||
ReqKey: reqKey,
|
||||
TaskId: result.Data.TaskId,
|
||||
}, SyncActionGetResult)
|
||||
if err != nil {
|
||||
return fmt.Errorf("query task failed: %w", err)
|
||||
}
|
||||
if resp.Data.Status != types.JMTaskStatusDone {
|
||||
time.Sleep(time.Second * 3)
|
||||
continue
|
||||
}
|
||||
var respData map[string]int
|
||||
if err := json.Unmarshal([]byte(resp.Data.RespData), &respData); err != nil {
|
||||
return fmt.Errorf("unmarshal response failed: %w", err)
|
||||
}
|
||||
logger.Debugf("Jimeng AvatarRecognition Response: %+v", resp)
|
||||
if respData["status"] == 1 {
|
||||
return nil
|
||||
} else {
|
||||
return errors.New("不包含人、类人、拟人等主体")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// QueryTask 查询任务结果
|
||||
func (c *Client) QueryTask(req *QueryTaskRequest, action string) (*QueryTaskResponse, error) {
|
||||
// 序列化请求
|
||||
jsonBody, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal request failed: %w", err)
|
||||
}
|
||||
|
||||
// 调用SDK的JSON方法
|
||||
respBody, statusCode, err := c.visual.Client.Json(action, nil, string(jsonBody))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query task failed (status: %d): %w", statusCode, err)
|
||||
}
|
||||
|
||||
logger.Infof("Jimeng QueryTask Response: %s", string(respBody))
|
||||
|
||||
// 解析响应
|
||||
var result QueryTaskResponse
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal response failed: %w", err)
|
||||
}
|
||||
|
||||
// 检查响应错误代码
|
||||
if err := HandleResponseError(result.Code, result.Message); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// SubmitSyncImageTask 提交同步生图任务
|
||||
func (c *Client) SubmitSyncImageTask(req types.JimengTaskRequest) (*model.ImagesResponse, error) {
|
||||
// 配置火山引擎访问密钥,目前只支持API Key验证
|
||||
client := arkruntime.NewClientWithApiKey(c.config.ApiKey)
|
||||
// 构造生图请求
|
||||
sequentialImageGeneration := model.SequentialImageGeneration("disabled")
|
||||
generateReq := model.GenerateImagesRequest{
|
||||
Model: req.ReqKey, // 模型名称
|
||||
Prompt: req.Prompt, // 提示词
|
||||
Size: volcengine.String(req.Size), // 图片尺寸
|
||||
SequentialImageGeneration: &sequentialImageGeneration, // 禁用序列生成
|
||||
ResponseFormat: volcengine.String(model.GenerateImagesResponseFormatURL), // 响应格式为 URL
|
||||
Watermark: volcengine.Bool(false), // 不添加水印
|
||||
OptimizePrompt: volcengine.Bool(true), // 优化提示词
|
||||
}
|
||||
if len(req.ImageUrls) > 0 {
|
||||
generateReq.Image = req.ImageUrls
|
||||
}
|
||||
// 调用生图 API
|
||||
resp, err := client.GenerateImages(context.Background(), generateReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
package jimeng
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service/oss"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
// Service 即梦服务(合并了消费者功能)
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
taskQueue *store.RedisQueue
|
||||
client *Client
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
running bool
|
||||
uploader *oss.UploaderManager
|
||||
}
|
||||
|
||||
// NewService 创建即梦服务
|
||||
func NewService(db *gorm.DB, redisCli *redis.Client, uploader *oss.UploaderManager, client *Client) *Service {
|
||||
taskQueue := store.NewRedisQueue("JimengTaskQueue", redisCli)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Service{
|
||||
db: db,
|
||||
redis: redisCli,
|
||||
taskQueue: taskQueue,
|
||||
client: client,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
running: false,
|
||||
uploader: uploader,
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务(包含消费者)
|
||||
func (s *Service) Start() {
|
||||
if s.running {
|
||||
return
|
||||
}
|
||||
logger.Info("Starting Jimeng service and task consumer...")
|
||||
s.running = true
|
||||
go s.consumeTasks()
|
||||
go s.pollTaskStatus()
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (s *Service) Stop() {
|
||||
if !s.running {
|
||||
return
|
||||
}
|
||||
logger.Info("Stopping Jimeng service and task consumer...")
|
||||
s.running = false
|
||||
s.cancel()
|
||||
}
|
||||
|
||||
// consumeTasks 消费任务
|
||||
func (s *Service) consumeTasks() {
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
logger.Info("Jimeng task consumer stopped")
|
||||
return
|
||||
default:
|
||||
s.processNextTask()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processNextTask 处理下一个任务
|
||||
func (s *Service) processNextTask() {
|
||||
var jobId uint
|
||||
if err := s.taskQueue.LPop(&jobId); err != nil {
|
||||
// 队列为空,等待1秒后重试
|
||||
time.Sleep(time.Second)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof("Processing Jimeng task: job_id=%d", jobId)
|
||||
|
||||
if err := s.ProcessTask(jobId); err != nil {
|
||||
logger.Errorf("process jimeng task failed: job_id=%d, error=%v", jobId, err)
|
||||
s.UpdateJobStatus(jobId, types.JMTaskStatusFailed, err.Error())
|
||||
} else {
|
||||
logger.Infof("Jimeng task processed successfully: job_id=%d", jobId)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTask 创建任务
|
||||
func (s *Service) CreateTask(userId uint, req *types.JimengTaskRequest) (*model.JimengJob, error) {
|
||||
// 生成任务ID
|
||||
taskId := utils.RandString(20)
|
||||
|
||||
// 创建任务记录
|
||||
job := &model.JimengJob{
|
||||
UserId: userId,
|
||||
TaskId: taskId,
|
||||
Type: req.TaskType,
|
||||
ReqKey: req.ReqKey,
|
||||
Prompt: req.Prompt,
|
||||
Params: utils.JsonEncode(req),
|
||||
Status: types.JMTaskStatusInQueue,
|
||||
Power: req.Power,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
if err := s.db.Create(job).Error; err != nil {
|
||||
return nil, fmt.Errorf("create jimeng job failed: %w", err)
|
||||
}
|
||||
|
||||
// 推送到任务队列
|
||||
if err := s.taskQueue.RPush(job.Id); err != nil {
|
||||
return nil, fmt.Errorf("push jimeng task to queue failed: %w", err)
|
||||
}
|
||||
|
||||
return job, nil
|
||||
}
|
||||
|
||||
// ProcessTask 处理任务
|
||||
func (s *Service) ProcessTask(jobId uint) error {
|
||||
// 获取任务记录
|
||||
var job model.JimengJob
|
||||
if err := s.db.First(&job, jobId).Error; err != nil {
|
||||
return fmt.Errorf("get jimeng job failed: %w", err)
|
||||
}
|
||||
|
||||
// 更新任务状态为处理中
|
||||
if err := s.UpdateJobStatus(job.Id, types.JMTaskStatusGenerating, ""); err != nil {
|
||||
return fmt.Errorf("update job status failed: %w", err)
|
||||
}
|
||||
|
||||
// 解析任务参数
|
||||
var req types.JimengTaskRequest
|
||||
err := utils.JsonDecode(job.Params, &req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse task params failed: %w", err)
|
||||
}
|
||||
|
||||
// 构建请求并提交任务
|
||||
params, err := s.buildTaskRequest(&req)
|
||||
if err != nil {
|
||||
return s.handleTaskError(job.Id, fmt.Sprintf("build task request failed: %v", err))
|
||||
}
|
||||
|
||||
// 数字人任务,先识别主体
|
||||
if req.TaskType == types.JMTaskTypeVirtualHuman {
|
||||
if err := s.client.AvatarRecognition(req.ImageUrls[0], req.RecognizeKey); err != nil {
|
||||
return s.handleTaskError(job.Id, fmt.Sprintf("avatar recognition failed: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// 同步任务 ,后台执行
|
||||
if req.ReqKey == DoubaoSeedream40ReqKey {
|
||||
go func() {
|
||||
resp, err := s.client.SubmitSyncImageTask(req)
|
||||
if err != nil {
|
||||
_ = s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %v", err))
|
||||
return
|
||||
}
|
||||
logger.Infof("同步任务提交成功: %+v", resp)
|
||||
// 更新原始数据
|
||||
rawData, _ := json.Marshal(resp)
|
||||
updates := map[string]any{
|
||||
"raw_data": string(rawData),
|
||||
}
|
||||
if resp.Error != nil {
|
||||
updates["status"] = types.JMTaskStatusFailed
|
||||
updates["err_msg"] = resp.Error.Message
|
||||
s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(updates)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新任务状态
|
||||
updates["status"] = types.JMTaskStatusSuccess
|
||||
// 下载图片
|
||||
imgUrl, err := s.uploader.GetUploadHandler().PutUrlFile(*resp.Data[0].Url, ".png", false)
|
||||
if err == nil {
|
||||
updates["img_url"] = imgUrl
|
||||
}
|
||||
s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(updates)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Debugf("提交即梦任务: %+v", params)
|
||||
// 异步任务 ,前台执行
|
||||
resp, err := s.client.SubmitTask(params)
|
||||
if err != nil {
|
||||
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %v", err))
|
||||
}
|
||||
|
||||
if resp.Code != CodeSuccess {
|
||||
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %s", resp.Message))
|
||||
}
|
||||
|
||||
// 更新任务ID和原始数据
|
||||
rawData, _ := json.Marshal(resp)
|
||||
if err := s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(map[string]any{
|
||||
"task_id": resp.Data.TaskId,
|
||||
"raw_data": string(rawData),
|
||||
"updated_at": time.Now(),
|
||||
}).Error; err != nil {
|
||||
logger.Errorf("update jimeng job task_id failed: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildTaskRequest 构建任务请求(统一的参数解析)
|
||||
func (s *Service) buildTaskRequest(req *types.JimengTaskRequest) (map[string]any, error) {
|
||||
var params map[string]any
|
||||
err := utils.JsonDecode(utils.JsonEncode(req), ¶ms)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse task params failed: %w", err)
|
||||
}
|
||||
// 把 size 转成 width 和 height
|
||||
if size, ok := params["size"]; ok {
|
||||
if sizeStr, ok := size.(string); ok {
|
||||
if strings.Contains(sizeStr, "x") {
|
||||
sizes := strings.Split(sizeStr, "x")
|
||||
params["width"] = sizes[0]
|
||||
params["height"] = sizes[1]
|
||||
}
|
||||
}
|
||||
delete(params, "size")
|
||||
}
|
||||
|
||||
// duration 转成 frames
|
||||
if duration, ok := params["duration"]; ok {
|
||||
if secs, ok := duration.(int); ok {
|
||||
params["frames"] = secs*24 + 1
|
||||
}
|
||||
delete(params, "duration")
|
||||
}
|
||||
|
||||
// 单独处理图片特效任务
|
||||
if req.ReqKey == ImageEffectReqKey {
|
||||
params["image_input1"] = req.ImageUrls[0]
|
||||
delete(params, "image_urls")
|
||||
}
|
||||
|
||||
// 动作迁移,数字人任务参数处理
|
||||
if req.TaskType == types.JMTaskTypeVirtualHuman || req.TaskType == types.JMTaskTypeActionTransfer {
|
||||
params["image_url"] = req.ImageUrls[0]
|
||||
delete(params, "image_urls")
|
||||
}
|
||||
if req.RecognizeKey != "" {
|
||||
delete(params, "recognize_key")
|
||||
}
|
||||
|
||||
// 删除多余参数,剩下的就是各个任务自己专有参数了
|
||||
delete(params, "type")
|
||||
delete(params, "power")
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// pollTaskStatus 轮询任务状态
|
||||
func (s *Service) pollTaskStatus() {
|
||||
|
||||
for {
|
||||
var jobs []model.JimengJob
|
||||
s.db.Where("status IN (?)", []types.JMTaskStatus{types.JMTaskStatusGenerating, types.JMTaskStatusInQueue}).Find(&jobs)
|
||||
if len(jobs) == 0 {
|
||||
logger.Debugf("no jimeng task to poll, sleep 10s")
|
||||
time.Sleep(10 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, job := range jobs {
|
||||
// 任务超时处理
|
||||
if job.UpdatedAt.Before(time.Now().Add(-10 * time.Minute)) {
|
||||
s.handleTaskError(job.Id, "task timeout")
|
||||
continue
|
||||
}
|
||||
|
||||
// 豆包生图 4.0 是同步任务,不需要轮询
|
||||
if job.ReqKey == DoubaoSeedream40ReqKey {
|
||||
continue
|
||||
}
|
||||
|
||||
// 查询任务状态
|
||||
resp, err := s.client.QueryTask(&QueryTaskRequest{
|
||||
ReqKey: job.ReqKey,
|
||||
TaskId: job.TaskId,
|
||||
ReqJson: `{"return_url":true}`,
|
||||
}, ASyncActionGetResult)
|
||||
|
||||
if err != nil {
|
||||
s.handleTaskError(job.Id, fmt.Sprintf("query task failed: %s", err.Error()))
|
||||
continue
|
||||
}
|
||||
|
||||
// 更新原始数据
|
||||
rawData, _ := json.Marshal(resp)
|
||||
s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Update("raw_data", string(rawData))
|
||||
|
||||
if resp.Code != CodeSuccess {
|
||||
s.handleTaskError(job.Id, fmt.Sprintf("query task failed: %s", resp.Message))
|
||||
continue
|
||||
}
|
||||
|
||||
switch resp.Data.Status {
|
||||
case types.JMTaskStatusDone:
|
||||
// 判断任务是否成功
|
||||
if resp.Message != "Success" {
|
||||
s.handleTaskError(job.Id, fmt.Sprintf("task failed: %s", resp.Data.AlgorithmBaseResp.StatusMessage))
|
||||
continue
|
||||
}
|
||||
|
||||
// 任务完成,更新结果
|
||||
updates := map[string]any{
|
||||
"status": types.JMTaskStatusSuccess,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
|
||||
// 设置结果URL
|
||||
if len(resp.Data.ImageUrls) > 0 {
|
||||
imgUrl, err := s.uploader.GetUploadHandler().PutUrlFile(resp.Data.ImageUrls[0], ".png", false)
|
||||
if err != nil {
|
||||
logger.Errorf("upload image failed: %v", err)
|
||||
imgUrl = resp.Data.ImageUrls[0]
|
||||
}
|
||||
updates["img_url"] = imgUrl
|
||||
}
|
||||
if resp.Data.VideoUrl != "" {
|
||||
videoUrl, err := s.uploader.GetUploadHandler().PutUrlFile(resp.Data.VideoUrl, ".mp4", false)
|
||||
if err != nil {
|
||||
logger.Errorf("upload video failed: %v", err)
|
||||
videoUrl = resp.Data.VideoUrl
|
||||
}
|
||||
updates["video_url"] = videoUrl
|
||||
}
|
||||
|
||||
s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(updates)
|
||||
case types.JMTaskStatusInQueue, types.JMTaskStatusGenerating:
|
||||
// 任务处理中
|
||||
s.UpdateJobStatus(job.Id, types.JMTaskStatusGenerating, "")
|
||||
|
||||
case types.JMTaskStatusNotFound:
|
||||
// 任务未找到
|
||||
s.handleTaskError(job.Id, "task not found")
|
||||
|
||||
case types.JMTaskStatusExpired:
|
||||
continue
|
||||
default:
|
||||
logger.Warnf("unknown task status: %s", resp.Data.Status)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// UpdateJobStatus 更新任务状态
|
||||
func (s *Service) UpdateJobStatus(jobId uint, status types.JMTaskStatus, errMsg string) error {
|
||||
updates := map[string]any{
|
||||
"status": status,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
if errMsg != "" {
|
||||
updates["err_msg"] = errMsg
|
||||
}
|
||||
return s.db.Model(&model.JimengJob{}).Where("id = ?", jobId).Updates(updates).Error
|
||||
}
|
||||
|
||||
// handleTaskError 处理任务错误
|
||||
func (s *Service) handleTaskError(jobId uint, errMsg string) error {
|
||||
logger.Errorf("Jimeng task error (job_id: %d): %s", jobId, errMsg)
|
||||
return s.UpdateJobStatus(jobId, types.JMTaskStatusFailed, errMsg)
|
||||
}
|
||||
|
||||
// PushTaskToQueue 推送任务到队列(用于手动重试)
|
||||
func (s *Service) PushTaskToQueue(jobId uint) error {
|
||||
return s.taskQueue.RPush(jobId)
|
||||
}
|
||||
|
||||
// GetTaskStats 获取任务统计信息
|
||||
func (s *Service) GetTaskStats() (map[string]any, error) {
|
||||
type StatResult struct {
|
||||
Status types.JMTaskStatus `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
var stats []StatResult
|
||||
err := s.db.Model(&model.JimengJob{}).
|
||||
Select("status, COUNT(*) as count").
|
||||
Group("status").
|
||||
Find(&stats).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := map[string]any{
|
||||
"total": int64(0),
|
||||
"completed": int64(0),
|
||||
"processing": int64(0),
|
||||
"failed": int64(0),
|
||||
"pending": int64(0),
|
||||
}
|
||||
|
||||
for _, stat := range stats {
|
||||
result["total"] = result["total"].(int64) + stat.Count
|
||||
result[string(stat.Status)] = stat.Count
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetJob 获取任务
|
||||
func (s *Service) GetJob(jobId uint) (*model.JimengJob, error) {
|
||||
var job model.JimengJob
|
||||
if err := s.db.First(&job, jobId).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &job, nil
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package jimeng
|
||||
|
||||
import (
|
||||
"geekai/core/types"
|
||||
)
|
||||
|
||||
// SubmitTaskResponse 提交任务响应
|
||||
type SubmitTaskResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
RequestId string `json:"request_id"`
|
||||
Status int `json:"status"`
|
||||
TimeElapsed string `json:"time_elapsed"`
|
||||
Data struct {
|
||||
TaskId string `json:"task_id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// QueryTaskRequest 查询任务请求
|
||||
type QueryTaskRequest struct {
|
||||
ReqKey string `json:"req_key"`
|
||||
TaskId string `json:"task_id"`
|
||||
ReqJson string `json:"req_json,omitempty"`
|
||||
}
|
||||
|
||||
// QueryTaskResponse 查询任务响应
|
||||
type QueryTaskResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
RequestId string `json:"request_id"`
|
||||
Status int `json:"status"`
|
||||
TimeElapsed string `json:"time_elapsed"`
|
||||
Data struct {
|
||||
AlgorithmBaseResp struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
StatusMessage string `json:"status_message"`
|
||||
} `json:"algorithm_base_resp"`
|
||||
BinaryDataBase64 []string `json:"binary_data_base64"`
|
||||
ImageUrls []string `json:"image_urls"`
|
||||
VideoUrl string `json:"video_url"`
|
||||
RespData string `json:"resp_data"`
|
||||
Status types.JMTaskStatus `json:"status"`
|
||||
LlmResult string `json:"llm_result"`
|
||||
PeResult string `json:"pe_result"`
|
||||
PredictTagsResult string `json:"predict_tags_result"`
|
||||
RephraserResult string `json:"rephraser_result"`
|
||||
VlmResult string `json:"vlm_result"`
|
||||
InferCtx any `json:"infer_ctx"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
const CodeSuccess = 10000
|
||||
|
||||
// 即梦AI错误代码常量
|
||||
const (
|
||||
// 成功
|
||||
ECSuccess = 10000
|
||||
|
||||
// 请求参数错误 (50200-50215)
|
||||
ECReqInvalidArgs = 50200 // 参数错误
|
||||
ECReqMissingArgs = 50201 // 缺少参数
|
||||
ECParseArgs = 50204 // 参数类型错误/参数缺失
|
||||
ECImageSizeLimited = 50205 // 图像尺寸超过限制
|
||||
ECImageEmpty = 50206 // 请求参数中没有获取到图像
|
||||
ECImageDecodeError = 50207 // 图像解码错误
|
||||
ECVideoEmpty = 50209 // 请求参数中没有获取到视频
|
||||
ECVideoDecodeError = 50210 // 视频解码错误
|
||||
ECVideoSizeLimited = 50211 // 视频尺寸超过限制
|
||||
ECReqBodySizeLimited = 50213 // 请求Body过大
|
||||
ECVideoTimeTooLong = 50214 // 输入视频时长过大
|
||||
ECRPCProcess = 50215 // 请求处理失败
|
||||
|
||||
// 算法服务错误 (60102-60208)
|
||||
ECJPFaceDetect = 60102 // 算法服务需要输入人脸图,但未检测到
|
||||
ECFSLeaderRiskError = 60208 // 输入图片中包含敏感信息,未通过审核
|
||||
|
||||
// 权限和系统错误 (50400-50501)
|
||||
ECAuth = 50400 // 权限校验失败
|
||||
ECReqMethod = 50402 // 访问的接口不存在
|
||||
ECReqLimit = 50429 // 超过调用QPS限制
|
||||
ECInternal = 50500 // 服务器内部错误
|
||||
ECRPCInternal = 50501 // 服务器内部RPC错误
|
||||
)
|
||||
|
||||
// 错误代码到错误信息的映射
|
||||
var errorCodeMessages = map[int]string{
|
||||
// 成功
|
||||
ECSuccess: "请求成功",
|
||||
|
||||
// 请求参数错误
|
||||
ECReqInvalidArgs: "参数错误,检查入参及MIME类型",
|
||||
ECReqMissingArgs: "缺少参数,检查入参及MIME类型",
|
||||
ECParseArgs: "参数类型错误/参数缺失,检查入参及MIME类型",
|
||||
ECImageSizeLimited: "图像尺寸超过限制,参考接口文档入参要求部分",
|
||||
ECImageEmpty: "请求参数中没有获取到图像,检查入参",
|
||||
ECImageDecodeError: "图像解码错误:没有获取到图像或者通过image_base64参数传递图像是base64解码错误,检查输出图片或检查base64是否错误携带前缀",
|
||||
ECVideoEmpty: "请求参数中没有获取到视频。输入为视频时可能返回此错误,检查入参",
|
||||
ECVideoDecodeError: "视频解码错误。输入为视频时可能返回此错误,检查输入视频是否不正确",
|
||||
ECVideoSizeLimited: "视频尺寸超过限制。输入为视频时可能返回此错误,检查输入视频大小",
|
||||
ECReqBodySizeLimited: "请求Body过大,超出接口限制,检查请求Body大小",
|
||||
ECVideoTimeTooLong: "输入视频时长过大,检查输入视频时长",
|
||||
ECRPCProcess: "由于输入的图片、视频、参数等不满足要求,导致请求处理失败。若接口文档中有具体说明,优先参考其具体含义,按照具体服务说明进行检查",
|
||||
|
||||
// 算法服务错误
|
||||
ECJPFaceDetect: "算法服务需要输入人脸图,但未检测到,检查输入图片是否包含人脸",
|
||||
ECFSLeaderRiskError: "输入图片中包含敏感信息,未通过审核",
|
||||
|
||||
// 权限和系统错误
|
||||
ECAuth: "权限校验失败,请检查是否已创建应用并开通服务或签名,参考接入指南及快速接入",
|
||||
ECReqMethod: "访问的接口不存在,检查入参",
|
||||
ECReqLimit: "超过调用QPS限制,购买QPS增项包",
|
||||
ECInternal: "服务器内部错误,提工单",
|
||||
ECRPCInternal: "服务器内部RPC错误,提工单",
|
||||
}
|
||||
|
||||
// CreateTaskRequest 创建任务请求
|
||||
type CreateTaskRequest struct {
|
||||
Type types.JMTaskType `json:"type"`
|
||||
Prompt string `json:"prompt"`
|
||||
Params map[string]any `json:"params"`
|
||||
ReqKey string `json:"req_key"`
|
||||
ImageUrls []string `json:"image_urls,omitempty"`
|
||||
Power int `json:"power,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
ImageEffectReqKey = "i2i_multi_style_zx2x"
|
||||
DoubaoSeedream40ReqKey = "doubao-seedream-4-0-250828"
|
||||
)
|
||||
|
||||
const (
|
||||
ASyncActionSubmit = "CVSync2AsyncSubmitTask" // 异步提交任务
|
||||
SyncActionSubmit = "CVSubmitTask" // 同步提交任务
|
||||
ASyncActionGetResult = "CVSync2AsyncGetResult" // 异步获取结果
|
||||
SyncActionGetResult = "CVGetResult" // 同步获取结果
|
||||
)
|
||||
@@ -10,39 +10,35 @@ package service
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type LicenseService struct {
|
||||
config types.ApiConfig
|
||||
levelDB *store.LevelDB
|
||||
license *types.License
|
||||
urlWhiteList []string
|
||||
machineId string
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewLicenseService(server *core.AppServer, levelDB *store.LevelDB) *LicenseService {
|
||||
var license types.License
|
||||
func NewLicenseService(sysConfig *types.SystemConfig, db *gorm.DB) *LicenseService {
|
||||
var machineId string
|
||||
err := levelDB.Get(types.LicenseKey, &license)
|
||||
logger.Infof("License: %+v", server.SysConfig)
|
||||
info, err := host.Info()
|
||||
if err == nil {
|
||||
machineId = info.HostID
|
||||
}
|
||||
logger.Infof("License: %+v", license)
|
||||
logger.Infof("License: %+v", sysConfig.License)
|
||||
return &LicenseService{
|
||||
config: server.Config.ApiConfig,
|
||||
levelDB: levelDB,
|
||||
license: &license,
|
||||
license: &sysConfig.License,
|
||||
machineId: machineId,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,15 +53,15 @@ type License struct {
|
||||
}
|
||||
|
||||
// ActiveLicense 激活 License
|
||||
func (s *LicenseService) ActiveLicense(license string, machineId string) error {
|
||||
func (s *LicenseService) ActiveLicense(license string) error {
|
||||
var res struct {
|
||||
Code types.BizCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data License `json:"data"`
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/%s", s.config.ApiURL, "api/license/active")
|
||||
apiURL := fmt.Sprintf("%s/%s", types.GeekAPIURL, "api/license/active")
|
||||
response, err := req.C().R().
|
||||
SetBody(map[string]string{"license": license, "machine_id": machineId}).
|
||||
SetBody(map[string]string{"license": license, "machine_id": s.machineId}).
|
||||
SetSuccessResult(&res).Post(apiURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("发送激活请求失败: %v", err)
|
||||
@@ -79,17 +75,24 @@ func (s *LicenseService) ActiveLicense(license string, machineId string) error {
|
||||
return fmt.Errorf("激活失败:%v", res.Message)
|
||||
}
|
||||
|
||||
if res.Data.ExpiredAt > 0 && res.Data.ExpiredAt < time.Now().Unix() {
|
||||
return fmt.Errorf("License 已过期")
|
||||
}
|
||||
|
||||
s.license = &types.License{
|
||||
Key: license,
|
||||
MachineId: machineId,
|
||||
MachineId: s.machineId,
|
||||
Configs: res.Data.Configs,
|
||||
ExpiredAt: res.Data.ExpiredAt,
|
||||
IsActive: true,
|
||||
}
|
||||
err = s.levelDB.Put(types.LicenseKey, s.license)
|
||||
|
||||
// 保存 License 到数据库
|
||||
err = s.db.Model(&model.Config{}).Where("name = ?", types.ConfigKeyLicense).UpdateColumn("value", utils.JsonEncode(s.license)).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存许可证书失败:%v", err)
|
||||
return fmt.Errorf("保存 License 到数据库失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -102,11 +105,16 @@ func (s *LicenseService) SyncLicense() {
|
||||
if err != nil {
|
||||
retryCounter++
|
||||
if retryCounter < 5 {
|
||||
logger.Warn(err)
|
||||
logger.Debug(err)
|
||||
}
|
||||
s.license.IsActive = false
|
||||
} else {
|
||||
s.license = license
|
||||
// 保存 License 到数据库
|
||||
err = s.db.Model(&model.Config{}).Where("name = ?", types.ConfigKeyLicense).UpdateColumn("value", utils.JsonEncode(s.license)).Error
|
||||
if err != nil {
|
||||
logger.Errorf("保存 License 到数据库失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
urls, err := s.fetchUrlWhiteList()
|
||||
@@ -125,18 +133,18 @@ func (s *LicenseService) fetchLicense() (*types.License, error) {
|
||||
Message string `json:"message"`
|
||||
Data License `json:"data"`
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/%s", s.config.ApiURL, "api/license/check")
|
||||
apiURL := fmt.Sprintf("%s/%s", types.GeekAPIURL, "api/license/check")
|
||||
response, err := req.C().R().
|
||||
SetBody(map[string]string{"license": s.license.Key, "machine_id": s.machineId}).
|
||||
SetSuccessResult(&res).Post(apiURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送激活请求失败: %v", err)
|
||||
return nil, fmt.Errorf("License 同步失败: %v", err)
|
||||
}
|
||||
if response.IsErrorState() {
|
||||
return nil, fmt.Errorf("激活失败:%v", response.Status)
|
||||
return nil, fmt.Errorf("License 同步失败:%v", response.Status)
|
||||
}
|
||||
if res.Code != types.Success {
|
||||
return nil, fmt.Errorf("激活失败:%v", res.Message)
|
||||
return nil, fmt.Errorf("License 同步失败:%v", res.Message)
|
||||
}
|
||||
|
||||
return &types.License{
|
||||
@@ -154,7 +162,7 @@ func (s *LicenseService) fetchUrlWhiteList() ([]string, error) {
|
||||
Message string `json:"message"`
|
||||
Data []string `json:"data"`
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/%s", s.config.ApiURL, "api/license/urls")
|
||||
apiURL := fmt.Sprintf("%s/%s", types.GeekAPIURL, "api/license/urls")
|
||||
response, err := req.C().R().SetSuccessResult(&res).Get(apiURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送请求失败: %v", err)
|
||||
@@ -171,9 +179,21 @@ func (s *LicenseService) fetchUrlWhiteList() ([]string, error) {
|
||||
|
||||
// GetLicense 获取许可信息
|
||||
func (s *LicenseService) GetLicense() *types.License {
|
||||
if s.license == nil {
|
||||
var config model.Config
|
||||
s.db.Model(&model.Config{}).Where("name = ?", types.ConfigKeyLicense).First(&config)
|
||||
if config.Value != "" {
|
||||
utils.JsonDecode(config.Value, &s.license)
|
||||
}
|
||||
}
|
||||
return s.license
|
||||
}
|
||||
|
||||
func (s *LicenseService) SetLicense(licenseKey string) {
|
||||
s.license.Key = licenseKey
|
||||
|
||||
}
|
||||
|
||||
// IsValidApiURL 判断是否合法的中转 URL
|
||||
func (s *LicenseService) IsValidApiURL(uri string) error {
|
||||
// 获得许可授权的直接放行
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
package service
|
||||
|
||||
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// Use of this source code is governed by a Apache-2.0 license
|
||||
// that can be found in the LICENSE file.
|
||||
// @Author yangjian102621@163.com
|
||||
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"strings"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
// 迁移状态Redis key
|
||||
MigrationStatusKey = "config_migration:status"
|
||||
// 迁移完成标志
|
||||
MigrationCompleted = "completed"
|
||||
)
|
||||
|
||||
// MigrationService 配置迁移服务
|
||||
type MigrationService struct {
|
||||
db *gorm.DB
|
||||
redisClient *redis.Client
|
||||
appConfig *types.AppConfig
|
||||
levelDB *store.LevelDB
|
||||
licenseService *LicenseService
|
||||
}
|
||||
|
||||
func NewMigrationService(db *gorm.DB, redisClient *redis.Client, appConfig *types.AppConfig, levelDB *store.LevelDB, licenseService *LicenseService) *MigrationService {
|
||||
return &MigrationService{
|
||||
db: db,
|
||||
redisClient: redisClient,
|
||||
appConfig: appConfig,
|
||||
levelDB: levelDB,
|
||||
licenseService: licenseService,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MigrationService) StartMigrate() {
|
||||
go func() {
|
||||
s.MigrateConfig(s.appConfig)
|
||||
s.TableMigration()
|
||||
s.MigrateLicense()
|
||||
}()
|
||||
}
|
||||
|
||||
// 迁移 License
|
||||
func (s *MigrationService) MigrateLicense() {
|
||||
key := "migrate:license"
|
||||
if s.redisClient.Get(context.Background(), key).Val() == "1" {
|
||||
logger.Info("License 已迁移,跳过迁移")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("开始迁移 License...")
|
||||
var license types.License
|
||||
err := s.levelDB.Get(types.LicenseKey, &license)
|
||||
if err != nil {
|
||||
license = types.License{
|
||||
Key: "",
|
||||
MachineId: "",
|
||||
Configs: types.LicenseConfig{UserNum: 0, DeCopy: false},
|
||||
ExpiredAt: 0,
|
||||
IsActive: false,
|
||||
}
|
||||
}
|
||||
logger.Infof("迁移 License: %+v", license)
|
||||
if err := s.saveConfig(types.ConfigKeyLicense, license); err != nil {
|
||||
logger.Errorf("迁移 License 失败: %v", err)
|
||||
return
|
||||
}
|
||||
s.licenseService.SetLicense(license.Key)
|
||||
logger.Info("迁移 License 完成")
|
||||
s.redisClient.Set(context.Background(), key, "1", 0)
|
||||
}
|
||||
|
||||
// 迁移配置内容
|
||||
func (s *MigrationService) MigrateConfigContent() error {
|
||||
// 用户协议
|
||||
if err := s.saveConfig(types.ConfigKeyPrivacy, map[string]string{
|
||||
"content": "用户协议内容",
|
||||
}); err != nil {
|
||||
return fmt.Errorf("迁移配置内容失败: %v", err)
|
||||
}
|
||||
// 隐私政策
|
||||
if err := s.saveConfig(types.ConfigKeyAgreement, map[string]string{
|
||||
"content": "隐私政策内容",
|
||||
}); err != nil {
|
||||
return fmt.Errorf("迁移配置内容失败: %v", err)
|
||||
}
|
||||
// 思维导图
|
||||
if err := s.saveConfig(types.ConfigKeyMarkMap, map[string]string{
|
||||
"content": `# GeekAI 演示站
|
||||
|
||||
- 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
|
||||
- 基于 Websocket 实现,完美的打字机体验。
|
||||
- 内置了各种预训练好的角色应用,轻松满足你的各种聊天和应用需求。
|
||||
- 支持 OPenAI,Azure,文心一言,讯飞星火,清华 ChatGLM等多个大语言模型。
|
||||
- 支持 MidJourney / Stable Diffusion AI 绘画集成,开箱即用。
|
||||
- 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。
|
||||
- 已集成支付宝支付功能,微信支付,支持多种会员套餐和点卡购买功能。
|
||||
- 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件。`,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("迁移配置内容失败: %v", err)
|
||||
}
|
||||
|
||||
// 微信登录配置
|
||||
if err := s.saveConfig(types.ConfigKeyWxLogin, map[string]string{
|
||||
"api_key": "",
|
||||
"notify_url": "",
|
||||
"enabled": "false",
|
||||
}); err != nil {
|
||||
return fmt.Errorf("迁移配置内容失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证码配置
|
||||
if err := s.saveConfig(types.ConfigKeyCaptcha, map[string]string{
|
||||
"api_key": "",
|
||||
"type": "dot",
|
||||
"enabled": "false",
|
||||
}); err != nil {
|
||||
return fmt.Errorf("迁移配置内容失败: %v", err)
|
||||
}
|
||||
|
||||
// 文本审核
|
||||
if err := s.saveConfig(types.ConfigKeyModeration, map[string]any{
|
||||
"enable": "false",
|
||||
"active": "gitee",
|
||||
"enable_guide": "false",
|
||||
"guide_prompt": "",
|
||||
"gitee": map[string]string{
|
||||
"api_key": "",
|
||||
"model": "Security-semantic-filtering",
|
||||
},
|
||||
"baidu": map[string]string{
|
||||
"access_key": "",
|
||||
"secret_key": "",
|
||||
},
|
||||
"tencent": map[string]string{
|
||||
"access_key": "",
|
||||
"secret_key": "",
|
||||
},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("迁移配置内容失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 数据表迁移
|
||||
func (s *MigrationService) TableMigration() {
|
||||
|
||||
// v4.2.7 数据表迁移
|
||||
if s.db.Migrator().HasColumn(&model.JimengJob{}, "task_params") {
|
||||
s.db.Migrator().RenameColumn(&model.JimengJob{}, "task_params", "params")
|
||||
}
|
||||
|
||||
// 新数据表
|
||||
if !s.db.Migrator().HasTable(&model.Moderation{}) {
|
||||
s.db.AutoMigrate(&model.Moderation{})
|
||||
}
|
||||
|
||||
// 订单字段整理
|
||||
if s.db.Migrator().HasColumn(&model.Order{}, "pay_type") {
|
||||
s.db.Migrator().RenameColumn(&model.Order{}, "pay_type", "channel")
|
||||
}
|
||||
if !s.db.Migrator().HasColumn(&model.Order{}, "checked") {
|
||||
s.db.Migrator().AddColumn(&model.Order{}, "checked")
|
||||
}
|
||||
|
||||
// 重命名 config 表字段
|
||||
if s.db.Migrator().HasColumn(&model.Config{}, "config_json") {
|
||||
s.db.Migrator().RenameColumn(&model.Config{}, "config_json", "value")
|
||||
}
|
||||
if s.db.Migrator().HasColumn(&model.Config{}, "marker") {
|
||||
s.db.Migrator().RenameColumn(&model.Config{}, "marker", "name")
|
||||
}
|
||||
if s.db.Migrator().HasIndex(&model.Config{}, "idx_chatgpt_configs_key") {
|
||||
s.db.Migrator().DropIndex(&model.Config{}, "idx_chatgpt_configs_key")
|
||||
}
|
||||
if s.db.Migrator().HasIndex(&model.Config{}, "marker") {
|
||||
s.db.Migrator().DropIndex(&model.Config{}, "marker")
|
||||
}
|
||||
|
||||
// 手动删除字段
|
||||
if s.db.Migrator().HasColumn(&model.Order{}, "deleted_at") {
|
||||
s.db.Migrator().DropColumn(&model.Order{}, "deleted_at")
|
||||
}
|
||||
if s.db.Migrator().HasColumn(&model.ChatItem{}, "deleted_at") {
|
||||
s.db.Migrator().DropColumn(&model.ChatItem{}, "deleted_at")
|
||||
}
|
||||
if s.db.Migrator().HasColumn(&model.ChatMessage{}, "deleted_at") {
|
||||
s.db.Migrator().DropColumn(&model.ChatMessage{}, "deleted_at")
|
||||
}
|
||||
if s.db.Migrator().HasColumn(&model.User{}, "chat_config") {
|
||||
s.db.Migrator().DropColumn(&model.User{}, "chat_config")
|
||||
}
|
||||
if s.db.Migrator().HasColumn(&model.ChatModel{}, "category") {
|
||||
s.db.Migrator().DropColumn(&model.ChatModel{}, "category")
|
||||
}
|
||||
if s.db.Migrator().HasColumn(&model.ChatModel{}, "description") {
|
||||
s.db.Migrator().DropColumn(&model.ChatModel{}, "description")
|
||||
}
|
||||
if s.db.Migrator().HasColumn(&model.Product{}, "discount") {
|
||||
s.db.Migrator().DropColumn(&model.Product{}, "discount")
|
||||
}
|
||||
if s.db.Migrator().HasColumn(&model.Product{}, "days") {
|
||||
s.db.Migrator().DropColumn(&model.Product{}, "days")
|
||||
}
|
||||
if s.db.Migrator().HasColumn(&model.Product{}, "app_url") {
|
||||
s.db.Migrator().DropColumn(&model.Product{}, "app_url")
|
||||
}
|
||||
if s.db.Migrator().HasColumn(&model.Product{}, "url") {
|
||||
s.db.Migrator().DropColumn(&model.Product{}, "url")
|
||||
}
|
||||
}
|
||||
|
||||
// 迁移配置数据
|
||||
func (s *MigrationService) MigrateConfig(config *types.AppConfig) error {
|
||||
|
||||
logger.Info("开始迁移配置到数据库...")
|
||||
|
||||
// 迁移支付配置
|
||||
if err := s.migratePaymentConfig(config); err != nil {
|
||||
logger.Errorf("迁移支付配置失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 迁移存储配置
|
||||
if err := s.migrateStorageConfig(config); err != nil {
|
||||
logger.Errorf("迁移存储配置失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 迁移通信配置
|
||||
if err := s.migrateCommunicationConfig(config); err != nil {
|
||||
logger.Errorf("迁移通信配置失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 迁移配置内容
|
||||
if err := s.MigrateConfigContent(); err != nil {
|
||||
logger.Errorf("迁移配置内容失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("配置迁移完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 迁移支付配置
|
||||
func (s *MigrationService) migratePaymentConfig(config *types.AppConfig) error {
|
||||
|
||||
paymentConfig := types.PaymentConfig{
|
||||
Alipay: config.AlipayConfig,
|
||||
Epay: config.GeekPayConfig,
|
||||
WxPay: config.WechatPayConfig,
|
||||
}
|
||||
if err := s.saveConfig(types.ConfigKeyPayment, paymentConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 迁移存储配置
|
||||
func (s *MigrationService) migrateStorageConfig(config *types.AppConfig) error {
|
||||
|
||||
ossConfig := types.OSSConfig{
|
||||
Active: config.OSS.Active,
|
||||
Local: config.OSS.Local,
|
||||
Minio: config.OSS.Minio,
|
||||
QiNiu: config.OSS.QiNiu,
|
||||
AliYun: config.OSS.AliYun,
|
||||
}
|
||||
return s.saveConfig(types.ConfigKeyOss, ossConfig)
|
||||
}
|
||||
|
||||
// 迁移通信配置
|
||||
func (s *MigrationService) migrateCommunicationConfig(config *types.AppConfig) error {
|
||||
// SMTP配置
|
||||
smtpConfig := map[string]any{
|
||||
"use_tls": config.SmtpConfig.UseTls,
|
||||
"host": config.SmtpConfig.Host,
|
||||
"port": config.SmtpConfig.Port,
|
||||
"app_name": config.SmtpConfig.AppName,
|
||||
"from": config.SmtpConfig.From,
|
||||
"password": config.SmtpConfig.Password,
|
||||
}
|
||||
if err := s.saveConfig(types.ConfigKeySmtp, smtpConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 短信配置
|
||||
smsConfig := map[string]any{
|
||||
"active": strings.ToLower(config.SMS.Active),
|
||||
"aliyun": map[string]any{
|
||||
"access_key": config.SMS.Ali.AccessKey,
|
||||
"access_secret": config.SMS.Ali.AccessSecret,
|
||||
"sign": config.SMS.Ali.Sign,
|
||||
"code_temp_id": config.SMS.Ali.CodeTempId,
|
||||
},
|
||||
"bao": map[string]any{
|
||||
"username": config.SMS.Bao.Username,
|
||||
"password": config.SMS.Bao.Password,
|
||||
"sign": config.SMS.Bao.Sign,
|
||||
"code_template": config.SMS.Bao.CodeTemplate,
|
||||
},
|
||||
}
|
||||
return s.saveConfig(types.ConfigKeySms, smsConfig)
|
||||
}
|
||||
|
||||
// 保存配置到数据库
|
||||
func (s *MigrationService) saveConfig(key string, config any) error {
|
||||
// 检查是否已存在
|
||||
var existingConfig model.Config
|
||||
if err := s.db.Where("name", key).First(&existingConfig).Error; err == nil {
|
||||
// 配置已存在,跳过
|
||||
logger.Infof("配置 %s 已存在,跳过迁移", key)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 序列化配置
|
||||
configJSON, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
newConfig := model.Config{
|
||||
Name: key,
|
||||
Value: string(configJSON),
|
||||
}
|
||||
if err := s.db.Create(&newConfig).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Infof("成功迁移配置 %s", key)
|
||||
return nil
|
||||
}
|
||||
@@ -67,25 +67,6 @@ func (s *Service) Run() {
|
||||
continue
|
||||
}
|
||||
|
||||
// translate prompt
|
||||
if utils.HasChinese(task.Prompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Prompt), task.TranslateModelId)
|
||||
if err == nil {
|
||||
task.Prompt = content
|
||||
} else {
|
||||
logger.Warnf("error with translate prompt: %v", err)
|
||||
}
|
||||
}
|
||||
// translate negative prompt
|
||||
if task.NegPrompt != "" && utils.HasChinese(task.NegPrompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.NegPrompt), task.TranslateModelId)
|
||||
if err == nil {
|
||||
task.NegPrompt = content
|
||||
} else {
|
||||
logger.Warnf("error with translate prompt: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// use fast mode as default
|
||||
if task.Mode == "" {
|
||||
task.Mode = "fast"
|
||||
@@ -191,7 +172,7 @@ func (s *Service) DownloadImages() {
|
||||
if strings.HasPrefix(v.OrgURL, "https://cdn.discordapp.com") {
|
||||
proxy = true
|
||||
}
|
||||
imgURL, err := s.uploaderManager.GetUploadHandler().PutUrlFile(v.OrgURL, proxy)
|
||||
imgURL, err := s.uploaderManager.GetUploadHandler().PutUrlFile(v.OrgURL, ".png", proxy)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("error with download image %s, %v", v.OrgURL, err)
|
||||
@@ -212,7 +193,9 @@ func (s *Service) DownloadImages() {
|
||||
// PushTask push a new mj task in to task queue
|
||||
func (s *Service) PushTask(task types.MjTask) {
|
||||
logger.Debugf("add a new MidJourney task to the task list: %+v", task)
|
||||
s.taskQueue.RPush(task)
|
||||
if err := s.taskQueue.RPush(task); err != nil {
|
||||
logger.Errorf("push mj task to queue failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// SyncTaskProgress 异步拉取任务
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user