Compare commits
	
		
			46 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					babef8baae | ||
| 
						 | 
					ae8239e5de | ||
| 
						 | 
					dae91ed243 | ||
| 
						 | 
					de42a428e6 | ||
| 
						 | 
					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 | 
							
								
								
									
										26
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						@@ -1,5 +1,31 @@
 | 
			
		||||
# 更新日志
 | 
			
		||||
 | 
			
		||||
## 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. 功能新增:新增会员套餐充值,点卡充值,订单系统,集成支付宝支付通道
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										334
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@@ -42,7 +42,7 @@ ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了
 | 
			
		||||
 | 
			
		||||
### 会员充值
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 自动调用函数插件
 | 
			
		||||
 | 
			
		||||
@@ -73,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. 支持微信支付充值
 | 
			
		||||
 | 
			
		||||
## 项目地址
 | 
			
		||||
 | 
			
		||||
@@ -117,301 +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.8.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 会不同。
 | 
			
		||||
  
 | 
			
		||||
[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. 如果你不知道如何获取 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.8 #这里改成最新的 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.8 #这里改成最新的 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)
 | 
			
		||||
你可以注册新用户,也可以使用系统默认有个账号:`18575670125/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/)。
 | 
			
		||||
 | 
			
		||||
## 参与贡献
 | 
			
		||||
 | 
			
		||||
@@ -421,7 +106,7 @@ make clean linux
 | 
			
		||||
 | 
			
		||||

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

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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]
 | 
			
		||||
 
 | 
			
		||||
@@ -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,15 +147,14 @@ 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/") ||
 | 
			
		||||
@@ -211,3 +218,126 @@ 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()
 | 
			
		||||
 | 
			
		||||
		contentType := c.Request.Header.Get("Content-Type")
 | 
			
		||||
		if strings.Contains(contentType, "multipart/form-data") {
 | 
			
		||||
			c.Next()
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 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()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -124,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"`
 | 
			
		||||
@@ -132,19 +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"`    // 启用众筹功能
 | 
			
		||||
	EnabledAlipay   bool     `json:"enabled_alipay"`    // 是否启用支付宝支付通道
 | 
			
		||||
	OrderPayTimeout int      `json:"order_pay_timeout"` //订单支付超时时间
 | 
			
		||||
	DefaultModels   []string `json:"default_models"`    // 默认开通的 AI 模型
 | 
			
		||||
	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{},
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,6 @@ require (
 | 
			
		||||
	github.com/qiniu/go-sdk/v7 v7.17.1
 | 
			
		||||
	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 | 
			
		||||
	github.com/smartwalle/alipay/v3 v3.2.15
 | 
			
		||||
	github.com/syndtr/goleveldb v1.0.0
 | 
			
		||||
	go.uber.org/zap v1.23.0
 | 
			
		||||
	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 | 
			
		||||
	gorm.io/driver/mysql v1.4.7
 | 
			
		||||
@@ -87,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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								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=
 | 
			
		||||
@@ -68,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=
 | 
			
		||||
@@ -89,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=
 | 
			
		||||
@@ -138,12 +132,9 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
 | 
			
		||||
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=
 | 
			
		||||
@@ -199,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=
 | 
			
		||||
@@ -242,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=
 | 
			
		||||
@@ -251,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=
 | 
			
		||||
@@ -304,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,6 +31,7 @@ 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"`
 | 
			
		||||
@@ -40,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, SortNum: data.SortNum, Weight: data.Weight}
 | 
			
		||||
	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)
 | 
			
		||||
@@ -89,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 {
 | 
			
		||||
@@ -100,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)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -70,6 +70,7 @@ func (h *UserHandler) Save(c *gin.Context) {
 | 
			
		||||
		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)
 | 
			
		||||
@@ -86,6 +87,7 @@ func (h *UserHandler) Save(c *gin.Context) {
 | 
			
		||||
			"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),
 | 
			
		||||
 
 | 
			
		||||
@@ -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))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,10 @@ func (h *ChatModelHandler) List(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res := h.db.Where("enabled = ?", true).Where("value IN ?", models).Order("sort_num ASC").Find(&items)
 | 
			
		||||
	// 查询用户有权限访问的模型以及所有开放的模型
 | 
			
		||||
	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
 | 
			
		||||
 
 | 
			
		||||
@@ -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, 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))
 | 
			
		||||
					}
 | 
			
		||||
 
 | 
			
		||||
@@ -84,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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ import (
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	logger2 "chatplus/logger"
 | 
			
		||||
	"chatplus/service/mj"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
@@ -34,15 +33,13 @@ 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,
 | 
			
		||||
	}
 | 
			
		||||
@@ -227,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
 | 
			
		||||
@@ -258,17 +252,15 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
 | 
			
		||||
 | 
			
		||||
			// loading the role context
 | 
			
		||||
			var messages []types.Message
 | 
			
		||||
			if len(messages) > 0 {
 | 
			
		||||
				err := utils.JsonDecode(role.Context, &messages)
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					for _, v := range messages {
 | 
			
		||||
						tks, _ := utils.CalcTokens(v.Content, req.Model)
 | 
			
		||||
						if tokens+tks >= types.ModelToTokens[req.Model] {
 | 
			
		||||
							break
 | 
			
		||||
						}
 | 
			
		||||
						tokens += tks
 | 
			
		||||
						chatCtx = append(chatCtx, v)
 | 
			
		||||
			err := utils.JsonDecode(role.Context, &messages)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				for _, v := range messages {
 | 
			
		||||
					tks, _ := utils.CalcTokens(v.Content, req.Model)
 | 
			
		||||
					if tokens+tks >= types.ModelToTokens[req.Model] {
 | 
			
		||||
						break
 | 
			
		||||
					}
 | 
			
		||||
					tokens += tks
 | 
			
		||||
					chatCtx = append(chatCtx, v)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
@@ -410,7 +402,7 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
 | 
			
		||||
	}
 | 
			
		||||
	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")
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -71,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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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, 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))
 | 
			
		||||
					}
 | 
			
		||||
 
 | 
			
		||||
@@ -48,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(
 | 
			
		||||
@@ -63,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
 | 
			
		||||
@@ -82,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)
 | 
			
		||||
@@ -138,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 {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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)
 | 
			
		||||
}
 | 
			
		||||
@@ -80,6 +80,7 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		SessionId string  `json:"session_id"`
 | 
			
		||||
		Prompt    string  `json:"prompt"`
 | 
			
		||||
		NegPrompt string  `json:"neg_prompt"`
 | 
			
		||||
		Rate      string  `json:"rate"`
 | 
			
		||||
		Model     string  `json:"model"`
 | 
			
		||||
		Chaos     int     `json:"chaos"`
 | 
			
		||||
@@ -87,6 +88,8 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
 | 
			
		||||
		Seed      int64   `json:"seed"`
 | 
			
		||||
		Stylize   int     `json:"stylize"`
 | 
			
		||||
		Img       string  `json:"img"`
 | 
			
		||||
		Tile      bool    `json:"tile"`
 | 
			
		||||
		Quality   float32 `json:"quality"`
 | 
			
		||||
		Weight    float32 `json:"weight"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
@@ -119,8 +122,17 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
 | 
			
		||||
	if data.Raw {
 | 
			
		||||
		prompt += " --style raw"
 | 
			
		||||
	}
 | 
			
		||||
	if data.Quality > 0 {
 | 
			
		||||
		prompt += fmt.Sprintf(" --q %.2f", data.Quality)
 | 
			
		||||
	}
 | 
			
		||||
	if data.NegPrompt != "" {
 | 
			
		||||
		prompt += fmt.Sprintf(" --no %s", data.NegPrompt)
 | 
			
		||||
	}
 | 
			
		||||
	if data.Tile {
 | 
			
		||||
		prompt += " --tile "
 | 
			
		||||
	}
 | 
			
		||||
	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 +360,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
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
@@ -96,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)
 | 
			
		||||
@@ -106,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
 | 
			
		||||
@@ -279,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"`
 | 
			
		||||
@@ -319,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)
 | 
			
		||||
@@ -338,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)
 | 
			
		||||
@@ -359,6 +430,6 @@ func (h *UserHandler) BindMobile(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_ = h.leveldb.Delete(key) // 删除短信验证码
 | 
			
		||||
	_ = h.redis.Del(c, key) // 删除短信验证码
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								api/main.go
									
									
									
									
									
								
							
							
						
						@@ -94,7 +94,6 @@ func main() {
 | 
			
		||||
		// 初始化数据库
 | 
			
		||||
		fx.Provide(store.NewGormConfig),
 | 
			
		||||
		fx.Provide(store.NewMysql),
 | 
			
		||||
		fx.Provide(store.NewLevelDB),
 | 
			
		||||
		fx.Provide(store.NewRedisClient),
 | 
			
		||||
 | 
			
		||||
		fx.Provide(func() embed.FS {
 | 
			
		||||
@@ -217,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/")
 | 
			
		||||
@@ -237,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) {
 | 
			
		||||
@@ -275,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/")
 | 
			
		||||
@@ -314,7 +312,7 @@ func main() {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/model/")
 | 
			
		||||
			group.POST("save", h.Save)
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.POST("enable", h.Enable)
 | 
			
		||||
			group.POST("set", h.Set)
 | 
			
		||||
			group.POST("sort", h.Sort)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
		}),
 | 
			
		||||
@@ -347,6 +345,14 @@ func main() {
 | 
			
		||||
			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)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -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),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -288,7 +288,7 @@ func (s *Service) callback(data CBReq) {
 | 
			
		||||
			jobVo.ImgURL = data.ImageData
 | 
			
		||||
		}
 | 
			
		||||
		// 扣减绘图次数
 | 
			
		||||
		s.db.Where("id = ?", jobVo.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
 | 
			
		||||
		s.db.Model(&model.User{}).Where("id = ?", jobVo.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
 | 
			
		||||
		// 推送任务到前端
 | 
			
		||||
		if client != nil {
 | 
			
		||||
			utils.ReplyChunkMessage(client, jobVo)
 | 
			
		||||
 
 | 
			
		||||
@@ -38,13 +38,13 @@ func NewXXLJobExecutor(config *types.AppConfig, db *gorm.DB) *XXLJobExecutor {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *XXLJobExecutor) Run() error {
 | 
			
		||||
	e.executor.RegTask("ClearOrder", e.ClearOrder)
 | 
			
		||||
	e.executor.RegTask("ClearOrders", e.ClearOrders)
 | 
			
		||||
	e.executor.RegTask("ResetVipCalls", e.ResetVipCalls)
 | 
			
		||||
	return e.executor.Run()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ClearOrder 清理未支付的订单,如果没有抛出异常则表示执行成功
 | 
			
		||||
func (e *XXLJobExecutor) ClearOrder(cxt context.Context, param *xxl.RunReq) (msg string) {
 | 
			
		||||
// 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)
 | 
			
		||||
@@ -63,7 +63,8 @@ func (e *XXLJobExecutor) ClearOrder(cxt context.Context, param *xxl.RunReq) (msg
 | 
			
		||||
	}
 | 
			
		||||
	timeout := time.Now().Unix() - int64(config.OrderPayTimeout)
 | 
			
		||||
	start := utils.Stamp2str(timeout)
 | 
			
		||||
	res = e.db.Where("status != ? AND created_at < ?", types.OrderPaidSuccess, start).Delete(&model.Order{})
 | 
			
		||||
	// 这里不是用软删除,而是永久删除订单
 | 
			
		||||
	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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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,5 +7,6 @@ type ChatModel struct {
 | 
			
		||||
	Value    string // API Key 的值
 | 
			
		||||
	SortNum  int
 | 
			
		||||
	Enabled  bool
 | 
			
		||||
	Weight   int // 对话权重,每次对话扣减多少次对话额度
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
@@ -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"` // 最后使用时间
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,4 +8,5 @@ type ChatModel struct {
 | 
			
		||||
	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"`
 | 
			
		||||
}
 | 
			
		||||
@@ -4,6 +4,7 @@ import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/lionsoul2014/ip2region/binding/golang/xdb"
 | 
			
		||||
	"github.com/nfnt/resize"
 | 
			
		||||
	"github.com/skip2/go-qrcode"
 | 
			
		||||
	"image"
 | 
			
		||||
@@ -14,8 +15,6 @@ import (
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/lionsoul2014/ip2region/binding/golang/xdb"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CopyObject 拷贝对象
 | 
			
		||||
@@ -60,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:
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
							
								
								
									
										1603
									
								
								database/chatgpt_plus-v3.1.9.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										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]
 | 
			
		||||
@@ -71,7 +67,7 @@ WeChatBot = false
 | 
			
		||||
 | 
			
		||||
[XXLConfig] # xxl-job 配置,需要你部署 XXL-JOB 定时任务工具,用来定期清理未支付订单和清理过期 VIP,如果你没有启用支付服务,则该服务也无需启动
 | 
			
		||||
  Enabled = false # 是否启用 XXL JOB 服务
 | 
			
		||||
  ServerAddr = "http://172.22.11.47:8080/xxl-job-admin" # xxl-job-admin 管理地址
 | 
			
		||||
  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
 | 
			
		||||
@@ -86,4 +82,4 @@ WeChatBot = false
 | 
			
		||||
  PublicKey = "certs/alipay/appPublicCert.crt" # 应用公钥证书
 | 
			
		||||
  AlipayPublicKey = "certs/alipay/alipayPublicCert.crt" # 支付宝公钥证书
 | 
			
		||||
  RootCert = "certs/alipay/alipayRootCert.crt" # 支付宝根证书
 | 
			
		||||
  NotifyURL = "http://r9it.com:6004/api/payment/alipay/notify" # 支付异步回调地址
 | 
			
		||||
  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/donate.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 170 KiB  | 
| 
		 Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB  | 
| 
		 Before Width: | Height: | Size: 88 KiB  | 
@@ -5,4 +5,4 @@ VUE_APP_PASS=12345678
 | 
			
		||||
VUE_APP_ADMIN_USER=admin
 | 
			
		||||
VUE_APP_ADMIN_PASS=admin123
 | 
			
		||||
VUE_APP_KEY_PREFIX=ChatPLUS_DEV_
 | 
			
		||||
VUE_APP_TITLE="ChatGPT-PLUS V3"
 | 
			
		||||
VUE_APP_TITLE="Chat-Plus V3"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
VUE_APP_API_HOST=
 | 
			
		||||
VUE_APP_WS_HOST=
 | 
			
		||||
VUE_APP_KEY_PREFIX=ChatPLUS_
 | 
			
		||||
VUE_APP_TITLE="ChatGPT-PLUS V3"
 | 
			
		||||
VUE_APP_TITLE="Chat-Plus V3"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										323
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						@@ -21,6 +21,7 @@
 | 
			
		||||
        "markdown-it": "^13.0.1",
 | 
			
		||||
        "md-editor-v3": "^2.2.1",
 | 
			
		||||
        "pinia": "^2.1.4",
 | 
			
		||||
        "qrcode": "^1.5.3",
 | 
			
		||||
        "qs": "^6.11.1",
 | 
			
		||||
        "sortablejs": "^1.15.0",
 | 
			
		||||
        "v3-waterfall": "^1.2.1",
 | 
			
		||||
@@ -3361,7 +3362,6 @@
 | 
			
		||||
      "version": "5.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=8"
 | 
			
		||||
      }
 | 
			
		||||
@@ -4763,6 +4763,14 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/decamelize": {
 | 
			
		||||
      "version": "1.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=0.10.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/decode-uri-component": {
 | 
			
		||||
      "version": "0.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
 | 
			
		||||
@@ -4973,6 +4981,11 @@
 | 
			
		||||
      "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/dijkstrajs": {
 | 
			
		||||
      "version": "1.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
 | 
			
		||||
      "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/dir-glob": {
 | 
			
		||||
      "version": "3.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz",
 | 
			
		||||
@@ -5150,8 +5163,7 @@
 | 
			
		||||
    "node_modules/emoji-regex": {
 | 
			
		||||
      "version": "8.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/emojis-list": {
 | 
			
		||||
      "version": "3.0.0",
 | 
			
		||||
@@ -5162,6 +5174,11 @@
 | 
			
		||||
        "node": ">= 4"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/encode-utf8": {
 | 
			
		||||
      "version": "1.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/encode-utf8/-/encode-utf8-1.0.3.tgz",
 | 
			
		||||
      "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/encodeurl": {
 | 
			
		||||
      "version": "1.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz",
 | 
			
		||||
@@ -6035,7 +6052,6 @@
 | 
			
		||||
      "version": "4.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "locate-path": "^5.0.0",
 | 
			
		||||
        "path-exists": "^4.0.0"
 | 
			
		||||
@@ -6181,7 +6197,6 @@
 | 
			
		||||
      "version": "2.0.5",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
 | 
			
		||||
      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": "6.* || 8.* || >= 10.*"
 | 
			
		||||
      }
 | 
			
		||||
@@ -6720,7 +6735,6 @@
 | 
			
		||||
      "version": "3.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=8"
 | 
			
		||||
      }
 | 
			
		||||
@@ -7089,7 +7103,6 @@
 | 
			
		||||
      "version": "5.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "p-locate": "^4.1.0"
 | 
			
		||||
      },
 | 
			
		||||
@@ -8115,7 +8128,6 @@
 | 
			
		||||
      "version": "2.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
 | 
			
		||||
      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "p-try": "^2.0.0"
 | 
			
		||||
      },
 | 
			
		||||
@@ -8127,7 +8139,6 @@
 | 
			
		||||
      "version": "4.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "p-limit": "^2.2.0"
 | 
			
		||||
      },
 | 
			
		||||
@@ -8152,7 +8163,6 @@
 | 
			
		||||
      "version": "2.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6"
 | 
			
		||||
      }
 | 
			
		||||
@@ -8238,7 +8248,6 @@
 | 
			
		||||
      "version": "4.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=8"
 | 
			
		||||
      }
 | 
			
		||||
@@ -8358,6 +8367,14 @@
 | 
			
		||||
        "node": ">=8"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/pngjs": {
 | 
			
		||||
      "version": "5.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=10.13.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/portfinder": {
 | 
			
		||||
      "version": "1.0.28",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/portfinder/-/portfinder-1.0.28.tgz",
 | 
			
		||||
@@ -9020,6 +9037,119 @@
 | 
			
		||||
        "node": ">=6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/qrcode": {
 | 
			
		||||
      "version": "1.5.3",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.3.tgz",
 | 
			
		||||
      "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "dijkstrajs": "^1.0.1",
 | 
			
		||||
        "encode-utf8": "^1.0.3",
 | 
			
		||||
        "pngjs": "^5.0.0",
 | 
			
		||||
        "yargs": "^15.3.1"
 | 
			
		||||
      },
 | 
			
		||||
      "bin": {
 | 
			
		||||
        "qrcode": "bin/qrcode"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=10.13.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/qrcode/node_modules/ansi-styles": {
 | 
			
		||||
      "version": "4.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
 | 
			
		||||
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "color-convert": "^2.0.1"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=8"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/qrcode/node_modules/camelcase": {
 | 
			
		||||
      "version": "5.3.1",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz",
 | 
			
		||||
      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/qrcode/node_modules/cliui": {
 | 
			
		||||
      "version": "6.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "string-width": "^4.2.0",
 | 
			
		||||
        "strip-ansi": "^6.0.0",
 | 
			
		||||
        "wrap-ansi": "^6.2.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/qrcode/node_modules/color-convert": {
 | 
			
		||||
      "version": "2.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "color-name": "~1.1.4"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=7.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/qrcode/node_modules/color-name": {
 | 
			
		||||
      "version": "1.1.4",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
 | 
			
		||||
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/qrcode/node_modules/wrap-ansi": {
 | 
			
		||||
      "version": "6.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "ansi-styles": "^4.0.0",
 | 
			
		||||
        "string-width": "^4.1.0",
 | 
			
		||||
        "strip-ansi": "^6.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=8"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/qrcode/node_modules/y18n": {
 | 
			
		||||
      "version": "4.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz",
 | 
			
		||||
      "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/qrcode/node_modules/yargs": {
 | 
			
		||||
      "version": "15.4.1",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz",
 | 
			
		||||
      "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "cliui": "^6.0.0",
 | 
			
		||||
        "decamelize": "^1.2.0",
 | 
			
		||||
        "find-up": "^4.1.0",
 | 
			
		||||
        "get-caller-file": "^2.0.1",
 | 
			
		||||
        "require-directory": "^2.1.1",
 | 
			
		||||
        "require-main-filename": "^2.0.0",
 | 
			
		||||
        "set-blocking": "^2.0.0",
 | 
			
		||||
        "string-width": "^4.2.0",
 | 
			
		||||
        "which-module": "^2.0.0",
 | 
			
		||||
        "y18n": "^4.0.0",
 | 
			
		||||
        "yargs-parser": "^18.1.2"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=8"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/qrcode/node_modules/yargs-parser": {
 | 
			
		||||
      "version": "18.1.3",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz",
 | 
			
		||||
      "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "camelcase": "^5.0.0",
 | 
			
		||||
        "decamelize": "^1.2.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/qs": {
 | 
			
		||||
      "version": "6.11.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz",
 | 
			
		||||
@@ -9258,7 +9388,6 @@
 | 
			
		||||
      "version": "2.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
 | 
			
		||||
      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=0.10.0"
 | 
			
		||||
      }
 | 
			
		||||
@@ -9272,6 +9401,11 @@
 | 
			
		||||
        "node": ">=0.10.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/require-main-filename": {
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/requires-port": {
 | 
			
		||||
      "version": "1.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz",
 | 
			
		||||
@@ -9547,6 +9681,11 @@
 | 
			
		||||
        "node": ">= 0.8.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/set-blocking": {
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/setprototypeof": {
 | 
			
		||||
      "version": "1.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
 | 
			
		||||
@@ -9852,7 +9991,6 @@
 | 
			
		||||
      "version": "4.2.3",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
 | 
			
		||||
      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "emoji-regex": "^8.0.0",
 | 
			
		||||
        "is-fullwidth-code-point": "^3.0.0",
 | 
			
		||||
@@ -9866,7 +10004,6 @@
 | 
			
		||||
      "version": "6.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "ansi-regex": "^5.0.1"
 | 
			
		||||
      },
 | 
			
		||||
@@ -11194,6 +11331,11 @@
 | 
			
		||||
        "which": "bin/which"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/which-module": {
 | 
			
		||||
      "version": "2.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/wildcard": {
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-2.0.0.tgz",
 | 
			
		||||
@@ -13972,8 +14114,7 @@
 | 
			
		||||
    "ansi-regex": {
 | 
			
		||||
      "version": "5.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "ansi-styles": {
 | 
			
		||||
      "version": "3.2.1",
 | 
			
		||||
@@ -15081,6 +15222,11 @@
 | 
			
		||||
        "ms": "2.1.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "decamelize": {
 | 
			
		||||
      "version": "1.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="
 | 
			
		||||
    },
 | 
			
		||||
    "decode-uri-component": {
 | 
			
		||||
      "version": "0.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
 | 
			
		||||
@@ -15242,6 +15388,11 @@
 | 
			
		||||
      "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "dijkstrajs": {
 | 
			
		||||
      "version": "1.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
 | 
			
		||||
      "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="
 | 
			
		||||
    },
 | 
			
		||||
    "dir-glob": {
 | 
			
		||||
      "version": "3.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz",
 | 
			
		||||
@@ -15399,8 +15550,7 @@
 | 
			
		||||
    "emoji-regex": {
 | 
			
		||||
      "version": "8.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
 | 
			
		||||
    },
 | 
			
		||||
    "emojis-list": {
 | 
			
		||||
      "version": "3.0.0",
 | 
			
		||||
@@ -15408,6 +15558,11 @@
 | 
			
		||||
      "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "encode-utf8": {
 | 
			
		||||
      "version": "1.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/encode-utf8/-/encode-utf8-1.0.3.tgz",
 | 
			
		||||
      "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw=="
 | 
			
		||||
    },
 | 
			
		||||
    "encodeurl": {
 | 
			
		||||
      "version": "1.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz",
 | 
			
		||||
@@ -16104,7 +16259,6 @@
 | 
			
		||||
      "version": "4.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "locate-path": "^5.0.0",
 | 
			
		||||
        "path-exists": "^4.0.0"
 | 
			
		||||
@@ -16210,8 +16364,7 @@
 | 
			
		||||
    "get-caller-file": {
 | 
			
		||||
      "version": "2.0.5",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
 | 
			
		||||
      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
 | 
			
		||||
    },
 | 
			
		||||
    "get-intrinsic": {
 | 
			
		||||
      "version": "1.1.1",
 | 
			
		||||
@@ -16639,8 +16792,7 @@
 | 
			
		||||
    "is-fullwidth-code-point": {
 | 
			
		||||
      "version": "3.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
 | 
			
		||||
    },
 | 
			
		||||
    "is-glob": {
 | 
			
		||||
      "version": "4.0.3",
 | 
			
		||||
@@ -16933,7 +17085,6 @@
 | 
			
		||||
      "version": "5.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "p-locate": "^4.1.0"
 | 
			
		||||
      }
 | 
			
		||||
@@ -17749,7 +17900,6 @@
 | 
			
		||||
      "version": "2.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
 | 
			
		||||
      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "p-try": "^2.0.0"
 | 
			
		||||
      }
 | 
			
		||||
@@ -17758,7 +17908,6 @@
 | 
			
		||||
      "version": "4.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "p-limit": "^2.2.0"
 | 
			
		||||
      }
 | 
			
		||||
@@ -17776,8 +17925,7 @@
 | 
			
		||||
    "p-try": {
 | 
			
		||||
      "version": "2.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "param-case": {
 | 
			
		||||
      "version": "3.0.4",
 | 
			
		||||
@@ -17852,8 +18000,7 @@
 | 
			
		||||
    "path-exists": {
 | 
			
		||||
      "version": "4.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
 | 
			
		||||
    },
 | 
			
		||||
    "path-is-absolute": {
 | 
			
		||||
      "version": "1.0.1",
 | 
			
		||||
@@ -17922,6 +18069,11 @@
 | 
			
		||||
        "find-up": "^4.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "pngjs": {
 | 
			
		||||
      "version": "5.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="
 | 
			
		||||
    },
 | 
			
		||||
    "portfinder": {
 | 
			
		||||
      "version": "1.0.28",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/portfinder/-/portfinder-1.0.28.tgz",
 | 
			
		||||
@@ -18364,6 +18516,97 @@
 | 
			
		||||
      "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "qrcode": {
 | 
			
		||||
      "version": "1.5.3",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.3.tgz",
 | 
			
		||||
      "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "dijkstrajs": "^1.0.1",
 | 
			
		||||
        "encode-utf8": "^1.0.3",
 | 
			
		||||
        "pngjs": "^5.0.0",
 | 
			
		||||
        "yargs": "^15.3.1"
 | 
			
		||||
      },
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "ansi-styles": {
 | 
			
		||||
          "version": "4.3.0",
 | 
			
		||||
          "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
 | 
			
		||||
          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "color-convert": "^2.0.1"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "camelcase": {
 | 
			
		||||
          "version": "5.3.1",
 | 
			
		||||
          "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz",
 | 
			
		||||
          "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
 | 
			
		||||
        },
 | 
			
		||||
        "cliui": {
 | 
			
		||||
          "version": "6.0.0",
 | 
			
		||||
          "resolved": "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz",
 | 
			
		||||
          "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "string-width": "^4.2.0",
 | 
			
		||||
            "strip-ansi": "^6.0.0",
 | 
			
		||||
            "wrap-ansi": "^6.2.0"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "color-convert": {
 | 
			
		||||
          "version": "2.0.1",
 | 
			
		||||
          "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
 | 
			
		||||
          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "color-name": "~1.1.4"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "color-name": {
 | 
			
		||||
          "version": "1.1.4",
 | 
			
		||||
          "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
 | 
			
		||||
          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
 | 
			
		||||
        },
 | 
			
		||||
        "wrap-ansi": {
 | 
			
		||||
          "version": "6.2.0",
 | 
			
		||||
          "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
 | 
			
		||||
          "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "ansi-styles": "^4.0.0",
 | 
			
		||||
            "string-width": "^4.1.0",
 | 
			
		||||
            "strip-ansi": "^6.0.0"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "y18n": {
 | 
			
		||||
          "version": "4.0.3",
 | 
			
		||||
          "resolved": "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz",
 | 
			
		||||
          "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
 | 
			
		||||
        },
 | 
			
		||||
        "yargs": {
 | 
			
		||||
          "version": "15.4.1",
 | 
			
		||||
          "resolved": "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz",
 | 
			
		||||
          "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "cliui": "^6.0.0",
 | 
			
		||||
            "decamelize": "^1.2.0",
 | 
			
		||||
            "find-up": "^4.1.0",
 | 
			
		||||
            "get-caller-file": "^2.0.1",
 | 
			
		||||
            "require-directory": "^2.1.1",
 | 
			
		||||
            "require-main-filename": "^2.0.0",
 | 
			
		||||
            "set-blocking": "^2.0.0",
 | 
			
		||||
            "string-width": "^4.2.0",
 | 
			
		||||
            "which-module": "^2.0.0",
 | 
			
		||||
            "y18n": "^4.0.0",
 | 
			
		||||
            "yargs-parser": "^18.1.2"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "yargs-parser": {
 | 
			
		||||
          "version": "18.1.3",
 | 
			
		||||
          "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz",
 | 
			
		||||
          "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "camelcase": "^5.0.0",
 | 
			
		||||
            "decamelize": "^1.2.0"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "qs": {
 | 
			
		||||
      "version": "6.11.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz",
 | 
			
		||||
@@ -18559,8 +18802,7 @@
 | 
			
		||||
    "require-directory": {
 | 
			
		||||
      "version": "2.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
 | 
			
		||||
      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="
 | 
			
		||||
    },
 | 
			
		||||
    "require-from-string": {
 | 
			
		||||
      "version": "2.0.2",
 | 
			
		||||
@@ -18568,6 +18810,11 @@
 | 
			
		||||
      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "require-main-filename": {
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
 | 
			
		||||
    },
 | 
			
		||||
    "requires-port": {
 | 
			
		||||
      "version": "1.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz",
 | 
			
		||||
@@ -18809,6 +19056,11 @@
 | 
			
		||||
        "send": "0.17.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "set-blocking": {
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
 | 
			
		||||
    },
 | 
			
		||||
    "setprototypeof": {
 | 
			
		||||
      "version": "1.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
 | 
			
		||||
@@ -19077,7 +19329,6 @@
 | 
			
		||||
      "version": "4.2.3",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
 | 
			
		||||
      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "emoji-regex": "^8.0.0",
 | 
			
		||||
        "is-fullwidth-code-point": "^3.0.0",
 | 
			
		||||
@@ -19088,7 +19339,6 @@
 | 
			
		||||
      "version": "6.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "ansi-regex": "^5.0.1"
 | 
			
		||||
      }
 | 
			
		||||
@@ -20109,6 +20359,11 @@
 | 
			
		||||
        "isexe": "^2.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "which-module": {
 | 
			
		||||
      "version": "2.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "wildcard": {
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-2.0.0.tgz",
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@
 | 
			
		||||
    "markdown-it": "^13.0.1",
 | 
			
		||||
    "md-editor-v3": "^2.2.1",
 | 
			
		||||
    "pinia": "^2.1.4",
 | 
			
		||||
    "qrcode": "^1.5.3",
 | 
			
		||||
    "qs": "^6.11.1",
 | 
			
		||||
    "sortablejs": "^1.15.0",
 | 
			
		||||
    "v3-waterfall": "^1.2.1",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								web/public/images/chat.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 5.1 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/mj.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 4.6 KiB  | 
| 
		 Before Width: | Height: | Size: 476 KiB After Width: | Height: | Size: 476 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/mj/mj-v4.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 27 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/mj/mj-v5.1.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 22 KiB  | 
| 
		 Before Width: | Height: | Size: 526 KiB After Width: | Height: | Size: 526 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/mj/mj-v5.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 20 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/mj/nj1.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 21 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/mj/nj2.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 22 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/mj/nj3.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 30 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/mj/nj4.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 32 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/mj/rate_16_9.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/mj/rate_1_1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.5 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/mj/rate_1_2.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.5 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/mj/rate_2_1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/mj/rate_3_4.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.5 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/mj/rate_4_3.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.5 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/mj/rate_9_16.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.5 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/sd.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 12 KiB  | 
							
								
								
									
										153
									
								
								web/src/App.vue
									
									
									
									
									
								
							
							
						
						@@ -39,4 +39,157 @@ html, body {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 省略显示 */
 | 
			
		||||
.ellipsis {
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
	text-overflow: ellipsis;
 | 
			
		||||
	white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
.sl {
 | 
			
		||||
	display: -webkit-box;
 | 
			
		||||
	-webkit-box-orient: vertical;
 | 
			
		||||
	-webkit-line-clamp: 2;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
.sl3 {
 | 
			
		||||
	display: -webkit-box;
 | 
			
		||||
	-webkit-box-orient: vertical;
 | 
			
		||||
	-webkit-line-clamp: 3;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
.sl4 {
 | 
			
		||||
	display: -webkit-box;
 | 
			
		||||
	-webkit-box-orient: vertical;
 | 
			
		||||
	-webkit-line-clamp: 3;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 居中布局 */
 | 
			
		||||
.auto_center{
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 50%;
 | 
			
		||||
    left: 50%;
 | 
			
		||||
    transform: translate(-50%,-50%);
 | 
			
		||||
}
 | 
			
		||||
.h_center{
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 50%;
 | 
			
		||||
    transform: translateY(-50%);
 | 
			
		||||
    -webkit-transform: translateY(-50%);
 | 
			
		||||
}
 | 
			
		||||
.w_center{
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    left: 50%;
 | 
			
		||||
    transform: translateX(-50%);
 | 
			
		||||
    -webkit-transform: translateX(-50%);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* flex布局 */
 | 
			
		||||
.flex-row {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	flex-direction: row;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.flex-col {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.justify-start {
 | 
			
		||||
	justify-content: flex-start;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.justify-end {
 | 
			
		||||
	justify-content: flex-end;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.justify-center {
 | 
			
		||||
	justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.justify-between {
 | 
			
		||||
	justify-content: space-between;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.justify-around {
 | 
			
		||||
	justify-content: space-around;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.justify-evenly {
 | 
			
		||||
	justify-content: space-evenly;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.items-start {
 | 
			
		||||
	align-items: flex-start;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.items-end {
 | 
			
		||||
	align-items: flex-end;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.items-center {
 | 
			
		||||
	align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.items-baseline {
 | 
			
		||||
	align-items: baseline;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.items-stretch {
 | 
			
		||||
	align-items: stretch;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.self-start {
 | 
			
		||||
	align-self: flex-start;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.self-end {
 | 
			
		||||
	align-self: flex-end;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.self-center {
 | 
			
		||||
	align-self: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.self-baseline {
 | 
			
		||||
	align-self: baseline;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.self-stretch {
 | 
			
		||||
	align-self: stretch;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.flex-1 {
 | 
			
		||||
	flex: 1 1 0%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.flex-auto {
 | 
			
		||||
	flex: 1 1 auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.grow {
 | 
			
		||||
	flex-grow: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.grow-0 {
 | 
			
		||||
	flex-grow: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.shrink {
 | 
			
		||||
	flex-shrink: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.shrink-0 {
 | 
			
		||||
	flex-shrink: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.shrink-1 {
 | 
			
		||||
	flex-shrink: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.relative {
 | 
			
		||||
	position: relative;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,6 @@
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .el-icon {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  top: 3px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content {
 | 
			
		||||
  background-color: #383838;
 | 
			
		||||
@@ -43,26 +42,62 @@
 | 
			
		||||
  padding: 8px 14px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  margin-bottom: 10px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content:hover {
 | 
			
		||||
  background-color: #585858;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .icon {
 | 
			
		||||
  width: 20px;
 | 
			
		||||
  height: 20px;
 | 
			
		||||
  margin-bottom: 5px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape {
 | 
			
		||||
  width: 16px;
 | 
			
		||||
  height: 16px;
 | 
			
		||||
  margin-right: 5px;
 | 
			
		||||
  margin-bottom: 5px;
 | 
			
		||||
  border: 1px solid #c4c4c4;
 | 
			
		||||
  border-radius: 3px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.vertical {
 | 
			
		||||
  width: 12px;
 | 
			
		||||
  height: 20px;
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size9-16 {
 | 
			
		||||
  width: 9px;
 | 
			
		||||
  height: 16px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.horizontal {
 | 
			
		||||
  height: 12px;
 | 
			
		||||
  width: 20px;
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size16-9 {
 | 
			
		||||
  height: 9px;
 | 
			
		||||
  width: 16px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  top: 3px;
 | 
			
		||||
  margin: 4px 0 7px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size3-4 {
 | 
			
		||||
  width: 12px;
 | 
			
		||||
  height: 16px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size4-3 {
 | 
			
		||||
  height: 12px;
 | 
			
		||||
  width: 16px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  margin: 4px 0 4px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size2-3 {
 | 
			
		||||
  width: 11px;
 | 
			
		||||
  height: 16px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size3-2 {
 | 
			
		||||
  height: 11px;
 | 
			
		||||
  width: 16px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  margin: 4px 0 5px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size1-2 {
 | 
			
		||||
  width: 8px;
 | 
			
		||||
  height: 16px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size2-1 {
 | 
			
		||||
  height: 8px;
 | 
			
		||||
  width: 16px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  margin: 4px 0 8px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content.active {
 | 
			
		||||
  color: #47fff1;
 | 
			
		||||
@@ -76,6 +111,7 @@
 | 
			
		||||
  border: 1px solid #454545;
 | 
			
		||||
  border-radius: 5px;
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
  margin-bottom: 10px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-flow: column;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
@@ -98,10 +134,20 @@
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .form-item-inner {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .form-item-inner .el-select {
 | 
			
		||||
  --el-select-input-focus-border-color: #47fff1;
 | 
			
		||||
  --el-input-focus-border-color: #47fff1;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .form-item-inner .el-input__wrapper {
 | 
			
		||||
  background: #383838;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .form-item-inner .el-input__inner {
 | 
			
		||||
  color: #fff;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .form-item-inner .el-icon {
 | 
			
		||||
  margin-left: 10px;
 | 
			
		||||
  margin-top: 2px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line .img-uploader .el-upload {
 | 
			
		||||
  border: 1px dashed var(--el-border-color);
 | 
			
		||||
@@ -123,19 +169,11 @@
 | 
			
		||||
  text-align: center;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .mj-params .param-line.pt {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  padding-top: 5px;
 | 
			
		||||
  padding-bottom: 5px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .submit-btn {
 | 
			
		||||
  padding: 10px 15px 0 15px;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .submit-btn .el-button {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .mj-box .submit-btn .el-button span {
 | 
			
		||||
  color: #2d3a4b;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .el-form .el-form-item__label {
 | 
			
		||||
  color: #fff;
 | 
			
		||||
}
 | 
			
		||||
@@ -149,6 +187,86 @@
 | 
			
		||||
  color: #fff;
 | 
			
		||||
  overflow-x: hidden;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .task-list-box .task-list-inner .el-tabs {
 | 
			
		||||
  --el-tabs-header-height: 55px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .task-list-box .task-list-inner .el-tabs__item {
 | 
			
		||||
  color: #fff;
 | 
			
		||||
  font-size: 18px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .task-list-box .task-list-inner .title-tabs .el-tabs__item.is-active {
 | 
			
		||||
  color: #47fff1;
 | 
			
		||||
  font-size: 18px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .task-list-box .task-list-inner .title-tabs .el-tabs__active-bar {
 | 
			
		||||
  background-color: #47fff1;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .task-list-box .task-list-inner .title-tabs .el-tabs__content {
 | 
			
		||||
  padding: 10px 0;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .task-list-box .task-list-inner .el-textarea {
 | 
			
		||||
  --el-input-focus-border-color: #47fff1;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .task-list-box .task-list-inner .el-textarea__inner {
 | 
			
		||||
  background: transparent;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .task-list-box .task-list-inner .el-input__wrapper {
 | 
			
		||||
  background: transparent;
 | 
			
		||||
  padding: 5px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .task-list-box .task-list-inner .text {
 | 
			
		||||
  margin-bottom: 10px;
 | 
			
		||||
  color: #6b778c;
 | 
			
		||||
  font-size: 15px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .task-list-box .task-list-inner .param-line.pt {
 | 
			
		||||
  padding-top: 5px;
 | 
			
		||||
  padding-bottom: 5px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .task-list-box .task-list-inner .form-item-inner {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .task-list-box .task-list-inner .form-item-inner .el-icon {
 | 
			
		||||
  margin-left: 10px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .task-list-box .task-list-inner .el-form-item__label {
 | 
			
		||||
  color: #fff;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .task-list-box .task-list-inner .img-uploader .el-upload {
 | 
			
		||||
  border: 1px dashed var(--el-border-color);
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  width: 300px;
 | 
			
		||||
  transition: var(--el-transition-duration-fast);
 | 
			
		||||
  margin-bottom: 20px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .task-list-box .task-list-inner .img-uploader .el-upload:hover {
 | 
			
		||||
  border-color: var(--el-color-primary);
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .task-list-box .task-list-inner .img-uploader .el-upload .el-icon.uploader-icon {
 | 
			
		||||
  font-size: 28px;
 | 
			
		||||
  color: #8c939d;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 120px;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .task-list-box .task-list-inner .submit-btn {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  margin: 20px 0;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .task-list-box .task-list-inner .submit-btn .el-button {
 | 
			
		||||
  width: 200px;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .task-list-box .task-list-inner .submit-btn .text-info {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: right;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
.page-mj .inner .task-list-box .running-job-list .job-item {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  padding: 2px;
 | 
			
		||||
@@ -253,5 +371,4 @@
 | 
			
		||||
  margin-left: 10px;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  top: 2px;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -40,7 +40,6 @@
 | 
			
		||||
 | 
			
		||||
          .el-icon {
 | 
			
		||||
            position relative
 | 
			
		||||
            top 3px
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          .grid-content {
 | 
			
		||||
@@ -49,29 +48,72 @@
 | 
			
		||||
            padding 8px 14px
 | 
			
		||||
            display flex
 | 
			
		||||
            cursor pointer
 | 
			
		||||
            margin-bottom: 10px;
 | 
			
		||||
 | 
			
		||||
            &:hover {
 | 
			
		||||
              background-color #585858
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .icon {
 | 
			
		||||
              width 20px
 | 
			
		||||
              height 20px
 | 
			
		||||
              margin-bottom 5px
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .shape {
 | 
			
		||||
              width 16px
 | 
			
		||||
              height 16px
 | 
			
		||||
              margin-right 5px
 | 
			
		||||
              margin-bottom 5px
 | 
			
		||||
              border 1px solid #C4C4C4
 | 
			
		||||
              border-radius 3px
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .shape.vertical {
 | 
			
		||||
              width 12px
 | 
			
		||||
              height 20px
 | 
			
		||||
            .shape.size9-16 {
 | 
			
		||||
              width 9px
 | 
			
		||||
              height 16px
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .shape.horizontal {
 | 
			
		||||
              height 12px
 | 
			
		||||
              width 20px
 | 
			
		||||
            .shape.size16-9 {
 | 
			
		||||
              height 9px
 | 
			
		||||
              width 16px
 | 
			
		||||
              position relative
 | 
			
		||||
              top 3px
 | 
			
		||||
              margin 4px 0 7px
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .shape.size3-4 {
 | 
			
		||||
              width 12px
 | 
			
		||||
              height 16px
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .shape.size4-3 {
 | 
			
		||||
              height 12px
 | 
			
		||||
              width 16px
 | 
			
		||||
              position relative
 | 
			
		||||
              margin 4px 0 4px
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .shape.size2-3 {
 | 
			
		||||
              width 11px
 | 
			
		||||
              height 16px
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .shape.size3-2 {
 | 
			
		||||
              height 11px
 | 
			
		||||
              width 16px
 | 
			
		||||
              position relative
 | 
			
		||||
              margin 4px 0 5px
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .shape.size1-2 {
 | 
			
		||||
              width 8px
 | 
			
		||||
              height 16px
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .shape.size2-1 {
 | 
			
		||||
              height 8px
 | 
			
		||||
              width 16px
 | 
			
		||||
              position relative
 | 
			
		||||
              margin 4px 0 8px
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
@@ -90,6 +132,7 @@
 | 
			
		||||
            border 1px solid #454545
 | 
			
		||||
            border-radius 5px
 | 
			
		||||
            padding 10px
 | 
			
		||||
            margin-bottom 10px
 | 
			
		||||
            display flex
 | 
			
		||||
            flex-flow column
 | 
			
		||||
            align-items center
 | 
			
		||||
@@ -118,14 +161,26 @@
 | 
			
		||||
 | 
			
		||||
          .form-item-inner {
 | 
			
		||||
            display flex
 | 
			
		||||
            align-items: center
 | 
			
		||||
 | 
			
		||||
            .el-select {
 | 
			
		||||
              --el-select-input-focus-border-color: #47FFF1;
 | 
			
		||||
              --el-input-focus-border-color: #47FFF1;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .el-input__wrapper {
 | 
			
		||||
              background: #383838;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .el-input__inner {
 | 
			
		||||
              color: #fff
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .el-icon {
 | 
			
		||||
              margin-left 10px
 | 
			
		||||
              margin-top 2px
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
          .img-uploader {
 | 
			
		||||
            .el-upload {
 | 
			
		||||
              border: 1px dashed var(--el-border-color);
 | 
			
		||||
@@ -153,23 +208,12 @@
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .param-line.pt {
 | 
			
		||||
          display: flex
 | 
			
		||||
          align-items: center
 | 
			
		||||
          padding-top 5px
 | 
			
		||||
          padding-bottom 5px
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .submit-btn {
 | 
			
		||||
        padding 10px 15px 0 15px
 | 
			
		||||
        text-align center
 | 
			
		||||
 | 
			
		||||
        .el-button {
 | 
			
		||||
          width 100%
 | 
			
		||||
 | 
			
		||||
          span {
 | 
			
		||||
            color #2D3A4B
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .el-form {
 | 
			
		||||
@@ -192,6 +236,5 @@
 | 
			
		||||
    margin-left 10px
 | 
			
		||||
    cursor pointer
 | 
			
		||||
    position relative
 | 
			
		||||
    top 2px
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -72,6 +72,86 @@
 | 
			
		||||
  color: #fff;
 | 
			
		||||
  overflow-x: hidden;
 | 
			
		||||
}
 | 
			
		||||
.page-sd .inner .task-list-box .task-list-inner .el-tabs {
 | 
			
		||||
  --el-tabs-header-height: 55px;
 | 
			
		||||
}
 | 
			
		||||
.page-sd .inner .task-list-box .task-list-inner .el-tabs__item {
 | 
			
		||||
  color: #fff;
 | 
			
		||||
  font-size: 18px;
 | 
			
		||||
}
 | 
			
		||||
.page-sd .inner .task-list-box .task-list-inner .title-tabs .el-tabs__item.is-active {
 | 
			
		||||
  color: #47fff1;
 | 
			
		||||
  font-size: 18px;
 | 
			
		||||
}
 | 
			
		||||
.page-sd .inner .task-list-box .task-list-inner .title-tabs .el-tabs__active-bar {
 | 
			
		||||
  background-color: #47fff1;
 | 
			
		||||
}
 | 
			
		||||
.page-sd .inner .task-list-box .task-list-inner .title-tabs .el-tabs__content {
 | 
			
		||||
  padding: 10px 0;
 | 
			
		||||
}
 | 
			
		||||
.page-sd .inner .task-list-box .task-list-inner .el-textarea {
 | 
			
		||||
  --el-input-focus-border-color: #47fff1;
 | 
			
		||||
}
 | 
			
		||||
.page-sd .inner .task-list-box .task-list-inner .el-textarea__inner {
 | 
			
		||||
  background: transparent;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
}
 | 
			
		||||
.page-sd .inner .task-list-box .task-list-inner .el-input__wrapper {
 | 
			
		||||
  background: transparent;
 | 
			
		||||
  padding: 5px;
 | 
			
		||||
}
 | 
			
		||||
.page-sd .inner .task-list-box .task-list-inner .text {
 | 
			
		||||
  margin-bottom: 10px;
 | 
			
		||||
  color: #6b778c;
 | 
			
		||||
  font-size: 15px;
 | 
			
		||||
}
 | 
			
		||||
.page-sd .inner .task-list-box .task-list-inner .param-line.pt {
 | 
			
		||||
  padding-top: 5px;
 | 
			
		||||
  padding-bottom: 5px;
 | 
			
		||||
}
 | 
			
		||||
.page-sd .inner .task-list-box .task-list-inner .form-item-inner {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
.page-sd .inner .task-list-box .task-list-inner .form-item-inner .el-icon {
 | 
			
		||||
  margin-left: 10px;
 | 
			
		||||
}
 | 
			
		||||
.page-sd .inner .task-list-box .task-list-inner .el-form-item__label {
 | 
			
		||||
  color: #fff;
 | 
			
		||||
}
 | 
			
		||||
.page-sd .inner .task-list-box .task-list-inner .img-uploader .el-upload {
 | 
			
		||||
  border: 1px dashed var(--el-border-color);
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  width: 300px;
 | 
			
		||||
  transition: var(--el-transition-duration-fast);
 | 
			
		||||
  margin-bottom: 20px;
 | 
			
		||||
}
 | 
			
		||||
.page-sd .inner .task-list-box .task-list-inner .img-uploader .el-upload:hover {
 | 
			
		||||
  border-color: var(--el-color-primary);
 | 
			
		||||
}
 | 
			
		||||
.page-sd .inner .task-list-box .task-list-inner .img-uploader .el-upload .el-icon.uploader-icon {
 | 
			
		||||
  font-size: 28px;
 | 
			
		||||
  color: #8c939d;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 120px;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
}
 | 
			
		||||
.page-sd .inner .task-list-box .task-list-inner .submit-btn {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  margin: 20px 0;
 | 
			
		||||
}
 | 
			
		||||
.page-sd .inner .task-list-box .task-list-inner .submit-btn .el-button {
 | 
			
		||||
  width: 200px;
 | 
			
		||||
}
 | 
			
		||||
.page-sd .inner .task-list-box .task-list-inner .submit-btn .text-info {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: right;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
.page-sd .inner .task-list-box .running-job-list .job-item {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  padding: 2px;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,109 @@
 | 
			
		||||
  color #ffffff
 | 
			
		||||
  overflow-x hidden
 | 
			
		||||
 | 
			
		||||
  .task-list-inner {
 | 
			
		||||
    .el-tabs {
 | 
			
		||||
      --el-tabs-header-height: 55px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .el-tabs__item {
 | 
			
		||||
      color: #fff;
 | 
			
		||||
      font-size: 18px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .title-tabs .el-tabs__item.is-active {
 | 
			
		||||
      color: #47FFF1;
 | 
			
		||||
      font-size: 18px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .title-tabs .el-tabs__active-bar {
 | 
			
		||||
      background-color: #47FFF1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .title-tabs .el-tabs__content {
 | 
			
		||||
      padding: 10px 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .el-textarea {
 | 
			
		||||
      --el-input-focus-border-color: #47FFF1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .el-textarea__inner {
 | 
			
		||||
      background: transparent;
 | 
			
		||||
      color: #fff;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .el-input__wrapper {
 | 
			
		||||
      background: transparent;
 | 
			
		||||
      padding 5px
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .text {
 | 
			
		||||
      margin-bottom: 10px;
 | 
			
		||||
      color: #6b778c;
 | 
			
		||||
      font-size: 15px
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .param-line.pt {
 | 
			
		||||
      padding-top 5px
 | 
			
		||||
      padding-bottom 5px
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .form-item-inner {
 | 
			
		||||
      display flex
 | 
			
		||||
      align-items: center
 | 
			
		||||
 | 
			
		||||
      .el-icon {
 | 
			
		||||
        margin-left 10px
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .el-form-item__label {
 | 
			
		||||
      color #ffffff
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .img-uploader {
 | 
			
		||||
      .el-upload {
 | 
			
		||||
        border: 1px dashed var(--el-border-color);
 | 
			
		||||
        border-radius: 6px;
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
        position: relative;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
        width 300px;
 | 
			
		||||
        transition: var(--el-transition-duration-fast);
 | 
			
		||||
        margin-bottom: 20px;
 | 
			
		||||
 | 
			
		||||
        &:hover {
 | 
			
		||||
          border-color: var(--el-color-primary);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .el-icon.uploader-icon {
 | 
			
		||||
          font-size: 28px
 | 
			
		||||
          color: #8c939d
 | 
			
		||||
          width 100%
 | 
			
		||||
          height: 120px
 | 
			
		||||
          text-align: center
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .submit-btn {
 | 
			
		||||
      display flex
 | 
			
		||||
      margin: 20px 0
 | 
			
		||||
 | 
			
		||||
      .el-button {
 | 
			
		||||
        width 200px
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .text-info {
 | 
			
		||||
        width 100%
 | 
			
		||||
        display flex
 | 
			
		||||
        justify-content right
 | 
			
		||||
        align-items center
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .running-job-list {
 | 
			
		||||
    .job-item {
 | 
			
		||||
      //border: 1px solid #454545;
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@
 | 
			
		||||
        <el-form-item label="手机验证码">
 | 
			
		||||
          <el-row :gutter="20">
 | 
			
		||||
            <el-col :span="16">
 | 
			
		||||
              <el-input v-model.number="form.code" maxlength="6"/>
 | 
			
		||||
              <el-input v-model="form.code" maxlength="6"/>
 | 
			
		||||
            </el-col>
 | 
			
		||||
            <el-col :span="8">
 | 
			
		||||
              <send-msg size="" :mobile="form.mobile"/>
 | 
			
		||||
 
 | 
			
		||||
@@ -31,11 +31,11 @@
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import {defineComponent} from "vue"
 | 
			
		||||
import {Clock, DocumentCopy} from "@element-plus/icons-vue";
 | 
			
		||||
import {Clock, DocumentCopy, Position} from "@element-plus/icons-vue";
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'ChatReply',
 | 
			
		||||
  components: {Clock, DocumentCopy},
 | 
			
		||||
  components: {Position, Clock, DocumentCopy},
 | 
			
		||||
  props: {
 | 
			
		||||
    content: {
 | 
			
		||||
      type: String,
 | 
			
		||||
@@ -111,8 +111,8 @@ export default defineComponent({
 | 
			
		||||
            line-height 1.5
 | 
			
		||||
 | 
			
		||||
            code {
 | 
			
		||||
              color #f1f1f1
 | 
			
		||||
              background-color #202121
 | 
			
		||||
              color #374151
 | 
			
		||||
              background-color #e7e7e8
 | 
			
		||||
              padding 0 3px;
 | 
			
		||||
              border-radius 5px;
 | 
			
		||||
            }
 | 
			
		||||
@@ -126,6 +126,39 @@ export default defineComponent({
 | 
			
		||||
            margin-top 0
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          .code-container {
 | 
			
		||||
            position relative
 | 
			
		||||
 | 
			
		||||
            .hljs {
 | 
			
		||||
              border-radius 10px
 | 
			
		||||
              line-height 1.5
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .copy-code-btn {
 | 
			
		||||
              position: absolute;
 | 
			
		||||
              right 10px
 | 
			
		||||
              top 10px
 | 
			
		||||
              cursor pointer
 | 
			
		||||
              font-size 12px
 | 
			
		||||
              color #c1c1c1
 | 
			
		||||
 | 
			
		||||
              &:hover {
 | 
			
		||||
                color #20a0ff
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          .lang-name {
 | 
			
		||||
            position absolute;
 | 
			
		||||
            right 10px
 | 
			
		||||
            bottom 50px
 | 
			
		||||
            padding 2px 6px 4px 6px
 | 
			
		||||
            background-color #444444
 | 
			
		||||
            border-radius 10px
 | 
			
		||||
            color #00e0e0
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // 设置表格边框
 | 
			
		||||
 | 
			
		||||
          table {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										99
									
								
								web/src/components/InviteList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,99 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="invite-list" v-loading="loading">
 | 
			
		||||
    <el-row v-if="items.length > 0">
 | 
			
		||||
      <el-table :data="items" :row-key="row => row.id" table-layout="auto" border
 | 
			
		||||
                style="--el-table-border-color:#373C47;
 | 
			
		||||
                --el-table-tr-bg-color:#2D323B;
 | 
			
		||||
                --el-table-row-hover-bg-color:#373C47;
 | 
			
		||||
                --el-table-header-bg-color:#474E5C;
 | 
			
		||||
                --el-table-text-color:#d1d1d1">
 | 
			
		||||
        <el-table-column prop="username" label="用户"/>
 | 
			
		||||
        <el-table-column prop="invite_code" label="邀请码"/>
 | 
			
		||||
        <el-table-column label="邀请奖励">
 | 
			
		||||
          <template #default="scope">
 | 
			
		||||
            <span>对话:{{ scope.row['reward']['chat_calls'] }}次</span>,
 | 
			
		||||
            <span>绘图:{{ scope.row['reward']['chat_calls'] }}次</span>
 | 
			
		||||
          </template>
 | 
			
		||||
        </el-table-column>
 | 
			
		||||
 | 
			
		||||
        <el-table-column label="注册时间">
 | 
			
		||||
          <template #default="scope">
 | 
			
		||||
            <span>{{ dateFormat(scope.row['created_at']) }}</span>
 | 
			
		||||
          </template>
 | 
			
		||||
        </el-table-column>
 | 
			
		||||
      </el-table>
 | 
			
		||||
    </el-row>
 | 
			
		||||
    <el-empty :image-size="100" v-else/>
 | 
			
		||||
    <div class="pagination">
 | 
			
		||||
      <el-pagination v-if="total > 0" background
 | 
			
		||||
                     layout="total,prev, pager, next"
 | 
			
		||||
                     :hide-on-single-page="true"
 | 
			
		||||
                     v-model:current-page="page"
 | 
			
		||||
                     v-model:page-size="pageSize"
 | 
			
		||||
                     @current-change="fetchData()"
 | 
			
		||||
                     :total="total"/>
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {onMounted, ref, watch} from "vue";
 | 
			
		||||
import {httpGet, httpPost} from "@/utils/http";
 | 
			
		||||
import {ElMessage} from "element-plus";
 | 
			
		||||
import {dateFormat} from "@/utils/libs";
 | 
			
		||||
import {DocumentCopy} from "@element-plus/icons-vue";
 | 
			
		||||
import Clipboard from "clipboard";
 | 
			
		||||
 | 
			
		||||
const items = ref([])
 | 
			
		||||
const total = ref(0)
 | 
			
		||||
const page = ref(1)
 | 
			
		||||
const pageSize = ref(10)
 | 
			
		||||
const loading = ref(true)
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  fetchData()
 | 
			
		||||
  const clipboard = new Clipboard('.copy-order-no');
 | 
			
		||||
  clipboard.on('success', () => {
 | 
			
		||||
    ElMessage.success("复制成功!");
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  clipboard.on('error', () => {
 | 
			
		||||
    ElMessage.error('复制失败!');
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 获取数据
 | 
			
		||||
const fetchData = () => {
 | 
			
		||||
  httpPost('/api/invite/list', {page: page.value, page_size: pageSize.value}).then((res) => {
 | 
			
		||||
    if (res.data) {
 | 
			
		||||
      items.value = res.data.items
 | 
			
		||||
      total.value = res.data.total
 | 
			
		||||
      page.value = res.data.page
 | 
			
		||||
      pageSize.value = res.data.page_size
 | 
			
		||||
    }
 | 
			
		||||
    loading.value = false
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    ElMessage.error("获取数据失败:" + e.message);
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="stylus">
 | 
			
		||||
.invite-list {
 | 
			
		||||
  .pagination {
 | 
			
		||||
    margin: 20px 0 0 0;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .copy-order-no {
 | 
			
		||||
    cursor pointer
 | 
			
		||||
    position relative
 | 
			
		||||
    left 6px
 | 
			
		||||
    top 2px
 | 
			
		||||
    color #20a0ff
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -45,9 +45,9 @@ onMounted(() => {
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const computeSize = () => {
 | 
			
		||||
  const w = container.value.offsetWidth - 8 // 减去滚动条的宽度
 | 
			
		||||
  const w = container.value.offsetWidth - 10 // 减去滚动条的宽度
 | 
			
		||||
  let cols = Math.floor(w / props.width)
 | 
			
		||||
  itemWidth.value = Math.ceil(w / cols)
 | 
			
		||||
  itemWidth.value = Math.floor(w / cols)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.onresize = () => {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										108
									
								
								web/src/components/ResetPass.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,108 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="reset-pass">
 | 
			
		||||
    <el-dialog
 | 
			
		||||
        v-model="showDialog"
 | 
			
		||||
        :close-on-click-modal="true"
 | 
			
		||||
        width="540px"
 | 
			
		||||
        :before-close="close"
 | 
			
		||||
        :title="title"
 | 
			
		||||
    >
 | 
			
		||||
      <div class="form">
 | 
			
		||||
 | 
			
		||||
        <el-form :model="form" label-width="120px" label-position="left">
 | 
			
		||||
          <el-form-item label="手机号码">
 | 
			
		||||
            <el-input v-model="form.mobile"/>
 | 
			
		||||
          </el-form-item>
 | 
			
		||||
          <el-form-item label="手机验证码">
 | 
			
		||||
            <el-row :gutter="20">
 | 
			
		||||
              <el-col :span="16">
 | 
			
		||||
                <el-input v-model="form.code" maxlength="6"/>
 | 
			
		||||
              </el-col>
 | 
			
		||||
              <el-col :span="8">
 | 
			
		||||
                <send-msg size="" :mobile="form.mobile"/>
 | 
			
		||||
              </el-col>
 | 
			
		||||
            </el-row>
 | 
			
		||||
          </el-form-item>
 | 
			
		||||
          <el-form-item label="新密码">
 | 
			
		||||
            <el-input v-model="form.password" type="password"/>
 | 
			
		||||
          </el-form-item>
 | 
			
		||||
          <el-form-item label="重复密码">
 | 
			
		||||
            <el-input v-model="form.repass" type="password"/>
 | 
			
		||||
          </el-form-item>
 | 
			
		||||
        </el-form>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <template #footer>
 | 
			
		||||
        <div class="dialog-footer">
 | 
			
		||||
          <el-button type="primary" @click="save" round>
 | 
			
		||||
            重置密码
 | 
			
		||||
          </el-button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </template>
 | 
			
		||||
    </el-dialog>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {computed, ref} from "vue";
 | 
			
		||||
import SendMsg from "@/components/SendMsg.vue";
 | 
			
		||||
import {ElMessage} from "element-plus";
 | 
			
		||||
import {httpPost} from "@/utils/http";
 | 
			
		||||
import {validateMobile} from "@/utils/validate";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  show: Boolean,
 | 
			
		||||
  mobile: String
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const showDialog = computed(() => {
 | 
			
		||||
  return props.show
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const title = ref('重置密码')
 | 
			
		||||
const form = ref({
 | 
			
		||||
  mobile: '',
 | 
			
		||||
  code: '',
 | 
			
		||||
  password: '',
 | 
			
		||||
  repass: ''
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const emits = defineEmits(['hide']);
 | 
			
		||||
 | 
			
		||||
const save = () => {
 | 
			
		||||
  if (!validateMobile(form.value.mobile)) {
 | 
			
		||||
    return ElMessage.error("请输入正确的手机号码");
 | 
			
		||||
  }
 | 
			
		||||
  if (form.value.code === '') {
 | 
			
		||||
    return ElMessage.error("请输入短信验证码");
 | 
			
		||||
  }
 | 
			
		||||
  if (form.value.repass !== form.value.password) {
 | 
			
		||||
    return ElMessage.error("两次输入密码不一致");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  httpPost('/api/user/resetPass', form.value).then(() => {
 | 
			
		||||
    ElMessage.success({
 | 
			
		||||
      message: '重置密码成功', duration: 1000, onClose: () => emits('hide', false)
 | 
			
		||||
    })
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    ElMessage.error("重置密码失败:" + e.message);
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const close = function () {
 | 
			
		||||
  emits('hide', false);
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus">
 | 
			
		||||
.reset-pass {
 | 
			
		||||
  .form {
 | 
			
		||||
    padding 10px 40px
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .el-dialog__footer {
 | 
			
		||||
    text-align center
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="user-bill" v-loading="loading">
 | 
			
		||||
    <el-row>
 | 
			
		||||
    <el-row v-if="items.length > 0">
 | 
			
		||||
      <el-table :data="items" :row-key="row => row.id" table-layout="auto" border
 | 
			
		||||
                style="--el-table-border-color:#373C47;
 | 
			
		||||
                --el-table-tr-bg-color:#2D323B;
 | 
			
		||||
@@ -31,7 +31,7 @@
 | 
			
		||||
        </el-table-column>
 | 
			
		||||
      </el-table>
 | 
			
		||||
    </el-row>
 | 
			
		||||
 | 
			
		||||
    <el-empty :image-size="100" v-else/>
 | 
			
		||||
    <div class="pagination">
 | 
			
		||||
      <el-pagination v-if="total > 0" background
 | 
			
		||||
                     layout="total,prev, pager, next"
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ export function setSessionId(sessionId) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getUserToken() {
 | 
			
		||||
    return Storage.get(UserTokenKey)
 | 
			
		||||
    return Storage.get(UserTokenKey) ?? ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function setUserToken(token) {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,6 @@ export function validateEmail(email) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function validateMobile(mobile) {
 | 
			
		||||
    const regex = /^1[345789]\d{9}$/;
 | 
			
		||||
    const regex = /^1[3456789]\d{9}$/;
 | 
			
		||||
    return regex.test(mobile);
 | 
			
		||||
}
 | 
			
		||||