Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63c7041e1f | ||
|
|
b1263ddc69 | ||
|
|
7e50e17aaf | ||
|
|
a7265c4251 | ||
|
|
6f39f639bd | ||
|
|
a7db123437 | ||
|
|
241c714a8b | ||
|
|
67ac3cfe32 | ||
|
|
c926e0afcc | ||
|
|
5bc07e6d57 | ||
|
|
c3666a9a71 | ||
|
|
23b5ffa97d | ||
|
|
a2c7a75705 | ||
|
|
d68f2ef12c | ||
|
|
67d30353f0 | ||
|
|
4813163eac | ||
|
|
5c5210625e | ||
|
|
a4a1eec30b | ||
|
|
d35164506a | ||
|
|
1ed08f01ea | ||
|
|
eca07ab830 | ||
|
|
3512715704 | ||
|
|
6d07881141 | ||
|
|
251fe626f2 | ||
|
|
5fee3a9288 | ||
|
|
9b68d8101e | ||
|
|
cfe6f27d48 | ||
|
|
b314dd0900 | ||
|
|
950fab6374 | ||
|
|
9d1f5c42ce | ||
|
|
a84046390b | ||
|
|
aa29323a8a | ||
|
|
d5617b7c3a | ||
|
|
1ef60a9e5e | ||
|
|
fb6e395ad8 | ||
|
|
d9216060bc | ||
|
|
bcaa9a92e5 | ||
|
|
576adc9036 | ||
|
|
00de18be9a | ||
|
|
c61d32816a | ||
|
|
f3fbb0b89c | ||
|
|
e311a39632 | ||
|
|
51407abe44 | ||
|
|
8a470b1038 | ||
|
|
baddabaa16 | ||
|
|
427b434ce3 | ||
|
|
5f921965e6 | ||
|
|
1e705c8ed5 | ||
|
|
b8ae65bb30 | ||
|
|
321e2087ea | ||
|
|
aac60edce2 | ||
|
|
9dc9a6923e | ||
|
|
7ca4dfe09b | ||
|
|
c584b82ddb | ||
|
|
5f17ab2501 | ||
|
|
c84e912dd8 | ||
|
|
2ebff2623f | ||
|
|
72418ce4d7 | ||
|
|
e221b1eed4 | ||
|
|
696306f066 | ||
|
|
1807d5b5d4 | ||
|
|
85c12aa322 | ||
|
|
da9d0dc3bc | ||
|
|
daaca822ac | ||
|
|
59ced3f947 | ||
|
|
22ae7dd1f3 | ||
|
|
1816e9d5cf | ||
|
|
9be6755f65 |
46
CHANGELOG.md
@@ -1,6 +1,52 @@
|
||||
# 更新日志
|
||||
|
||||
## v3.2.0
|
||||
* 功能新增:新增邀请注册功能
|
||||
* 功能优化:增加中间件自动对HTTP请求的参数去掉首尾空格
|
||||
* 功能优化:增加中间件自动为大图片生成缩略图
|
||||
* 功能优化:MidJourney 页面图片加载优化,实现图片预览懒加载
|
||||
* 功能新增:新增 DALL-E-3 绘画支持,并作为对话页面默认绘画插件
|
||||
* Bug修复:修复阿里云 OSS 域名设置不起做用的bug
|
||||
* Bug修复:修复MidJourney绘图失败后重复添加到队列的问题
|
||||
|
||||
## v3.1.9
|
||||
* 功能新增:增加讯飞星火大模型 v3.0 支持
|
||||
* 功能新增:新增找回密码功能
|
||||
* 功能新增:支持 Markdown 代码复制功能
|
||||
* Bug修复: xxl-job 任务调度失败的 Bug
|
||||
* 功能优化:优化前端页面菜单图标,使用自定义图标替换 icon-font
|
||||
* Bug修复:Stable-Diffusion 绘画成功之后没有扣减用户画图次数
|
||||
* 功能优化:优化会员充值页面 ItemList 组件
|
||||
* 功能优化:给首页 Logo 增加链接
|
||||
* Bug修复:[新建会话时,提示"请输入合法的手机号" ](https://github.com/yangjian102621/chatgpt-plus/issues/51)
|
||||
* Bug修复:聊天上下文失效问题
|
||||
* 功能优化:关闭注册时显示联系管理员二维码
|
||||
* 功能优化:移除 leveldb 依赖,使用 redis 替换相应的功能
|
||||
* Bug修复:后台启用用户 VIP 不生效问题
|
||||
* 功能优化:充值支付页面的支付说明文字可以后台配置
|
||||
* Bug修复:ChatGLM,百度文心,科大讯飞模型输出代码不换行问题
|
||||
|
||||
## v3.1.8
|
||||
|
||||
1. 功能新增:新增会员套餐充值,点卡充值,订单系统,集成支付宝支付通道
|
||||
2. Bug修复:修复 MidJourney API 参数版本更新导致调用失败问题
|
||||
3. Bug修复:修复 Stable Diffusion 调用后没有更新绘图调用次数问题
|
||||
4. Bug修复:修复七牛云上传报错 expired token
|
||||
5. Bug修复:修复高权重模型导致的对话次数为负数的漏洞
|
||||
6. 功能优化:将聊天报错信息定义为统一常量,方便修改
|
||||
7. 功能优化:优化 markdown 表格显示样式,覆写 Element-Plus 表格样式
|
||||
8. 功能优化:增加倒数计时组件,定期自动清理未支付的订单
|
||||
|
||||
## v3.1.7
|
||||
|
||||
1. 功能新增:支持文心4.0 AI 模型
|
||||
2. 功能新增:可以在管理后台为用户绑定指定的 AI 模型,如只给某个用户使用 GPT-4 模型
|
||||
3. 功能新增:模型新增权重字段,不同的模型每次调用耗费的点数可以设置不同,比如GPT4是GPT3.5的10倍
|
||||
4. 功能新增:新增系统配置关闭 AI 模型的函数功能
|
||||
5. 功能优化:优化 MidJourney 专业绘画页面图片预览样式
|
||||
|
||||
## v3.1.6
|
||||
|
||||
1. 功能新增:新增AI 绘画照片墙功能页面,供用户查看所有的 AI 绘画作品
|
||||
2. 功能新增:新增 AI 角色应用功能页面,用户可以添加自己感兴趣的应用
|
||||
3. 功能优化:优化瀑布流组件的页面布局
|
||||
|
||||
357
README.md
@@ -4,11 +4,14 @@
|
||||
ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了 MidJourney 和 Stable Diffusion AI绘画功能。主要有如下特性:
|
||||
|
||||
* 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
|
||||
* 聊天体验跟 ChatGPT 官方版本完全一致。
|
||||
* 内置了各种预训练好的角色,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
|
||||
* 基于 Websocket 实现,完美的打字机体验。
|
||||
* 内置了各种预训练好的角色应用,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
|
||||
* 支持 OPenAI,Azure,文心一言,讯飞星火,清华 ChatGLM等多个大语言模型。
|
||||
* 支持 MidJourney / Stable Diffusion AI 绘画集成,开箱即用。
|
||||
* 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。(可定制开发其他支付通道支持)
|
||||
* 集成插件 API 功能,可结合 GPT 开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI 绘画函数插件。
|
||||
* 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。
|
||||
* 已集成支付宝支付功能,支持多种会员套餐和点卡购买功能。
|
||||
* 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI
|
||||
绘画函数插件。
|
||||
|
||||
## 功能截图
|
||||
|
||||
@@ -16,34 +19,41 @@ ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了
|
||||
|
||||

|
||||
|
||||
### 新版聊天界面
|
||||
### AI 对话界面
|
||||
|
||||

|
||||
|
||||
### MidJourney 专业绘画界面(v3.1.3)
|
||||
### MidJourney 专业绘画界面
|
||||
|
||||

|
||||

|
||||
|
||||
### 自动调用函数插件
|
||||
### Stable-Diffusion 专业绘画页面
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
### 绘图作品展
|
||||
|
||||

|
||||
|
||||
### 登录页面
|
||||
### AI应用列表
|
||||
|
||||

|
||||

|
||||
|
||||
### 会员充值
|
||||
|
||||

|
||||
|
||||
### 自动调用函数插件
|
||||
|
||||

|
||||

|
||||
|
||||
### 管理后台
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
### 移动端 Web 页面
|
||||
@@ -63,39 +73,6 @@ ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了
|
||||
1. 本项目基于 MIT 协议,免费开放全部源代码,可以作为个人学习使用或者商用。
|
||||
2. 如需商用必须保留版权信息,请自觉遵守。确保合法合规使用,在运营过程中产生的一切任何后果自负,与作者无关。
|
||||
|
||||
## 项目介绍
|
||||
|
||||
这一套完整的系统,包括前端聊天应用和一个后台管理系统。系统有用户鉴权,你可以自己使用,也可以部署直接给 C 端用户提供
|
||||
ChatGPT 的服务。
|
||||
|
||||
### 项目的技术架构
|
||||
|
||||
新版的系统前后端都进行大改动的重构,后端还是用的 Gin Web 框架,但是作者整合了 fx 自动注入框架,整个后端应用结构非常简洁,特别适合二次开发。
|
||||
另外,数据存储用 MySQL 替换了 leveldb, 因为要对 C 端,后期会涉及到很多业务数据查询统计,leveldb 已经完全不够用了。
|
||||
|
||||
> Gin + fx + MySQL
|
||||
|
||||
3.0 版本之后会陆续添加其他语言的 API 实现,比如 PHP,Java 等。考虑到作者精力有限,api 目录已经添加了,有兴趣的同学自主去认领各自擅长的语言去实现。
|
||||
|
||||
前端的框架还是:
|
||||
|
||||
> Vue3 + Element-Plus
|
||||
|
||||
前后台的页面风格已经全部变了,几乎所有页面样式代码都重写了。逻辑代码还是沿用之前的,毕竟功能没有太大的变化。
|
||||
|
||||
此次重构改版主要是为了后面功能的扩展准备了。
|
||||
|
||||
新版本已经实现的功能如下:
|
||||
|
||||
1. 引入用户体系,新增用户注册和登录功能。
|
||||
2. 聊天页面改版,实现了跟 ChatGPT 官方版本一致的聊天体验。
|
||||
3. 创建会话的时候可以选择聊天角色和模型。
|
||||
4. 新增聊天设置功能,用户可以导入自己的 API KEY
|
||||
5. 保存聊天记录,支持聊天上下文。
|
||||
6. 重构后台管理模块,更友好,扩展性更好的后台管理系统。
|
||||
7. 引入 ip2region 组件,记录用户的登录IP和地址。
|
||||
8. 支持会话搜索过滤。
|
||||
9. 支持微信支付充值
|
||||
|
||||
## 项目地址
|
||||
|
||||
@@ -107,282 +84,19 @@ ChatGPT 的服务。
|
||||
目前已经支持 Win/Linux/Mac/Android 客户端,下载地址为:https://github.com/yangjian102621/chatgpt-plus/releases/tag/v3.1.2
|
||||
|
||||
## TODOLIST
|
||||
|
||||
* [x] 整合 Midjourney AI 绘画 API
|
||||
* [x] 开发移动端聊天页面
|
||||
* [x] 接入微信收款功能
|
||||
* [x] 支持 ChatGPT 函数功能,通过函数实现插件
|
||||
* [x] 开发桌面版应用
|
||||
* [x] 开发手机 App 客户端
|
||||
* [x] 支付宝支付功能
|
||||
* [ ] 支持基于知识库的 AI 问答
|
||||
* [ ] 会员推广功能
|
||||
* [ ] 会员邀请注册推广功能
|
||||
* [ ] 微信支付功能
|
||||
|
||||
## Docker 快速部署
|
||||
|
||||
>
|
||||
鉴于最新不少网友反馈在部署的时候遇到一些问题,大部分问题都是相同的,所以我这边做了一个视频教程 [五分钟部署自己的 ChatGPT 服务](https://www.bilibili.com/video/BV1H14y1B7Qw/)。
|
||||
> 习惯看视频教程的朋友可以去看视频教程,视频的语速比较慢,建议 2 倍速观看。
|
||||
|
||||
V3.0.0 版本以后已经支持使用容器部署了,跳过所有的繁琐的环境准备,一条命令就可以轻松部署上线。
|
||||
|
||||
### 1. 导入数据库
|
||||
|
||||
首先我们需要创建一个 MySQL 容器,并导入初始数据库。
|
||||
## 项目文档
|
||||
|
||||
chatgpt-plus v3.2.0 一键部署脚本来了,真的只需运行一条命令,就可以完成部署:
|
||||
```shell
|
||||
cd docker/mysql
|
||||
# 创建 mysql 容器
|
||||
docker-compose up -d
|
||||
# 导入数据库
|
||||
docker exec -i chatgpt-plus-mysql sh -c 'exec mysql -uroot -p12345678' < ../../database/chatgpt_plus-v3.1.6.sql
|
||||
bash -c "$(curl -fsSL https://img.r9it.com/tmp/install-v3.2.0-24e4849229.sh)"
|
||||
```
|
||||
目前只支持 Ubuntu 系统,推荐 Ubuntu 22.04 LTS
|
||||
|
||||
如果你本地已经安装了 MySQL 服务,那么你只需手动导入数据库即可。
|
||||
|
||||
```shell
|
||||
# 连接数据库
|
||||
mysql -u username -p password
|
||||
# 导入数据库
|
||||
source database/chatgpt_plus.sql
|
||||
```
|
||||
|
||||
### 2. 修改配置文档
|
||||
|
||||
修改配置文档 `docker/conf/config.toml` 配置文档,修改代理地址和管理员密码:
|
||||
|
||||
```toml
|
||||
Listen = "0.0.0.0:5678"
|
||||
ProxyURL = "" # 如 http://127.0.0.1:7777
|
||||
MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local"
|
||||
StaticDir = "./static" # 静态资源的目录
|
||||
StaticUrl = "/static" # 静态资源访问 URL
|
||||
AesEncryptKey = ""
|
||||
WeChatBot = false # 是否启动微信机器人
|
||||
|
||||
[Session]
|
||||
SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" # 注意:这个是 JWT Token 授权密钥,生产环境请务必更换
|
||||
MaxAge = 86400
|
||||
|
||||
[Manager]
|
||||
Username = "admin"
|
||||
Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改
|
||||
|
||||
[Redis] # redis 配置信息
|
||||
Host = "localhost"
|
||||
Port = 6379
|
||||
Password = ""
|
||||
DB = 0
|
||||
|
||||
[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通
|
||||
ApiURL = ""
|
||||
AppId = ""
|
||||
Token = ""
|
||||
|
||||
[SmsConfig] # 阿里云短信服务配置
|
||||
AccessKey = ""
|
||||
AccessSecret = ""
|
||||
Product = "Dysmsapi"
|
||||
Domain = "dysmsapi.aliyuncs.com"
|
||||
|
||||
[ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署
|
||||
ApiURL = "" # 插件扩展 API 地址
|
||||
Token = "" # 这个 token 随便填,只要确保跟 chatgpt-plus-exts 项目的 token 一样就行
|
||||
|
||||
[OSS] # OSS 配置,用于存储 MJ 绘画图片
|
||||
Active = "local" # 默认使用本地文件存储引擎
|
||||
[OSS.Local]
|
||||
BasePath = "./static/upload" # 本地文件上传根路径
|
||||
BaseURL = "http://localhost:5678/static/upload" # 本地上传文件根 URL 如果是线上,则直接设置为 /static/upload 即可
|
||||
[OSS.Minio]
|
||||
Endpoint = "" # 如 172.22.11.200:9000
|
||||
AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key
|
||||
AccessSecret = ""
|
||||
Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket,注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。
|
||||
UseSSL = false
|
||||
Domain = "" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。
|
||||
[OSS.QiNiu] # 七牛云 OSS 配置
|
||||
Zone = "z2" # 区域,z0:华东,z1: 华北,na0:北美,as0:新加坡
|
||||
AccessKey = ""
|
||||
AccessSecret = ""
|
||||
Bucket = ""
|
||||
Domain = "" # OSS Bucket 所绑定的域名,如 https://img.r9it.com
|
||||
|
||||
[MjConfig] # MidJourney AI 绘画配置
|
||||
Enabled = false # 是否启动 MidJourney 机器人服务
|
||||
UserToken = "" # 用户授权 Token
|
||||
BotToken = "" # Discord 机器人 Token
|
||||
GuildId = "" # 服务器 ID
|
||||
ChanelId = "" # 频道 ID
|
||||
|
||||
[SdConfig]
|
||||
Enabled = false # 是否启动 Stable Diffusion 机器人服务
|
||||
ApiURL = "http://172.22.11.200:7860" # stable-diffusion-webui API 地址
|
||||
ApiKey = "" # 如果开启了授权,这里需要配置授权的 ApiKey
|
||||
Txt2ImgJsonPath = "res/text2img.json" # 文生图的 API 请求报文 json 模板,允许自定义请求json报文,因为不同版本的 API 绘图的参数以及 fn_index 会不同。
|
||||
```
|
||||
|
||||
> 1. 如果你不知道如何获取 Discord 用户 Token 和 Bot Token
|
||||
请查参考 [Midjourney|如何集成到自己的平台](https://zhuanlan.zhihu.com/p/631079476)。
|
||||
> 2. `Txt2ImgJsonPath`
|
||||
的默认用的是使用最广泛的 [stable-diffusion-webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui) 项目的
|
||||
API,如果你用的是其他版本,比如秋叶的懒人包部署的,那么请将对应的 text2img 的参数报文复制放在 `res/text2img.json`
|
||||
文件中即可。
|
||||
|
||||
修改 nginx 配置文档 `docker/conf/nginx/conf.d/chatgpt-plus.conf`,把后端转发的地址改成当前主机的内网 IP 地址。
|
||||
|
||||
```shell
|
||||
# 这里配置后端 API 的转发
|
||||
location /api/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_connect_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 12s;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_pass http://172.28.173.76:6789; # 这里改成后端服务的内网 IP 地址
|
||||
|
||||
# 静态资源转发
|
||||
location /static/ {
|
||||
proxy_pass http://172.22.11.47:5678; # 这里改成后端服务的内网 IP 地址
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 启动应用
|
||||
|
||||
先修改 `docker/docker-compose.yaml` 文件中的镜像地址,改成最新的版本:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
services:
|
||||
# 后端 API 镜像
|
||||
chatgpt-plus-api:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:v3.1.5 #这里改成最新的 release 版本地址
|
||||
container_name: chatgpt-plus-api
|
||||
restart: always
|
||||
environment:
|
||||
- DEBUG=false
|
||||
- LOG_LEVEL=info
|
||||
- CONFIG_FILE=config.toml
|
||||
ports:
|
||||
- "5678:5678"
|
||||
volumes:
|
||||
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime
|
||||
- ./conf/config.toml:/var/www/app/config.toml
|
||||
- ./logs:/var/www/app/logs
|
||||
- ./static:/var/www/app/static
|
||||
|
||||
# 前端应用镜像
|
||||
chatgpt-plus-web:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:v3.1.5 #这里改成最新的 release 版本地址
|
||||
container_name: chatgpt-plus-web
|
||||
restart: always
|
||||
ports:
|
||||
- "8080:8080" # 这边是对外的端口,支持 8080,80和443
|
||||
volumes:
|
||||
- ./logs/nginx:/var/log/nginx
|
||||
- ./conf/nginx/conf.d:/etc/nginx/conf.d
|
||||
- ./conf/nginx/nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./ssl:/etc/nginx/ssl
|
||||
```
|
||||
|
||||
```shell
|
||||
cd docker
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
* 前端访问地址:http://localhost:8080/chat
|
||||
* 后台管理地址:http://localhost:8080/admin
|
||||
* 移动端地址:http://localhost:8080/mobile
|
||||
|
||||
> 注意:你得访问后台管理系统 http://localhost:8080/admin
|
||||
> 输入你前面配置文档中设置的管理员用户名和密码登录。
|
||||
> 然后进入 `API KEY 管理` 菜单,添加一个 OpenAI 的 API KEY 才可以正常开启 AI 对话。
|
||||
|
||||

|
||||
|
||||
最后登录前端聊天页面 [http://localhost:8080/chat](http://localhost:8080/chat)
|
||||
你可以注册新用户,也可以使用系统默认有个账号:`geekmaster/12345678` 登录聊天。
|
||||
|
||||
祝你使用愉快!!!
|
||||
|
||||
## 本地开发调试
|
||||
|
||||
本地开发同样要分别运行前端和后端程序。
|
||||
|
||||
### 运行后端程序
|
||||
|
||||
1. 同样你首先要 [导入数据库](#1-导入数据库)
|
||||
2. 然后 [修改配置文档](#2-修改配置文档)
|
||||
3. 运行后端程序:
|
||||
|
||||
```shell
|
||||
cd api
|
||||
# 1. 先下载依赖
|
||||
go mod tidy
|
||||
# 2. 运行程序
|
||||
go run main.go
|
||||
# 如果你安装了 fresh 可以使用 fresh 实现热启动
|
||||
fresh -c fresh.conf
|
||||
```
|
||||
|
||||
### 运行前端程序
|
||||
|
||||
同样先拷贝配置文档:
|
||||
|
||||
```shell
|
||||
cd web
|
||||
cp .env.production .env.development
|
||||
```
|
||||
|
||||
编辑 `.env.development` 文件,修改后端 API 的访问路径:
|
||||
|
||||
```ini
|
||||
VUE_APP_API_HOST=http://localhost:5678
|
||||
VUE_APP_WS_HOST=ws://localhost:5678
|
||||
```
|
||||
|
||||
配置好了之后就可以运行前端应用了:
|
||||
|
||||
```
|
||||
# 安装依赖
|
||||
npm install
|
||||
# 运行
|
||||
npm run dev
|
||||
```
|
||||
|
||||
* 前端页面:http://localhost:8888/chat
|
||||
* 后台管理页面:http://localhost:8888/admin
|
||||
|
||||
## 项目打包
|
||||
|
||||
由于本项目是采用异构开发的方式,所项目打包分成两步:首先编译后端程序,然后再打包前端应用。
|
||||
|
||||
### 打包前端
|
||||
|
||||
```shell
|
||||
cd web
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 打包后端
|
||||
|
||||
你可以根据个人需求将项目打包成 windows/linux/darwin 平台项目。
|
||||
|
||||
```shell
|
||||
cd api
|
||||
# for all platforms
|
||||
make clean all
|
||||
# for linux only
|
||||
make clean linux
|
||||
```
|
||||
|
||||
打包后的可执行文件在 `bin` 目录下。
|
||||
详细部署文档请参考 [ChatGPT-Plus 文档](https://ai.r9it.com/docs/)。
|
||||
|
||||
## 参与贡献
|
||||
|
||||
@@ -392,7 +106,7 @@ make clean linux
|
||||
|
||||

|
||||
|
||||
#### 特此声明:不接受在微信或者微信群给开发者提 Bug,有问题或者优化建议请提交 Issue 和 PR。非常感谢您的配合!
|
||||
#### 特此声明:由于个人时间有限,不接受在微信或者微信群给开发者提 Bug,有问题或者优化建议请提交 Issue 和 PR。非常感谢您的配合!
|
||||
|
||||
### Commit 类型
|
||||
|
||||
@@ -408,8 +122,9 @@ make clean linux
|
||||
|
||||
如果你觉得这个项目对你有帮助,并且情况允许的话,可以请作者喝杯咖啡,非常感谢你的支持~
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
1
api/.gitignore
vendored
@@ -18,3 +18,4 @@ data
|
||||
config.toml
|
||||
static/upload
|
||||
storage.json
|
||||
certs/alipay/*
|
||||
|
||||
@@ -33,10 +33,6 @@ WeChatBot = false
|
||||
Sign = ""
|
||||
CodeTempId = ""
|
||||
|
||||
[ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署
|
||||
ApiURL = "" # 插件扩展 API 地址
|
||||
Token = "" # 这个 token 随便填,只要确保跟 chatgpt-plus-exts 项目的 token 一样就行
|
||||
|
||||
[OSS] # OSS 配置,用于存储 MJ 绘画图片
|
||||
Active = "local" # 默认使用本地文件存储引擎
|
||||
[OSS.Local]
|
||||
@@ -67,4 +63,23 @@ WeChatBot = false
|
||||
Enabled = false
|
||||
ApiURL = "http://172.22.11.200:7860"
|
||||
ApiKey = ""
|
||||
Txt2ImgJsonPath = "res/text2img.json"
|
||||
Txt2ImgJsonPath = "res/text2img.json"
|
||||
|
||||
[XXLConfig] # xxl-job 配置,需要你部署 XXL-JOB 定时任务工具,用来定期清理未支付订单和清理过期 VIP,如果你没有启用支付服务,则该服务也无需启动
|
||||
Enabled = false # 是否启用 XXL JOB 服务
|
||||
ServerAddr = "http://172.22.11.47:8080/xxl-job-admin" # xxl-job-admin 管理地址
|
||||
ExecutorIp = "172.22.11.47" # 执行器 IP 地址
|
||||
ExecutorPort = "9999" # 执行器服务端口
|
||||
AccessToken = "xxl-job-api-token" # 执行器 API 通信 token
|
||||
RegistryKey = "chatgpt-plus" # 任务注册 key
|
||||
|
||||
[AlipayConfig]
|
||||
Enabled = false # 启用支付宝支付通道
|
||||
SandBox = false # 是否启用沙盒模式
|
||||
UserId = "2088721020750581" # 商户ID
|
||||
AppId = "9021000131658023" # App Id
|
||||
PrivateKey = "certs/alipay/privateKey.txt" # 应用私钥
|
||||
PublicKey = "certs/alipay/appPublicCert.crt" # 应用公钥证书
|
||||
AlipayPublicKey = "certs/alipay/alipayPublicCert.crt" # 支付宝公钥证书
|
||||
RootCert = "certs/alipay/alipayRootCert.crt" # 支付宝根证书
|
||||
NotifyURL = "http://r9it.com:6004/api/payment/alipay/notify" # 支付异步回调地址
|
||||
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"chatplus/core/types"
|
||||
"chatplus/service/fun"
|
||||
"chatplus/store/model"
|
||||
@@ -11,9 +12,14 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/nfnt/resize"
|
||||
"gorm.io/gorm"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -57,7 +63,9 @@ func (s *AppServer) Init(debug bool, client *redis.Client) {
|
||||
logger.Info("Enabled debug mode")
|
||||
}
|
||||
s.Engine.Use(corsMiddleware())
|
||||
s.Engine.Use(staticResourceMiddleware())
|
||||
s.Engine.Use(authorizeMiddleware(s, client))
|
||||
s.Engine.Use(parameterHandlerMiddleware())
|
||||
s.Engine.Use(errorHandler)
|
||||
// 添加静态资源访问
|
||||
s.Engine.Static("/static", s.Config.StaticDir)
|
||||
@@ -139,18 +147,18 @@ func corsMiddleware() gin.HandlerFunc {
|
||||
func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if c.Request.URL.Path == "/api/user/login" ||
|
||||
c.Request.URL.Path == "/api/user/resetPass" ||
|
||||
c.Request.URL.Path == "/api/admin/login" ||
|
||||
c.Request.URL.Path == "/api/user/register" ||
|
||||
c.Request.URL.Path == "/api/reward/notify" ||
|
||||
c.Request.URL.Path == "/api/mj/notify" ||
|
||||
c.Request.URL.Path == "/api/chat/history" ||
|
||||
c.Request.URL.Path == "/api/chat/detail" ||
|
||||
c.Request.URL.Path == "/api/role/list" ||
|
||||
c.Request.URL.Path == "/api/mj/jobs" ||
|
||||
c.Request.URL.Path == "/api/mj/proxy" ||
|
||||
c.Request.URL.Path == "/api/invite/hits" ||
|
||||
c.Request.URL.Path == "/api/sd/jobs" ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/payment/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/static/") ||
|
||||
c.Request.URL.Path == "/api/admin/config/get" {
|
||||
c.Next()
|
||||
@@ -210,3 +218,120 @@ func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
|
||||
c.Set(types.LoginUserID, claims["user_id"])
|
||||
}
|
||||
}
|
||||
|
||||
// 统一参数处理
|
||||
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)
|
||||
}
|
||||
}
|
||||
// 更新参数
|
||||
c.Request.URL.RawQuery = params.Encode()
|
||||
|
||||
// POST JSON 参数处理
|
||||
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)
|
||||
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 {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// 直接输出图像数据流
|
||||
c.Data(http.StatusOK, "image/jpeg", buffer.Bytes())
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +33,10 @@ func NewDefaultConfig() *types.AppConfig {
|
||||
BasePath: "./static/upload",
|
||||
},
|
||||
},
|
||||
MjConfig: types.MidJourneyConfig{Enabled: false},
|
||||
SdConfig: types.StableDiffusionConfig{Enabled: false, Txt2ImgJsonPath: "res/text2img.json"},
|
||||
WeChatBot: false,
|
||||
MjConfig: types.MidJourneyConfig{Enabled: false},
|
||||
SdConfig: types.StableDiffusionConfig{Enabled: false, Txt2ImgJsonPath: "res/text2img.json"},
|
||||
WeChatBot: false,
|
||||
AlipayConfig: types.AlipayConfig{Enabled: false, SandBox: false},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ type ChatModel struct {
|
||||
Id uint `json:"id"`
|
||||
Platform Platform `json:"platform"`
|
||||
Value string `json:"value"`
|
||||
Weight int `json:"weight"`
|
||||
}
|
||||
|
||||
type ApiError struct {
|
||||
|
||||
@@ -21,6 +21,9 @@ type AppConfig struct {
|
||||
MjConfig MidJourneyConfig // mj 绘画配置
|
||||
WeChatBot bool // 是否启用微信机器人
|
||||
SdConfig StableDiffusionConfig // sd 绘画配置
|
||||
|
||||
XXLConfig XXLConfig
|
||||
AlipayConfig AlipayConfig
|
||||
}
|
||||
|
||||
type ChatPlusApiConfig struct {
|
||||
@@ -57,6 +60,27 @@ type AliYunSmsConfig struct {
|
||||
CodeTempId string // 验证码短信模板 ID
|
||||
}
|
||||
|
||||
type AlipayConfig struct {
|
||||
Enabled bool // 是否启用该服务
|
||||
SandBox bool // 是否沙盒环境
|
||||
AppId string // 应用 ID
|
||||
UserId string // 支付宝用户 ID
|
||||
PrivateKey string // 用户私钥文件路径
|
||||
PublicKey string // 用户公钥文件路径
|
||||
AlipayPublicKey string // 支付宝公钥文件路径
|
||||
RootCert string // Root 秘钥路径
|
||||
NotifyURL string // 异步通知回调
|
||||
}
|
||||
|
||||
type XXLConfig struct { // XXL 任务调度配置
|
||||
Enabled bool
|
||||
ServerAddr string
|
||||
ExecutorIp string
|
||||
ExecutorPort string
|
||||
AccessToken string
|
||||
RegistryKey string
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
@@ -100,6 +124,11 @@ type UserChatConfig struct {
|
||||
ApiKeys map[Platform]string `json:"api_keys"`
|
||||
}
|
||||
|
||||
type InviteReward struct {
|
||||
ChatCalls int `json:"chat_calls"`
|
||||
ImgCalls int `json:"img_calls"`
|
||||
}
|
||||
|
||||
type ModelAPIConfig struct {
|
||||
ApiURL string `json:"api_url,omitempty"`
|
||||
Temperature float32 `json:"temperature"`
|
||||
@@ -108,16 +137,22 @@ type ModelAPIConfig struct {
|
||||
}
|
||||
|
||||
type SystemConfig struct {
|
||||
Title string `json:"title"`
|
||||
AdminTitle string `json:"admin_title"`
|
||||
Models []string `json:"models"`
|
||||
UserInitCalls int `json:"user_init_calls"` // 新用户注册默认总送多少次调用
|
||||
InitImgCalls int `json:"init_img_calls"`
|
||||
VipMonthCalls int `json:"vip_month_calls"` // 会员每个赠送的调用次数
|
||||
EnabledRegister bool `json:"enabled_register"`
|
||||
EnabledMsg bool `json:"enabled_msg"` // 启用短信验证码服务
|
||||
EnabledDraw bool `json:"enabled_draw"` // 启动 AI 绘画功能
|
||||
RewardImg string `json:"reward_img"` // 众筹收款二维码地址
|
||||
EnabledFunction bool `json:"enabled_function"` // 启用 API 函数功能
|
||||
EnabledReward bool `json:"enabled_reward"` // 启用众筹功能
|
||||
Title string `json:"title"`
|
||||
AdminTitle string `json:"admin_title"`
|
||||
Models []string `json:"models"`
|
||||
InitChatCalls int `json:"init_chat_calls"` // 新用户注册赠送对话次数
|
||||
InitImgCalls int `json:"init_img_calls"` // 新用户注册赠送绘图次数
|
||||
VipMonthCalls int `json:"vip_month_calls"` // 会员每个赠送的调用次数
|
||||
EnabledRegister bool `json:"enabled_register"` // 是否启用注册功能,关闭注册功能之后将无法注册
|
||||
EnabledMsg bool `json:"enabled_msg"` // 是否启用短信验证码服务
|
||||
RewardImg string `json:"reward_img"` // 众筹收款二维码地址
|
||||
EnabledFunction bool `json:"enabled_function"` // 启用 API 函数功能
|
||||
EnabledReward bool `json:"enabled_reward"` // 启用众筹功能
|
||||
EnabledAlipay bool `json:"enabled_alipay"` // 是否启用支付宝支付通道
|
||||
OrderPayTimeout int `json:"order_pay_timeout"` //订单支付超时时间
|
||||
DefaultModels []string `json:"default_models"` // 默认开通的 AI 模型
|
||||
OrderPayInfoText string `json:"order_pay_info_text"` // 订单支付页面说明文字
|
||||
InviteChatCalls int `json:"invite_chat_calls"` // 邀请用户注册奖励对话次数
|
||||
InviteImgCalls int `json:"invite_img_calls"` // 邀请用户注册奖励绘图次数
|
||||
ForceInvite bool `json:"force_invite"` // 是否强制必须使用邀请码才能注册
|
||||
}
|
||||
|
||||
@@ -23,10 +23,10 @@ type Property struct {
|
||||
}
|
||||
|
||||
const (
|
||||
FuncZaoBao = "zao_bao" // 每日早报
|
||||
FuncHeadLine = "headline" // 今日头条
|
||||
FuncWeibo = "weibo_hot" // 微博热搜
|
||||
FuncMidJourney = "mid_journey" // MJ 绘画
|
||||
FuncZaoBao = "zao_bao" // 每日早报
|
||||
FuncHeadLine = "headline" // 今日头条
|
||||
FuncWeibo = "weibo_hot" // 微博热搜
|
||||
FuncImage = "draw_image" // AI 绘画
|
||||
)
|
||||
|
||||
var InnerFunctions = []Function{
|
||||
@@ -76,14 +76,14 @@ var InnerFunctions = []Function{
|
||||
},
|
||||
|
||||
{
|
||||
Name: FuncMidJourney,
|
||||
Description: "AI 绘画工具,使用 MJ MidJourney API 进行 AI 绘画",
|
||||
Name: FuncImage,
|
||||
Description: "AI 绘画工具,根据输入的绘图描述用 AI 工具进行绘画",
|
||||
Parameters: Parameters{
|
||||
Type: "object",
|
||||
Properties: map[string]Property{
|
||||
"prompt": {
|
||||
Type: "string",
|
||||
Description: "提示词,如果该参数中有中文的话,则需要翻译成英文。提示词中的参数作为提示的一部分,不要删除",
|
||||
Description: "提示词,如果该参数中有中文的话,则需要翻译成英文。",
|
||||
},
|
||||
},
|
||||
Required: []string{},
|
||||
|
||||
17
api/core/types/order.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package types
|
||||
|
||||
type OrderStatus int
|
||||
|
||||
const (
|
||||
OrderNotPaid = OrderStatus(0)
|
||||
OrderScanned = OrderStatus(1) // 已扫码
|
||||
OrderPaidSuccess = OrderStatus(2)
|
||||
)
|
||||
|
||||
type OrderRemark struct {
|
||||
Days int `json:"days"` // 有效期
|
||||
Calls int `json:"calls"` // 增加调用次数
|
||||
Name string `json:"name"` // 产品名称
|
||||
Price float64 `json:"price"`
|
||||
Discount float64 `json:"discount"`
|
||||
}
|
||||
10
api/go.mod
@@ -18,12 +18,14 @@ 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/syndtr/goleveldb v1.0.0
|
||||
github.com/smartwalle/alipay/v3 v3.2.15
|
||||
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/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
@@ -34,6 +36,7 @@ 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
|
||||
@@ -49,6 +52,7 @@ require (
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||
github.com/onsi/ginkgo/v2 v2.10.0 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
@@ -59,6 +63,9 @@ require (
|
||||
github.com/refraction-networking/utls v1.3.2 // indirect
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/smartwalle/ncrypto v1.0.2 // indirect
|
||||
github.com/smartwalle/ngx v1.0.6 // indirect
|
||||
github.com/smartwalle/nsign v1.0.8 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
go.uber.org/dig v1.16.1 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
@@ -79,7 +86,6 @@ require (
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/golang/snappy v0.0.1 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
|
||||
31
api/go.sum
@@ -29,7 +29,6 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/eatmoreapple/openwechat v1.2.1 h1:ez4oqF/Y2NSEX/DbPV8lvj7JlfkYqvieeo4awx5lzfU=
|
||||
github.com/eatmoreapple/openwechat v1.2.1/go.mod h1:61HOzTyvLobGdgWhL68jfGNwTJEv0mhQ1miCXQrvWU8=
|
||||
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/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
@@ -39,6 +38,8 @@ 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-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
@@ -66,12 +67,8 @@ github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJ
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
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/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -87,7 +84,6 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imroc/req/v3 v3.37.2 h1:vEemuA0cq9zJ6lhe+mSRhsZm951bT0CdiSH47+KTn6I=
|
||||
github.com/imroc/req/v3 v3.37.2/go.mod h1:DECzjVIrj6jcUr5n6e+z0ygmCO93rx4Jy0RjOEe1YCI=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
@@ -133,13 +129,12 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo/v2 v2.10.0 h1:sfUl4qgLdvkChZrWCYndY2EAu9BRIw1YphNAzy1VNWs=
|
||||
github.com/onsi/ginkgo/v2 v2.10.0/go.mod h1:UDQOh5wbQUlMnkLfVaIUMtQ1Vus92oM+P2JX1aulgcE=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
|
||||
@@ -175,6 +170,14 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/smartwalle/alipay/v3 v3.2.15 h1:3fvFJnINKKAOXHR/Iv20k1Z7KJ+nOh3oK214lELPqG8=
|
||||
github.com/smartwalle/alipay/v3 v3.2.15/go.mod h1:niTNB609KyUYuAx9Bex/MawEjv2yPx4XOjxSAkqmGjE=
|
||||
github.com/smartwalle/ncrypto v1.0.2 h1:pTAhCqtPCMhpOwFXX+EcMdR6PNzruBNoGQrN2S1GbGI=
|
||||
github.com/smartwalle/ncrypto v1.0.2/go.mod h1:Dwlp6sfeNaPMnOxMNayMTacvC5JGEVln3CVdiVDgbBk=
|
||||
github.com/smartwalle/ngx v1.0.6 h1:JPNqNOIj+2nxxFtrSkJO+vKJfeNUSEQueck/Wworjps=
|
||||
github.com/smartwalle/ngx v1.0.6/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0=
|
||||
github.com/smartwalle/nsign v1.0.8 h1:78KWtwKPrdt4Xsn+tNEBVxaTLIJBX9YRX0ZSrMUeuHo=
|
||||
github.com/smartwalle/nsign v1.0.8/go.mod h1:eY6I4CJlyNdVMP+t6z1H6Jpd4m5/V+8xi44ufSTxXgc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -187,8 +190,6 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
|
||||
@@ -197,6 +198,8 @@ 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/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
@@ -228,7 +231,6 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
|
||||
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/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=
|
||||
@@ -237,13 +239,11 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/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-20210220032951-036812b2e83c/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.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -290,15 +290,12 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
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=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
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.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=
|
||||
|
||||
@@ -5,13 +5,10 @@ import (
|
||||
"chatplus/core/types"
|
||||
"chatplus/handler"
|
||||
logger2 "chatplus/logger"
|
||||
"chatplus/store/model"
|
||||
"chatplus/utils"
|
||||
"chatplus/utils/resp"
|
||||
"context"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -82,67 +79,3 @@ func (h *ManagerHandler) Session(c *gin.Context) {
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate 数据修正
|
||||
func (h *ManagerHandler) Migrate(c *gin.Context) {
|
||||
opt := c.Query("opt")
|
||||
switch opt {
|
||||
case "user":
|
||||
// 将用户订阅角色的数据结构从 map 改成数组
|
||||
var users []model.User
|
||||
h.db.Find(&users)
|
||||
for _, u := range users {
|
||||
var m map[string]int
|
||||
var roleKeys = make([]string, 0)
|
||||
err := utils.JsonDecode(u.ChatRoles, &m)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for k := range m {
|
||||
roleKeys = append(roleKeys, k)
|
||||
}
|
||||
u.ChatRoles = utils.JsonEncode(roleKeys)
|
||||
h.db.Updates(&u)
|
||||
|
||||
}
|
||||
break
|
||||
case "role":
|
||||
// 修改角色图片,改成绝对路径
|
||||
var roles []model.ChatRole
|
||||
h.db.Find(&roles)
|
||||
for _, r := range roles {
|
||||
if !strings.HasPrefix(r.Icon, "/") {
|
||||
r.Icon = "/" + r.Icon
|
||||
h.db.Updates(&r)
|
||||
}
|
||||
}
|
||||
break
|
||||
case "history":
|
||||
// 修改角色图片,改成绝对路径
|
||||
var message []model.HistoryMessage
|
||||
h.db.Find(&message)
|
||||
for _, r := range message {
|
||||
if !strings.HasPrefix(r.Icon, "/") {
|
||||
r.Icon = "/" + r.Icon
|
||||
h.db.Updates(&r)
|
||||
}
|
||||
|
||||
}
|
||||
break
|
||||
|
||||
case "avatar":
|
||||
// 更新用户的头像地址
|
||||
var users []model.User
|
||||
h.db.Find(&users)
|
||||
for _, u := range users {
|
||||
if !strings.HasPrefix(u.Avatar, "/") {
|
||||
u.Avatar = "/" + u.Avatar
|
||||
h.db.Updates(&u)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, "SUCCESS")
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Platform string `json:"platform"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
@@ -40,7 +41,8 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
|
||||
}
|
||||
apiKey.Platform = data.Platform
|
||||
apiKey.Value = data.Value
|
||||
res := h.db.Debug().Save(&apiKey)
|
||||
apiKey.Type = data.Type
|
||||
res := h.db.Save(&apiKey)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
|
||||
@@ -31,7 +31,9 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
|
||||
Value string `json:"value"`
|
||||
Enabled bool `json:"enabled"`
|
||||
SortNum int `json:"sort_num"`
|
||||
Open bool `json:"open"`
|
||||
Platform string `json:"platform"`
|
||||
Weight int `json:"weight"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
@@ -39,7 +41,14 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
item := model.ChatModel{Platform: data.Platform, Name: data.Name, Value: data.Value, Enabled: data.Enabled}
|
||||
item := model.ChatModel{
|
||||
Platform: data.Platform,
|
||||
Name: data.Name,
|
||||
Value: data.Value,
|
||||
Enabled: data.Enabled,
|
||||
SortNum: data.SortNum,
|
||||
Open: data.Open,
|
||||
Weight: data.Weight}
|
||||
item.Id = data.Id
|
||||
if item.Id > 0 {
|
||||
item.CreatedAt = time.Unix(data.CreatedAt, 0)
|
||||
@@ -88,10 +97,11 @@ func (h *ChatModelHandler) List(c *gin.Context) {
|
||||
resp.SUCCESS(c, cms)
|
||||
}
|
||||
|
||||
func (h *ChatModelHandler) Enable(c *gin.Context) {
|
||||
func (h *ChatModelHandler) Set(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Id uint `json:"id"`
|
||||
Filed string `json:"filed"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
@@ -99,7 +109,7 @@ func (h *ChatModelHandler) Enable(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
res := h.db.Model(&model.ChatModel{}).Where("id = ?", data.Id).Update("enabled", data.Enabled)
|
||||
res := h.db.Model(&model.ChatModel{}).Where("id = ?", data.Id).Update(data.Filed, data.Value)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
|
||||
@@ -2,6 +2,7 @@ package admin
|
||||
|
||||
import (
|
||||
"chatplus/core"
|
||||
"chatplus/core/types"
|
||||
"chatplus/handler"
|
||||
"chatplus/store/model"
|
||||
"chatplus/utils/resp"
|
||||
@@ -22,10 +23,10 @@ func NewDashboardHandler(app *core.AppServer, db *gorm.DB) *DashboardHandler {
|
||||
}
|
||||
|
||||
type statsVo struct {
|
||||
Users int64 `json:"users"`
|
||||
Chats int64 `json:"chats"`
|
||||
Tokens int64 `json:"tokens"`
|
||||
Rewards float64 `json:"rewards"`
|
||||
Users int64 `json:"users"`
|
||||
Chats int64 `json:"chats"`
|
||||
Tokens int `json:"tokens"`
|
||||
Income float64 `json:"income"`
|
||||
}
|
||||
|
||||
func (h *DashboardHandler) Stats(c *gin.Context) {
|
||||
@@ -47,17 +48,24 @@ func (h *DashboardHandler) Stats(c *gin.Context) {
|
||||
}
|
||||
|
||||
// tokens took stats
|
||||
var tokenCount int64
|
||||
res = h.db.Model(&model.HistoryMessage{}).Select("sum(tokens) as total").Where("created_at > ?", zeroTime).Scan(&tokenCount)
|
||||
if res.Error == nil {
|
||||
stats.Tokens = tokenCount
|
||||
var historyMessages []model.HistoryMessage
|
||||
res = h.db.Where("created_at > ?", zeroTime).Find(&historyMessages)
|
||||
for _, item := range historyMessages {
|
||||
stats.Tokens += item.Tokens
|
||||
}
|
||||
|
||||
// reward revenue
|
||||
var amount float64
|
||||
res = h.db.Model(&model.Reward{}).Select("sum(amount) as total").Where("created_at > ?", zeroTime).Scan(&amount)
|
||||
if res.Error == nil {
|
||||
stats.Rewards = amount
|
||||
// 众筹收入
|
||||
var rewards []model.Reward
|
||||
res = h.db.Where("created_at > ?", zeroTime).Find(&rewards)
|
||||
for _, item := range rewards {
|
||||
stats.Income += item.Amount
|
||||
}
|
||||
|
||||
// 订单收入
|
||||
var orders []model.Order
|
||||
res = h.db.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Find(&orders)
|
||||
for _, item := range orders {
|
||||
stats.Income += item.Amount
|
||||
}
|
||||
resp.SUCCESS(c, stats)
|
||||
}
|
||||
|
||||
93
api/handler/admin/order_handler.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"chatplus/core"
|
||||
"chatplus/core/types"
|
||||
"chatplus/handler"
|
||||
"chatplus/store/model"
|
||||
"chatplus/store/vo"
|
||||
"chatplus/utils"
|
||||
"chatplus/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type OrderHandler struct {
|
||||
handler.BaseHandler
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewOrderHandler(app *core.AppServer, db *gorm.DB) *OrderHandler {
|
||||
h := OrderHandler{db: db}
|
||||
h.App = app
|
||||
return &h
|
||||
}
|
||||
|
||||
func (h *OrderHandler) List(c *gin.Context) {
|
||||
var data struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
PayTime []string `json:"pay_time"`
|
||||
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.OrderNo != "" {
|
||||
session = session.Where("order_no", data.OrderNo)
|
||||
}
|
||||
if len(data.PayTime) == 2 {
|
||||
start := utils.Str2stamp(data.PayTime[0] + " 00:00:00")
|
||||
end := utils.Str2stamp(data.PayTime[1] + " 00:00:00")
|
||||
session = session.Where("pay_time >= ? AND pay_time <= ?", start, end)
|
||||
}
|
||||
var total int64
|
||||
session.Model(&model.Order{}).Count(&total)
|
||||
var items []model.Order
|
||||
var list = make([]vo.Order, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var order vo.Order
|
||||
err := utils.CopyObject(item, &order)
|
||||
if err == nil {
|
||||
order.Id = item.Id
|
||||
order.CreatedAt = item.CreatedAt.Unix()
|
||||
order.UpdatedAt = item.UpdatedAt.Unix()
|
||||
list = append(list, order)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
|
||||
}
|
||||
|
||||
func (h *OrderHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
|
||||
if id > 0 {
|
||||
var item model.Order
|
||||
res := h.db.First(&item, id)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "记录不存在!")
|
||||
return
|
||||
}
|
||||
|
||||
if item.Status == types.OrderPaidSuccess {
|
||||
resp.ERROR(c, "已支付订单不允许删除!")
|
||||
return
|
||||
}
|
||||
|
||||
res = h.db.Where("id = ?", id).Delete(&model.Order{})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
144
api/handler/admin/product_handler.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"chatplus/core"
|
||||
"chatplus/core/types"
|
||||
"chatplus/handler"
|
||||
"chatplus/store/model"
|
||||
"chatplus/store/vo"
|
||||
"chatplus/utils"
|
||||
"chatplus/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ProductHandler struct {
|
||||
handler.BaseHandler
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewProductHandler(app *core.AppServer, db *gorm.DB) *ProductHandler {
|
||||
h := ProductHandler{db: db}
|
||||
h.App = app
|
||||
return &h
|
||||
}
|
||||
|
||||
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"`
|
||||
Calls int `json:"calls"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
item := model.Product{Name: data.Name, Price: data.Price, Discount: data.Discount, Days: data.Days, Calls: data.Calls, Enabled: data.Enabled}
|
||||
item.Id = data.Id
|
||||
if item.Id > 0 {
|
||||
item.CreatedAt = time.Unix(data.CreatedAt, 0)
|
||||
}
|
||||
res := h.db.Save(&item)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
|
||||
var itemVo vo.Product
|
||||
err := utils.CopyObject(item, &itemVo)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "数据拷贝失败!")
|
||||
return
|
||||
}
|
||||
itemVo.Id = item.Id
|
||||
itemVo.UpdatedAt = item.UpdatedAt.Unix()
|
||||
resp.SUCCESS(c, itemVo)
|
||||
}
|
||||
|
||||
// List 模型列表
|
||||
func (h *ProductHandler) List(c *gin.Context) {
|
||||
session := h.db.Session(&gorm.Session{})
|
||||
enable := h.GetBool(c, "enable")
|
||||
if enable {
|
||||
session = session.Where("enabled", enable)
|
||||
}
|
||||
var items []model.Product
|
||||
var list = make([]vo.Product, 0)
|
||||
res := session.Order("sort_num ASC").Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var product vo.Product
|
||||
err := utils.CopyObject(item, &product)
|
||||
if err == nil {
|
||||
product.Id = item.Id
|
||||
product.CreatedAt = item.CreatedAt.Unix()
|
||||
product.UpdatedAt = item.UpdatedAt.Unix()
|
||||
list = append(list, product)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, list)
|
||||
}
|
||||
|
||||
func (h *ProductHandler) Enable(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
res := h.db.Model(&model.Product{}).Where("id = ?", data.Id).Update("enabled", data.Enabled)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *ProductHandler) Sort(c *gin.Context) {
|
||||
var data struct {
|
||||
Ids []uint `json:"ids"`
|
||||
Sorts []int `json:"sorts"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
for index, id := range data.Ids {
|
||||
res := h.db.Model(&model.Product{}).Where("id = ?", id).Update("sort_num", data.Sorts[index])
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *ProductHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
|
||||
if id > 0 {
|
||||
res := h.db.Where("id = ?", id).Delete(&model.Product{})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -67,8 +67,10 @@ func (h *UserHandler) Save(c *gin.Context) {
|
||||
Calls int `json:"calls"`
|
||||
ImgCalls int `json:"img_calls"`
|
||||
ChatRoles []string `json:"chat_roles"`
|
||||
ChatModels []string `json:"chat_models"`
|
||||
ExpiredTime string `json:"expired_time"`
|
||||
Status bool `json:"status"`
|
||||
Vip bool `json:"vip"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
@@ -81,12 +83,14 @@ func (h *UserHandler) Save(c *gin.Context) {
|
||||
user.Id = data.Id
|
||||
// 此处需要用 map 更新,用结构体无法更新 0 值
|
||||
res = h.db.Model(&user).Updates(map[string]interface{}{
|
||||
"mobile": data.Mobile,
|
||||
"calls": data.Calls,
|
||||
"img_calls": data.ImgCalls,
|
||||
"status": data.Status,
|
||||
"chat_roles_json": utils.JsonEncode(data.ChatRoles),
|
||||
"expired_time": utils.Str2stamp(data.ExpiredTime),
|
||||
"mobile": data.Mobile,
|
||||
"calls": data.Calls,
|
||||
"img_calls": data.ImgCalls,
|
||||
"status": data.Status,
|
||||
"vip": data.Vip,
|
||||
"chat_roles_json": utils.JsonEncode(data.ChatRoles),
|
||||
"chat_models_json": utils.JsonEncode(data.ChatModels),
|
||||
"expired_time": utils.Str2stamp(data.ExpiredTime),
|
||||
})
|
||||
} else {
|
||||
salt := utils.RandString(8)
|
||||
@@ -97,6 +101,7 @@ func (h *UserHandler) Save(c *gin.Context) {
|
||||
Salt: salt,
|
||||
Status: true,
|
||||
ChatRoles: utils.JsonEncode(data.ChatRoles),
|
||||
ChatModels: utils.JsonEncode(data.ChatModels),
|
||||
ExpiredTime: utils.Str2stamp(data.ExpiredTime),
|
||||
ChatConfig: utils.JsonEncode(types.UserChatConfig{
|
||||
ApiKeys: map[types.Platform]string{
|
||||
|
||||
@@ -49,3 +49,11 @@ func (h *BaseHandler) GetUserKey(c *gin.Context) string {
|
||||
}
|
||||
return fmt.Sprintf("users/%v", userId)
|
||||
}
|
||||
|
||||
func (h *BaseHandler) GetLoginUserId(c *gin.Context) uint {
|
||||
userId, ok := c.Get(types.LoginUserID)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return uint(utils.IntValue(utils.InterfaceToString(userId), 0))
|
||||
}
|
||||
|
||||
@@ -24,8 +24,25 @@ func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
|
||||
// List 模型列表
|
||||
func (h *ChatModelHandler) List(c *gin.Context) {
|
||||
var items []model.ChatModel
|
||||
var cms = make([]vo.ChatModel, 0)
|
||||
res := h.db.Where("enabled = ?", true).Order("sort_num ASC").Find(&items)
|
||||
var chatModels = make([]vo.ChatModel, 0)
|
||||
// 只加载用户订阅的 AI 模型
|
||||
user, err := utils.GetLoginUser(c, h.db)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
var models []string
|
||||
err = utils.JsonDecode(user.ChatModels, &models)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "当前用户没有订阅任何模型")
|
||||
return
|
||||
}
|
||||
|
||||
// 查询用户有权限访问的模型以及所有开放的模型
|
||||
res := h.db.Where("enabled = ?", true).Where(
|
||||
h.db.Where("value IN ?", models).Or("open =?", true),
|
||||
).Order("sort_num ASC").Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var cm vo.ChatModel
|
||||
@@ -34,11 +51,11 @@ func (h *ChatModelHandler) List(c *gin.Context) {
|
||||
cm.Id = item.Id
|
||||
cm.CreatedAt = item.CreatedAt.Unix()
|
||||
cm.UpdatedAt = item.UpdatedAt.Unix()
|
||||
cms = append(cms, cm)
|
||||
chatModels = append(chatModels, cm)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, cms)
|
||||
resp.SUCCESS(c, chatModels)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func (h *ChatHandler) sendAzureMessage(
|
||||
}
|
||||
|
||||
utils.ReplyMessage(ws, ErrorMsg)
|
||||
utils.ReplyMessage(ws, "")
|
||||
utils.ReplyMessage(ws, ErrImg)
|
||||
return err
|
||||
} else {
|
||||
defer response.Body.Close()
|
||||
@@ -71,7 +71,7 @@ func (h *ChatHandler) sendAzureMessage(
|
||||
if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错
|
||||
logger.Error(err, line)
|
||||
utils.ReplyMessage(ws, ErrorMsg)
|
||||
utils.ReplyMessage(ws, "")
|
||||
utils.ReplyMessage(ws, ErrImg)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -127,12 +127,12 @@ func (h *ChatHandler) sendAzureMessage(
|
||||
logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params)
|
||||
|
||||
// for creating image, check if the user's img_calls > 0
|
||||
if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 {
|
||||
if functionName == types.FuncImage && userVo.ImgCalls <= 0 {
|
||||
utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**")
|
||||
utils.ReplyMessage(ws, "")
|
||||
utils.ReplyMessage(ws, ErrImg)
|
||||
} else {
|
||||
f := h.App.Functions[functionName]
|
||||
if functionName == types.FuncMidJourney {
|
||||
if functionName == types.FuncImage {
|
||||
params["user_id"] = userVo.Id
|
||||
params["role_id"] = role.Id
|
||||
params["chat_id"] = session.ChatId
|
||||
@@ -149,9 +149,8 @@ func (h *ChatHandler) sendAzureMessage(
|
||||
contents = append(contents, msg)
|
||||
} else {
|
||||
content := data
|
||||
if functionName == types.FuncMidJourney {
|
||||
content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data)
|
||||
h.mjService.ChatClients.Put(session.SessionId, ws)
|
||||
if functionName == types.FuncImage {
|
||||
content = fmt.Sprintf("下面是根据您的描述创作的图片,他们描绘了 【%s】 的场景", params["prompt"])
|
||||
// update user's img_calls
|
||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
|
||||
}
|
||||
@@ -168,9 +167,7 @@ func (h *ChatHandler) sendAzureMessage(
|
||||
// 消息发送成功
|
||||
if len(contents) > 0 {
|
||||
// 更新用户的对话次数
|
||||
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
|
||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
|
||||
}
|
||||
h.subUserCalls(userVo, session)
|
||||
|
||||
if message.Role == "" {
|
||||
message.Role = "assistant"
|
||||
@@ -244,8 +241,7 @@ func (h *ChatHandler) sendAzureMessage(
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
|
||||
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
|
||||
h.incUserTokenFee(userVo.Id, totalTokens)
|
||||
}
|
||||
|
||||
// 保存当前会话
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -61,7 +60,7 @@ func (h *ChatHandler) sendBaiduMessage(
|
||||
}
|
||||
|
||||
utils.ReplyMessage(ws, ErrorMsg)
|
||||
utils.ReplyMessage(ws, "")
|
||||
utils.ReplyMessage(ws, ErrImg)
|
||||
return err
|
||||
} else {
|
||||
defer response.Body.Close()
|
||||
@@ -85,6 +84,11 @@ func (h *ChatHandler) sendBaiduMessage(
|
||||
content = line[5:]
|
||||
}
|
||||
|
||||
// 处理代码换行
|
||||
if len(content) == 0 {
|
||||
content = "\n"
|
||||
}
|
||||
|
||||
var resp baiduResp
|
||||
err := utils.JsonDecode(content, &resp)
|
||||
if err != nil {
|
||||
@@ -124,9 +128,7 @@ func (h *ChatHandler) sendBaiduMessage(
|
||||
// 消息发送成功
|
||||
if len(contents) > 0 {
|
||||
// 更新用户的对话次数
|
||||
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
|
||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
|
||||
}
|
||||
h.subUserCalls(userVo, session)
|
||||
|
||||
if message.Role == "" {
|
||||
message.Role = "assistant"
|
||||
@@ -186,8 +188,7 @@ func (h *ChatHandler) sendBaiduMessage(
|
||||
logger.Error("failed to save reply history message: ", res.Error)
|
||||
}
|
||||
// 更新用户信息
|
||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
|
||||
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
|
||||
h.incUserTokenFee(userVo.Id, totalTokens)
|
||||
}
|
||||
|
||||
// 保存当前会话
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"chatplus/handler"
|
||||
logger2 "chatplus/logger"
|
||||
"chatplus/service/mj"
|
||||
"chatplus/store"
|
||||
"chatplus/store/model"
|
||||
"chatplus/store/vo"
|
||||
"chatplus/utils"
|
||||
@@ -27,21 +26,20 @@ import (
|
||||
)
|
||||
|
||||
const ErrorMsg = "抱歉,AI 助手开小差了,请稍后再试。"
|
||||
const ErrImg = ""
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
type ChatHandler struct {
|
||||
handler.BaseHandler
|
||||
db *gorm.DB
|
||||
leveldb *store.LevelDB
|
||||
redis *redis.Client
|
||||
mjService *mj.Service
|
||||
}
|
||||
|
||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB, redis *redis.Client, service *mj.Service) *ChatHandler {
|
||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, service *mj.Service) *ChatHandler {
|
||||
h := ChatHandler{
|
||||
db: db,
|
||||
leveldb: levelDB,
|
||||
redis: redis,
|
||||
mjService: service,
|
||||
}
|
||||
@@ -103,6 +101,7 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
|
||||
session.Model = types.ChatModel{
|
||||
Id: chatModel.Id,
|
||||
Value: chatModel.Value,
|
||||
Weight: chatModel.Weight,
|
||||
Platform: types.Platform(chatModel.Platform)}
|
||||
logger.Infof("New websocket connected, IP: %s, Username: %s", c.ClientIP(), session.Username)
|
||||
var chatRole model.ChatRole
|
||||
@@ -181,19 +180,25 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
|
||||
if userVo.Status == false {
|
||||
utils.ReplyMessage(ws, "您的账号已经被禁用,如果疑问,请联系管理员!")
|
||||
utils.ReplyMessage(ws, "")
|
||||
utils.ReplyMessage(ws, ErrImg)
|
||||
return nil
|
||||
}
|
||||
|
||||
if userVo.Calls < session.Model.Weight {
|
||||
utils.ReplyMessage(ws, fmt.Sprintf("您当前剩余对话次数(%d)已不足以支付当前模型的单次对话需要消耗的对话额度(%d)!", userVo.Calls, session.Model.Weight))
|
||||
utils.ReplyMessage(ws, ErrImg)
|
||||
return nil
|
||||
}
|
||||
|
||||
if userVo.Calls <= 0 && userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
|
||||
utils.ReplyMessage(ws, "您的对话次数已经用尽,请联系管理员或者点击左下角菜单加入众筹获得100次对话!")
|
||||
utils.ReplyMessage(ws, "")
|
||||
utils.ReplyMessage(ws, "您的对话次数已经用尽,请联系管理员或者充值点卡继续对话!")
|
||||
utils.ReplyMessage(ws, ErrImg)
|
||||
return nil
|
||||
}
|
||||
|
||||
if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() {
|
||||
utils.ReplyMessage(ws, "您的账号已经过期,请联系管理员!")
|
||||
utils.ReplyMessage(ws, "")
|
||||
utils.ReplyMessage(ws, ErrImg)
|
||||
return nil
|
||||
}
|
||||
var req = types.ApiRequest{
|
||||
@@ -219,9 +224,6 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
if h.App.SysConfig.EnabledFunction {
|
||||
var functions = make([]types.Function, 0)
|
||||
for _, f := range types.InnerFunctions {
|
||||
if !h.App.SysConfig.EnabledDraw && f.Name == types.FuncMidJourney {
|
||||
continue
|
||||
}
|
||||
functions = append(functions, f)
|
||||
}
|
||||
req.Functions = functions
|
||||
@@ -231,7 +233,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
req.MaxTokens = h.App.ChatConfig.XunFei.MaxTokens
|
||||
default:
|
||||
utils.ReplyMessage(ws, "不支持的平台:"+session.Model.Platform+",请联系管理员!")
|
||||
utils.ReplyMessage(ws, "")
|
||||
utils.ReplyMessage(ws, ErrImg)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -393,14 +395,14 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
|
||||
req.Messages = nil
|
||||
break
|
||||
case types.Baidu:
|
||||
apiURL = h.App.ChatConfig.Baidu.ApiURL
|
||||
apiURL = strings.Replace(h.App.ChatConfig.Baidu.ApiURL, "{model}", req.Model, 1)
|
||||
break
|
||||
default:
|
||||
apiURL = h.App.ChatConfig.OpenAI.ApiURL
|
||||
}
|
||||
if *apiKey == "" {
|
||||
var key model.ApiKey
|
||||
res := h.db.Where("platform = ?", platform).Order("last_used_at ASC").First(&key)
|
||||
res := h.db.Where("platform = ? AND type = ?", platform, "chat").Order("last_used_at ASC").First(&key)
|
||||
if res.Error != nil {
|
||||
return nil, errors.New("no available key, please import key")
|
||||
}
|
||||
@@ -463,3 +465,22 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
|
||||
}
|
||||
return client.Do(request)
|
||||
}
|
||||
|
||||
// 扣减用户的对话次数
|
||||
func (h *ChatHandler) subUserCalls(userVo vo.User, session *types.ChatSession) {
|
||||
// 仅当用户没有导入自己的 API KEY 时才进行扣减
|
||||
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
|
||||
num := 1
|
||||
if session.Model.Weight > 0 {
|
||||
num = session.Model.Weight
|
||||
}
|
||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", num))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ChatHandler) incUserTokenFee(userId uint, tokens int) {
|
||||
h.db.Model(&model.User{}).Where("id = ?", userId).
|
||||
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", tokens))
|
||||
h.db.Model(&model.User{}).Where("id = ?", userId).
|
||||
UpdateColumn("tokens", gorm.Expr("tokens + ?", tokens))
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -45,7 +44,7 @@ func (h *ChatHandler) sendChatGLMMessage(
|
||||
}
|
||||
|
||||
utils.ReplyMessage(ws, ErrorMsg)
|
||||
utils.ReplyMessage(ws, "")
|
||||
utils.ReplyMessage(ws, ErrImg)
|
||||
return err
|
||||
} else {
|
||||
defer response.Body.Close()
|
||||
@@ -72,6 +71,10 @@ func (h *ChatHandler) sendChatGLMMessage(
|
||||
if strings.HasPrefix(line, "data:") {
|
||||
content = line[5:]
|
||||
}
|
||||
// 处理代码换行
|
||||
if len(content) == 0 {
|
||||
content = "\n"
|
||||
}
|
||||
switch event {
|
||||
case "add":
|
||||
if len(contents) == 0 {
|
||||
@@ -104,9 +107,7 @@ func (h *ChatHandler) sendChatGLMMessage(
|
||||
// 消息发送成功
|
||||
if len(contents) > 0 {
|
||||
// 更新用户的对话次数
|
||||
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
|
||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
|
||||
}
|
||||
h.subUserCalls(userVo, session)
|
||||
|
||||
if message.Role == "" {
|
||||
message.Role = "assistant"
|
||||
@@ -166,8 +167,7 @@ func (h *ChatHandler) sendChatGLMMessage(
|
||||
logger.Error("failed to save reply history message: ", res.Error)
|
||||
}
|
||||
// 更新用户信息
|
||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
|
||||
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
|
||||
h.incUserTokenFee(userVo.Id, totalTokens)
|
||||
}
|
||||
|
||||
// 保存当前会话
|
||||
|
||||
@@ -43,7 +43,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
}
|
||||
|
||||
utils.ReplyMessage(ws, ErrorMsg)
|
||||
utils.ReplyMessage(ws, "")
|
||||
utils.ReplyMessage(ws, ErrImg)
|
||||
return err
|
||||
} else {
|
||||
defer response.Body.Close()
|
||||
@@ -70,7 +70,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错
|
||||
logger.Error(err, line)
|
||||
utils.ReplyMessage(ws, ErrorMsg)
|
||||
utils.ReplyMessage(ws, "")
|
||||
utils.ReplyMessage(ws, ErrImg)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -126,12 +126,12 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params)
|
||||
|
||||
// for creating image, check if the user's img_calls > 0
|
||||
if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 {
|
||||
if functionName == types.FuncImage && userVo.ImgCalls <= 0 {
|
||||
utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**")
|
||||
utils.ReplyMessage(ws, "")
|
||||
utils.ReplyMessage(ws, ErrImg)
|
||||
} else {
|
||||
f := h.App.Functions[functionName]
|
||||
if functionName == types.FuncMidJourney {
|
||||
if functionName == types.FuncImage {
|
||||
params["user_id"] = userVo.Id
|
||||
params["role_id"] = role.Id
|
||||
params["chat_id"] = session.ChatId
|
||||
@@ -148,9 +148,8 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
contents = append(contents, msg)
|
||||
} else {
|
||||
content := data
|
||||
if functionName == types.FuncMidJourney {
|
||||
content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data)
|
||||
h.mjService.ChatClients.Put(session.SessionId, ws)
|
||||
if functionName == types.FuncImage {
|
||||
content = fmt.Sprintf("下面是根据您的描述创作的图片,他们描绘了 【%s】 的场景。%s", params["prompt"], data)
|
||||
// update user's img_calls
|
||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
|
||||
}
|
||||
@@ -167,9 +166,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
// 消息发送成功
|
||||
if len(contents) > 0 {
|
||||
// 更新用户的对话次数
|
||||
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
|
||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
|
||||
}
|
||||
h.subUserCalls(userVo, session)
|
||||
|
||||
if message.Role == "" {
|
||||
message.Role = "assistant"
|
||||
@@ -243,8 +240,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
|
||||
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
|
||||
h.incUserTokenFee(userVo.Id, totalTokens)
|
||||
}
|
||||
|
||||
// 保存当前会话
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/websocket"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -49,6 +48,12 @@ type xunFeiResp struct {
|
||||
} `json:"payload"`
|
||||
}
|
||||
|
||||
var Model2URL = map[string]string{
|
||||
"generalv1": "1.1",
|
||||
"generalv2": "v2.1",
|
||||
"generalv3": "v3.1",
|
||||
}
|
||||
|
||||
// 科大讯飞消息发送实现
|
||||
|
||||
func (h *ChatHandler) sendXunFeiMessage(
|
||||
@@ -64,7 +69,7 @@ func (h *ChatHandler) sendXunFeiMessage(
|
||||
var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
|
||||
if apiKey == "" {
|
||||
var key model.ApiKey
|
||||
res := h.db.Where("platform = ?", session.Model.Platform).Order("last_used_at ASC").First(&key)
|
||||
res := h.db.Where("platform = ? AND type = ?", session.Model.Platform, "chat").Order("last_used_at ASC").First(&key)
|
||||
if res.Error != nil {
|
||||
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||
return nil
|
||||
@@ -83,13 +88,7 @@ func (h *ChatHandler) sendXunFeiMessage(
|
||||
return nil
|
||||
}
|
||||
|
||||
var apiURL string
|
||||
if req.Model == "generalv2" {
|
||||
apiURL = strings.Replace(h.App.ChatConfig.XunFei.ApiURL, "{version}", "v2.1", 1)
|
||||
} else {
|
||||
apiURL = strings.Replace(h.App.ChatConfig.XunFei.ApiURL, "{version}", "v1.1", 1)
|
||||
}
|
||||
|
||||
apiURL := strings.Replace(h.App.ChatConfig.XunFei.ApiURL, "{version}", Model2URL[req.Model], 1)
|
||||
wsURL, err := assembleAuthUrl(apiURL, key[1], key[2])
|
||||
//握手并建立websocket 连接
|
||||
conn, resp, err := d.Dial(wsURL, nil)
|
||||
@@ -139,6 +138,10 @@ func (h *ChatHandler) sendXunFeiMessage(
|
||||
}
|
||||
|
||||
content = result.Payload.Choices.Text[0].Content
|
||||
// 处理代码换行
|
||||
if len(content) == 0 {
|
||||
content = "\n"
|
||||
}
|
||||
contents = append(contents, content)
|
||||
// 第一个结果
|
||||
if result.Payload.Choices.Status == 0 {
|
||||
@@ -167,9 +170,7 @@ func (h *ChatHandler) sendXunFeiMessage(
|
||||
// 消息发送成功
|
||||
if len(contents) > 0 {
|
||||
// 更新用户的对话次数
|
||||
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
|
||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
|
||||
}
|
||||
h.subUserCalls(userVo, session)
|
||||
|
||||
if message.Role == "" {
|
||||
message.Role = "assistant"
|
||||
@@ -229,8 +230,7 @@ func (h *ChatHandler) sendXunFeiMessage(
|
||||
logger.Error("failed to save reply history message: ", res.Error)
|
||||
}
|
||||
// 更新用户信息
|
||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
|
||||
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
|
||||
h.incUserTokenFee(userVo.Id, totalTokens)
|
||||
}
|
||||
|
||||
// 保存当前会话
|
||||
|
||||
96
api/handler/invite_handler.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"chatplus/core"
|
||||
"chatplus/core/types"
|
||||
"chatplus/store/model"
|
||||
"chatplus/store/vo"
|
||||
"chatplus/utils"
|
||||
"chatplus/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// InviteHandler 用户邀请
|
||||
type InviteHandler struct {
|
||||
BaseHandler
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewInviteHandler(app *core.AppServer, db *gorm.DB) *InviteHandler {
|
||||
h := InviteHandler{db: db}
|
||||
h.App = app
|
||||
return &h
|
||||
}
|
||||
|
||||
// Code 获取当前用户邀请码
|
||||
func (h *InviteHandler) Code(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
var inviteCode model.InviteCode
|
||||
res := h.db.Where("user_id = ?", userId).First(&inviteCode)
|
||||
// 如果邀请码不存在,则创建一个
|
||||
if res.Error != nil {
|
||||
code := strings.ToUpper(utils.RandString(8))
|
||||
for {
|
||||
res = h.db.Where("code = ?", code).First(&inviteCode)
|
||||
if res.Error != nil { // 不存在相同的邀请码则退出
|
||||
break
|
||||
}
|
||||
}
|
||||
inviteCode.UserId = userId
|
||||
inviteCode.Code = code
|
||||
h.db.Create(&inviteCode)
|
||||
}
|
||||
|
||||
var codeVo vo.InviteCode
|
||||
err := utils.CopyObject(inviteCode, &codeVo)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "拷贝对象失败")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, codeVo)
|
||||
}
|
||||
|
||||
// List Log 用户邀请记录
|
||||
func (h *InviteHandler) List(c *gin.Context) {
|
||||
|
||||
var data struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
userId := h.GetLoginUserId(c)
|
||||
session := h.db.Session(&gorm.Session{}).Where("inviter_id = ?", userId)
|
||||
var total int64
|
||||
session.Model(&model.InviteLog{}).Count(&total)
|
||||
var items []model.InviteLog
|
||||
var list = make([]vo.InviteLog, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
res := session.Order("id DESC").Offset(offset).Limit(data.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
|
||||
}
|
||||
|
||||
// Hits 访问邀请码
|
||||
func (h *InviteHandler) Hits(c *gin.Context) {
|
||||
code := c.Query("code")
|
||||
h.db.Model(&model.InviteCode{}).Where("code = ?", code).UpdateColumn("hits", gorm.Expr("hits + ?", 1))
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -120,7 +120,7 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
|
||||
prompt += " --style raw"
|
||||
}
|
||||
if data.Model != "" && !strings.Contains(prompt, "--v") && !strings.Contains(prompt, "--niji") {
|
||||
prompt += data.Model
|
||||
prompt += fmt.Sprintf(" %s", data.Model)
|
||||
}
|
||||
|
||||
idValue, _ := c.Get(types.LoginUserID)
|
||||
@@ -348,8 +348,8 @@ func (h *MidJourneyHandler) JobList(c *gin.Context) {
|
||||
continue
|
||||
}
|
||||
if item.Progress < 100 {
|
||||
// 30 分钟还没完成的任务直接删除
|
||||
if time.Now().Sub(item.CreatedAt) > time.Minute*30 {
|
||||
// 10 分钟还没完成的任务直接删除
|
||||
if time.Now().Sub(item.CreatedAt) > time.Minute*10 {
|
||||
h.db.Delete(&item)
|
||||
continue
|
||||
}
|
||||
|
||||
57
api/handler/order_handler.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"chatplus/core"
|
||||
"chatplus/core/types"
|
||||
"chatplus/store/model"
|
||||
"chatplus/store/vo"
|
||||
"chatplus/utils"
|
||||
"chatplus/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type OrderHandler struct {
|
||||
BaseHandler
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewOrderHandler(app *core.AppServer, db *gorm.DB) *OrderHandler {
|
||||
h := OrderHandler{db: db}
|
||||
h.App = app
|
||||
return &h
|
||||
}
|
||||
|
||||
func (h *OrderHandler) List(c *gin.Context) {
|
||||
var data struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
user, _ := utils.GetLoginUser(c, h.db)
|
||||
session := h.db.Session(&gorm.Session{}).Where("user_id = ? AND status = ?", user.Id, types.OrderPaidSuccess)
|
||||
var total int64
|
||||
session.Model(&model.Order{}).Count(&total)
|
||||
var items []model.Order
|
||||
var list = make([]vo.Order, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var order vo.Order
|
||||
err := utils.CopyObject(item, &order)
|
||||
if err == nil {
|
||||
order.Id = item.Id
|
||||
order.CreatedAt = item.CreatedAt.Unix()
|
||||
order.UpdatedAt = item.UpdatedAt.Unix()
|
||||
list = append(list, order)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
|
||||
}
|
||||
273
api/handler/payment_handler.go
Normal file
@@ -0,0 +1,273 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"chatplus/core"
|
||||
"chatplus/core/types"
|
||||
"chatplus/service"
|
||||
"chatplus/service/payment"
|
||||
"chatplus/store/model"
|
||||
"chatplus/utils"
|
||||
"chatplus/utils/resp"
|
||||
"embed"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
PayWayAlipay = "支付宝"
|
||||
PayWayWechat = "微信支付"
|
||||
)
|
||||
|
||||
// PaymentHandler 支付服务回调 handler
|
||||
type PaymentHandler struct {
|
||||
BaseHandler
|
||||
alipayService *payment.AlipayService
|
||||
snowflake *service.Snowflake
|
||||
db *gorm.DB
|
||||
fs embed.FS
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func NewPaymentHandler(server *core.AppServer, alipayService *payment.AlipayService, snowflake *service.Snowflake, db *gorm.DB, fs embed.FS) *PaymentHandler {
|
||||
h := PaymentHandler{lock: sync.Mutex{}}
|
||||
h.App = server
|
||||
h.alipayService = alipayService
|
||||
h.snowflake = snowflake
|
||||
h.db = db
|
||||
h.fs = fs
|
||||
return &h
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) Alipay(c *gin.Context) {
|
||||
orderNo := h.GetTrim(c, "order_no")
|
||||
if orderNo == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var order model.Order
|
||||
res := h.db.Where("order_no = ?", orderNo).First(&order)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Order not found")
|
||||
return
|
||||
}
|
||||
|
||||
// 更新扫码状态
|
||||
h.db.Model(&order).UpdateColumn("status", types.OrderScanned)
|
||||
// 生成支付链接
|
||||
notifyURL := h.App.Config.AlipayConfig.NotifyURL
|
||||
returnURL := "" // 关闭同步回跳
|
||||
amount := fmt.Sprintf("%.2f", order.Amount)
|
||||
|
||||
uri, err := h.alipayService.PayUrlMobile(order.OrderNo, notifyURL, returnURL, amount, order.Subject)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate pay url: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(302, uri)
|
||||
}
|
||||
|
||||
// OrderQuery 清单状态查询
|
||||
func (h *PaymentHandler) OrderQuery(c *gin.Context) {
|
||||
var data struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var order model.Order
|
||||
res := h.db.Where("order_no = ?", data.OrderNo).First(&order)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Order not found")
|
||||
return
|
||||
}
|
||||
|
||||
if order.Status == types.OrderPaidSuccess {
|
||||
resp.SUCCESS(c, gin.H{"status": order.Status})
|
||||
return
|
||||
}
|
||||
|
||||
counter := 0
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
var item model.Order
|
||||
h.db.Where("order_no = ?", data.OrderNo).First(&item)
|
||||
if counter >= 15 || item.Status == types.OrderPaidSuccess || item.Status != order.Status {
|
||||
order.Status = item.Status
|
||||
break
|
||||
}
|
||||
counter++
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{"status": order.Status})
|
||||
}
|
||||
|
||||
// AlipayQrcode 生成支付宝支付 URL 二维码
|
||||
func (h *PaymentHandler) AlipayQrcode(c *gin.Context) {
|
||||
if !h.App.SysConfig.EnabledAlipay || h.alipayService == nil {
|
||||
resp.ERROR(c, "当前支付通道已经关闭,请联系管理员开通!")
|
||||
return
|
||||
}
|
||||
|
||||
var data struct {
|
||||
ProductId uint `json:"product_id"`
|
||||
UserId int `json:"user_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var product model.Product
|
||||
res := h.db.First(&product, data.ProductId)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Product not found")
|
||||
return
|
||||
}
|
||||
|
||||
orderNo, err := h.snowflake.Next()
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate trade no: "+err.Error())
|
||||
return
|
||||
}
|
||||
var user model.User
|
||||
res = h.db.First(&user, data.UserId)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Invalid user ID")
|
||||
return
|
||||
}
|
||||
|
||||
// 创建订单
|
||||
remark := types.OrderRemark{
|
||||
Days: product.Days,
|
||||
Calls: product.Calls,
|
||||
Name: product.Name,
|
||||
Price: product.Price,
|
||||
Discount: product.Discount,
|
||||
}
|
||||
order := model.Order{
|
||||
UserId: user.Id,
|
||||
Mobile: user.Mobile,
|
||||
ProductId: product.Id,
|
||||
OrderNo: orderNo,
|
||||
Subject: product.Name,
|
||||
Amount: product.Price - product.Discount,
|
||||
Status: types.OrderNotPaid,
|
||||
PayWay: PayWayAlipay,
|
||||
Remark: utils.JsonEncode(remark),
|
||||
}
|
||||
res = h.db.Create(&order)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "error with create order: "+res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 生成二维码图片
|
||||
file, err := h.fs.Open("res/img/alipay.jpg")
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
parse, err := url.Parse(h.App.Config.AlipayConfig.NotifyURL)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
imageURL := fmt.Sprintf("%s://%s/api/payment/alipay?order_no=%s", parse.Scheme, parse.Host, orderNo)
|
||||
imgData, err := utils.GenQrcode(imageURL, 400, file)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
imgDataBase64 := base64.StdEncoding.EncodeToString(imgData)
|
||||
resp.SUCCESS(c, gin.H{"order_no": orderNo, "image": fmt.Sprintf("data:image/jpg;base64, %s", imgDataBase64), "url": imageURL})
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
|
||||
err := c.Request.ParseForm()
|
||||
if err != nil {
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO:这里最好用支付宝的公钥签名签证一下交易真假
|
||||
//res := h.alipayService.TradeVerify(c.Request.Form)
|
||||
r := h.alipayService.TradeQuery(c.Request.Form.Get("out_trade_no"))
|
||||
logger.Infof("验证支付结果:%+v", r)
|
||||
if !r.Success() {
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
|
||||
var order model.Order
|
||||
res := h.db.Where("order_no = ?", r.OutTradeNo).First(&order)
|
||||
if res.Error != nil {
|
||||
logger.Error(res.Error)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
var user model.User
|
||||
res = h.db.First(&user, order.UserId)
|
||||
if res.Error != nil {
|
||||
logger.Error(res.Error)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
var remark types.OrderRemark
|
||||
err = utils.JsonDecode(order.Remark, &remark)
|
||||
if err != nil {
|
||||
logger.Error(res.Error)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
// 1. 点卡:days == 0, calls > 0
|
||||
// 2. vip 套餐:days > 0, calls == 0
|
||||
if remark.Days > 0 {
|
||||
if user.ExpiredTime > time.Now().Unix() {
|
||||
user.ExpiredTime = time.Unix(user.ExpiredTime, 0).AddDate(0, 0, remark.Days).Unix()
|
||||
} else {
|
||||
user.ExpiredTime = time.Now().AddDate(0, 0, remark.Days).Unix()
|
||||
}
|
||||
user.Vip = true
|
||||
|
||||
} else if !user.Vip { // 充值点卡的非 VIP 用户
|
||||
user.ExpiredTime = time.Now().AddDate(0, 0, 30).Unix()
|
||||
}
|
||||
|
||||
if remark.Calls > 0 { // 充值点卡
|
||||
user.Calls += remark.Calls
|
||||
} else {
|
||||
user.Calls += h.App.SysConfig.VipMonthCalls
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
res = h.db.Updates(&user)
|
||||
if res.Error != nil {
|
||||
logger.Error(res.Error)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
// 更新订单状态
|
||||
order.PayTime = time.Now().Unix()
|
||||
order.Status = types.OrderPaidSuccess
|
||||
h.db.Updates(&order)
|
||||
|
||||
// 更新产品销量
|
||||
h.db.Model(&model.Product{}).Where("id = ?", order.ProductId).UpdateColumn("sales", gorm.Expr("sales + ?", 1))
|
||||
|
||||
c.String(http.StatusOK, "success")
|
||||
}
|
||||
44
api/handler/product_handler.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"chatplus/core"
|
||||
"chatplus/store/model"
|
||||
"chatplus/store/vo"
|
||||
"chatplus/utils"
|
||||
"chatplus/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProductHandler struct {
|
||||
BaseHandler
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewProductHandler(app *core.AppServer, db *gorm.DB) *ProductHandler {
|
||||
h := ProductHandler{db: db}
|
||||
h.App = app
|
||||
return &h
|
||||
}
|
||||
|
||||
// List 模型列表
|
||||
func (h *ProductHandler) List(c *gin.Context) {
|
||||
var items []model.Product
|
||||
var list = make([]vo.Product, 0)
|
||||
res := h.db.Where("enabled", true).Order("sort_num ASC").Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var product vo.Product
|
||||
err := utils.CopyObject(item, &product)
|
||||
if err == nil {
|
||||
product.Id = item.Id
|
||||
product.CreatedAt = item.CreatedAt.Unix()
|
||||
product.UpdatedAt = item.UpdatedAt.Unix()
|
||||
list = append(list, product)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, list)
|
||||
}
|
||||
@@ -4,23 +4,23 @@ import (
|
||||
"chatplus/core"
|
||||
"chatplus/core/types"
|
||||
"chatplus/service"
|
||||
"chatplus/store"
|
||||
"chatplus/utils"
|
||||
"chatplus/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
const CodeStorePrefix = "/verify/codes/"
|
||||
|
||||
type SmsHandler struct {
|
||||
BaseHandler
|
||||
leveldb *store.LevelDB
|
||||
redis *redis.Client
|
||||
sms *service.AliYunSmsService
|
||||
captcha *service.CaptchaService
|
||||
}
|
||||
|
||||
func NewSmsHandler(app *core.AppServer, db *store.LevelDB, sms *service.AliYunSmsService, captcha *service.CaptchaService) *SmsHandler {
|
||||
handler := &SmsHandler{leveldb: db, sms: sms, captcha: captcha}
|
||||
func NewSmsHandler(app *core.AppServer, client *redis.Client, sms *service.AliYunSmsService, captcha *service.CaptchaService) *SmsHandler {
|
||||
handler := &SmsHandler{redis: client, sms: sms, captcha: captcha}
|
||||
handler.App = app
|
||||
return handler
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 存储验证码,等待后面注册验证
|
||||
err = h.leveldb.Put(CodeStorePrefix+data.Mobile, code)
|
||||
_, err = h.redis.Set(c, CodeStorePrefix+data.Mobile, code, 0).Result()
|
||||
if err != nil {
|
||||
resp.ERROR(c, "验证码保存失败")
|
||||
return
|
||||
@@ -58,13 +58,3 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
type statusVo struct {
|
||||
EnabledMsgService bool `json:"enabled_msg_service"`
|
||||
EnabledRegister bool `json:"enabled_register"`
|
||||
}
|
||||
|
||||
// Status check if the message service is enabled
|
||||
func (h *SmsHandler) Status(c *gin.Context) {
|
||||
resp.SUCCESS(c, statusVo{EnabledMsgService: h.App.SysConfig.EnabledMsg, EnabledRegister: h.App.SysConfig.EnabledRegister})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package handler
|
||||
import (
|
||||
"chatplus/core"
|
||||
"chatplus/core/types"
|
||||
"chatplus/store"
|
||||
"chatplus/store/model"
|
||||
"chatplus/store/vo"
|
||||
"chatplus/utils"
|
||||
@@ -23,7 +22,6 @@ type UserHandler struct {
|
||||
BaseHandler
|
||||
db *gorm.DB
|
||||
searcher *xdb.Searcher
|
||||
leveldb *store.LevelDB
|
||||
redis *redis.Client
|
||||
}
|
||||
|
||||
@@ -31,9 +29,8 @@ func NewUserHandler(
|
||||
app *core.AppServer,
|
||||
db *gorm.DB,
|
||||
searcher *xdb.Searcher,
|
||||
levelDB *store.LevelDB,
|
||||
client *redis.Client) *UserHandler {
|
||||
handler := &UserHandler{db: db, searcher: searcher, leveldb: levelDB, redis: client}
|
||||
handler := &UserHandler{db: db, searcher: searcher, redis: client}
|
||||
handler.App = app
|
||||
return handler
|
||||
}
|
||||
@@ -42,9 +39,10 @@ func NewUserHandler(
|
||||
func (h *UserHandler) Register(c *gin.Context) {
|
||||
// parameters process
|
||||
var data struct {
|
||||
Mobile string `json:"mobile"`
|
||||
Password string `json:"password"`
|
||||
Code int `json:"code"`
|
||||
Mobile string `json:"mobile"`
|
||||
Password string `json:"password"`
|
||||
Code string `json:"code"`
|
||||
InviteCode string `json:"invite_code"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
@@ -64,14 +62,28 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
// 检查验证码
|
||||
key := CodeStorePrefix + data.Mobile
|
||||
if h.App.SysConfig.EnabledMsg {
|
||||
var code int
|
||||
err := h.leveldb.Get(key, &code)
|
||||
code, err := h.redis.Get(c, key).Result()
|
||||
if err != nil || code != data.Code {
|
||||
resp.ERROR(c, "短信验证码错误")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 验证邀请码
|
||||
inviteCode := model.InviteCode{}
|
||||
if data.InviteCode == "" {
|
||||
if h.App.SysConfig.ForceInvite {
|
||||
resp.ERROR(c, "当前系统设定必须使用邀请码才能注册")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
res := h.db.Where("code = ?", data.InviteCode).First(&inviteCode)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "无效的邀请码")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// check if the username is exists
|
||||
var item model.User
|
||||
res := h.db.Where("mobile = ?", data.Mobile).First(&item)
|
||||
@@ -82,12 +94,13 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
|
||||
salt := utils.RandString(8)
|
||||
user := model.User{
|
||||
Password: utils.GenPassword(data.Password, salt),
|
||||
Avatar: "/images/avatar/user.png",
|
||||
Salt: salt,
|
||||
Status: true,
|
||||
Mobile: data.Mobile,
|
||||
ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
|
||||
Password: utils.GenPassword(data.Password, salt),
|
||||
Avatar: "/images/avatar/user.png",
|
||||
Salt: salt,
|
||||
Status: true,
|
||||
Mobile: data.Mobile,
|
||||
ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
|
||||
ChatModels: utils.JsonEncode(h.App.SysConfig.DefaultModels), // 默认开通的模型
|
||||
ChatConfig: utils.JsonEncode(types.UserChatConfig{
|
||||
ApiKeys: map[types.Platform]string{
|
||||
types.OpenAI: "",
|
||||
@@ -95,7 +108,7 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
types.ChatGLM: "",
|
||||
},
|
||||
}),
|
||||
Calls: h.App.SysConfig.UserInitCalls,
|
||||
Calls: h.App.SysConfig.InitChatCalls,
|
||||
ImgCalls: h.App.SysConfig.InitImgCalls,
|
||||
}
|
||||
res = h.db.Create(&user)
|
||||
@@ -105,8 +118,28 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
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.InviteChatCalls > 0 {
|
||||
h.db.Model(&model.User{}).Where("id = ?", inviteCode.UserId).UpdateColumn("calls", gorm.Expr("calls + ?", h.App.SysConfig.InviteChatCalls))
|
||||
}
|
||||
if h.App.SysConfig.InviteImgCalls > 0 {
|
||||
h.db.Model(&model.User{}).Where("id = ?", inviteCode.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", h.App.SysConfig.InviteImgCalls))
|
||||
}
|
||||
|
||||
// 添加邀请记录
|
||||
h.db.Create(&model.InviteLog{
|
||||
InviterId: inviteCode.UserId,
|
||||
UserId: user.Id,
|
||||
Username: user.Mobile,
|
||||
InviteCode: inviteCode.Code,
|
||||
Reward: utils.JsonEncode(types.InviteReward{ChatCalls: h.App.SysConfig.InviteChatCalls, ImgCalls: h.App.SysConfig.InviteImgCalls}),
|
||||
})
|
||||
}
|
||||
if h.App.SysConfig.EnabledMsg {
|
||||
_ = h.leveldb.Delete(key) // 注册成功,删除短信验证码
|
||||
_ = h.redis.Del(c, key) // 注册成功,删除短信验证码
|
||||
}
|
||||
|
||||
// 自动登录创建 token
|
||||
@@ -229,6 +262,9 @@ type userProfile struct {
|
||||
Calls int `json:"calls"`
|
||||
ImgCalls int `json:"img_calls"`
|
||||
TotalTokens int64 `json:"total_tokens"`
|
||||
Tokens int64 `json:"tokens"`
|
||||
ExpiredTime int64 `json:"expired_time"`
|
||||
Vip bool `json:"vip"`
|
||||
}
|
||||
|
||||
func (h *UserHandler) Profile(c *gin.Context) {
|
||||
@@ -275,8 +311,8 @@ func (h *UserHandler) ProfileUpdate(c *gin.Context) {
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Password 更新密码
|
||||
func (h *UserHandler) Password(c *gin.Context) {
|
||||
// UpdatePass 更新密码
|
||||
func (h *UserHandler) UpdatePass(c *gin.Context) {
|
||||
var data struct {
|
||||
OldPass string `json:"old_pass"`
|
||||
Password string `json:"password"`
|
||||
@@ -315,17 +351,65 @@ func (h *UserHandler) Password(c *gin.Context) {
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// ResetPass 重置密码
|
||||
func (h *UserHandler) ResetPass(c *gin.Context) {
|
||||
var data struct {
|
||||
Mobile string
|
||||
Code string // 验证码
|
||||
Password string // 新密码
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var user model.User
|
||||
res := h.db.Where("mobile", data.Mobile).First(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "用户不存在!")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查验证码
|
||||
key := CodeStorePrefix + data.Mobile
|
||||
if h.App.SysConfig.EnabledMsg {
|
||||
code, err := h.redis.Get(c, key).Result()
|
||||
if err != nil || code != data.Code {
|
||||
resp.ERROR(c, "短信验证码错误")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
password := utils.GenPassword(data.Password, user.Salt)
|
||||
user.Password = password
|
||||
res = h.db.Updates(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c)
|
||||
} else {
|
||||
h.redis.Del(c, key)
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
}
|
||||
|
||||
// BindMobile 绑定手机号
|
||||
func (h *UserHandler) BindMobile(c *gin.Context) {
|
||||
var data struct {
|
||||
Mobile string `json:"mobile"`
|
||||
Code int `json:"code"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查验证码
|
||||
key := CodeStorePrefix + data.Mobile
|
||||
code, err := h.redis.Get(c, key).Result()
|
||||
if err != nil || code != data.Code {
|
||||
resp.ERROR(c, "短信验证码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查手机号是否被其他账号绑定
|
||||
var item model.User
|
||||
res := h.db.Where("mobile = ?", data.Mobile).First(&item)
|
||||
@@ -334,15 +418,6 @@ func (h *UserHandler) BindMobile(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查验证码
|
||||
key := CodeStorePrefix + data.Mobile
|
||||
var code int
|
||||
err := h.leveldb.Get(key, &code)
|
||||
if err != nil || code != data.Code {
|
||||
resp.ERROR(c, "短信验证码错误")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := utils.GetLoginUser(c, h.db)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
@@ -355,6 +430,6 @@ func (h *UserHandler) BindMobile(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
_ = h.leveldb.Delete(key) // 删除短信验证码
|
||||
_ = h.redis.Del(c, key) // 删除短信验证码
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
66
api/main.go
@@ -11,6 +11,7 @@ import (
|
||||
"chatplus/service/fun"
|
||||
"chatplus/service/mj"
|
||||
"chatplus/service/oss"
|
||||
"chatplus/service/payment"
|
||||
"chatplus/service/sd"
|
||||
"chatplus/service/wx"
|
||||
"chatplus/store"
|
||||
@@ -32,7 +33,7 @@ import (
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
//go:embed res/ip2region.xdb
|
||||
//go:embed res
|
||||
var xdbFS embed.FS
|
||||
|
||||
// AppLifecycle 应用程序生命周期
|
||||
@@ -93,9 +94,12 @@ func main() {
|
||||
// 初始化数据库
|
||||
fx.Provide(store.NewGormConfig),
|
||||
fx.Provide(store.NewMysql),
|
||||
fx.Provide(store.NewLevelDB),
|
||||
fx.Provide(store.NewRedisClient),
|
||||
|
||||
fx.Provide(func() embed.FS {
|
||||
return xdbFS
|
||||
}),
|
||||
|
||||
// 创建 Ip2Region 查询对象
|
||||
fx.Provide(func() (*xdb.Searcher, error) {
|
||||
file, err := xdbFS.Open("res/ip2region.xdb")
|
||||
@@ -124,6 +128,9 @@ func main() {
|
||||
fx.Provide(handler.NewMidJourneyHandler),
|
||||
fx.Provide(handler.NewChatModelHandler),
|
||||
fx.Provide(handler.NewSdJobHandler),
|
||||
fx.Provide(handler.NewPaymentHandler),
|
||||
fx.Provide(handler.NewOrderHandler),
|
||||
fx.Provide(handler.NewProductHandler),
|
||||
|
||||
fx.Provide(admin.NewConfigHandler),
|
||||
fx.Provide(admin.NewAdminHandler),
|
||||
@@ -133,6 +140,8 @@ func main() {
|
||||
fx.Provide(admin.NewRewardHandler),
|
||||
fx.Provide(admin.NewDashboardHandler),
|
||||
fx.Provide(admin.NewChatModelHandler),
|
||||
fx.Provide(admin.NewProductHandler),
|
||||
fx.Provide(admin.NewOrderHandler),
|
||||
|
||||
// 创建服务
|
||||
fx.Provide(service.NewAliYunSmsService),
|
||||
@@ -181,6 +190,18 @@ func main() {
|
||||
}()
|
||||
}
|
||||
}),
|
||||
|
||||
fx.Provide(payment.NewAlipayService),
|
||||
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.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) {
|
||||
group := s.Engine.Group("/api/role/")
|
||||
@@ -195,8 +216,9 @@ func main() {
|
||||
group.GET("session", h.Session)
|
||||
group.GET("profile", h.Profile)
|
||||
group.POST("profile/update", h.ProfileUpdate)
|
||||
group.POST("password", h.Password)
|
||||
group.POST("password", h.UpdatePass)
|
||||
group.POST("bind/mobile", h.BindMobile)
|
||||
group.POST("resetPass", h.ResetPass)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *chatimpl.ChatHandler) {
|
||||
group := s.Engine.Group("/api/chat/")
|
||||
@@ -215,7 +237,6 @@ func main() {
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) {
|
||||
group := s.Engine.Group("/api/sms/")
|
||||
group.GET("status", h.Status)
|
||||
group.POST("code", h.SendCode)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) {
|
||||
@@ -253,7 +274,6 @@ func main() {
|
||||
group.POST("login", h.Login)
|
||||
group.GET("logout", h.Logout)
|
||||
group.GET("session", h.Session)
|
||||
group.GET("migrate", h.Migrate)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ApiKeyHandler) {
|
||||
group := s.Engine.Group("/api/admin/apikey/")
|
||||
@@ -292,10 +312,46 @@ func main() {
|
||||
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)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.PaymentHandler) {
|
||||
group := s.Engine.Group("/api/payment/")
|
||||
group.GET("alipay", h.Alipay)
|
||||
group.POST("query", h.OrderQuery)
|
||||
group.POST("alipay/qrcode", h.AlipayQrcode)
|
||||
group.POST("alipay/notify", h.AlipayNotify)
|
||||
}),
|
||||
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)
|
||||
}),
|
||||
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)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.OrderHandler) {
|
||||
group := s.Engine.Group("/api/order/")
|
||||
group.POST("list", h.List)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ProductHandler) {
|
||||
group := s.Engine.Group("/api/product/")
|
||||
group.GET("list", h.List)
|
||||
}),
|
||||
|
||||
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.POST("list", h.List)
|
||||
group.GET("hits", h.Hits)
|
||||
}),
|
||||
|
||||
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
|
||||
err := s.Run(db)
|
||||
|
||||
BIN
api/res/img/alipay.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
@@ -1,37 +1,38 @@
|
||||
{
|
||||
"data": [
|
||||
"task(38194gitxp745ha)",
|
||||
"A beautiful Chinese girl riding on a tiger",
|
||||
"task(m1wpaa4v60zedj8)",
|
||||
"a cute cat",
|
||||
"",
|
||||
[],
|
||||
20,
|
||||
"Euler a",
|
||||
false,
|
||||
false,
|
||||
"DPM++ 2M Karras",
|
||||
1,
|
||||
1,
|
||||
7,
|
||||
-1,
|
||||
-1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
512,
|
||||
512,
|
||||
384,
|
||||
true,
|
||||
0.7,
|
||||
2,
|
||||
"ESRGAN_4x",
|
||||
10,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
"Use same checkpoint",
|
||||
"Use same sampler",
|
||||
"",
|
||||
"",
|
||||
[],
|
||||
"None",
|
||||
null,
|
||||
false,
|
||||
"",
|
||||
0.8,
|
||||
-1,
|
||||
false,
|
||||
-1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
"positive",
|
||||
@@ -54,45 +55,13 @@
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
"Not set",
|
||||
true,
|
||||
true,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
1.3,
|
||||
"Not set",
|
||||
"Not set",
|
||||
1.3,
|
||||
"Not set",
|
||||
1.3,
|
||||
"Not set",
|
||||
1.3,
|
||||
1.3,
|
||||
"Not set",
|
||||
1.3,
|
||||
"Not set",
|
||||
1.3,
|
||||
"Not set",
|
||||
1.3,
|
||||
"Not set",
|
||||
1.3,
|
||||
"Not set",
|
||||
1.3,
|
||||
"Not set",
|
||||
false,
|
||||
"None",
|
||||
null,
|
||||
false,
|
||||
50,
|
||||
[],
|
||||
"",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"event_data": null,
|
||||
"fn_index": 232,
|
||||
"session_hash": "3xedmn4nuzq"
|
||||
"fn_index": 96,
|
||||
"session_hash": "kmb0ojjfhdj"
|
||||
}
|
||||
@@ -2,18 +2,16 @@ package service
|
||||
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
"chatplus/store"
|
||||
"fmt"
|
||||
"github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi"
|
||||
)
|
||||
|
||||
type AliYunSmsService struct {
|
||||
config *types.AliYunSmsConfig
|
||||
db *store.LevelDB
|
||||
client *dysmsapi.Client
|
||||
}
|
||||
|
||||
func NewAliYunSmsService(config *types.AppConfig, db *store.LevelDB) (*AliYunSmsService, error) {
|
||||
func NewAliYunSmsService(config *types.AppConfig) (*AliYunSmsService, error) {
|
||||
// 创建阿里云短信客户端
|
||||
client, err := dysmsapi.NewClientWithAccessKey(
|
||||
"cn-hangzhou",
|
||||
@@ -25,7 +23,6 @@ func NewAliYunSmsService(config *types.AppConfig, db *store.LevelDB) (*AliYunSms
|
||||
|
||||
return &AliYunSmsService{
|
||||
config: &config.SmsConfig,
|
||||
db: db,
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
92
api/service/fun/func_img.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package fun
|
||||
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
"chatplus/service/oss"
|
||||
"chatplus/store/model"
|
||||
"chatplus/utils"
|
||||
"fmt"
|
||||
"github.com/imroc/req/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AI 绘画函数
|
||||
|
||||
type FuncImage struct {
|
||||
name string
|
||||
apiURL string
|
||||
db *gorm.DB
|
||||
uploadManager *oss.UploaderManager
|
||||
proxyURL string
|
||||
}
|
||||
|
||||
func NewImageFunc(db *gorm.DB, manager *oss.UploaderManager, config *types.AppConfig) FuncImage {
|
||||
return FuncImage{
|
||||
db: db,
|
||||
name: "DALL-E3 绘画",
|
||||
uploadManager: manager,
|
||||
proxyURL: config.ProxyURL,
|
||||
apiURL: "https://api.openai.com/v1/images/generations",
|
||||
}
|
||||
}
|
||||
|
||||
type imgReq struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n"`
|
||||
Size string `json:"size"`
|
||||
}
|
||||
|
||||
type imgRes struct {
|
||||
Created int64 `json:"created"`
|
||||
Data []struct {
|
||||
RevisedPrompt string `json:"revised_prompt"`
|
||||
Url string `json:"url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type ErrRes struct {
|
||||
Error struct {
|
||||
Code interface{} `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Param interface{} `json:"param"`
|
||||
Type string `json:"type"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
func (f FuncImage) Invoke(params map[string]interface{}) (string, error) {
|
||||
logger.Infof("绘画参数:%+v", params)
|
||||
prompt := utils.InterfaceToString(params["prompt"])
|
||||
// 获取绘图 API KEY
|
||||
var apiKey model.ApiKey
|
||||
f.db.Where("platform = ? AND type = ?", types.OpenAI, "img").Order("last_used_at ASC").First(&apiKey)
|
||||
var res imgRes
|
||||
var errRes ErrRes
|
||||
r, err := req.C().SetProxyURL(f.proxyURL).R().SetHeader("Content-Type", "application/json").
|
||||
SetHeader("Authorization", "Bearer "+apiKey.Value).
|
||||
SetBody(imgReq{
|
||||
Model: "dall-e-3",
|
||||
Prompt: prompt,
|
||||
N: 1,
|
||||
Size: "1024x1024",
|
||||
}).
|
||||
SetErrorResult(&errRes).
|
||||
SetSuccessResult(&res).Post(f.apiURL)
|
||||
if err != nil || r.IsErrorState() {
|
||||
return "", fmt.Errorf("error with http request: %v%v%s", err, r.Err, errRes.Error.Message)
|
||||
}
|
||||
// 存储图片
|
||||
imgURL, err := f.uploadManager.GetUploadHandler().PutImg(res.Data[0].Url, false)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("下载图片失败: %s", err.Error())
|
||||
}
|
||||
|
||||
logger.Info(imgURL)
|
||||
return fmt.Sprintf("\n\n\n", imgURL), nil
|
||||
}
|
||||
|
||||
func (f FuncImage) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
var _ Function = &FuncImage{}
|
||||
@@ -1,42 +0,0 @@
|
||||
package fun
|
||||
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
"chatplus/service/mj"
|
||||
"chatplus/utils"
|
||||
)
|
||||
|
||||
// AI 绘画函数
|
||||
|
||||
type FuncMidJourney struct {
|
||||
name string
|
||||
service *mj.Service
|
||||
}
|
||||
|
||||
func NewMidJourneyFunc(mjService *mj.Service) FuncMidJourney {
|
||||
return FuncMidJourney{
|
||||
name: "MidJourney AI 绘画",
|
||||
service: mjService}
|
||||
}
|
||||
|
||||
func (f FuncMidJourney) Invoke(params map[string]interface{}) (string, error) {
|
||||
logger.Infof("MJ 绘画参数:%+v", params)
|
||||
prompt := utils.InterfaceToString(params["prompt"])
|
||||
f.service.PushTask(types.MjTask{
|
||||
SessionId: utils.InterfaceToString(params["session_id"]),
|
||||
Src: types.TaskSrcChat,
|
||||
Type: types.TaskImage,
|
||||
Prompt: prompt,
|
||||
UserId: utils.IntValue(utils.InterfaceToString(params["user_id"]), 0),
|
||||
RoleId: utils.IntValue(utils.InterfaceToString(params["role_id"]), 0),
|
||||
Icon: utils.InterfaceToString(params["icon"]),
|
||||
ChatId: utils.InterfaceToString(params["chat_id"]),
|
||||
})
|
||||
return prompt, nil
|
||||
}
|
||||
|
||||
func (f FuncMidJourney) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
var _ Function = &FuncMidJourney{}
|
||||
@@ -3,7 +3,8 @@ package fun
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
logger2 "chatplus/logger"
|
||||
"chatplus/service/mj"
|
||||
"chatplus/service/oss"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Function interface {
|
||||
@@ -29,11 +30,11 @@ type dataItem struct {
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
func NewFunctions(config *types.AppConfig, mjService *mj.Service) map[string]Function {
|
||||
func NewFunctions(config *types.AppConfig, db *gorm.DB, manager *oss.UploaderManager) map[string]Function {
|
||||
return map[string]Function{
|
||||
types.FuncZaoBao: NewZaoBao(config.ApiConfig),
|
||||
types.FuncWeibo: NewWeiboHot(config.ApiConfig),
|
||||
types.FuncHeadLine: NewHeadLines(config.ApiConfig),
|
||||
types.FuncMidJourney: NewMidJourneyFunc(mjService),
|
||||
types.FuncZaoBao: NewZaoBao(config.ApiConfig),
|
||||
types.FuncWeibo: NewWeiboHot(config.ApiConfig),
|
||||
types.FuncHeadLine: NewHeadLines(config.ApiConfig),
|
||||
types.FuncImage: NewImageFunc(db, manager, config),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func (c *Client) Imagine(prompt string) error {
|
||||
ChannelID: c.config.ChanelId,
|
||||
SessionID: SessionID,
|
||||
Data: map[string]any{
|
||||
"version": "1118961510123847772",
|
||||
"version": "1166847114203123795",
|
||||
"id": "938956540159881230",
|
||||
"name": "imagine",
|
||||
"type": "1",
|
||||
|
||||
@@ -72,11 +72,19 @@ func (s *Service) Run() {
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error("绘画任务执行失败:", err)
|
||||
if task.RetryCount <= 5 {
|
||||
s.taskQueue.RPush(task)
|
||||
// 删除任务
|
||||
s.db.Delete(&model.MidJourneyJob{Id: uint(task.Id)})
|
||||
// 推送任务到前端
|
||||
client := s.Clients.Get(task.SessionId)
|
||||
if client != nil {
|
||||
utils.ReplyChunkMessage(client, vo.MidJourneyJob{
|
||||
Type: task.Type.String(),
|
||||
UserId: task.UserId,
|
||||
MessageId: task.MessageId,
|
||||
Progress: -1,
|
||||
Prompt: task.Prompt,
|
||||
})
|
||||
}
|
||||
task.RetryCount += 1
|
||||
time.Sleep(time.Second * 3)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ func (s AliYunOss) PutFile(ctx *gin.Context, name string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("https://%s.%s/%s", s.config.Bucket, s.config.Endpoint, objectKey), nil
|
||||
return fmt.Sprintf("https://%s.%s/%s", s.config.Bucket, s.config.Domain, objectKey), nil
|
||||
}
|
||||
|
||||
func (s AliYunOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
||||
@@ -86,7 +86,7 @@ func (s AliYunOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("https://%s.%s/%s", s.config.Bucket, s.config.Endpoint, objectKey), nil
|
||||
return fmt.Sprintf("https://%s.%s/%s", s.config.Bucket, s.config.Domain, objectKey), nil
|
||||
}
|
||||
|
||||
func (s AliYunOss) Delete(fileURL string) error {
|
||||
|
||||
@@ -15,12 +15,13 @@ import (
|
||||
)
|
||||
|
||||
type QinNiuOss struct {
|
||||
config *types.QiNiuOssConfig
|
||||
token string
|
||||
uploader *storage.FormUploader
|
||||
manager *storage.BucketManager
|
||||
proxyURL string
|
||||
dir string
|
||||
config *types.QiNiuOssConfig
|
||||
mac *qbox.Mac
|
||||
putPolicy storage.PutPolicy
|
||||
uploader *storage.FormUploader
|
||||
manager *storage.BucketManager
|
||||
proxyURL string
|
||||
dir string
|
||||
}
|
||||
|
||||
func NewQiNiuOss(appConfig *types.AppConfig) QinNiuOss {
|
||||
@@ -38,12 +39,13 @@ func NewQiNiuOss(appConfig *types.AppConfig) QinNiuOss {
|
||||
Scope: config.Bucket,
|
||||
}
|
||||
return QinNiuOss{
|
||||
config: config,
|
||||
token: putPolicy.UploadToken(mac),
|
||||
uploader: formUploader,
|
||||
manager: storage.NewBucketManager(mac, &storeConfig),
|
||||
proxyURL: appConfig.ProxyURL,
|
||||
dir: "chatgpt-plus",
|
||||
config: config,
|
||||
mac: mac,
|
||||
putPolicy: putPolicy,
|
||||
uploader: formUploader,
|
||||
manager: storage.NewBucketManager(mac, &storeConfig),
|
||||
proxyURL: appConfig.ProxyURL,
|
||||
dir: "chatgpt-plus",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +67,7 @@ func (s QinNiuOss) PutFile(ctx *gin.Context, name string) (string, error) {
|
||||
// 上传文件
|
||||
ret := storage.PutRet{}
|
||||
extra := storage.PutExtra{}
|
||||
err = s.uploader.Put(ctx, &ret, s.token, key, src, file.Size, &extra)
|
||||
err = s.uploader.Put(ctx, &ret, s.putPolicy.UploadToken(s.mac), key, src, file.Size, &extra)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -93,7 +95,7 @@ func (s QinNiuOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
||||
ret := storage.PutRet{}
|
||||
extra := storage.PutExtra{}
|
||||
// 上传文件字节数据
|
||||
err = s.uploader.Put(context.Background(), &ret, s.token, key, bytes.NewReader(imageData), int64(len(imageData)), &extra)
|
||||
err = s.uploader.Put(context.Background(), &ret, s.putPolicy.UploadToken(s.mac), key, bytes.NewReader(imageData), int64(len(imageData)), &extra)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
142
api/service/payment/alipay_service.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
logger2 "chatplus/logger"
|
||||
"fmt"
|
||||
"github.com/smartwalle/alipay/v3"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
)
|
||||
|
||||
type AlipayService struct {
|
||||
config *types.AlipayConfig
|
||||
client *alipay.Client
|
||||
}
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
func NewAlipayService(appConfig *types.AppConfig) (*AlipayService, error) {
|
||||
config := appConfig.AlipayConfig
|
||||
if !config.Enabled {
|
||||
logger.Info("Disabled Alipay service")
|
||||
return nil, nil
|
||||
}
|
||||
priKey, err := readKey(config.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error with read App Private key: %v", err)
|
||||
}
|
||||
|
||||
xClient, err := alipay.New(config.AppId, priKey, !config.SandBox)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error with initialize alipay service: %v", err)
|
||||
}
|
||||
|
||||
if err = xClient.LoadAppCertPublicKeyFromFile(config.PublicKey); err != nil {
|
||||
return nil, fmt.Errorf("error with loading App PublicKey: %v", err)
|
||||
}
|
||||
if err = xClient.LoadAliPayRootCertFromFile(config.RootCert); err != nil {
|
||||
return nil, fmt.Errorf("error with loading alipay RootCert: %v", err)
|
||||
}
|
||||
if err = xClient.LoadAlipayCertPublicKeyFromFile(config.AlipayPublicKey); err != nil {
|
||||
return nil, fmt.Errorf("error with loading Alipay PublicKey: %v", err)
|
||||
}
|
||||
|
||||
return &AlipayService{config: &config, client: xClient}, nil
|
||||
}
|
||||
|
||||
func (s *AlipayService) PayUrlMobile(outTradeNo string, notifyURL string, returnURL string, Amount string, subject string) (string, error) {
|
||||
var p = alipay.TradeWapPay{}
|
||||
p.NotifyURL = notifyURL
|
||||
p.ReturnURL = returnURL
|
||||
p.Subject = subject
|
||||
p.OutTradeNo = outTradeNo
|
||||
p.TotalAmount = Amount
|
||||
p.ProductCode = "QUICK_WAP_WAY"
|
||||
res, err := s.client.TradeWapPay(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return res.String(), err
|
||||
}
|
||||
|
||||
func (s *AlipayService) PayUrlPc(outTradeNo string, notifyURL string, returnURL string, amount string, subject string) (string, error) {
|
||||
var p = alipay.TradePagePay{}
|
||||
p.NotifyURL = notifyURL
|
||||
p.ReturnURL = returnURL
|
||||
p.Subject = subject
|
||||
p.OutTradeNo = outTradeNo
|
||||
p.TotalAmount = amount
|
||||
p.ProductCode = "FAST_INSTANT_TRADE_PAY"
|
||||
res, err := s.client.TradePagePay(p)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return res.String(), err
|
||||
}
|
||||
|
||||
// TradeVerify 交易验证
|
||||
func (s *AlipayService) TradeVerify(reqForm url.Values) NotifyVo {
|
||||
err := s.client.VerifySign(reqForm)
|
||||
if err != nil {
|
||||
log.Println("异步通知验证签名发生错误", err)
|
||||
return NotifyVo{
|
||||
Status: 0,
|
||||
Message: "异步通知验证签名发生错误",
|
||||
}
|
||||
}
|
||||
|
||||
return s.TradeQuery(reqForm.Get("out_trade_no"))
|
||||
}
|
||||
|
||||
func (s *AlipayService) TradeQuery(outTradeNo string) NotifyVo {
|
||||
var p = alipay.TradeQuery{}
|
||||
p.OutTradeNo = outTradeNo
|
||||
rsp, err := s.client.TradeQuery(p)
|
||||
if err != nil {
|
||||
return NotifyVo{
|
||||
Status: 0,
|
||||
Message: "异步查询验证订单信息发生错误" + outTradeNo + err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
if rsp.IsSuccess() == true && rsp.TradeStatus == "TRADE_SUCCESS" {
|
||||
return NotifyVo{
|
||||
Status: 1,
|
||||
OutTradeNo: rsp.OutTradeNo,
|
||||
TradeNo: rsp.TradeNo,
|
||||
Amount: rsp.TotalAmount,
|
||||
Subject: rsp.Subject,
|
||||
Message: "OK",
|
||||
}
|
||||
} else {
|
||||
return NotifyVo{
|
||||
Status: 0,
|
||||
Message: "异步查询验证订单信息发生错误" + outTradeNo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readKey(filename string) (string, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
type NotifyVo struct {
|
||||
Status int
|
||||
OutTradeNo string
|
||||
TradeNo string
|
||||
Amount string
|
||||
Message string
|
||||
Subject string
|
||||
}
|
||||
|
||||
func (v NotifyVo) Success() bool {
|
||||
return v.Status == 1
|
||||
}
|
||||
@@ -81,7 +81,7 @@ func (s *Service) Run() {
|
||||
|
||||
// PushTask 推送任务到队列
|
||||
func (s *Service) PushTask(task types.SdTask) {
|
||||
logger.Infof("add a new MidJourney Task: %+v", task)
|
||||
logger.Infof("add a new Stable Diffusion Task: %+v", task)
|
||||
s.taskQueue.RPush(task)
|
||||
}
|
||||
|
||||
@@ -105,7 +105,8 @@ func (s *Service) Txt2Img(task types.SdTask) error {
|
||||
data[ParamKeys["negative_prompt"]] = params.NegativePrompt
|
||||
data[ParamKeys["steps"]] = params.Steps
|
||||
data[ParamKeys["sampler"]] = params.Sampler
|
||||
data[ParamKeys["face_fix"]] = params.FaceFix
|
||||
// @fix bug: 有些 stable diffusion 没有面部修复功能
|
||||
//data[ParamKeys["face_fix"]] = params.FaceFix
|
||||
data[ParamKeys["cfg_scale"]] = params.CfgScale
|
||||
data[ParamKeys["seed"]] = params.Seed
|
||||
data[ParamKeys["height"]] = params.Height
|
||||
@@ -176,7 +177,8 @@ func (s *Service) runTask(taskInfo TaskInfo, client *req.Client) {
|
||||
var info map[string]any
|
||||
err = utils.JsonDecode(utils.InterfaceToString(res.Data[1]), &info)
|
||||
if err != nil {
|
||||
cbReq.Message = err.Error()
|
||||
logger.Error(res.Data)
|
||||
cbReq.Message = "error with decode image url:" + err.Error()
|
||||
cbReq.Success = false
|
||||
result <- cbReq
|
||||
return
|
||||
@@ -229,6 +231,7 @@ func (s *Service) runTask(taskInfo TaskInfo, client *req.Client) {
|
||||
|
||||
cbReq.ImageData = progressRes.LivePreview
|
||||
cbReq.Progress = int(progressRes.Progress * 100)
|
||||
logger.Debug(cbReq)
|
||||
s.callback(cbReq)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
@@ -284,7 +287,8 @@ func (s *Service) callback(data CBReq) {
|
||||
if data.Progress < 100 && data.ImageData != "" {
|
||||
jobVo.ImgURL = data.ImageData
|
||||
}
|
||||
|
||||
// 扣减绘图次数
|
||||
s.db.Model(&model.User{}).Where("id = ?", jobVo.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
|
||||
// 推送任务到前端
|
||||
if client != nil {
|
||||
utils.ReplyChunkMessage(client, jobVo)
|
||||
|
||||
@@ -32,14 +32,14 @@ var ParamKeys = map[string]int{
|
||||
"negative_prompt": 2,
|
||||
"steps": 4,
|
||||
"sampler": 5,
|
||||
"face_fix": 6,
|
||||
"cfg_scale": 10,
|
||||
"seed": 11,
|
||||
"height": 17,
|
||||
"width": 18,
|
||||
"hd_fix": 19,
|
||||
"hd_redraw_rate": 20, //高清修复重绘幅度
|
||||
"hd_scale": 21, // 高清修复放大倍数
|
||||
"hd_scale_alg": 22, // 高清修复放大算法
|
||||
"hd_sample_num": 23, // 高清修复采样次数
|
||||
"face_fix": 6, // 面部修复
|
||||
"cfg_scale": 8,
|
||||
"seed": 27,
|
||||
"height": 9,
|
||||
"width": 10,
|
||||
"hd_fix": 11,
|
||||
"hd_redraw_rate": 12, //高清修复重绘幅度
|
||||
"hd_scale": 13, // 高清修复放大倍数
|
||||
"hd_scale_alg": 14, // 高清修复放大算法
|
||||
"hd_sample_num": 15, // 高清修复采样次数
|
||||
}
|
||||
|
||||
56
api/service/snowflake.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Snowflake 雪花算法实现
|
||||
type Snowflake struct {
|
||||
mu sync.Mutex
|
||||
lastTimestamp int64
|
||||
workerID int
|
||||
sequence int
|
||||
}
|
||||
|
||||
func NewSnowflake() *Snowflake {
|
||||
return &Snowflake{
|
||||
lastTimestamp: -1,
|
||||
workerID: 0, // TODO: 增加 WorkID 参数
|
||||
sequence: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Next 生成一个新的唯一ID
|
||||
func (s *Snowflake) Next() (string, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
timestamp := time.Now().UnixNano() / 1000000 // 转换为毫秒
|
||||
if timestamp < s.lastTimestamp {
|
||||
return "", fmt.Errorf("clock moved backwards. Refusing to generate id for %d milliseconds", s.lastTimestamp-timestamp)
|
||||
}
|
||||
|
||||
if timestamp == s.lastTimestamp {
|
||||
s.sequence = (s.sequence + 1) & 4095
|
||||
if s.sequence == 0 {
|
||||
timestamp = s.waitNextMillis()
|
||||
}
|
||||
} else {
|
||||
s.sequence = 0
|
||||
}
|
||||
|
||||
s.lastTimestamp = timestamp
|
||||
id := (timestamp << 22) | (int64(s.workerID) << 10) | int64(s.sequence)
|
||||
now := time.Now()
|
||||
return fmt.Sprintf("%d%02d%02d%d", now.Year(), now.Month(), now.Day(), id), nil
|
||||
}
|
||||
|
||||
func (s *Snowflake) waitNextMillis() int64 {
|
||||
timestamp := time.Now().UnixNano() / 1000000
|
||||
for timestamp <= s.lastTimestamp {
|
||||
timestamp = time.Now().UnixNano() / 1000000
|
||||
}
|
||||
return timestamp
|
||||
}
|
||||
145
api/service/xxl_job_service.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
logger2 "chatplus/logger"
|
||||
"chatplus/store/model"
|
||||
"chatplus/utils"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/xxl-job/xxl-job-executor-go"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
type XXLJobExecutor struct {
|
||||
executor xxl.Executor
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewXXLJobExecutor(config *types.AppConfig, db *gorm.DB) *XXLJobExecutor {
|
||||
if !config.XXLConfig.Enabled {
|
||||
logger.Info("XXL-JOB service is disabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
exec := xxl.NewExecutor(
|
||||
xxl.ServerAddr(config.XXLConfig.ServerAddr),
|
||||
xxl.AccessToken(config.XXLConfig.AccessToken), //请求令牌(默认为空)
|
||||
xxl.ExecutorIp(config.XXLConfig.ExecutorIp), //可自动获取
|
||||
xxl.ExecutorPort(config.XXLConfig.ExecutorPort), //默认9999(非必填)
|
||||
xxl.RegistryKey(config.XXLConfig.RegistryKey), //执行器名称
|
||||
xxl.SetLogger(&customLogger{}), //自定义日志
|
||||
)
|
||||
exec.Init()
|
||||
return &XXLJobExecutor{executor: exec, db: db}
|
||||
}
|
||||
|
||||
func (e *XXLJobExecutor) Run() error {
|
||||
e.executor.RegTask("ClearOrders", e.ClearOrders)
|
||||
e.executor.RegTask("ResetVipCalls", e.ResetVipCalls)
|
||||
return e.executor.Run()
|
||||
}
|
||||
|
||||
// ClearOrders 清理未支付的订单,如果没有抛出异常则表示执行成功
|
||||
func (e *XXLJobExecutor) ClearOrders(cxt context.Context, param *xxl.RunReq) (msg string) {
|
||||
logger.Debug("执行清理未支付订单...")
|
||||
var sysConfig model.Config
|
||||
res := e.db.Where("marker", "system").First(&sysConfig)
|
||||
if res.Error != nil {
|
||||
return "error with get system config: " + res.Error.Error()
|
||||
}
|
||||
|
||||
var config types.SystemConfig
|
||||
err := utils.JsonDecode(sysConfig.Config, &config)
|
||||
if err != nil {
|
||||
return "error with decode system config: " + err.Error()
|
||||
}
|
||||
|
||||
if config.OrderPayTimeout == 0 { // 默认未支付订单的生命周期为 30 分钟
|
||||
config.OrderPayTimeout = 1800
|
||||
}
|
||||
timeout := time.Now().Unix() - int64(config.OrderPayTimeout)
|
||||
start := utils.Stamp2str(timeout)
|
||||
// 这里不是用软删除,而是永久删除订单
|
||||
res = e.db.Unscoped().Where("status != ? AND created_at < ?", types.OrderPaidSuccess, start).Delete(&model.Order{})
|
||||
return fmt.Sprintf("Clear order successfully, affect rows: %d", res.RowsAffected)
|
||||
}
|
||||
|
||||
// ResetVipCalls 清理过期的 VIP 会员
|
||||
func (e *XXLJobExecutor) ResetVipCalls(cxt context.Context, param *xxl.RunReq) (msg string) {
|
||||
logger.Info("开始进行月底账号盘点...")
|
||||
var users []model.User
|
||||
res := e.db.Where("vip = ?", 1).Find(&users)
|
||||
if res.Error != nil {
|
||||
return "No vip users found"
|
||||
}
|
||||
|
||||
var sysConfig model.Config
|
||||
res = e.db.Where("marker", "system").First(&sysConfig)
|
||||
if res.Error != nil {
|
||||
return "error with get system config: " + res.Error.Error()
|
||||
}
|
||||
|
||||
var config types.SystemConfig
|
||||
err := utils.JsonDecode(sysConfig.Config, &config)
|
||||
if err != nil {
|
||||
return "error with decode system config: " + err.Error()
|
||||
}
|
||||
|
||||
// 获取本月月初时间
|
||||
currentTime := time.Now()
|
||||
year, month, _ := currentTime.Date()
|
||||
firstOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, currentTime.Location()).Unix()
|
||||
for _, u := range users {
|
||||
// 账号到期,直接清零
|
||||
if u.ExpiredTime <= currentTime.Unix() {
|
||||
logger.Info("账号过期:", u.Mobile)
|
||||
u.Calls = 0
|
||||
u.Vip = false
|
||||
} else {
|
||||
if u.Calls <= 0 {
|
||||
u.Calls = config.VipMonthCalls
|
||||
} else {
|
||||
// 如果该用户当月有充值点卡,则将点卡中未用完的点数结余到下个月
|
||||
var orders []model.Order
|
||||
e.db.Debug().Where("user_id = ? AND pay_time > ?", u.Id, firstOfMonth).Find(&orders)
|
||||
var calls = 0
|
||||
for _, o := range orders {
|
||||
var remark types.OrderRemark
|
||||
err = utils.JsonDecode(o.Remark, &remark)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if remark.Days > 0 { // 会员续费
|
||||
continue
|
||||
}
|
||||
calls += remark.Calls
|
||||
}
|
||||
if u.Calls > calls { // 本月套餐没有用完
|
||||
u.Calls = calls + config.VipMonthCalls
|
||||
} else {
|
||||
u.Calls = u.Calls + config.VipMonthCalls
|
||||
}
|
||||
logger.Infof("%s 点卡结余:%d", u.Mobile, calls)
|
||||
}
|
||||
}
|
||||
u.Tokens = 0
|
||||
// update user
|
||||
e.db.Updates(&u)
|
||||
}
|
||||
logger.Info("月底盘点完成!")
|
||||
return "success"
|
||||
}
|
||||
|
||||
type customLogger struct{}
|
||||
|
||||
func (l *customLogger) Info(format string, a ...interface{}) {
|
||||
logger.Debugf(format, a...)
|
||||
}
|
||||
|
||||
func (l *customLogger) Error(format string, a ...interface{}) {
|
||||
logger.Errorf(format, a...)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
hello, world!
|
||||
@@ -1,96 +0,0 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"chatplus/store/vo"
|
||||
"encoding/json"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
type LevelDB struct {
|
||||
driver *leveldb.DB
|
||||
}
|
||||
|
||||
func NewLevelDB() (*LevelDB, error) {
|
||||
db, err := leveldb.OpenFile("data/leveldb", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &LevelDB{
|
||||
driver: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (db *LevelDB) Put(key string, value interface{}) error {
|
||||
bytes, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.driver.Put([]byte(key), bytes, nil)
|
||||
}
|
||||
|
||||
func (db *LevelDB) Get(key string, value interface{}) error {
|
||||
bytes, err := db.driver.Get([]byte(key), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return json.Unmarshal(bytes, &value)
|
||||
}
|
||||
|
||||
func (db *LevelDB) Search(prefix string) []string {
|
||||
var items = make([]string, 0)
|
||||
iter := db.driver.NewIterator(util.BytesPrefix([]byte(prefix)), nil)
|
||||
defer iter.Release()
|
||||
|
||||
for iter.Next() {
|
||||
items = append(items, string(iter.Value()))
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func (db *LevelDB) SearchPage(prefix string, page int, pageSize int) *vo.Page {
|
||||
var items = make([]string, 0)
|
||||
iter := db.driver.NewIterator(util.BytesPrefix([]byte(prefix)), nil)
|
||||
defer iter.Release()
|
||||
|
||||
res := &vo.Page{Page: page, PageSize: pageSize}
|
||||
// 计算数据总数和总页数
|
||||
total := 0
|
||||
for iter.Next() {
|
||||
total++
|
||||
}
|
||||
res.TotalPage = (total + pageSize - 1) / pageSize
|
||||
res.Total = int64(total)
|
||||
|
||||
// 计算目标页码的起始和结束位置
|
||||
start := (page - 1) * pageSize
|
||||
if start > total {
|
||||
return nil
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
|
||||
// 跳转到目标页码的起始位置
|
||||
count := 0
|
||||
for iter.Next() {
|
||||
if count >= start {
|
||||
items = append(items, string(iter.Value()))
|
||||
}
|
||||
count++
|
||||
}
|
||||
iter.Release()
|
||||
res.Items = items
|
||||
return res
|
||||
}
|
||||
|
||||
func (db *LevelDB) Delete(key string) error {
|
||||
return db.driver.Delete([]byte(key), nil)
|
||||
}
|
||||
|
||||
// Close release resources
|
||||
func (db *LevelDB) Close() error {
|
||||
return db.driver.Close()
|
||||
}
|
||||
@@ -4,6 +4,7 @@ package model
|
||||
type ApiKey struct {
|
||||
BaseModel
|
||||
Platform string
|
||||
Type string // 用途 chat => 聊天,img => 绘图
|
||||
Value string // API Key 的值
|
||||
LastUsedAt int64 // 最后使用时间
|
||||
}
|
||||
|
||||
@@ -7,4 +7,6 @@ type ChatModel struct {
|
||||
Value string // API Key 的值
|
||||
SortNum int
|
||||
Enabled bool
|
||||
Weight int // 对话权重,每次对话扣减多少次对话额度
|
||||
Open bool // 是否开放模型给所有人使用
|
||||
}
|
||||
|
||||
12
api/store/model/invite_code.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type InviteCode struct {
|
||||
Id uint `gorm:"primarykey;column:id"`
|
||||
UserId uint
|
||||
Code string
|
||||
Hits int // 点击次数
|
||||
RegNum int // 注册人数
|
||||
CreatedAt time.Time
|
||||
}
|
||||
15
api/store/model/invite_log.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type InviteLog struct {
|
||||
Id uint `gorm:"primarykey;column:id"`
|
||||
InviterId uint
|
||||
UserId uint
|
||||
Username string
|
||||
InviteCode string
|
||||
Reward string `gorm:"column:reward_json"` // 邀请奖励
|
||||
CreatedAt time.Time
|
||||
}
|
||||
22
api/store/model/order.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Order 充值订单
|
||||
type Order struct {
|
||||
BaseModel
|
||||
UserId uint
|
||||
ProductId uint
|
||||
Mobile string
|
||||
OrderNo string
|
||||
Subject string
|
||||
Amount float64
|
||||
Status types.OrderStatus
|
||||
Remark string
|
||||
PayTime int64
|
||||
PayWay string // 支付方式
|
||||
DeletedAt gorm.DeletedAt
|
||||
}
|
||||
14
api/store/model/product.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package model
|
||||
|
||||
// Product 充值产品
|
||||
type Product struct {
|
||||
BaseModel
|
||||
Name string
|
||||
Price float64
|
||||
Discount float64
|
||||
Days int
|
||||
Calls int
|
||||
Enabled bool
|
||||
Sales int
|
||||
SortNum int
|
||||
}
|
||||
@@ -11,8 +11,11 @@ type User struct {
|
||||
ImgCalls int // 剩余绘图次数
|
||||
ChatConfig string `gorm:"column:chat_config_json"` // 聊天配置 json
|
||||
ChatRoles string `gorm:"column:chat_roles_json"` // 聊天角色
|
||||
ChatModels string `gorm:"column:chat_models_json"` // AI 模型,不同的用户拥有不同的聊天模型
|
||||
ExpiredTime int64 // 账户到期时间
|
||||
Status bool `gorm:"default:true"` // 当前状态
|
||||
LastLoginAt int64 // 最后登录时间
|
||||
LastLoginIp string // 最后登录 IP
|
||||
Vip bool // 是否 VIP 会员
|
||||
Tokens int
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ func NewGormConfig() *gorm.Config {
|
||||
|
||||
func NewMysql(config *gorm.Config, appConfig *types.AppConfig) (*gorm.DB, error) {
|
||||
db, err := gorm.Open(mysql.Open(appConfig.MysqlDns), config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
@@ -29,8 +32,6 @@ func NewMysql(config *gorm.Config, appConfig *types.AppConfig) (*gorm.DB, error)
|
||||
sqlDB.SetMaxIdleConns(32)
|
||||
sqlDB.SetMaxOpenConns(512)
|
||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package vo
|
||||
type ApiKey struct {
|
||||
BaseVo
|
||||
Platform string `json:"platform"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"` // API Key 的值
|
||||
LastUsedAt int64 `json:"last_used_at"` // 最后使用时间
|
||||
}
|
||||
|
||||
@@ -6,4 +6,7 @@ type ChatModel struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Enabled bool `json:"enabled"`
|
||||
SortNum int `json:"sort_num"`
|
||||
Weight int `json:"weight"`
|
||||
Open bool `json:"open"`
|
||||
}
|
||||
|
||||
10
api/store/vo/invite_code.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package vo
|
||||
|
||||
type InviteCode struct {
|
||||
Id uint `json:"id"`
|
||||
UserId uint `json:"user_id"`
|
||||
Code string `json:"code"`
|
||||
Hits int `json:"hits"`
|
||||
RegNum int `json:"reg_num"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
15
api/store/vo/invite_log.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package vo
|
||||
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
)
|
||||
|
||||
type InviteLog struct {
|
||||
Id uint `json:"id"`
|
||||
InviterId uint `json:"inviter_id"`
|
||||
UserId uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
InviteCode string `json:"invite_code"`
|
||||
Reward types.InviteReward `json:"reward"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
19
api/store/vo/order.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package vo
|
||||
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
)
|
||||
|
||||
type Order struct {
|
||||
BaseVo
|
||||
UserId uint `json:"user_id"`
|
||||
ProductId uint `json:"product_id"`
|
||||
Mobile string `json:"mobile"`
|
||||
OrderNo string `json:"order_no"`
|
||||
Subject string `json:"subject"`
|
||||
Amount float64 `json:"amount"`
|
||||
Status types.OrderStatus `json:"status"`
|
||||
PayTime int64 `json:"pay_time"`
|
||||
PayWay string `json:"pay_way"`
|
||||
Remark types.OrderRemark `json:"remark"`
|
||||
}
|
||||
13
api/store/vo/product.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package vo
|
||||
|
||||
type Product struct {
|
||||
BaseVo
|
||||
Name string `json:"name"`
|
||||
Price float64 `json:"price"`
|
||||
Discount float64 `json:"discount"`
|
||||
Days int `json:"days"`
|
||||
Calls int `json:"calls"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Sales int `json:"sales"`
|
||||
SortNum int `json:"sort_num"`
|
||||
}
|
||||
@@ -12,8 +12,11 @@ type User struct {
|
||||
ImgCalls int `json:"img_calls"`
|
||||
ChatConfig types.UserChatConfig `json:"chat_config"` // 聊天配置
|
||||
ChatRoles []string `json:"chat_roles"` // 聊天角色集合
|
||||
ChatModels []string `json:"chat_models"` // AI模型集合
|
||||
ExpiredTime int64 `json:"expired_time"` // 账户到期时间
|
||||
Status bool `json:"status"` // 当前状态
|
||||
LastLoginAt int64 `json:"last_login_at"` // 最后登录时间
|
||||
LastLoginIp string `json:"last_login_ip"` // 最后登录 IP
|
||||
Vip bool `json:"vip"`
|
||||
Tokens int `json:"token"` // 当月消耗的 fee
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
||||
"github.com/nfnt/resize"
|
||||
"github.com/skip2/go-qrcode"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
||||
)
|
||||
|
||||
// CopyObject 拷贝对象
|
||||
@@ -52,6 +59,8 @@ func CopyObject(src interface{}, dst interface{}) error {
|
||||
value.Set(reflect.ValueOf(""))
|
||||
}
|
||||
}
|
||||
} else if field.Type.Kind() != value.Type().Kind() { // 不同类型的字段过滤掉
|
||||
continue
|
||||
} else { // 简单数据类型的强制类型转换
|
||||
switch value.Kind() {
|
||||
case reflect.Int:
|
||||
@@ -140,13 +149,57 @@ func IntValue(str string, defaultValue int) int {
|
||||
}
|
||||
|
||||
func ForceCovert(src any, dst interface{}) error {
|
||||
bytes, err := json.Marshal(src)
|
||||
b, err := json.Marshal(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(bytes, dst)
|
||||
err = json.Unmarshal(b, dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GenQrcode(text string, size int, logo io.Reader) ([]byte, error) {
|
||||
qr, err := qrcode.New(text, qrcode.Medium)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qr.BackgroundColor = color.White
|
||||
qr.ForegroundColor = color.Black
|
||||
if logo == nil {
|
||||
return qr.PNG(size)
|
||||
}
|
||||
|
||||
// 生成带Logo的二维码图像
|
||||
logoImage, _, err := image.Decode(logo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 缩放 Logo
|
||||
scaledLogo := resize.Resize(uint(size/9), uint(size/9), logoImage, resize.Lanczos3)
|
||||
// 将Logo叠加到二维码图像上
|
||||
qrWithLogo := overlayLogo(qr.Image(size), scaledLogo)
|
||||
|
||||
// 将带Logo的二维码图像以JPEG格式编码为图片数据
|
||||
var buf bytes.Buffer
|
||||
err = jpeg.Encode(&buf, qrWithLogo, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// 叠加Logo到图片上
|
||||
func overlayLogo(qrImage, logoImage image.Image) image.Image {
|
||||
offsetX := (qrImage.Bounds().Dx() - logoImage.Bounds().Dx()) / 2
|
||||
offsetY := (qrImage.Bounds().Dy() - logoImage.Bounds().Dy()) / 2
|
||||
|
||||
combinedImage := image.NewRGBA(qrImage.Bounds())
|
||||
draw.Draw(combinedImage, qrImage.Bounds(), qrImage, image.Point{}, draw.Over)
|
||||
draw.Draw(combinedImage, logoImage.Bounds().Add(image.Pt(offsetX, offsetY)), logoImage, image.Point{}, draw.Over)
|
||||
|
||||
return combinedImage
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ make clean linux
|
||||
cd ../web
|
||||
npm run build
|
||||
|
||||
cd ../docker
|
||||
cd ../build
|
||||
|
||||
# remove docker image if exists
|
||||
docker rmi -f registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:$version
|
||||
@@ -23,4 +23,4 @@ docker build --platform linux/amd64 -t registry.cn-shenzhen.aliyuncs.com/geekmas
|
||||
if [ "$2" = "push" ];then
|
||||
docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:$version
|
||||
docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:$version
|
||||
fi
|
||||
fi
|
||||
@@ -1,5 +1,5 @@
|
||||
# 前端 Vue 项目构建
|
||||
FROM nginx:1.20
|
||||
FROM nginx:1.20.2
|
||||
|
||||
MAINTAINER yangjian<yangjian102621@163.com>
|
||||
|
||||
@@ -8,4 +8,4 @@ COPY ./web/dist /var/www/app/dist
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
EXPOSE 8080
|
||||
EXPOSE 8080
|
||||
1227
database/chatgpt_plus-v3.1.7.sql
Normal file
1548
database/chatgpt_plus-v3.1.8.sql
Normal file
1603
database/chatgpt_plus-v3.1.9.sql
Normal file
9
database/update-v3.1.7.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
-- 增加字段保存用户开通的 AI 模型
|
||||
ALTER TABLE `chatgpt_users` ADD `chat_models_json` TEXT NOT NULL COMMENT 'AI模型 json' AFTER `chat_roles_json`;
|
||||
UPDATE `chatgpt_users` SET chat_models_json = '["completions_pro","eb-instant","general","generalv2","chatglm_pro","chatglm_lite","chatglm_std","gpt-3.5-turbo-16k"]';
|
||||
-- 为每个模型设置对话权重
|
||||
ALTER TABLE `chatgpt_chat_models` ADD `weight` TINYINT(3) NOT NULL COMMENT '对话权重,每次对话扣减多少次对话额度' AFTER `enabled`;
|
||||
UPDATE `chatgpt_chat_models` SET weight = 1;
|
||||
|
||||
-- 更新系统配置,支持文心4.0模型
|
||||
UPDATE `chatgpt_configs` SET config_json = '{"azure":{"api_url":"https://chat-bot-api.openai.azure.com/openai/deployments/{model}/chat/completions?api-version=2023-05-15","max_tokens":1024,"temperature":1},"baidu":{"api_url":"https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/{model}","max_tokens":1024,"temperature":0.95},"chat_gml":{"api_url":"https://open.bigmodel.cn/api/paas/v3/model-api/{model}/sse-invoke","max_tokens":1024,"temperature":0.95},"context_deep":4,"enable_context":true,"enable_history":true,"open_ai":{"api_url":"https://api.openai.com/v1/chat/completions","max_tokens":1024,"temperature":1},"xun_fei":{"api_url":"wss://spark-api.xf-yun.com/{version}/chat","max_tokens":1024,"temperature":0.5}}' WHERE marker ='chat';
|
||||
48
database/update-v3.1.8.sql
Normal file
@@ -0,0 +1,48 @@
|
||||
ALTER TABLE `chatgpt_users` ADD `vip` TINYINT(1) NOT NULL DEFAULT '0' COMMENT '是否会员' AFTER `last_login_at`;
|
||||
ALTER TABLE `chatgpt_users` ADD `tokens` BIGINT NOT NULL DEFAULT '0' COMMENT '当月消耗 tokens' AFTER `total_tokens`;
|
||||
|
||||
CREATE TABLE `chatgpt_orders` (
|
||||
`id` int NOT NULL,
|
||||
`user_id` int NOT NULL COMMENT '用户ID',
|
||||
`product_id` int NOT NULL COMMENT '产品ID',
|
||||
`mobile` char(11) NOT NULL COMMENT '用户手机号',
|
||||
`order_no` varchar(30) NOT NULL COMMENT '订单ID',
|
||||
`subject` varchar(100) NOT NULL COMMENT '订单产品',
|
||||
`amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单金额',
|
||||
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '订单状态(0:待支付,1:已扫码,2:支付失败)',
|
||||
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '备注',
|
||||
`pay_time` int DEFAULT NULL COMMENT '支付时间',
|
||||
`created_at` datetime NOT NULL,
|
||||
`updated_at` datetime NOT NULL,
|
||||
`deleted_at` datetime DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='充值订单表';
|
||||
|
||||
-- 创建索引
|
||||
ALTER TABLE `chatgpt_orders` ADD PRIMARY KEY (`id`), ADD UNIQUE KEY `order_no` (`order_no`);
|
||||
ALTER TABLE `chatgpt_orders` MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=13;
|
||||
|
||||
CREATE TABLE `chatgpt_products` (
|
||||
`id` int NOT NULL,
|
||||
`name` varchar(30) NOT NULL COMMENT '名称',
|
||||
`price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '价格',
|
||||
`discount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '优惠金额',
|
||||
`days` smallint NOT NULL DEFAULT '0' COMMENT '延长天数',
|
||||
`calls` int NOT NULL DEFAULT '0' COMMENT '调用次数',
|
||||
`enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启动',
|
||||
`sales` int NOT NULL DEFAULT '0' COMMENT '销量',
|
||||
`sort_num` tinyint NOT NULL DEFAULT '0' COMMENT '排序',
|
||||
`created_at` datetime NOT NULL,
|
||||
`updated_at` datetime NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='会员套餐表';
|
||||
|
||||
INSERT INTO `chatgpt_products` (`id`, `name`, `price`, `discount`, `days`, `calls`, `enabled`, `sales`, `sort_num`, `created_at`, `updated_at`) VALUES
|
||||
(1, '会员1个月', '1.01', '1.00', 30, 0, 1, 0, 0, '2023-08-28 10:48:57', '2023-08-31 16:24:26'),
|
||||
(2, '会员3个月', '140.00', '30.00', 90, 0, 1, 0, 0, '2023-08-28 10:52:22', '2023-08-31 16:24:31'),
|
||||
(3, '会员6个月', '290.00', '100.00', 180, 0, 1, 0, 0, '2023-08-28 10:53:39', '2023-08-31 16:24:36'),
|
||||
(4, '会员12个月', '580.00', '200.00', 365, 0, 1, 0, 0, '2023-08-28 10:54:15', '2023-08-31 16:24:42'),
|
||||
(5, '100次点卡', '10.03', '10.00', 0, 100, 1, 0, 0, '2023-08-28 10:55:08', '2023-08-31 17:34:43');
|
||||
|
||||
ALTER TABLE `chatgpt_products` ADD PRIMARY KEY (`id`);
|
||||
ALTER TABLE `chatgpt_products` MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
|
||||
|
||||
ALTER TABLE `chatgpt_orders` ADD `pay_way` VARCHAR(20) DEFAULT '0' NOT NULL COMMENT '支付方式' AFTER `pay_time`;
|
||||
1
database/update-v3.1.9.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE `chatgpt_chat_models` ADD `open` TINYINT(1) NOT NULL COMMENT '是否开放模型' AFTER `weight`;
|
||||
28
database/update-v3.2.0.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
CREATE TABLE `chatgpt_invite_logs` (
|
||||
`id` int NOT NULL,
|
||||
`inviter_id` int NOT NULL COMMENT '邀请人ID',
|
||||
`user_id` int NOT NULL COMMENT '注册用户ID',
|
||||
`username` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
|
||||
`invite_code` char(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '邀请码',
|
||||
`calls` smallint NOT NULL COMMENT '奖励对话次数',
|
||||
`created_at` datetime NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='邀请注册日志';
|
||||
ALTER TABLE `chatgpt_invite_logs` ADD PRIMARY KEY (`id`);
|
||||
ALTER TABLE `chatgpt_invite_logs` MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
ALTER TABLE `chatgpt_invite_logs` CHANGE `calls` `reward_json` TEXT NOT NULL COMMENT '邀请奖励';
|
||||
|
||||
|
||||
CREATE TABLE `chatgpt_invite_codes` (
|
||||
`id` int NOT NULL,
|
||||
`user_id` int NOT NULL COMMENT '用户ID',
|
||||
`code` char(8) NOT NULL COMMENT '邀请码',
|
||||
`hits` int NOT NULL COMMENT '点击次数',
|
||||
`reg_num` smallint NOT NULL COMMENT '注册数量',
|
||||
`created_at` datetime NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户邀请码';
|
||||
ALTER TABLE `chatgpt_invite_codes` ADD PRIMARY KEY (`id`);
|
||||
ALTER TABLE `chatgpt_invite_codes` MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
ALTER TABLE `chatgpt_invite_codes` ADD UNIQUE(`code`);
|
||||
|
||||
ALTER TABLE `chatgpt_api_keys` ADD `type` VARCHAR(10) NOT NULL DEFAULT 'chat' COMMENT '用途(chat=>聊天,img=>图片)' AFTER `value`;
|
||||
1
docker/.gitignore → deploy/.gitignore
vendored
@@ -2,3 +2,4 @@ mysql/data/*
|
||||
mysql/logs/*
|
||||
logs
|
||||
static/*
|
||||
redis/data/*
|
||||
@@ -1,6 +1,6 @@
|
||||
Listen = "0.0.0.0:5678"
|
||||
ProxyURL = "" # 如 http://127.0.0.1:7777
|
||||
MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local"
|
||||
MysqlDns = "root:12345678@tcp(chatgpt-plus-mysql:3306)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local"
|
||||
StaticDir = "./static" # 静态资源的目录
|
||||
StaticUrl = "/static" # 静态资源访问 URL
|
||||
AesEncryptKey = ""
|
||||
@@ -15,9 +15,9 @@ WeChatBot = false
|
||||
Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改
|
||||
|
||||
[Redis] # redis 配置信息
|
||||
Host = "localhost"
|
||||
Host = "chatgpt-plus-redis"
|
||||
Port = 6379
|
||||
Password = ""
|
||||
Password = "12345678"
|
||||
DB = 0
|
||||
|
||||
[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通
|
||||
@@ -33,10 +33,6 @@ WeChatBot = false
|
||||
Sign = ""
|
||||
CodeTempId = ""
|
||||
|
||||
[ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署
|
||||
ApiURL = "" # 插件扩展 API 地址
|
||||
Token = "" # 这个 token 随便填,只要确保跟 chatgpt-plus-exts 项目的 token 一样就行
|
||||
|
||||
[OSS] # OSS 配置,用于存储 MJ 绘画图片
|
||||
Active = "local" # 默认使用本地文件存储引擎
|
||||
[OSS.Local]
|
||||
@@ -67,4 +63,23 @@ WeChatBot = false
|
||||
Enabled = false
|
||||
ApiURL = "http://172.22.11.200:7860"
|
||||
ApiKey = ""
|
||||
Txt2ImgJsonPath = "res/text2img.json"
|
||||
Txt2ImgJsonPath = "res/text2img.json"
|
||||
|
||||
[XXLConfig] # xxl-job 配置,需要你部署 XXL-JOB 定时任务工具,用来定期清理未支付订单和清理过期 VIP,如果你没有启用支付服务,则该服务也无需启动
|
||||
Enabled = false # 是否启用 XXL JOB 服务
|
||||
ServerAddr = "http://chatgpt-plus-xxl-job:8080/xxl-job-admin" # xxl-job-admin 管理地址
|
||||
ExecutorIp = "172.22.11.47" # 执行器 IP 地址
|
||||
ExecutorPort = "9999" # 执行器服务端口
|
||||
AccessToken = "xxl-job-api-token" # 执行器 API 通信 token
|
||||
RegistryKey = "chatgpt-plus" # 任务注册 key
|
||||
|
||||
[AlipayConfig]
|
||||
Enabled = false # 启用支付宝支付通道
|
||||
SandBox = false # 是否启用沙盒模式
|
||||
UserId = "2088721020750581" # 商户ID
|
||||
AppId = "9021000131658023" # App Id
|
||||
PrivateKey = "certs/alipay/privateKey.txt" # 应用私钥
|
||||
PublicKey = "certs/alipay/appPublicCert.crt" # 应用公钥证书
|
||||
AlipayPublicKey = "certs/alipay/alipayPublicCert.crt" # 支付宝公钥证书
|
||||
RootCert = "certs/alipay/alipayRootCert.crt" # 支付宝根证书
|
||||
NotifyURL = "http://r9it.com:6004/api/payment/alipay/notify" # 支付异步回调地址
|
||||
@@ -35,12 +35,12 @@ server {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_pass http://172.22.11.47:5678; # 这里改成后端服务的内网 IP 地址
|
||||
proxy_pass http://chatgpt-plus-api:5678;
|
||||
}
|
||||
|
||||
# 静态资源转发
|
||||
location /static/ {
|
||||
proxy_pass http://172.22.11.47:5678; # 这里改成后端服务的内网 IP 地址
|
||||
proxy_pass http://chatgpt-plus-api:5678;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,47 @@
|
||||
version: '3'
|
||||
services:
|
||||
# mysql
|
||||
chatgpt-plus-mysql:
|
||||
image: mysql:8.0.33
|
||||
container_name: chatgpt-plus-mysql
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
restart: always
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=12345678
|
||||
ports:
|
||||
- "3307:3306"
|
||||
volumes :
|
||||
- ./mysql/conf/my.cnf:/etc/mysql/my.cnf
|
||||
- ./mysql/data:/var/lib/mysql
|
||||
- ./mysql/logs:/var/log/mysql
|
||||
- ./mysql/init.d:/docker-entrypoint-initdb.d/
|
||||
|
||||
# redis
|
||||
chatgpt-plus-redis:
|
||||
image: redis:6.0.16
|
||||
restart: always
|
||||
container_name: chatgpt-plus-redis
|
||||
command: redis-server --requirepass 12345678
|
||||
volumes :
|
||||
- ./redis/data:/data
|
||||
ports:
|
||||
- "6380:6379"
|
||||
|
||||
# 后端 API 程序
|
||||
chatgpt-plus-api:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:v3.1.5
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:v3.1.8.1
|
||||
container_name: chatgpt-plus-api
|
||||
restart: always
|
||||
depends_on:
|
||||
- chatgpt-plus-mysql
|
||||
- chatgpt-plus-redis
|
||||
environment:
|
||||
- DEBUG=false
|
||||
- LOG_LEVEL=info
|
||||
- CONFIG_FILE=config.toml
|
||||
ports:
|
||||
- "5678:5678"
|
||||
- "9999:9999"
|
||||
volumes:
|
||||
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime
|
||||
- ./conf/config.toml:/var/www/app/config.toml
|
||||
@@ -19,9 +50,11 @@ services:
|
||||
|
||||
# 前端应用
|
||||
chatgpt-plus-web:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:v3.1.5
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:v3.1.8.1
|
||||
container_name: chatgpt-plus-web
|
||||
restart: always
|
||||
depends_on:
|
||||
- chatgpt-plus-api
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
1603
deploy/mysql/init.d/chatgpt_plus-v3.1.9.sql
Normal file
@@ -1,14 +0,0 @@
|
||||
version: '3'
|
||||
services:
|
||||
minio:
|
||||
image: minio/minio
|
||||
container_name: minio
|
||||
volumes:
|
||||
- ./data:/data
|
||||
ports:
|
||||
- "9010:9000"
|
||||
- "9011:9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: minio
|
||||
MINIO_ROOT_PASSWORD: minio@pass
|
||||
command: server /data --console-address ":9001" --address ":9000"
|
||||
@@ -1,19 +0,0 @@
|
||||
version: '3'
|
||||
services:
|
||||
# 后端 API 程序
|
||||
mysql:
|
||||
image: mysql:8.0.33
|
||||
container_name: chatgpt-plus-mysql
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
restart: always
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=12345678
|
||||
ports:
|
||||
- "3307:3306"
|
||||
volumes:
|
||||
- ./conf/my.cnf:/etc/mysql/my.cnf
|
||||
- ./data:/var/lib/mysql
|
||||
- ./logs:/var/log/mysql
|
||||
|
||||
|
||||
|
||||
BIN
docs/imgs/admin_config.jpg
Normal file
|
After Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 201 KiB |
BIN
docs/imgs/admin_models.jpg
Normal file
|
After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 94 KiB |
BIN
docs/imgs/app-list.jpg
Normal file
|
After Width: | Height: | Size: 579 KiB |
BIN
docs/imgs/donate.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 1.9 MiB |
BIN
docs/imgs/member.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
docs/imgs/mj_image.jpg
Normal file
|
After Width: | Height: | Size: 623 KiB |
|
Before Width: | Height: | Size: 2.2 MiB |
BIN
docs/imgs/sd_image.jpg
Normal file
|
After Width: | Height: | Size: 622 KiB |
BIN
docs/imgs/sd_image_detail.jpg
Normal file
|
After Width: | Height: | Size: 510 KiB |
|
Before Width: | Height: | Size: 88 KiB |