mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-09-18 01:06:39 +08:00
Merge branch 'alipay'
This commit is contained in:
commit
05f501af52
@ -1,8 +1,9 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
## v3.1.7
|
## v3.1.8
|
||||||
1. Bug修复:修复 MidJourney API 参数版本更新导致调用失败的 Bug
|
1. 功能新增:新增会员套餐充值,点卡充值,订单系统,集成支付宝支付通道
|
||||||
2. 功能优化:将聊天报错信息定义为统一常量,方便修改
|
2. Bug修复:修复 MidJourney API 参数版本更新导致调用失败的 Bug
|
||||||
|
3. 功能优化:将聊天报错信息定义为统一常量,方便修改
|
||||||
|
|
||||||
## v3.1.7
|
## v3.1.7
|
||||||
1. 功能新增:支持文心4.0 AI 模型
|
1. 功能新增:支持文心4.0 AI 模型
|
||||||
|
28
README.md
28
README.md
@ -8,7 +8,8 @@ ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了
|
|||||||
* 内置了各种预训练好的角色应用,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
|
* 内置了各种预训练好的角色应用,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
|
||||||
* 支持 OPenAI,Azure,文心一言,讯飞星火,清华 ChatGLM等多个大语言模型。
|
* 支持 OPenAI,Azure,文心一言,讯飞星火,清华 ChatGLM等多个大语言模型。
|
||||||
* 支持 MidJourney / Stable Diffusion AI 绘画集成,开箱即用。
|
* 支持 MidJourney / Stable Diffusion AI 绘画集成,开箱即用。
|
||||||
* 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。(可定制开发其他支付通道支持)
|
* 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。
|
||||||
|
* 已集成支付宝支付功能,支持多种会员套餐和点卡购买功能。
|
||||||
* 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI 绘画函数插件。
|
* 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI 绘画函数插件。
|
||||||
|
|
||||||
## 功能截图
|
## 功能截图
|
||||||
@ -133,7 +134,7 @@ cd docker/mysql
|
|||||||
# 创建 mysql 容器
|
# 创建 mysql 容器
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
# 导入数据库
|
# 导入数据库
|
||||||
docker exec -i chatgpt-plus-mysql sh -c 'exec mysql -uroot -p12345678' < ../../database/chatgpt_plus-v3.1.7.sql
|
docker exec -i chatgpt-plus-mysql sh -c 'exec mysql -uroot -p12345678' < ../../database/chatgpt_plus-v3.1.8.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
如果你本地已经安装了 MySQL 服务,那么你只需手动导入数据库即可。
|
如果你本地已经安装了 MySQL 服务,那么你只需手动导入数据库即可。
|
||||||
@ -218,6 +219,25 @@ WeChatBot = false # 是否启动微信机器人
|
|||||||
ApiURL = "http://172.22.11.200:7860" # stable-diffusion-webui API 地址
|
ApiURL = "http://172.22.11.200:7860" # stable-diffusion-webui API 地址
|
||||||
ApiKey = "" # 如果开启了授权,这里需要配置授权的 ApiKey
|
ApiKey = "" # 如果开启了授权,这里需要配置授权的 ApiKey
|
||||||
Txt2ImgJsonPath = "res/text2img.json" # 文生图的 API 请求报文 json 模板,允许自定义请求json报文,因为不同版本的 API 绘图的参数以及 fn_index 会不同。
|
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
|
> 1. 如果你不知道如何获取 Discord 用户 Token 和 Bot Token
|
||||||
@ -259,7 +279,7 @@ version: '3'
|
|||||||
services:
|
services:
|
||||||
# 后端 API 镜像
|
# 后端 API 镜像
|
||||||
chatgpt-plus-api:
|
chatgpt-plus-api:
|
||||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:v3.1.5 #这里改成最新的 release 版本地址
|
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:v3.1.8 #这里改成最新的 release 版本
|
||||||
container_name: chatgpt-plus-api
|
container_name: chatgpt-plus-api
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
@ -276,7 +296,7 @@ services:
|
|||||||
|
|
||||||
# 前端应用镜像
|
# 前端应用镜像
|
||||||
chatgpt-plus-web:
|
chatgpt-plus-web:
|
||||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:v3.1.5 #这里改成最新的 release 版本地址
|
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:v3.1.8 #这里改成最新的 release 版本
|
||||||
container_name: chatgpt-plus-web
|
container_name: chatgpt-plus-web
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
|
1
api/.gitignore
vendored
1
api/.gitignore
vendored
@ -18,3 +18,4 @@ data
|
|||||||
config.toml
|
config.toml
|
||||||
static/upload
|
static/upload
|
||||||
storage.json
|
storage.json
|
||||||
|
certs/alipay/*
|
||||||
|
@ -68,3 +68,22 @@ WeChatBot = false
|
|||||||
ApiURL = "http://172.22.11.200:7860"
|
ApiURL = "http://172.22.11.200:7860"
|
||||||
ApiKey = ""
|
ApiKey = ""
|
||||||
Txt2ImgJsonPath = "res/text2img.json"
|
Txt2ImgJsonPath = "res/text2img.json"
|
||||||
|
|
||||||
|
[XXLConfig] # xxl-job 配置,需要你部署 XXL-JOB 定时任务工具,用来定期清理未支付订单和清理过期 VIP,如果你没有启用支付服务,则该服务也无需启动
|
||||||
|
Enabled = false # 是否启用 XXL JOB 服务
|
||||||
|
ServerAddr = "http://172.22.11.47:8080/xxl-job-admin" # xxl-job-admin 管理地址
|
||||||
|
ExecutorIp = "172.22.11.47" # 执行器 IP 地址
|
||||||
|
ExecutorPort = "9999" # 执行器服务端口
|
||||||
|
AccessToken = "xxl-job-api-token" # 执行器 API 通信 token
|
||||||
|
RegistryKey = "chatgpt-plus" # 任务注册 key
|
||||||
|
|
||||||
|
[AlipayConfig]
|
||||||
|
Enabled = false # 启用支付宝支付通道
|
||||||
|
SandBox = false # 是否启用沙盒模式
|
||||||
|
UserId = "2088721020750581" # 商户ID
|
||||||
|
AppId = "9021000131658023" # App Id
|
||||||
|
PrivateKey = "certs/alipay/privateKey.txt" # 应用私钥
|
||||||
|
PublicKey = "certs/alipay/appPublicCert.crt" # 应用公钥证书
|
||||||
|
AlipayPublicKey = "certs/alipay/alipayPublicCert.crt" # 支付宝公钥证书
|
||||||
|
RootCert = "certs/alipay/alipayRootCert.crt" # 支付宝根证书
|
||||||
|
NotifyURL = "http://r9it.com:6004/api/payment/alipay/notify" # 支付异步回调地址
|
@ -151,6 +151,7 @@ func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
|
|||||||
c.Request.URL.Path == "/api/sd/jobs" ||
|
c.Request.URL.Path == "/api/sd/jobs" ||
|
||||||
strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
|
strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
|
||||||
strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") ||
|
strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") ||
|
||||||
|
strings.HasPrefix(c.Request.URL.Path, "/api/payment/") ||
|
||||||
strings.HasPrefix(c.Request.URL.Path, "/static/") ||
|
strings.HasPrefix(c.Request.URL.Path, "/static/") ||
|
||||||
c.Request.URL.Path == "/api/admin/config/get" {
|
c.Request.URL.Path == "/api/admin/config/get" {
|
||||||
c.Next()
|
c.Next()
|
||||||
|
@ -33,9 +33,10 @@ func NewDefaultConfig() *types.AppConfig {
|
|||||||
BasePath: "./static/upload",
|
BasePath: "./static/upload",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
MjConfig: types.MidJourneyConfig{Enabled: false},
|
MjConfig: types.MidJourneyConfig{Enabled: false},
|
||||||
SdConfig: types.StableDiffusionConfig{Enabled: false, Txt2ImgJsonPath: "res/text2img.json"},
|
SdConfig: types.StableDiffusionConfig{Enabled: false, Txt2ImgJsonPath: "res/text2img.json"},
|
||||||
WeChatBot: false,
|
WeChatBot: false,
|
||||||
|
AlipayConfig: types.AlipayConfig{Enabled: false, SandBox: false},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,9 @@ type AppConfig struct {
|
|||||||
MjConfig MidJourneyConfig // mj 绘画配置
|
MjConfig MidJourneyConfig // mj 绘画配置
|
||||||
WeChatBot bool // 是否启用微信机器人
|
WeChatBot bool // 是否启用微信机器人
|
||||||
SdConfig StableDiffusionConfig // sd 绘画配置
|
SdConfig StableDiffusionConfig // sd 绘画配置
|
||||||
|
|
||||||
|
XXLConfig XXLConfig
|
||||||
|
AlipayConfig AlipayConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChatPlusApiConfig struct {
|
type ChatPlusApiConfig struct {
|
||||||
@ -57,6 +60,27 @@ type AliYunSmsConfig struct {
|
|||||||
CodeTempId string // 验证码短信模板 ID
|
CodeTempId string // 验证码短信模板 ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AlipayConfig struct {
|
||||||
|
Enabled bool // 是否启用该服务
|
||||||
|
SandBox bool // 是否沙盒环境
|
||||||
|
AppId string // 应用 ID
|
||||||
|
UserId string // 支付宝用户 ID
|
||||||
|
PrivateKey string // 用户私钥文件路径
|
||||||
|
PublicKey string // 用户公钥文件路径
|
||||||
|
AlipayPublicKey string // 支付宝公钥文件路径
|
||||||
|
RootCert string // Root 秘钥路径
|
||||||
|
NotifyURL string // 异步通知回调
|
||||||
|
}
|
||||||
|
|
||||||
|
type XXLConfig struct { // XXL 任务调度配置
|
||||||
|
Enabled bool
|
||||||
|
ServerAddr string
|
||||||
|
ExecutorIp string
|
||||||
|
ExecutorPort string
|
||||||
|
AccessToken string
|
||||||
|
RegistryKey string
|
||||||
|
}
|
||||||
|
|
||||||
type RedisConfig struct {
|
type RedisConfig struct {
|
||||||
Host string
|
Host string
|
||||||
Port int
|
Port int
|
||||||
@ -115,10 +139,12 @@ type SystemConfig struct {
|
|||||||
InitImgCalls int `json:"init_img_calls"`
|
InitImgCalls int `json:"init_img_calls"`
|
||||||
VipMonthCalls int `json:"vip_month_calls"` // 会员每个赠送的调用次数
|
VipMonthCalls int `json:"vip_month_calls"` // 会员每个赠送的调用次数
|
||||||
EnabledRegister bool `json:"enabled_register"`
|
EnabledRegister bool `json:"enabled_register"`
|
||||||
EnabledMsg bool `json:"enabled_msg"` // 启用短信验证码服务
|
EnabledMsg bool `json:"enabled_msg"` // 启用短信验证码服务
|
||||||
EnabledDraw bool `json:"enabled_draw"` // 启动 AI 绘画功能
|
EnabledDraw bool `json:"enabled_draw"` // 启动 AI 绘画功能
|
||||||
RewardImg string `json:"reward_img"` // 众筹收款二维码地址
|
RewardImg string `json:"reward_img"` // 众筹收款二维码地址
|
||||||
EnabledFunction bool `json:"enabled_function"` // 启用 API 函数功能
|
EnabledFunction bool `json:"enabled_function"` // 启用 API 函数功能
|
||||||
EnabledReward bool `json:"enabled_reward"` // 启用众筹功能
|
EnabledReward bool `json:"enabled_reward"` // 启用众筹功能
|
||||||
DefaultModels []string `json:"default_models"` // 默认开通的 AI 模型
|
EnabledAlipay bool `json:"enabled_alipay"` // 是否启用支付宝支付通道
|
||||||
|
OrderPayTimeout int `json:"order_pay_timeout"` //订单支付超时时间
|
||||||
|
DefaultModels []string `json:"default_models"` // 默认开通的 AI 模型
|
||||||
}
|
}
|
||||||
|
17
api/core/types/order.go
Normal file
17
api/core/types/order.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type OrderStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
OrderNotPaid = OrderStatus(0)
|
||||||
|
OrderScanned = OrderStatus(1) // 已扫码
|
||||||
|
OrderPaidSuccess = OrderStatus(2)
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderRemark struct {
|
||||||
|
Days int `json:"days"` // 有效期
|
||||||
|
Calls int `json:"calls"` // 增加调用次数
|
||||||
|
Name string `json:"name"` // 产品名称
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
Discount float64 `json:"discount"`
|
||||||
|
}
|
@ -18,12 +18,15 @@ require (
|
|||||||
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480
|
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480
|
||||||
github.com/qiniu/go-sdk/v7 v7.17.1
|
github.com/qiniu/go-sdk/v7 v7.17.1
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
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
|
github.com/syndtr/goleveldb v1.0.0
|
||||||
go.uber.org/zap v1.23.0
|
go.uber.org/zap v1.23.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gorm.io/driver/mysql v1.4.7
|
gorm.io/driver/mysql v1.4.7
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require github.com/xxl-job/xxl-job-executor-go v1.2.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||||
github.com/bytedance/sonic v1.9.1 // indirect
|
github.com/bytedance/sonic v1.9.1 // indirect
|
||||||
@ -34,6 +37,7 @@ require (
|
|||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
github.com/gaukas/godicttls v0.0.3 // indirect
|
github.com/gaukas/godicttls v0.0.3 // indirect
|
||||||
|
github.com/go-basic/ipv4 v1.0.0 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
@ -49,6 +53,7 @@ require (
|
|||||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||||
github.com/minio/md5-simd v1.1.2 // indirect
|
github.com/minio/md5-simd v1.1.2 // indirect
|
||||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||||
github.com/onsi/ginkgo/v2 v2.10.0 // indirect
|
github.com/onsi/ginkgo/v2 v2.10.0 // indirect
|
||||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||||
@ -59,6 +64,9 @@ require (
|
|||||||
github.com/refraction-networking/utls v1.3.2 // indirect
|
github.com/refraction-networking/utls v1.3.2 // indirect
|
||||||
github.com/rs/xid v1.5.0 // indirect
|
github.com/rs/xid v1.5.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
|
github.com/smartwalle/ncrypto v1.0.2 // indirect
|
||||||
|
github.com/smartwalle/ngx v1.0.6 // indirect
|
||||||
|
github.com/smartwalle/nsign v1.0.8 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
go.uber.org/dig v1.16.1 // indirect
|
go.uber.org/dig v1.16.1 // indirect
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
|
14
api/go.sum
14
api/go.sum
@ -39,6 +39,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
|
|||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||||
|
github.com/go-basic/ipv4 v1.0.0 h1:gjyFAa1USC1hhXTkPOwBWDPfMcUaIM+tvo1XzV9EZxs=
|
||||||
|
github.com/go-basic/ipv4 v1.0.0/go.mod h1:etLBnaxbidQfuqE6wgZQfs38nEWNmzALkxDZe4xY8Dg=
|
||||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
@ -133,6 +135,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
|
|||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||||
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
github.com/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.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.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
@ -175,6 +179,14 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
|
|||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||||
|
github.com/smartwalle/alipay/v3 v3.2.15 h1:3fvFJnINKKAOXHR/Iv20k1Z7KJ+nOh3oK214lELPqG8=
|
||||||
|
github.com/smartwalle/alipay/v3 v3.2.15/go.mod h1:niTNB609KyUYuAx9Bex/MawEjv2yPx4XOjxSAkqmGjE=
|
||||||
|
github.com/smartwalle/ncrypto v1.0.2 h1:pTAhCqtPCMhpOwFXX+EcMdR6PNzruBNoGQrN2S1GbGI=
|
||||||
|
github.com/smartwalle/ncrypto v1.0.2/go.mod h1:Dwlp6sfeNaPMnOxMNayMTacvC5JGEVln3CVdiVDgbBk=
|
||||||
|
github.com/smartwalle/ngx v1.0.6 h1:JPNqNOIj+2nxxFtrSkJO+vKJfeNUSEQueck/Wworjps=
|
||||||
|
github.com/smartwalle/ngx v1.0.6/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0=
|
||||||
|
github.com/smartwalle/nsign v1.0.8 h1:78KWtwKPrdt4Xsn+tNEBVxaTLIJBX9YRX0ZSrMUeuHo=
|
||||||
|
github.com/smartwalle/nsign v1.0.8/go.mod h1:eY6I4CJlyNdVMP+t6z1H6Jpd4m5/V+8xi44ufSTxXgc=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
@ -197,6 +209,8 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK
|
|||||||
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
||||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
github.com/xxl-job/xxl-job-executor-go v1.2.0 h1:MTl2DpwrK2+hNjRRks2k7vB3oy+3onqm9OaSarneeLQ=
|
||||||
|
github.com/xxl-job/xxl-job-executor-go v1.2.0/go.mod h1:bUFhz/5Irp9zkdYk5MxhQcDDT6LlZrI8+rv5mHtQ1mo=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
93
api/handler/admin/order_handler.go
Normal file
93
api/handler/admin/order_handler.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core"
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/handler"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"chatplus/utils/resp"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderHandler struct {
|
||||||
|
handler.BaseHandler
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOrderHandler(app *core.AppServer, db *gorm.DB) *OrderHandler {
|
||||||
|
h := OrderHandler{db: db}
|
||||||
|
h.App = app
|
||||||
|
return &h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *OrderHandler) List(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
OrderNo string `json:"order_no"`
|
||||||
|
PayTime []string `json:"pay_time"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"page_size"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session := h.db.Session(&gorm.Session{})
|
||||||
|
if data.OrderNo != "" {
|
||||||
|
session = session.Where("order_no", data.OrderNo)
|
||||||
|
}
|
||||||
|
if len(data.PayTime) == 2 {
|
||||||
|
start := utils.Str2stamp(data.PayTime[0] + " 00:00:00")
|
||||||
|
end := utils.Str2stamp(data.PayTime[1] + " 00:00:00")
|
||||||
|
session = session.Where("pay_time >= ? AND pay_time <= ?", start, end)
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
session.Model(&model.Order{}).Count(&total)
|
||||||
|
var items []model.Order
|
||||||
|
var list = make([]vo.Order, 0)
|
||||||
|
offset := (data.Page - 1) * data.PageSize
|
||||||
|
res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
|
||||||
|
if res.Error == nil {
|
||||||
|
for _, item := range items {
|
||||||
|
var order vo.Order
|
||||||
|
err := utils.CopyObject(item, &order)
|
||||||
|
if err == nil {
|
||||||
|
order.Id = item.Id
|
||||||
|
order.CreatedAt = item.CreatedAt.Unix()
|
||||||
|
order.UpdatedAt = item.UpdatedAt.Unix()
|
||||||
|
list = append(list, order)
|
||||||
|
} else {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *OrderHandler) Remove(c *gin.Context) {
|
||||||
|
id := h.GetInt(c, "id", 0)
|
||||||
|
|
||||||
|
if id > 0 {
|
||||||
|
var item model.Order
|
||||||
|
res := h.db.First(&item, id)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "记录不存在!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.Status == types.OrderPaidSuccess {
|
||||||
|
resp.ERROR(c, "已支付订单不允许删除!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res = h.db.Where("id = ?", id).Delete(&model.Order{})
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c)
|
||||||
|
}
|
144
api/handler/admin/product_handler.go
Normal file
144
api/handler/admin/product_handler.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core"
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/handler"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"chatplus/utils/resp"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductHandler struct {
|
||||||
|
handler.BaseHandler
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductHandler(app *core.AppServer, db *gorm.DB) *ProductHandler {
|
||||||
|
h := ProductHandler{db: db}
|
||||||
|
h.App = app
|
||||||
|
return &h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) Save(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
Discount float64 `json:"discount"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Days int `json:"days"`
|
||||||
|
Calls int `json:"calls"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item := model.Product{Name: data.Name, Price: data.Price, Discount: data.Discount, Days: data.Days, Calls: data.Calls, Enabled: data.Enabled}
|
||||||
|
item.Id = data.Id
|
||||||
|
if item.Id > 0 {
|
||||||
|
item.CreatedAt = time.Unix(data.CreatedAt, 0)
|
||||||
|
}
|
||||||
|
res := h.db.Save(&item)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemVo vo.Product
|
||||||
|
err := utils.CopyObject(item, &itemVo)
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, "数据拷贝失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
itemVo.Id = item.Id
|
||||||
|
itemVo.UpdatedAt = item.UpdatedAt.Unix()
|
||||||
|
resp.SUCCESS(c, itemVo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 模型列表
|
||||||
|
func (h *ProductHandler) List(c *gin.Context) {
|
||||||
|
session := h.db.Session(&gorm.Session{})
|
||||||
|
enable := h.GetBool(c, "enable")
|
||||||
|
if enable {
|
||||||
|
session = session.Where("enabled", enable)
|
||||||
|
}
|
||||||
|
var items []model.Product
|
||||||
|
var list = make([]vo.Product, 0)
|
||||||
|
res := session.Order("sort_num ASC").Find(&items)
|
||||||
|
if res.Error == nil {
|
||||||
|
for _, item := range items {
|
||||||
|
var product vo.Product
|
||||||
|
err := utils.CopyObject(item, &product)
|
||||||
|
if err == nil {
|
||||||
|
product.Id = item.Id
|
||||||
|
product.CreatedAt = item.CreatedAt.Unix()
|
||||||
|
product.UpdatedAt = item.UpdatedAt.Unix()
|
||||||
|
list = append(list, product)
|
||||||
|
} else {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) Enable(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := h.db.Model(&model.Product{}).Where("id = ?", data.Id).Update("enabled", data.Enabled)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) Sort(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
Ids []uint `json:"ids"`
|
||||||
|
Sorts []int `json:"sorts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for index, id := range data.Ids {
|
||||||
|
res := h.db.Model(&model.Product{}).Where("id = ?", id).Update("sort_num", data.Sorts[index])
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) Remove(c *gin.Context) {
|
||||||
|
id := h.GetInt(c, "id", 0)
|
||||||
|
|
||||||
|
if id > 0 {
|
||||||
|
res := h.db.Where("id = ?", id).Delete(&model.Product{})
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c)
|
||||||
|
}
|
@ -242,8 +242,7 @@ func (h *ChatHandler) sendAzureMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新用户信息
|
// 更新用户信息
|
||||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
|
h.incUserTokenFee(userVo.Id, totalTokens)
|
||||||
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存当前会话
|
// 保存当前会话
|
||||||
|
@ -9,7 +9,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gorm.io/gorm"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@ -184,8 +183,7 @@ func (h *ChatHandler) sendBaiduMessage(
|
|||||||
logger.Error("failed to save reply history message: ", res.Error)
|
logger.Error("failed to save reply history message: ", res.Error)
|
||||||
}
|
}
|
||||||
// 更新用户信息
|
// 更新用户信息
|
||||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
|
h.incUserTokenFee(userVo.Id, totalTokens)
|
||||||
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存当前会话
|
// 保存当前会话
|
||||||
|
@ -187,8 +187,14 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if userVo.Calls < session.Model.Weight {
|
||||||
|
utils.ReplyMessage(ws, fmt.Sprintf("您当前剩余对话次数(%d)已不足以支付当前模型的单次对话需要消耗的对话额度(%d)!", userVo.Calls, session.Model.Weight))
|
||||||
|
utils.ReplyMessage(ws, ErrImg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if userVo.Calls <= 0 && userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
|
if userVo.Calls <= 0 && userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
|
||||||
utils.ReplyMessage(ws, "您的对话次数已经用尽,请联系管理员或者点击左下角菜单加入众筹获得100次对话!")
|
utils.ReplyMessage(ws, "您的对话次数已经用尽,请联系管理员或者充值点卡继续对话!")
|
||||||
utils.ReplyMessage(ws, ErrImg)
|
utils.ReplyMessage(ws, ErrImg)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -479,3 +485,10 @@ func (h *ChatHandler) subUserCalls(userVo vo.User, session *types.ChatSession) {
|
|||||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", num))
|
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", num))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *ChatHandler) incUserTokenFee(userId uint, tokens int) {
|
||||||
|
h.db.Model(&model.User{}).Where("id = ?", userId).
|
||||||
|
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", tokens))
|
||||||
|
h.db.Model(&model.User{}).Where("id = ?", userId).
|
||||||
|
UpdateColumn("tokens", gorm.Expr("tokens + ?", tokens))
|
||||||
|
}
|
||||||
|
@ -10,7 +10,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"gorm.io/gorm"
|
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -164,8 +163,7 @@ func (h *ChatHandler) sendChatGLMMessage(
|
|||||||
logger.Error("failed to save reply history message: ", res.Error)
|
logger.Error("failed to save reply history message: ", res.Error)
|
||||||
}
|
}
|
||||||
// 更新用户信息
|
// 更新用户信息
|
||||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
|
h.incUserTokenFee(userVo.Id, totalTokens)
|
||||||
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存当前会话
|
// 保存当前会话
|
||||||
|
@ -241,8 +241,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新用户信息
|
// 更新用户信息
|
||||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
|
h.incUserTokenFee(userVo.Id, totalTokens)
|
||||||
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存当前会话
|
// 保存当前会话
|
||||||
|
@ -12,7 +12,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"gorm.io/gorm"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -227,8 +226,7 @@ func (h *ChatHandler) sendXunFeiMessage(
|
|||||||
logger.Error("failed to save reply history message: ", res.Error)
|
logger.Error("failed to save reply history message: ", res.Error)
|
||||||
}
|
}
|
||||||
// 更新用户信息
|
// 更新用户信息
|
||||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
|
h.incUserTokenFee(userVo.Id, totalTokens)
|
||||||
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存当前会话
|
// 保存当前会话
|
||||||
|
57
api/handler/order_handler.go
Normal file
57
api/handler/order_handler.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core"
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"chatplus/utils/resp"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderHandler struct {
|
||||||
|
BaseHandler
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOrderHandler(app *core.AppServer, db *gorm.DB) *OrderHandler {
|
||||||
|
h := OrderHandler{db: db}
|
||||||
|
h.App = app
|
||||||
|
return &h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *OrderHandler) List(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"page_size"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user, _ := utils.GetLoginUser(c, h.db)
|
||||||
|
session := h.db.Session(&gorm.Session{}).Where("user_id = ? AND status = ?", user.Id, types.OrderPaidSuccess)
|
||||||
|
var total int64
|
||||||
|
session.Model(&model.Order{}).Count(&total)
|
||||||
|
var items []model.Order
|
||||||
|
var list = make([]vo.Order, 0)
|
||||||
|
offset := (data.Page - 1) * data.PageSize
|
||||||
|
res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
|
||||||
|
if res.Error == nil {
|
||||||
|
for _, item := range items {
|
||||||
|
var order vo.Order
|
||||||
|
err := utils.CopyObject(item, &order)
|
||||||
|
if err == nil {
|
||||||
|
order.Id = item.Id
|
||||||
|
order.CreatedAt = item.CreatedAt.Unix()
|
||||||
|
order.UpdatedAt = item.UpdatedAt.Unix()
|
||||||
|
list = append(list, order)
|
||||||
|
} else {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
|
||||||
|
}
|
273
api/handler/payment_handler.go
Normal file
273
api/handler/payment_handler.go
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core"
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/service"
|
||||||
|
"chatplus/service/payment"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/utils"
|
||||||
|
"chatplus/utils/resp"
|
||||||
|
"embed"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PayWayAlipay = "支付宝"
|
||||||
|
PayWayWechat = "微信支付"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PaymentHandler 支付服务回调 handler
|
||||||
|
type PaymentHandler struct {
|
||||||
|
BaseHandler
|
||||||
|
alipayService *payment.AlipayService
|
||||||
|
snowflake *service.Snowflake
|
||||||
|
db *gorm.DB
|
||||||
|
fs embed.FS
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPaymentHandler(server *core.AppServer, alipayService *payment.AlipayService, snowflake *service.Snowflake, db *gorm.DB, fs embed.FS) *PaymentHandler {
|
||||||
|
h := PaymentHandler{lock: sync.Mutex{}}
|
||||||
|
h.App = server
|
||||||
|
h.alipayService = alipayService
|
||||||
|
h.snowflake = snowflake
|
||||||
|
h.db = db
|
||||||
|
h.fs = fs
|
||||||
|
return &h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PaymentHandler) Alipay(c *gin.Context) {
|
||||||
|
orderNo := h.GetTrim(c, "order_no")
|
||||||
|
if orderNo == "" {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var order model.Order
|
||||||
|
res := h.db.Where("order_no = ?", orderNo).First(&order)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "Order not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新扫码状态
|
||||||
|
h.db.Model(&order).UpdateColumn("status", types.OrderScanned)
|
||||||
|
// 生成支付链接
|
||||||
|
notifyURL := h.App.Config.AlipayConfig.NotifyURL
|
||||||
|
returnURL := "" // 关闭同步回跳
|
||||||
|
amount := fmt.Sprintf("%.2f", order.Amount)
|
||||||
|
|
||||||
|
uri, err := h.alipayService.PayUrlMobile(order.OrderNo, notifyURL, returnURL, amount, order.Subject)
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, "error with generate pay url: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(302, uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrderQuery 清单状态查询
|
||||||
|
func (h *PaymentHandler) OrderQuery(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
OrderNo string `json:"order_no"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var order model.Order
|
||||||
|
res := h.db.Where("order_no = ?", data.OrderNo).First(&order)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "Order not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if order.Status == types.OrderPaidSuccess {
|
||||||
|
resp.SUCCESS(c, gin.H{"status": order.Status})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
counter := 0
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
var item model.Order
|
||||||
|
h.db.Where("order_no = ?", data.OrderNo).First(&item)
|
||||||
|
if counter >= 15 || item.Status == types.OrderPaidSuccess || item.Status != order.Status {
|
||||||
|
order.Status = item.Status
|
||||||
|
break
|
||||||
|
}
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c, gin.H{"status": order.Status})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlipayQrcode 生成支付宝支付 URL 二维码
|
||||||
|
func (h *PaymentHandler) AlipayQrcode(c *gin.Context) {
|
||||||
|
if !h.App.SysConfig.EnabledAlipay || h.alipayService == nil {
|
||||||
|
resp.ERROR(c, "当前支付通道已经关闭,请联系管理员开通!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
ProductId uint `json:"product_id"`
|
||||||
|
UserId int `json:"user_id"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var product model.Product
|
||||||
|
res := h.db.First(&product, data.ProductId)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "Product not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orderNo, err := h.snowflake.Next()
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, "error with generate trade no: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var user model.User
|
||||||
|
res = h.db.First(&user, data.UserId)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "Invalid user ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建订单
|
||||||
|
remark := types.OrderRemark{
|
||||||
|
Days: product.Days,
|
||||||
|
Calls: product.Calls,
|
||||||
|
Name: product.Name,
|
||||||
|
Price: product.Price,
|
||||||
|
Discount: product.Discount,
|
||||||
|
}
|
||||||
|
order := model.Order{
|
||||||
|
UserId: user.Id,
|
||||||
|
Mobile: user.Mobile,
|
||||||
|
ProductId: product.Id,
|
||||||
|
OrderNo: orderNo,
|
||||||
|
Subject: product.Name,
|
||||||
|
Amount: product.Price - product.Discount,
|
||||||
|
Status: types.OrderNotPaid,
|
||||||
|
PayWay: PayWayAlipay,
|
||||||
|
Remark: utils.JsonEncode(remark),
|
||||||
|
}
|
||||||
|
res = h.db.Create(&order)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "error with create order: "+res.Error.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成二维码图片
|
||||||
|
file, err := h.fs.Open("res/img/alipay.jpg")
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parse, err := url.Parse(h.App.Config.AlipayConfig.NotifyURL)
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
imageURL := fmt.Sprintf("%s://%s/api/payment/alipay?order_no=%s", parse.Scheme, parse.Host, orderNo)
|
||||||
|
imgData, err := utils.GenQrcode(imageURL, 400, file)
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
imgDataBase64 := base64.StdEncoding.EncodeToString(imgData)
|
||||||
|
resp.SUCCESS(c, gin.H{"order_no": orderNo, "image": fmt.Sprintf("data:image/jpg;base64, %s", imgDataBase64), "url": imageURL})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
|
||||||
|
err := c.Request.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusOK, "fail")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO:这里最好用支付宝的公钥签名签证一下交易真假
|
||||||
|
//res := h.alipayService.TradeVerify(c.Request.Form)
|
||||||
|
r := h.alipayService.TradeQuery(c.Request.Form.Get("out_trade_no"))
|
||||||
|
logger.Infof("验证支付结果:%+v", r)
|
||||||
|
if !r.Success() {
|
||||||
|
c.String(http.StatusOK, "fail")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.lock.Lock()
|
||||||
|
defer h.lock.Unlock()
|
||||||
|
|
||||||
|
var order model.Order
|
||||||
|
res := h.db.Where("order_no = ?", r.OutTradeNo).First(&order)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Error(res.Error)
|
||||||
|
c.String(http.StatusOK, "fail")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var user model.User
|
||||||
|
res = h.db.First(&user, order.UserId)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Error(res.Error)
|
||||||
|
c.String(http.StatusOK, "fail")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var remark types.OrderRemark
|
||||||
|
err = utils.JsonDecode(order.Remark, &remark)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(res.Error)
|
||||||
|
c.String(http.StatusOK, "fail")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 1. 点卡:days == 0, calls > 0
|
||||||
|
// 2. vip 套餐:days > 0, calls == 0
|
||||||
|
if remark.Days > 0 {
|
||||||
|
if user.ExpiredTime > time.Now().Unix() {
|
||||||
|
user.ExpiredTime = time.Unix(user.ExpiredTime, 0).AddDate(0, 0, remark.Days).Unix()
|
||||||
|
} else {
|
||||||
|
user.ExpiredTime = time.Now().AddDate(0, 0, remark.Days).Unix()
|
||||||
|
}
|
||||||
|
user.Vip = true
|
||||||
|
|
||||||
|
} else if !user.Vip { // 充值点卡的非 VIP 用户
|
||||||
|
user.ExpiredTime = time.Now().AddDate(0, 0, 30).Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
if remark.Calls > 0 { // 充值点卡
|
||||||
|
user.Calls += remark.Calls
|
||||||
|
} else {
|
||||||
|
user.Calls += h.App.SysConfig.VipMonthCalls
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户信息
|
||||||
|
res = h.db.Updates(&user)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Error(res.Error)
|
||||||
|
c.String(http.StatusOK, "fail")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新订单状态
|
||||||
|
order.PayTime = time.Now().Unix()
|
||||||
|
order.Status = types.OrderPaidSuccess
|
||||||
|
h.db.Updates(&order)
|
||||||
|
|
||||||
|
// 更新产品销量
|
||||||
|
h.db.Model(&model.Product{}).Where("id = ?", order.ProductId).UpdateColumn("sales", gorm.Expr("sales + ?", 1))
|
||||||
|
|
||||||
|
c.String(http.StatusOK, "success")
|
||||||
|
}
|
44
api/handler/product_handler.go
Normal file
44
api/handler/product_handler.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"chatplus/utils/resp"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductHandler struct {
|
||||||
|
BaseHandler
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductHandler(app *core.AppServer, db *gorm.DB) *ProductHandler {
|
||||||
|
h := ProductHandler{db: db}
|
||||||
|
h.App = app
|
||||||
|
return &h
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 模型列表
|
||||||
|
func (h *ProductHandler) List(c *gin.Context) {
|
||||||
|
var items []model.Product
|
||||||
|
var list = make([]vo.Product, 0)
|
||||||
|
res := h.db.Where("enabled", true).Order("sort_num ASC").Find(&items)
|
||||||
|
if res.Error == nil {
|
||||||
|
for _, item := range items {
|
||||||
|
var product vo.Product
|
||||||
|
err := utils.CopyObject(item, &product)
|
||||||
|
if err == nil {
|
||||||
|
product.Id = item.Id
|
||||||
|
product.CreatedAt = item.CreatedAt.Unix()
|
||||||
|
product.UpdatedAt = item.UpdatedAt.Unix()
|
||||||
|
list = append(list, product)
|
||||||
|
} else {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c, list)
|
||||||
|
}
|
@ -230,6 +230,9 @@ type userProfile struct {
|
|||||||
Calls int `json:"calls"`
|
Calls int `json:"calls"`
|
||||||
ImgCalls int `json:"img_calls"`
|
ImgCalls int `json:"img_calls"`
|
||||||
TotalTokens int64 `json:"total_tokens"`
|
TotalTokens int64 `json:"total_tokens"`
|
||||||
|
Tokens int64 `json:"tokens"`
|
||||||
|
ExpiredTime int64 `json:"expired_time"`
|
||||||
|
Vip bool `json:"vip"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UserHandler) Profile(c *gin.Context) {
|
func (h *UserHandler) Profile(c *gin.Context) {
|
||||||
|
52
api/main.go
52
api/main.go
@ -11,6 +11,7 @@ import (
|
|||||||
"chatplus/service/fun"
|
"chatplus/service/fun"
|
||||||
"chatplus/service/mj"
|
"chatplus/service/mj"
|
||||||
"chatplus/service/oss"
|
"chatplus/service/oss"
|
||||||
|
"chatplus/service/payment"
|
||||||
"chatplus/service/sd"
|
"chatplus/service/sd"
|
||||||
"chatplus/service/wx"
|
"chatplus/service/wx"
|
||||||
"chatplus/store"
|
"chatplus/store"
|
||||||
@ -32,7 +33,7 @@ import (
|
|||||||
|
|
||||||
var logger = logger2.GetLogger()
|
var logger = logger2.GetLogger()
|
||||||
|
|
||||||
//go:embed res/ip2region.xdb
|
//go:embed res
|
||||||
var xdbFS embed.FS
|
var xdbFS embed.FS
|
||||||
|
|
||||||
// AppLifecycle 应用程序生命周期
|
// AppLifecycle 应用程序生命周期
|
||||||
@ -96,6 +97,10 @@ func main() {
|
|||||||
fx.Provide(store.NewLevelDB),
|
fx.Provide(store.NewLevelDB),
|
||||||
fx.Provide(store.NewRedisClient),
|
fx.Provide(store.NewRedisClient),
|
||||||
|
|
||||||
|
fx.Provide(func() embed.FS {
|
||||||
|
return xdbFS
|
||||||
|
}),
|
||||||
|
|
||||||
// 创建 Ip2Region 查询对象
|
// 创建 Ip2Region 查询对象
|
||||||
fx.Provide(func() (*xdb.Searcher, error) {
|
fx.Provide(func() (*xdb.Searcher, error) {
|
||||||
file, err := xdbFS.Open("res/ip2region.xdb")
|
file, err := xdbFS.Open("res/ip2region.xdb")
|
||||||
@ -124,6 +129,9 @@ func main() {
|
|||||||
fx.Provide(handler.NewMidJourneyHandler),
|
fx.Provide(handler.NewMidJourneyHandler),
|
||||||
fx.Provide(handler.NewChatModelHandler),
|
fx.Provide(handler.NewChatModelHandler),
|
||||||
fx.Provide(handler.NewSdJobHandler),
|
fx.Provide(handler.NewSdJobHandler),
|
||||||
|
fx.Provide(handler.NewPaymentHandler),
|
||||||
|
fx.Provide(handler.NewOrderHandler),
|
||||||
|
fx.Provide(handler.NewProductHandler),
|
||||||
|
|
||||||
fx.Provide(admin.NewConfigHandler),
|
fx.Provide(admin.NewConfigHandler),
|
||||||
fx.Provide(admin.NewAdminHandler),
|
fx.Provide(admin.NewAdminHandler),
|
||||||
@ -133,6 +141,8 @@ func main() {
|
|||||||
fx.Provide(admin.NewRewardHandler),
|
fx.Provide(admin.NewRewardHandler),
|
||||||
fx.Provide(admin.NewDashboardHandler),
|
fx.Provide(admin.NewDashboardHandler),
|
||||||
fx.Provide(admin.NewChatModelHandler),
|
fx.Provide(admin.NewChatModelHandler),
|
||||||
|
fx.Provide(admin.NewProductHandler),
|
||||||
|
fx.Provide(admin.NewOrderHandler),
|
||||||
|
|
||||||
// 创建服务
|
// 创建服务
|
||||||
fx.Provide(service.NewAliYunSmsService),
|
fx.Provide(service.NewAliYunSmsService),
|
||||||
@ -181,6 +191,18 @@ func main() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
fx.Provide(payment.NewAlipayService),
|
||||||
|
fx.Provide(service.NewSnowflake),
|
||||||
|
fx.Provide(service.NewXXLJobExecutor),
|
||||||
|
fx.Invoke(func(exec *service.XXLJobExecutor, config *types.AppConfig) {
|
||||||
|
if config.XXLConfig.Enabled {
|
||||||
|
go func() {
|
||||||
|
log.Fatal(exec.Run())
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
// 注册路由
|
// 注册路由
|
||||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) {
|
fx.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) {
|
||||||
group := s.Engine.Group("/api/role/")
|
group := s.Engine.Group("/api/role/")
|
||||||
@ -296,6 +318,34 @@ func main() {
|
|||||||
group.POST("sort", h.Sort)
|
group.POST("sort", h.Sort)
|
||||||
group.GET("remove", h.Remove)
|
group.GET("remove", h.Remove)
|
||||||
}),
|
}),
|
||||||
|
fx.Invoke(func(s *core.AppServer, h *handler.PaymentHandler) {
|
||||||
|
group := s.Engine.Group("/api/payment/")
|
||||||
|
group.GET("alipay", h.Alipay)
|
||||||
|
group.POST("query", h.OrderQuery)
|
||||||
|
group.POST("alipay/qrcode", h.AlipayQrcode)
|
||||||
|
group.POST("alipay/notify", h.AlipayNotify)
|
||||||
|
}),
|
||||||
|
fx.Invoke(func(s *core.AppServer, h *admin.ProductHandler) {
|
||||||
|
group := s.Engine.Group("/api/admin/product/")
|
||||||
|
group.POST("save", h.Save)
|
||||||
|
group.GET("list", h.List)
|
||||||
|
group.POST("enable", h.Enable)
|
||||||
|
group.POST("sort", h.Sort)
|
||||||
|
group.GET("remove", h.Remove)
|
||||||
|
}),
|
||||||
|
fx.Invoke(func(s *core.AppServer, h *admin.OrderHandler) {
|
||||||
|
group := s.Engine.Group("/api/admin/order/")
|
||||||
|
group.POST("list", h.List)
|
||||||
|
group.GET("remove", h.Remove)
|
||||||
|
}),
|
||||||
|
fx.Invoke(func(s *core.AppServer, h *handler.OrderHandler) {
|
||||||
|
group := s.Engine.Group("/api/order/")
|
||||||
|
group.POST("list", h.List)
|
||||||
|
}),
|
||||||
|
fx.Invoke(func(s *core.AppServer, h *handler.ProductHandler) {
|
||||||
|
group := s.Engine.Group("/api/product/")
|
||||||
|
group.GET("list", h.List)
|
||||||
|
}),
|
||||||
|
|
||||||
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
|
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
|
||||||
err := s.Run(db)
|
err := s.Run(db)
|
||||||
|
BIN
api/res/img/alipay.jpg
Normal file
BIN
api/res/img/alipay.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
@ -1,37 +1,38 @@
|
|||||||
{
|
{
|
||||||
"data": [
|
"data": [
|
||||||
"task(38194gitxp745ha)",
|
"task(m1wpaa4v60zedj8)",
|
||||||
"A beautiful Chinese girl riding on a tiger",
|
"a cute cat",
|
||||||
"",
|
"",
|
||||||
[],
|
[],
|
||||||
20,
|
20,
|
||||||
"Euler a",
|
"DPM++ 2M Karras",
|
||||||
false,
|
|
||||||
false,
|
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
7,
|
7,
|
||||||
-1,
|
|
||||||
-1,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
false,
|
|
||||||
512,
|
|
||||||
512,
|
512,
|
||||||
|
384,
|
||||||
true,
|
true,
|
||||||
0.7,
|
0.7,
|
||||||
2,
|
2,
|
||||||
"ESRGAN_4x",
|
"ESRGAN_4x",
|
||||||
|
10,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0,
|
"Use same checkpoint",
|
||||||
"Use same sampler",
|
"Use same sampler",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
[],
|
[],
|
||||||
"None",
|
"None",
|
||||||
null,
|
false,
|
||||||
|
"",
|
||||||
|
0.8,
|
||||||
|
-1,
|
||||||
|
false,
|
||||||
|
-1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
"positive",
|
"positive",
|
||||||
@ -54,45 +55,13 @@
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
0,
|
0,
|
||||||
"Not set",
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
1.3,
|
|
||||||
"Not set",
|
|
||||||
"Not set",
|
|
||||||
1.3,
|
|
||||||
"Not set",
|
|
||||||
1.3,
|
|
||||||
"Not set",
|
|
||||||
1.3,
|
|
||||||
1.3,
|
|
||||||
"Not set",
|
|
||||||
1.3,
|
|
||||||
"Not set",
|
|
||||||
1.3,
|
|
||||||
"Not set",
|
|
||||||
1.3,
|
|
||||||
"Not set",
|
|
||||||
1.3,
|
|
||||||
"Not set",
|
|
||||||
1.3,
|
|
||||||
"Not set",
|
|
||||||
false,
|
false,
|
||||||
"None",
|
|
||||||
null,
|
|
||||||
false,
|
|
||||||
50,
|
|
||||||
[],
|
[],
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
""
|
""
|
||||||
],
|
],
|
||||||
"event_data": null,
|
"event_data": null,
|
||||||
"fn_index": 232,
|
"fn_index": 96,
|
||||||
"session_hash": "3xedmn4nuzq"
|
"session_hash": "kmb0ojjfhdj"
|
||||||
}
|
}
|
@ -15,12 +15,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type QinNiuOss struct {
|
type QinNiuOss struct {
|
||||||
config *types.QiNiuOssConfig
|
config *types.QiNiuOssConfig
|
||||||
token string
|
mac *qbox.Mac
|
||||||
uploader *storage.FormUploader
|
putPolicy storage.PutPolicy
|
||||||
manager *storage.BucketManager
|
uploader *storage.FormUploader
|
||||||
proxyURL string
|
manager *storage.BucketManager
|
||||||
dir string
|
proxyURL string
|
||||||
|
dir string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewQiNiuOss(appConfig *types.AppConfig) QinNiuOss {
|
func NewQiNiuOss(appConfig *types.AppConfig) QinNiuOss {
|
||||||
@ -38,12 +39,13 @@ func NewQiNiuOss(appConfig *types.AppConfig) QinNiuOss {
|
|||||||
Scope: config.Bucket,
|
Scope: config.Bucket,
|
||||||
}
|
}
|
||||||
return QinNiuOss{
|
return QinNiuOss{
|
||||||
config: config,
|
config: config,
|
||||||
token: putPolicy.UploadToken(mac),
|
mac: mac,
|
||||||
uploader: formUploader,
|
putPolicy: putPolicy,
|
||||||
manager: storage.NewBucketManager(mac, &storeConfig),
|
uploader: formUploader,
|
||||||
proxyURL: appConfig.ProxyURL,
|
manager: storage.NewBucketManager(mac, &storeConfig),
|
||||||
dir: "chatgpt-plus",
|
proxyURL: appConfig.ProxyURL,
|
||||||
|
dir: "chatgpt-plus",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,7 +67,7 @@ func (s QinNiuOss) PutFile(ctx *gin.Context, name string) (string, error) {
|
|||||||
// 上传文件
|
// 上传文件
|
||||||
ret := storage.PutRet{}
|
ret := storage.PutRet{}
|
||||||
extra := storage.PutExtra{}
|
extra := storage.PutExtra{}
|
||||||
err = s.uploader.Put(ctx, &ret, s.token, key, src, file.Size, &extra)
|
err = s.uploader.Put(ctx, &ret, s.putPolicy.UploadToken(s.mac), key, src, file.Size, &extra)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@ -93,7 +95,7 @@ func (s QinNiuOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
|||||||
ret := storage.PutRet{}
|
ret := storage.PutRet{}
|
||||||
extra := storage.PutExtra{}
|
extra := storage.PutExtra{}
|
||||||
// 上传文件字节数据
|
// 上传文件字节数据
|
||||||
err = s.uploader.Put(context.Background(), &ret, s.token, key, bytes.NewReader(imageData), int64(len(imageData)), &extra)
|
err = s.uploader.Put(context.Background(), &ret, s.putPolicy.UploadToken(s.mac), key, bytes.NewReader(imageData), int64(len(imageData)), &extra)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
142
api/service/payment/alipay_service.go
Normal file
142
api/service/payment/alipay_service.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package payment
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core/types"
|
||||||
|
logger2 "chatplus/logger"
|
||||||
|
"fmt"
|
||||||
|
"github.com/smartwalle/alipay/v3"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AlipayService struct {
|
||||||
|
config *types.AlipayConfig
|
||||||
|
client *alipay.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
var logger = logger2.GetLogger()
|
||||||
|
|
||||||
|
func NewAlipayService(appConfig *types.AppConfig) (*AlipayService, error) {
|
||||||
|
config := appConfig.AlipayConfig
|
||||||
|
if !config.Enabled {
|
||||||
|
logger.Info("Disabled Alipay service")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
priKey, err := readKey(config.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error with read App Private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
xClient, err := alipay.New(config.AppId, priKey, !config.SandBox)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error with initialize alipay service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = xClient.LoadAppCertPublicKeyFromFile(config.PublicKey); err != nil {
|
||||||
|
return nil, fmt.Errorf("error with loading App PublicKey: %v", err)
|
||||||
|
}
|
||||||
|
if err = xClient.LoadAliPayRootCertFromFile(config.RootCert); err != nil {
|
||||||
|
return nil, fmt.Errorf("error with loading alipay RootCert: %v", err)
|
||||||
|
}
|
||||||
|
if err = xClient.LoadAlipayCertPublicKeyFromFile(config.AlipayPublicKey); err != nil {
|
||||||
|
return nil, fmt.Errorf("error with loading Alipay PublicKey: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AlipayService{config: &config, client: xClient}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AlipayService) PayUrlMobile(outTradeNo string, notifyURL string, returnURL string, Amount string, subject string) (string, error) {
|
||||||
|
var p = alipay.TradeWapPay{}
|
||||||
|
p.NotifyURL = notifyURL
|
||||||
|
p.ReturnURL = returnURL
|
||||||
|
p.Subject = subject
|
||||||
|
p.OutTradeNo = outTradeNo
|
||||||
|
p.TotalAmount = Amount
|
||||||
|
p.ProductCode = "QUICK_WAP_WAY"
|
||||||
|
res, err := s.client.TradeWapPay(p)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.String(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AlipayService) PayUrlPc(outTradeNo string, notifyURL string, returnURL string, amount string, subject string) (string, error) {
|
||||||
|
var p = alipay.TradePagePay{}
|
||||||
|
p.NotifyURL = notifyURL
|
||||||
|
p.ReturnURL = returnURL
|
||||||
|
p.Subject = subject
|
||||||
|
p.OutTradeNo = outTradeNo
|
||||||
|
p.TotalAmount = amount
|
||||||
|
p.ProductCode = "FAST_INSTANT_TRADE_PAY"
|
||||||
|
res, err := s.client.TradePagePay(p)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.String(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TradeVerify 交易验证
|
||||||
|
func (s *AlipayService) TradeVerify(reqForm url.Values) NotifyVo {
|
||||||
|
err := s.client.VerifySign(reqForm)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("异步通知验证签名发生错误", err)
|
||||||
|
return NotifyVo{
|
||||||
|
Status: 0,
|
||||||
|
Message: "异步通知验证签名发生错误",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.TradeQuery(reqForm.Get("out_trade_no"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AlipayService) TradeQuery(outTradeNo string) NotifyVo {
|
||||||
|
var p = alipay.TradeQuery{}
|
||||||
|
p.OutTradeNo = outTradeNo
|
||||||
|
rsp, err := s.client.TradeQuery(p)
|
||||||
|
if err != nil {
|
||||||
|
return NotifyVo{
|
||||||
|
Status: 0,
|
||||||
|
Message: "异步查询验证订单信息发生错误" + outTradeNo + err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rsp.IsSuccess() == true && rsp.TradeStatus == "TRADE_SUCCESS" {
|
||||||
|
return NotifyVo{
|
||||||
|
Status: 1,
|
||||||
|
OutTradeNo: rsp.OutTradeNo,
|
||||||
|
TradeNo: rsp.TradeNo,
|
||||||
|
Amount: rsp.TotalAmount,
|
||||||
|
Subject: rsp.Subject,
|
||||||
|
Message: "OK",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return NotifyVo{
|
||||||
|
Status: 0,
|
||||||
|
Message: "异步查询验证订单信息发生错误" + outTradeNo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readKey(filename string) (string, error) {
|
||||||
|
data, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotifyVo struct {
|
||||||
|
Status int
|
||||||
|
OutTradeNo string
|
||||||
|
TradeNo string
|
||||||
|
Amount string
|
||||||
|
Message string
|
||||||
|
Subject string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NotifyVo) Success() bool {
|
||||||
|
return v.Status == 1
|
||||||
|
}
|
@ -81,7 +81,7 @@ func (s *Service) Run() {
|
|||||||
|
|
||||||
// PushTask 推送任务到队列
|
// PushTask 推送任务到队列
|
||||||
func (s *Service) PushTask(task types.SdTask) {
|
func (s *Service) PushTask(task types.SdTask) {
|
||||||
logger.Infof("add a new MidJourney Task: %+v", task)
|
logger.Infof("add a new Stable Diffusion Task: %+v", task)
|
||||||
s.taskQueue.RPush(task)
|
s.taskQueue.RPush(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +105,8 @@ func (s *Service) Txt2Img(task types.SdTask) error {
|
|||||||
data[ParamKeys["negative_prompt"]] = params.NegativePrompt
|
data[ParamKeys["negative_prompt"]] = params.NegativePrompt
|
||||||
data[ParamKeys["steps"]] = params.Steps
|
data[ParamKeys["steps"]] = params.Steps
|
||||||
data[ParamKeys["sampler"]] = params.Sampler
|
data[ParamKeys["sampler"]] = params.Sampler
|
||||||
data[ParamKeys["face_fix"]] = params.FaceFix
|
// @fix bug: 有些 stable diffusion 没有面部修复功能
|
||||||
|
//data[ParamKeys["face_fix"]] = params.FaceFix
|
||||||
data[ParamKeys["cfg_scale"]] = params.CfgScale
|
data[ParamKeys["cfg_scale"]] = params.CfgScale
|
||||||
data[ParamKeys["seed"]] = params.Seed
|
data[ParamKeys["seed"]] = params.Seed
|
||||||
data[ParamKeys["height"]] = params.Height
|
data[ParamKeys["height"]] = params.Height
|
||||||
@ -176,7 +177,8 @@ func (s *Service) runTask(taskInfo TaskInfo, client *req.Client) {
|
|||||||
var info map[string]any
|
var info map[string]any
|
||||||
err = utils.JsonDecode(utils.InterfaceToString(res.Data[1]), &info)
|
err = utils.JsonDecode(utils.InterfaceToString(res.Data[1]), &info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cbReq.Message = err.Error()
|
logger.Error(res.Data)
|
||||||
|
cbReq.Message = "error with decode image url:" + err.Error()
|
||||||
cbReq.Success = false
|
cbReq.Success = false
|
||||||
result <- cbReq
|
result <- cbReq
|
||||||
return
|
return
|
||||||
@ -229,6 +231,7 @@ func (s *Service) runTask(taskInfo TaskInfo, client *req.Client) {
|
|||||||
|
|
||||||
cbReq.ImageData = progressRes.LivePreview
|
cbReq.ImageData = progressRes.LivePreview
|
||||||
cbReq.Progress = int(progressRes.Progress * 100)
|
cbReq.Progress = int(progressRes.Progress * 100)
|
||||||
|
logger.Debug(cbReq)
|
||||||
s.callback(cbReq)
|
s.callback(cbReq)
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
}
|
}
|
||||||
|
@ -32,14 +32,14 @@ var ParamKeys = map[string]int{
|
|||||||
"negative_prompt": 2,
|
"negative_prompt": 2,
|
||||||
"steps": 4,
|
"steps": 4,
|
||||||
"sampler": 5,
|
"sampler": 5,
|
||||||
"face_fix": 6,
|
"face_fix": 6, // 面部修复
|
||||||
"cfg_scale": 10,
|
"cfg_scale": 8,
|
||||||
"seed": 11,
|
"seed": 27,
|
||||||
"height": 17,
|
"height": 9,
|
||||||
"width": 18,
|
"width": 10,
|
||||||
"hd_fix": 19,
|
"hd_fix": 11,
|
||||||
"hd_redraw_rate": 20, //高清修复重绘幅度
|
"hd_redraw_rate": 12, //高清修复重绘幅度
|
||||||
"hd_scale": 21, // 高清修复放大倍数
|
"hd_scale": 13, // 高清修复放大倍数
|
||||||
"hd_scale_alg": 22, // 高清修复放大算法
|
"hd_scale_alg": 14, // 高清修复放大算法
|
||||||
"hd_sample_num": 23, // 高清修复采样次数
|
"hd_sample_num": 15, // 高清修复采样次数
|
||||||
}
|
}
|
||||||
|
56
api/service/snowflake.go
Normal file
56
api/service/snowflake.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Snowflake 雪花算法实现
|
||||||
|
type Snowflake struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
lastTimestamp int64
|
||||||
|
workerID int
|
||||||
|
sequence int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSnowflake() *Snowflake {
|
||||||
|
return &Snowflake{
|
||||||
|
lastTimestamp: -1,
|
||||||
|
workerID: 0, // TODO: 增加 WorkID 参数
|
||||||
|
sequence: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next 生成一个新的唯一ID
|
||||||
|
func (s *Snowflake) Next() (string, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
timestamp := time.Now().UnixNano() / 1000000 // 转换为毫秒
|
||||||
|
if timestamp < s.lastTimestamp {
|
||||||
|
return "", fmt.Errorf("clock moved backwards. Refusing to generate id for %d milliseconds", s.lastTimestamp-timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if timestamp == s.lastTimestamp {
|
||||||
|
s.sequence = (s.sequence + 1) & 4095
|
||||||
|
if s.sequence == 0 {
|
||||||
|
timestamp = s.waitNextMillis()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.sequence = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
s.lastTimestamp = timestamp
|
||||||
|
id := (timestamp << 22) | (int64(s.workerID) << 10) | int64(s.sequence)
|
||||||
|
now := time.Now()
|
||||||
|
return fmt.Sprintf("%d%02d%02d%d", now.Year(), now.Month(), now.Day(), id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Snowflake) waitNextMillis() int64 {
|
||||||
|
timestamp := time.Now().UnixNano() / 1000000
|
||||||
|
for timestamp <= s.lastTimestamp {
|
||||||
|
timestamp = time.Now().UnixNano() / 1000000
|
||||||
|
}
|
||||||
|
return timestamp
|
||||||
|
}
|
144
api/service/xxl_job_service.go
Normal file
144
api/service/xxl_job_service.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core/types"
|
||||||
|
logger2 "chatplus/logger"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/utils"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/xxl-job/xxl-job-executor-go"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logger = logger2.GetLogger()
|
||||||
|
|
||||||
|
type XXLJobExecutor struct {
|
||||||
|
executor xxl.Executor
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewXXLJobExecutor(config *types.AppConfig, db *gorm.DB) *XXLJobExecutor {
|
||||||
|
if !config.XXLConfig.Enabled {
|
||||||
|
logger.Info("XXL-JOB service is disabled")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
exec := xxl.NewExecutor(
|
||||||
|
xxl.ServerAddr(config.XXLConfig.ServerAddr),
|
||||||
|
xxl.AccessToken(config.XXLConfig.AccessToken), //请求令牌(默认为空)
|
||||||
|
xxl.ExecutorIp(config.XXLConfig.ExecutorIp), //可自动获取
|
||||||
|
xxl.ExecutorPort(config.XXLConfig.ExecutorPort), //默认9999(非必填)
|
||||||
|
xxl.RegistryKey(config.XXLConfig.RegistryKey), //执行器名称
|
||||||
|
xxl.SetLogger(&customLogger{}), //自定义日志
|
||||||
|
)
|
||||||
|
exec.Init()
|
||||||
|
return &XXLJobExecutor{executor: exec, db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *XXLJobExecutor) Run() error {
|
||||||
|
e.executor.RegTask("ClearOrder", e.ClearOrder)
|
||||||
|
e.executor.RegTask("ResetVipCalls", e.ResetVipCalls)
|
||||||
|
return e.executor.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearOrder 清理未支付的订单,如果没有抛出异常则表示执行成功
|
||||||
|
func (e *XXLJobExecutor) ClearOrder(cxt context.Context, param *xxl.RunReq) (msg string) {
|
||||||
|
logger.Debug("执行清理未支付订单...")
|
||||||
|
var sysConfig model.Config
|
||||||
|
res := e.db.Where("marker", "system").First(&sysConfig)
|
||||||
|
if res.Error != nil {
|
||||||
|
return "error with get system config: " + res.Error.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
var config types.SystemConfig
|
||||||
|
err := utils.JsonDecode(sysConfig.Config, &config)
|
||||||
|
if err != nil {
|
||||||
|
return "error with decode system config: " + err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.OrderPayTimeout == 0 { // 默认未支付订单的生命周期为 30 分钟
|
||||||
|
config.OrderPayTimeout = 1800
|
||||||
|
}
|
||||||
|
timeout := time.Now().Unix() - int64(config.OrderPayTimeout)
|
||||||
|
start := utils.Stamp2str(timeout)
|
||||||
|
res = e.db.Where("status != ? AND created_at < ?", types.OrderPaidSuccess, start).Delete(&model.Order{})
|
||||||
|
return fmt.Sprintf("Clear order successfully, affect rows: %d", res.RowsAffected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetVipCalls 清理过期的 VIP 会员
|
||||||
|
func (e *XXLJobExecutor) ResetVipCalls(cxt context.Context, param *xxl.RunReq) (msg string) {
|
||||||
|
logger.Info("开始进行月底账号盘点...")
|
||||||
|
var users []model.User
|
||||||
|
res := e.db.Where("vip = ?", 1).Find(&users)
|
||||||
|
if res.Error != nil {
|
||||||
|
return "No vip users found"
|
||||||
|
}
|
||||||
|
|
||||||
|
var sysConfig model.Config
|
||||||
|
res = e.db.Where("marker", "system").First(&sysConfig)
|
||||||
|
if res.Error != nil {
|
||||||
|
return "error with get system config: " + res.Error.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
var config types.SystemConfig
|
||||||
|
err := utils.JsonDecode(sysConfig.Config, &config)
|
||||||
|
if err != nil {
|
||||||
|
return "error with decode system config: " + err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取本月月初时间
|
||||||
|
currentTime := time.Now()
|
||||||
|
year, month, _ := currentTime.Date()
|
||||||
|
firstOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, currentTime.Location()).Unix()
|
||||||
|
for _, u := range users {
|
||||||
|
// 账号到期,直接清零
|
||||||
|
if u.ExpiredTime <= currentTime.Unix() {
|
||||||
|
logger.Info("账号过期:", u.Mobile)
|
||||||
|
u.Calls = 0
|
||||||
|
u.Vip = false
|
||||||
|
} else {
|
||||||
|
if u.Calls <= 0 {
|
||||||
|
u.Calls = config.VipMonthCalls
|
||||||
|
} else {
|
||||||
|
// 如果该用户当月有充值点卡,则将点卡中未用完的点数结余到下个月
|
||||||
|
var orders []model.Order
|
||||||
|
e.db.Debug().Where("user_id = ? AND pay_time > ?", u.Id, firstOfMonth).Find(&orders)
|
||||||
|
var calls = 0
|
||||||
|
for _, o := range orders {
|
||||||
|
var remark types.OrderRemark
|
||||||
|
err = utils.JsonDecode(o.Remark, &remark)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if remark.Days > 0 { // 会员续费
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
calls += remark.Calls
|
||||||
|
}
|
||||||
|
if u.Calls > calls { // 本月套餐没有用完
|
||||||
|
u.Calls = calls + config.VipMonthCalls
|
||||||
|
} else {
|
||||||
|
u.Calls = u.Calls + config.VipMonthCalls
|
||||||
|
}
|
||||||
|
logger.Infof("%s 点卡结余:%d", u.Mobile, calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u.Tokens = 0
|
||||||
|
// update user
|
||||||
|
e.db.Updates(&u)
|
||||||
|
}
|
||||||
|
logger.Info("月底盘点完成!")
|
||||||
|
return "success"
|
||||||
|
}
|
||||||
|
|
||||||
|
type customLogger struct{}
|
||||||
|
|
||||||
|
func (l *customLogger) Info(format string, a ...interface{}) {
|
||||||
|
logger.Debugf(format, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *customLogger) Error(format string, a ...interface{}) {
|
||||||
|
logger.Errorf(format, a...)
|
||||||
|
}
|
22
api/store/model/order.go
Normal file
22
api/store/model/order.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core/types"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Order 充值订单
|
||||||
|
type Order struct {
|
||||||
|
BaseModel
|
||||||
|
UserId uint
|
||||||
|
ProductId uint
|
||||||
|
Mobile string
|
||||||
|
OrderNo string
|
||||||
|
Subject string
|
||||||
|
Amount float64
|
||||||
|
Status types.OrderStatus
|
||||||
|
Remark string
|
||||||
|
PayTime int64
|
||||||
|
PayWay string // 支付方式
|
||||||
|
DeletedAt gorm.DeletedAt
|
||||||
|
}
|
14
api/store/model/product.go
Normal file
14
api/store/model/product.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// Product 充值产品
|
||||||
|
type Product struct {
|
||||||
|
BaseModel
|
||||||
|
Name string
|
||||||
|
Price float64
|
||||||
|
Discount float64
|
||||||
|
Days int
|
||||||
|
Calls int
|
||||||
|
Enabled bool
|
||||||
|
Sales int
|
||||||
|
SortNum int
|
||||||
|
}
|
@ -16,4 +16,6 @@ type User struct {
|
|||||||
Status bool `gorm:"default:true"` // 当前状态
|
Status bool `gorm:"default:true"` // 当前状态
|
||||||
LastLoginAt int64 // 最后登录时间
|
LastLoginAt int64 // 最后登录时间
|
||||||
LastLoginIp string // 最后登录 IP
|
LastLoginIp string // 最后登录 IP
|
||||||
|
Vip bool // 是否 VIP 会员
|
||||||
|
Tokens int
|
||||||
}
|
}
|
||||||
|
19
api/store/vo/order.go
Normal file
19
api/store/vo/order.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package vo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Order struct {
|
||||||
|
BaseVo
|
||||||
|
UserId uint `json:"user_id"`
|
||||||
|
ProductId uint `json:"product_id"`
|
||||||
|
Mobile string `json:"mobile"`
|
||||||
|
OrderNo string `json:"order_no"`
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
Status types.OrderStatus `json:"status"`
|
||||||
|
PayTime int64 `json:"pay_time"`
|
||||||
|
PayWay string `json:"pay_way"`
|
||||||
|
Remark types.OrderRemark `json:"remark"`
|
||||||
|
}
|
13
api/store/vo/product.go
Normal file
13
api/store/vo/product.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package vo
|
||||||
|
|
||||||
|
type Product struct {
|
||||||
|
BaseVo
|
||||||
|
Name string `json:"name"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
Discount float64 `json:"discount"`
|
||||||
|
Days int `json:"days"`
|
||||||
|
Calls int `json:"calls"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Sales int `json:"sales"`
|
||||||
|
SortNum int `json:"sort_num"`
|
||||||
|
}
|
@ -17,4 +17,6 @@ type User struct {
|
|||||||
Status bool `json:"status"` // 当前状态
|
Status bool `json:"status"` // 当前状态
|
||||||
LastLoginAt int64 `json:"last_login_at"` // 最后登录时间
|
LastLoginAt int64 `json:"last_login_at"` // 最后登录时间
|
||||||
LastLoginIp string `json:"last_login_ip"` // 最后登录 IP
|
LastLoginIp string `json:"last_login_ip"` // 最后登录 IP
|
||||||
|
Vip bool `json:"vip"`
|
||||||
|
Tokens int `json:"token"` // 当月消耗的 fee
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,16 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/nfnt/resize"
|
||||||
|
"github.com/skip2/go-qrcode"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/draw"
|
||||||
|
"image/jpeg"
|
||||||
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -140,13 +148,57 @@ func IntValue(str string, defaultValue int) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ForceCovert(src any, dst interface{}) error {
|
func ForceCovert(src any, dst interface{}) error {
|
||||||
bytes, err := json.Marshal(src)
|
b, err := json.Marshal(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(bytes, dst)
|
err = json.Unmarshal(b, dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenQrcode(text string, size int, logo io.Reader) ([]byte, error) {
|
||||||
|
qr, err := qrcode.New(text, qrcode.Medium)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
qr.BackgroundColor = color.White
|
||||||
|
qr.ForegroundColor = color.Black
|
||||||
|
if logo == nil {
|
||||||
|
return qr.PNG(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成带Logo的二维码图像
|
||||||
|
logoImage, _, err := image.Decode(logo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缩放 Logo
|
||||||
|
scaledLogo := resize.Resize(uint(size/9), uint(size/9), logoImage, resize.Lanczos3)
|
||||||
|
// 将Logo叠加到二维码图像上
|
||||||
|
qrWithLogo := overlayLogo(qr.Image(size), scaledLogo)
|
||||||
|
|
||||||
|
// 将带Logo的二维码图像以JPEG格式编码为图片数据
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = jpeg.Encode(&buf, qrWithLogo, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 叠加Logo到图片上
|
||||||
|
func overlayLogo(qrImage, logoImage image.Image) image.Image {
|
||||||
|
offsetX := (qrImage.Bounds().Dx() - logoImage.Bounds().Dx()) / 2
|
||||||
|
offsetY := (qrImage.Bounds().Dy() - logoImage.Bounds().Dy()) / 2
|
||||||
|
|
||||||
|
combinedImage := image.NewRGBA(qrImage.Bounds())
|
||||||
|
draw.Draw(combinedImage, qrImage.Bounds(), qrImage, image.Point{}, draw.Over)
|
||||||
|
draw.Draw(combinedImage, logoImage.Bounds().Add(image.Pt(offsetX, offsetY)), logoImage, image.Point{}, draw.Over)
|
||||||
|
|
||||||
|
return combinedImage
|
||||||
|
}
|
||||||
|
1548
database/chatgpt_plus-v3.1.8.sql
Normal file
1548
database/chatgpt_plus-v3.1.8.sql
Normal file
File diff suppressed because it is too large
Load Diff
60
database/update-v3.1.8.sql
Normal file
60
database/update-v3.1.8.sql
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
ALTER TABLE `chatgpt_users` ADD `vip` TINYINT(1) NOT NULL DEFAULT '0' COMMENT '是否会员' AFTER `last_login_at`;
|
||||||
|
ALTER TABLE `chatgpt_users` ADD `tokens` BIGINT NOT NULL DEFAULT '0' COMMENT '当月消耗 tokens' AFTER `total_tokens`;
|
||||||
|
|
||||||
|
CREATE TABLE `chatgpt_orders` (
|
||||||
|
`id` int NOT NULL,
|
||||||
|
`user_id` int NOT NULL COMMENT '用户ID',
|
||||||
|
`product_id` int NOT NULL COMMENT '产品ID',
|
||||||
|
`mobile` char(11) NOT NULL COMMENT '用户手机号',
|
||||||
|
`order_no` varchar(30) NOT NULL COMMENT '订单ID',
|
||||||
|
`subject` varchar(100) NOT NULL COMMENT '订单产品',
|
||||||
|
`amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单金额',
|
||||||
|
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '订单状态(0:待支付,1:已扫码,2:支付失败)',
|
||||||
|
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '备注',
|
||||||
|
`pay_time` int DEFAULT NULL COMMENT '支付时间',
|
||||||
|
`created_at` datetime NOT NULL,
|
||||||
|
`updated_at` datetime NOT NULL,
|
||||||
|
`deleted_at` datetime DEFAULT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='充值订单表';
|
||||||
|
|
||||||
|
--
|
||||||
|
-- 转存表中的数据 `chatgpt_orders`
|
||||||
|
--
|
||||||
|
INSERT INTO `chatgpt_orders` (`id`, `user_id`, `product_id`, `mobile`, `order_no`, `subject`, `amount`, `status`, `remark`, `pay_time`, `created_at`, `updated_at`, `deleted_at`) VALUES
|
||||||
|
(4, 4, 1, '18575670125', '202308317102915300416290816', '会员1个月', '0.01', 2, '{\"days\":30,\"calls\":500,\"name\":\"会员1个月\",\"discount\":10.99}', 1693466990, '2023-08-31 15:29:33', '2023-08-31 15:29:51', NULL),
|
||||||
|
(5, 4, 5, '18575670125', '202308317102946758199607296', '100次点卡', '0.30', 2, '{\"days\":0,\"calls\":100,\"name\":\"100次点卡\"}', 1693466990, '2023-08-31 17:34:34', '2023-08-31 17:34:34', NULL),
|
||||||
|
(6, 4, 5, '18575670125', '202308317102946843595636736', '100次点卡', '0.03', 2, '{\"days\":0,\"calls\":100,\"name\":\"100次点卡\"}', 1693474722, '2023-08-31 17:34:54', '2023-08-31 17:38:43', NULL),
|
||||||
|
(7, 4, 1, '18575670125', '202309017103252664456052736', '会员1个月', '0.01', 2, '{\"days\":30,\"calls\":0,\"name\":\"会员1个月\"}', 1693466990, '2023-09-01 13:50:07', '2023-09-01 13:50:07', NULL),
|
||||||
|
(8, 4, 1, '18575670125', '202309017103252894391992320', '会员1个月', '0.01', 2, '{\"days\":30,\"calls\":0,\"name\":\"会员1个月\"}', 1693466990, '2023-09-01 13:51:02', '2023-09-01 13:51:02', NULL),
|
||||||
|
(9, 4, 5, '18575670125', '202309017103254657538981888', '100次点卡', '0.03', 2, '{\"days\":0,\"calls\":100,\"name\":\"100次点卡\"}', 1693474722, '2023-09-01 13:58:02', '2023-09-01 13:58:02', NULL),
|
||||||
|
(10, 4, 1, '18575670125', '202309017103259375405367296', '会员1个月', '0.01', 2, '{\"days\":30,\"calls\":0,\"name\":\"会员1个月\"}', 1693474722, '2023-09-01 14:16:47', '2023-09-01 14:16:47', NULL),
|
||||||
|
(11, 4, 3, '18575670125', '202309017103290730432430080', '会员6个月', '190.00', 2, '{\"days\":180,\"calls\":0,\"name\":\"会员6个月\",\"price\":290,\"discount\":100}', 1693474722, '2023-09-01 16:21:23', '2023-09-01 16:21:23', NULL),
|
||||||
|
(12, 4, 4, '18575670125', '202309017103291707520712704', '会员12个月', '380.00', 2, '{\"days\":365,\"calls\":0,\"name\":\"会员12个月\",\"price\":580,\"discount\":200}', 1693466990, '2023-09-01 16:25:16', '2023-09-01 16:25:16', NULL);
|
||||||
|
|
||||||
|
-- 创建索引
|
||||||
|
ALTER TABLE `chatgpt_orders` ADD PRIMARY KEY (`id`), ADD UNIQUE KEY `order_no` (`order_no`);
|
||||||
|
ALTER TABLE `chatgpt_orders` MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=13;
|
||||||
|
|
||||||
|
CREATE TABLE `chatgpt_products` (
|
||||||
|
`id` int NOT NULL,
|
||||||
|
`name` varchar(30) NOT NULL COMMENT '名称',
|
||||||
|
`price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '价格',
|
||||||
|
`discount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '优惠金额',
|
||||||
|
`days` smallint NOT NULL DEFAULT '0' COMMENT '延长天数',
|
||||||
|
`calls` int NOT NULL DEFAULT '0' COMMENT '调用次数',
|
||||||
|
`enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启动',
|
||||||
|
`sales` int NOT NULL DEFAULT '0' COMMENT '销量',
|
||||||
|
`sort_num` tinyint NOT NULL DEFAULT '0' COMMENT '排序',
|
||||||
|
`created_at` datetime NOT NULL,
|
||||||
|
`updated_at` datetime NOT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='会员套餐表';
|
||||||
|
|
||||||
|
INSERT INTO `chatgpt_products` (`id`, `name`, `price`, `discount`, `days`, `calls`, `enabled`, `sales`, `sort_num`, `created_at`, `updated_at`) VALUES
|
||||||
|
(1, '会员1个月', '1.01', '1.00', 30, 0, 1, 0, 0, '2023-08-28 10:48:57', '2023-08-31 16:24:26'),
|
||||||
|
(2, '会员3个月', '140.00', '30.00', 90, 0, 1, 0, 0, '2023-08-28 10:52:22', '2023-08-31 16:24:31'),
|
||||||
|
(3, '会员6个月', '290.00', '100.00', 180, 0, 1, 0, 0, '2023-08-28 10:53:39', '2023-08-31 16:24:36'),
|
||||||
|
(4, '会员12个月', '580.00', '200.00', 365, 0, 1, 0, 0, '2023-08-28 10:54:15', '2023-08-31 16:24:42'),
|
||||||
|
(5, '100次点卡', '10.03', '10.00', 0, 100, 1, 0, 0, '2023-08-28 10:55:08', '2023-08-31 17:34:43');
|
||||||
|
|
||||||
|
ALTER TABLE `chatgpt_products` ADD PRIMARY KEY (`id`);
|
||||||
|
ALTER TABLE `chatgpt_products` MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
|
@ -68,3 +68,22 @@ WeChatBot = false
|
|||||||
ApiURL = "http://172.22.11.200:7860"
|
ApiURL = "http://172.22.11.200:7860"
|
||||||
ApiKey = ""
|
ApiKey = ""
|
||||||
Txt2ImgJsonPath = "res/text2img.json"
|
Txt2ImgJsonPath = "res/text2img.json"
|
||||||
|
|
||||||
|
[XXLConfig] # xxl-job 配置,需要你部署 XXL-JOB 定时任务工具,用来定期清理未支付订单和清理过期 VIP,如果你没有启用支付服务,则该服务也无需启动
|
||||||
|
Enabled = false # 是否启用 XXL JOB 服务
|
||||||
|
ServerAddr = "http://172.22.11.47:8080/xxl-job-admin" # xxl-job-admin 管理地址
|
||||||
|
ExecutorIp = "172.22.11.47" # 执行器 IP 地址
|
||||||
|
ExecutorPort = "9999" # 执行器服务端口
|
||||||
|
AccessToken = "xxl-job-api-token" # 执行器 API 通信 token
|
||||||
|
RegistryKey = "chatgpt-plus" # 任务注册 key
|
||||||
|
|
||||||
|
[AlipayConfig]
|
||||||
|
Enabled = false # 启用支付宝支付通道
|
||||||
|
SandBox = false # 是否启用沙盒模式
|
||||||
|
UserId = "2088721020750581" # 商户ID
|
||||||
|
AppId = "9021000131658023" # App Id
|
||||||
|
PrivateKey = "certs/alipay/privateKey.txt" # 应用私钥
|
||||||
|
PublicKey = "certs/alipay/appPublicCert.crt" # 应用公钥证书
|
||||||
|
AlipayPublicKey = "certs/alipay/alipayPublicCert.crt" # 支付宝公钥证书
|
||||||
|
RootCert = "certs/alipay/alipayRootCert.crt" # 支付宝根证书
|
||||||
|
NotifyURL = "http://r9it.com:6004/api/payment/alipay/notify" # 支付异步回调地址
|
BIN
web/public/images/alipay.jpg
Normal file
BIN
web/public/images/alipay.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
web/public/images/vip.png
Normal file
BIN
web/public/images/vip.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
@ -16,8 +16,14 @@
|
|||||||
<el-input v-model="form.mobile"/>
|
<el-input v-model="form.mobile"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="手机验证码">
|
<el-form-item label="手机验证码">
|
||||||
<el-input v-model.number="form.code" maxlength="6" style="max-width: 200px; margin-right: 10px;"/>
|
<el-row :gutter="20">
|
||||||
<send-msg size="" :mobile="form.mobile"/>
|
<el-col :span="16">
|
||||||
|
<el-input v-model.number="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>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
@ -81,6 +87,12 @@ const close = function () {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="stylus" scoped>
|
||||||
|
#bind-mobile-form {
|
||||||
|
.el-form-item__content {
|
||||||
|
.el-row {
|
||||||
|
width 100%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
@ -125,6 +125,31 @@ export default defineComponent({
|
|||||||
p:first-child {
|
p:first-child {
|
||||||
margin-top 0
|
margin-top 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置表格边框
|
||||||
|
|
||||||
|
table {
|
||||||
|
width 100%
|
||||||
|
margin-bottom 1rem
|
||||||
|
color #212529
|
||||||
|
border-collapse collapse;
|
||||||
|
border 1px solid #dee2e6;
|
||||||
|
background-color #ffffff
|
||||||
|
|
||||||
|
thead {
|
||||||
|
th {
|
||||||
|
border 1px solid #dee2e6
|
||||||
|
vertical-align: bottom
|
||||||
|
border-bottom: 2px solid #dee2e6
|
||||||
|
padding 10px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
border 1px solid #dee2e6
|
||||||
|
padding 10px
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,55 +5,30 @@
|
|||||||
:close-on-click-modal="true"
|
:close-on-click-modal="true"
|
||||||
:before-close="close"
|
:before-close="close"
|
||||||
style="max-width: 600px"
|
style="max-width: 600px"
|
||||||
title="用户设置"
|
title="账户信息"
|
||||||
>
|
>
|
||||||
<div class="user-info" id="user-info">
|
<div class="user-info" id="user-info">
|
||||||
<el-form v-if="form.id" :model="form" label-width="150px">
|
<el-form v-if="user.id" :model="user" label-width="150px">
|
||||||
<el-form-item label="账户">
|
<el-form-item label="账户">
|
||||||
<span>{{ form.mobile }}</span>
|
<span>{{ user.mobile }}</span>
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="头像">
|
|
||||||
<el-upload
|
|
||||||
class="avatar-uploader"
|
|
||||||
:auto-upload="true"
|
|
||||||
:show-file-list="false"
|
|
||||||
:http-request="afterRead"
|
|
||||||
>
|
|
||||||
<el-avatar v-if="form.avatar" :src="form.avatar" shape="square" :size="100"/>
|
|
||||||
<el-icon v-else class="avatar-uploader-icon">
|
|
||||||
<Plus/>
|
|
||||||
</el-icon>
|
|
||||||
</el-upload>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="剩余对话次数">
|
<el-form-item label="剩余对话次数">
|
||||||
<el-tag>{{ form['calls'] }}</el-tag>
|
<el-tag>{{ user['calls'] }}</el-tag>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="剩余绘图次数">
|
<el-form-item label="剩余绘图次数">
|
||||||
<el-tag>{{ form['img_calls'] }}</el-tag>
|
<el-tag>{{ user['img_calls'] }}</el-tag>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="累计消耗 Tokens">
|
<el-form-item label="本月消耗电量">
|
||||||
<el-tag type="info">{{ form['total_tokens'] }}</el-tag>
|
<el-tag type="info">{{ user['tokens'] }}</el-tag>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="OpenAI API KEY">
|
<el-form-item label="累计消耗电量">
|
||||||
<el-input v-model="form.chat_config['api_keys']['OpenAI']"/>
|
<el-tag type="info">{{ user['total_tokens'] }}</el-tag>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Azure API KEY">
|
<el-form-item label="会员到期时间" v-if="user['expired_time'] > 0">
|
||||||
<el-input v-model="form['chat_config']['api_keys']['Azure']"/>
|
<el-tag type="danger">{{ dateFormat(user['expired_time']) }}</el-tag>
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="ChatGLM API KEY">
|
|
||||||
<el-input v-model="form['chat_config']['api_keys']['ChatGLM']"/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<span class="dialog-footer">
|
|
||||||
<el-button @click="close">关闭</el-button>
|
|
||||||
<el-button type="primary" @click="save">
|
|
||||||
保存
|
|
||||||
</el-button>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -63,6 +38,7 @@ import {httpGet, httpPost} from "@/utils/http";
|
|||||||
import {ElMessage} from "element-plus";
|
import {ElMessage} from "element-plus";
|
||||||
import {Plus} from "@element-plus/icons-vue";
|
import {Plus} from "@element-plus/icons-vue";
|
||||||
import Compressor from "compressorjs";
|
import Compressor from "compressorjs";
|
||||||
|
import {dateFormat} from "@/utils/libs";
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -74,7 +50,7 @@ const props = defineProps({
|
|||||||
const showDialog = computed(() => {
|
const showDialog = computed(() => {
|
||||||
return props.show
|
return props.show
|
||||||
})
|
})
|
||||||
const form = ref({
|
const user = ref({
|
||||||
username: '',
|
username: '',
|
||||||
nickname: '',
|
nickname: '',
|
||||||
avatar: '',
|
avatar: '',
|
||||||
@ -87,50 +63,15 @@ const form = ref({
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 获取最新用户信息
|
// 获取最新用户信息
|
||||||
httpGet('/api/user/profile').then(res => {
|
httpGet('/api/user/profile').then(res => {
|
||||||
form.value = res.data
|
user.value = res.data
|
||||||
form.value.chat_config.api_keys = res.data.chat_config.api_keys ?? {OpenAI: "", Azure: "", ChatGLM: ""}
|
user.value.chat_config.api_keys = res.data.chat_config.api_keys ?? {OpenAI: "", Azure: "", ChatGLM: ""}
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
ElMessage.error("获取用户信息失败:" + e.message)
|
ElMessage.error("获取用户信息失败:" + e.message)
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
const afterRead = (file) => {
|
|
||||||
// console.log(file)
|
|
||||||
// 压缩图片并上传
|
|
||||||
new Compressor(file.file, {
|
|
||||||
quality: 0.6,
|
|
||||||
success(result) {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', result, result.name);
|
|
||||||
// 执行上传操作
|
|
||||||
httpPost('/api/upload', formData).then((res) => {
|
|
||||||
form.value.avatar = res.data
|
|
||||||
ElMessage.success({message: "上传成功", duration: 500})
|
|
||||||
}).catch((e) => {
|
|
||||||
ElMessage.error('上传失败:' + e.message)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
error(err) {
|
|
||||||
console.log(err.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
const emits = defineEmits(['hide', 'update-user']);
|
const emits = defineEmits(['hide']);
|
||||||
const save = function () {
|
|
||||||
httpPost('/api/user/profile/update', form.value).then(() => {
|
|
||||||
ElMessage.success({
|
|
||||||
message: '更新成功',
|
|
||||||
duration: 500,
|
|
||||||
onClose: () => emits('hide', false)
|
|
||||||
})
|
|
||||||
// 更新用户数据
|
|
||||||
emits('update-user', {nickname: form.value['nickname'], avatar: form.value['avatar']});
|
|
||||||
}).catch((e) => {
|
|
||||||
ElMessage.error('更新失败:' + e.message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const close = function () {
|
const close = function () {
|
||||||
emits('hide', false);
|
emits('hide', false);
|
||||||
}
|
}
|
||||||
|
97
web/src/components/CountDown.vue
Normal file
97
web/src/components/CountDown.vue
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 倒计时组件 -->
|
||||||
|
<div class="countdown">
|
||||||
|
<el-tag size="large" :type="type">{{ timerStr }}</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
import {onMounted, ref, watch} from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
second: Number,
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(['timeout']);
|
||||||
|
const counter = ref(props.second)
|
||||||
|
const timerStr = ref("")
|
||||||
|
const handler = ref(null)
|
||||||
|
|
||||||
|
watch(() => props.second, (newVal) => {
|
||||||
|
counter.value = newVal
|
||||||
|
resetTimer()
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
resetTimer()
|
||||||
|
})
|
||||||
|
|
||||||
|
const resetTimer = () => {
|
||||||
|
if (handler.value) {
|
||||||
|
clearInterval(handler.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
counter.value = props.second
|
||||||
|
formatTimer(counter.value)
|
||||||
|
handler.value = setInterval(() => {
|
||||||
|
formatTimer(counter.value)
|
||||||
|
if (counter.value === 0) {
|
||||||
|
clearInterval(handler.value)
|
||||||
|
emits("timeout")
|
||||||
|
}
|
||||||
|
counter.value--
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTimer = (secs) => {
|
||||||
|
const timer = []
|
||||||
|
let hour, min
|
||||||
|
// 计算小时
|
||||||
|
if (secs > 3600) {
|
||||||
|
hour = Math.floor(secs / 3600)
|
||||||
|
if (hour < 10) {
|
||||||
|
hour = "0" + hour
|
||||||
|
}
|
||||||
|
secs = secs % 3600
|
||||||
|
timer.push(hour + " 时 ")
|
||||||
|
} else {
|
||||||
|
timer.push("00 时 ")
|
||||||
|
}
|
||||||
|
// 计算分钟
|
||||||
|
if (secs > 60) {
|
||||||
|
min = Math.floor(secs / 60)
|
||||||
|
if (min < 10) {
|
||||||
|
min = "0" + min
|
||||||
|
}
|
||||||
|
secs = secs % 60
|
||||||
|
timer.push(min + " 分 ")
|
||||||
|
} else {
|
||||||
|
timer.push("00 分 ")
|
||||||
|
}
|
||||||
|
// 计算秒数
|
||||||
|
if (secs < 10) {
|
||||||
|
secs = "0" + secs
|
||||||
|
}
|
||||||
|
timer.push(secs + " 秒")
|
||||||
|
timerStr.value = timer.join("")
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({resetTimer})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.countdown {
|
||||||
|
display flex
|
||||||
|
|
||||||
|
.el-tag--large {
|
||||||
|
.el-tag__content {
|
||||||
|
font-size 14px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
@ -9,7 +9,7 @@
|
|||||||
title="用户登录"
|
title="用户登录"
|
||||||
>
|
>
|
||||||
<div class="form">
|
<div class="form">
|
||||||
<el-form label-width="65px">
|
<el-form label-width="75px">
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<template #label>
|
<template #label>
|
||||||
<div class="label">
|
<div class="label">
|
||||||
@ -20,7 +20,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<el-input v-model="username" placeholder="手机号码"/>
|
<el-input v-model="username" size="large" placeholder="手机号码"/>
|
||||||
</template>
|
</template>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
@ -33,12 +33,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<el-input v-model="password" type="password" placeholder="密码"/>
|
<el-input v-model="password" type="password" size="large" placeholder="密码"/>
|
||||||
</template>
|
</template>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<div class="login-btn">
|
<div class="login-btn">
|
||||||
<el-button type="primary" @click="submit" round>登录</el-button>
|
<el-button type="primary" @click="submit" size="large" round>登录</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
@ -90,9 +90,17 @@ const close = function () {
|
|||||||
border-radius 20px
|
border-radius 20px
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
|
padding-top 3px
|
||||||
|
|
||||||
.el-icon {
|
.el-icon {
|
||||||
font-size 16px
|
position relative
|
||||||
|
font-size 20px
|
||||||
margin-right 6px
|
margin-right 6px
|
||||||
|
top 4px
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size 16px
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
143
web/src/components/UserProfile.vue
Normal file
143
web/src/components/UserProfile.vue
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
<template>
|
||||||
|
<div class="user-info" id="user-info">
|
||||||
|
<el-form v-if="user.id" :model="user" label-width="150px">
|
||||||
|
<el-row>
|
||||||
|
<el-upload
|
||||||
|
class="avatar-uploader"
|
||||||
|
:auto-upload="true"
|
||||||
|
:show-file-list="false"
|
||||||
|
:http-request="afterRead"
|
||||||
|
>
|
||||||
|
<el-avatar v-if="user.avatar" :src="user.avatar" shape="circle" :size="100"/>
|
||||||
|
<el-icon v-else class="avatar-uploader-icon">
|
||||||
|
<Plus/>
|
||||||
|
</el-icon>
|
||||||
|
</el-upload>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item label="账户">
|
||||||
|
<span>{{ user.mobile }}</span>
|
||||||
|
<el-tooltip
|
||||||
|
class="box-item"
|
||||||
|
effect="light"
|
||||||
|
content="您已经是 VIP 会员"
|
||||||
|
placement="right"
|
||||||
|
>
|
||||||
|
<el-image v-if="user.vip" :src="vipImg" style="height: 25px;margin-left: 10px"/>
|
||||||
|
</el-tooltip>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="剩余对话次数">
|
||||||
|
<el-tag>{{ user['calls'] }}</el-tag>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="剩余绘图次数">
|
||||||
|
<el-tag>{{ user['img_calls'] }}</el-tag>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="本月消耗电量">
|
||||||
|
<el-tag type="info">{{ user['tokens'] }}</el-tag>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="累计消耗电量">
|
||||||
|
<el-tag type="info">{{ user['total_tokens'] }}</el-tag>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="会员到期时间" v-if="user['expired_time'] > 0">
|
||||||
|
<el-tag type="danger">{{ dateFormat(user['expired_time']) }}</el-tag>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="OpenAI API KEY">
|
||||||
|
<el-input v-model="user.chat_config['api_keys']['OpenAI']"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Azure API KEY">
|
||||||
|
<el-input v-model="user['chat_config']['api_keys']['Azure']"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="ChatGLM API KEY">
|
||||||
|
<el-input v-model="user['chat_config']['api_keys']['ChatGLM']"/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-row class="opt-line">
|
||||||
|
<el-button color="#47fff1" :dark="false" round @click="save">保存</el-button>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {onMounted, ref} from "vue"
|
||||||
|
import {httpGet, httpPost} from "@/utils/http";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
import {Plus} from "@element-plus/icons-vue";
|
||||||
|
import Compressor from "compressorjs";
|
||||||
|
import {dateFormat} from "@/utils/libs";
|
||||||
|
import {checkSession} from "@/action/session";
|
||||||
|
|
||||||
|
const user = ref({
|
||||||
|
vip: false,
|
||||||
|
username: '',
|
||||||
|
nickname: '',
|
||||||
|
avatar: '',
|
||||||
|
mobile: '',
|
||||||
|
calls: 0,
|
||||||
|
tokens: 0,
|
||||||
|
chat_config: {api_keys: {OpenAI: "", Azure: "", ChatGLM: ""}}
|
||||||
|
})
|
||||||
|
const vipImg = ref("/images/vip.png")
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkSession().then(() => {
|
||||||
|
// 获取最新用户信息
|
||||||
|
httpGet('/api/user/profile').then(res => {
|
||||||
|
user.value = res.data
|
||||||
|
user.value.chat_config.api_keys = res.data.chat_config.api_keys ?? {OpenAI: "", Azure: "", ChatGLM: ""}
|
||||||
|
}).catch(e => {
|
||||||
|
ElMessage.error("获取用户信息失败:" + e.message)
|
||||||
|
});
|
||||||
|
}).catch(e => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const afterRead = (file) => {
|
||||||
|
// 压缩图片并上传
|
||||||
|
new Compressor(file.file, {
|
||||||
|
quality: 0.6,
|
||||||
|
success(result) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', result, result.name);
|
||||||
|
// 执行上传操作
|
||||||
|
httpPost('/api/upload', formData).then((res) => {
|
||||||
|
user.value.avatar = res.data
|
||||||
|
ElMessage.success({message: "上传成功", duration: 500})
|
||||||
|
}).catch((e) => {
|
||||||
|
ElMessage.error('图片上传失败:' + e.message)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
error(err) {
|
||||||
|
console.log(err.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
httpPost('/api/user/profile/update', user.value).then(() => {
|
||||||
|
ElMessage.success({message: '更新成功', duration: 500})
|
||||||
|
}).catch((e) => {
|
||||||
|
ElMessage.error('更新失败:' + e.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.user-info {
|
||||||
|
padding 20px
|
||||||
|
|
||||||
|
.el-row {
|
||||||
|
justify-content center
|
||||||
|
margin-bottom 10px
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-line {
|
||||||
|
padding-top 20px
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
width 100%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -96,6 +96,16 @@ const items = [
|
|||||||
index: '/admin/chat/model',
|
index: '/admin/chat/model',
|
||||||
title: '语言模型',
|
title: '语言模型',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: 'recharge',
|
||||||
|
index: '/admin/product',
|
||||||
|
title: '充值产品',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'order',
|
||||||
|
index: '/admin/order',
|
||||||
|
title: '充值订单',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: 'reward',
|
icon: 'reward',
|
||||||
index: '/admin/reward',
|
index: '/admin/reward',
|
||||||
|
@ -32,7 +32,7 @@ import {
|
|||||||
TextEllipsis,
|
TextEllipsis,
|
||||||
Uploader
|
Uploader
|
||||||
} from "vant";
|
} from "vant";
|
||||||
import router from "@/router";
|
import {router} from "@/router";
|
||||||
import 'v3-waterfall/dist/style.css'
|
import 'v3-waterfall/dist/style.css'
|
||||||
import V3waterfall from "v3-waterfall";
|
import V3waterfall from "v3-waterfall";
|
||||||
|
|
||||||
|
@ -126,6 +126,18 @@ const routes = [
|
|||||||
meta: {title: '语言模型'},
|
meta: {title: '语言模型'},
|
||||||
component: () => import('@/views/admin/ChatModel.vue'),
|
component: () => import('@/views/admin/ChatModel.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/product',
|
||||||
|
name: 'admin-product',
|
||||||
|
meta: {title: '充值产品'},
|
||||||
|
component: () => import('@/views/admin/Product.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/order',
|
||||||
|
name: 'admin-order',
|
||||||
|
meta: {title: '充值订单'},
|
||||||
|
component: () => import('@/views/admin/Order.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/reward',
|
path: '/admin/reward',
|
||||||
name: 'admin-reward',
|
name: 'admin-reward',
|
||||||
@ -214,12 +226,14 @@ const router = createRouter({
|
|||||||
routes: routes,
|
routes: routes,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let prevRoute = null
|
||||||
// dynamic change the title when router change
|
// dynamic change the title when router change
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
if (to.meta.title) {
|
if (to.meta.title) {
|
||||||
document.title = `${to.meta.title} | ${process.env.VUE_APP_TITLE}`
|
document.title = `${to.meta.title} | ${process.env.VUE_APP_TITLE}`
|
||||||
}
|
}
|
||||||
|
prevRoute = from
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router;
|
export {router, prevRoute};
|
@ -40,7 +40,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {nextTick, onMounted, ref} from "vue"
|
import {onMounted, ref} from "vue"
|
||||||
import {ElMessage} from "element-plus";
|
import {ElMessage} from "element-plus";
|
||||||
import {httpGet, httpPost} from "@/utils/http";
|
import {httpGet, httpPost} from "@/utils/http";
|
||||||
import ItemList from "@/components/ItemList.vue";
|
import ItemList from "@/components/ItemList.vue";
|
||||||
@ -48,7 +48,6 @@ import {Delete, Plus} from "@element-plus/icons-vue";
|
|||||||
import LoginDialog from "@/components/LoginDialog.vue";
|
import LoginDialog from "@/components/LoginDialog.vue";
|
||||||
import {checkSession} from "@/action/session";
|
import {checkSession} from "@/action/session";
|
||||||
import {arrayContains, removeArrayItem, substr} from "@/utils/libs";
|
import {arrayContains, removeArrayItem, substr} from "@/utils/libs";
|
||||||
import router from "@/router";
|
|
||||||
|
|
||||||
const listBoxHeight = window.innerHeight - 97
|
const listBoxHeight = window.innerHeight - 97
|
||||||
const list = ref([])
|
const list = ref([])
|
||||||
@ -71,8 +70,8 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const getRoles = () => {
|
const getRoles = () => {
|
||||||
|
showLoginDialog.value = false
|
||||||
checkSession().then(user => {
|
checkSession().then(user => {
|
||||||
showLoginDialog.value = false
|
|
||||||
roles.value = user.chat_roles
|
roles.value = user.chat_roles
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
})
|
})
|
||||||
|
@ -52,33 +52,7 @@
|
|||||||
<el-icon>
|
<el-icon>
|
||||||
<Tools/>
|
<Tools/>
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>聊天设置</span>
|
<span>账户信息</span>
|
||||||
</el-dropdown-item>
|
|
||||||
|
|
||||||
<el-dropdown-item @click="showPasswordDialog=true">
|
|
||||||
<i class="iconfont icon-password"></i>
|
|
||||||
<span>修改密码</span>
|
|
||||||
</el-dropdown-item>
|
|
||||||
|
|
||||||
<el-dropdown-item @click="showBindMobileDialog = true">
|
|
||||||
<el-icon>
|
|
||||||
<Iphone/>
|
|
||||||
</el-icon>
|
|
||||||
<span>绑定手机号</span>
|
|
||||||
</el-dropdown-item>
|
|
||||||
|
|
||||||
<el-dropdown-item @click="showRewardDialog = true" v-if="enableReward">
|
|
||||||
<el-icon>
|
|
||||||
<Present/>
|
|
||||||
</el-icon>
|
|
||||||
<span>加入众筹</span>
|
|
||||||
</el-dropdown-item>
|
|
||||||
|
|
||||||
<el-dropdown-item @click="showRewardVerifyDialog = true" v-if="enableReward">
|
|
||||||
<el-icon>
|
|
||||||
<Checked/>
|
|
||||||
</el-icon>
|
|
||||||
<span>众筹核销</span>
|
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
|
|
||||||
<el-dropdown-item @click="clearAllChats">
|
<el-dropdown-item @click="clearAllChats">
|
||||||
@ -220,32 +194,7 @@
|
|||||||
</el-main>
|
</el-main>
|
||||||
</el-container>
|
</el-container>
|
||||||
|
|
||||||
<config-dialog v-if="isLogin" :show="showConfigDialog" :models="models" @hide="showConfigDialog = false"
|
<config-dialog v-if="isLogin" :show="showConfigDialog" :models="models" @hide="showConfigDialog = false"/>
|
||||||
@update-user="updateUser"/>
|
|
||||||
<password-dialog v-if="isLogin" :show="showPasswordDialog" @hide="showPasswordDialog = false"
|
|
||||||
@logout="logout"/>
|
|
||||||
|
|
||||||
<bind-mobile v-if="isLogin" :show="showBindMobileDialog" :mobile="loginUser.mobile"
|
|
||||||
@hide="showBindMobileDialog = false"/>
|
|
||||||
|
|
||||||
<reward-verify v-if="isLogin" :show="showRewardVerifyDialog" @hide="showRewardVerifyDialog = false"/>
|
|
||||||
|
|
||||||
<el-dialog
|
|
||||||
v-model="showRewardDialog"
|
|
||||||
:show-close="true"
|
|
||||||
width="400px"
|
|
||||||
title="参与众筹"
|
|
||||||
>
|
|
||||||
<el-alert type="info" :closable="false">
|
|
||||||
<div style="font-size: 14px">您好,众筹 9.9元,就可以兑换 100 次对话,以此来覆盖我们的 OpenAI
|
|
||||||
账单和服务器的费用。<strong
|
|
||||||
style="color: #f56c6c">由于本人没有开通微信支付,付款后请凭借转账单号进入核销【众筹核销】菜单手动核销。</strong>
|
|
||||||
</div>
|
|
||||||
</el-alert>
|
|
||||||
<div style="text-align: center;padding-top: 10px;">
|
|
||||||
<el-image v-if="enableReward" :src="rewardImg"/>
|
|
||||||
</div>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@ -257,13 +206,10 @@ import ChatReply from "@/components/ChatReply.vue";
|
|||||||
import {
|
import {
|
||||||
ArrowDown,
|
ArrowDown,
|
||||||
Check,
|
Check,
|
||||||
Checked,
|
|
||||||
Close,
|
Close,
|
||||||
Delete,
|
Delete,
|
||||||
Edit,
|
Edit,
|
||||||
Iphone,
|
|
||||||
Plus,
|
Plus,
|
||||||
Present,
|
|
||||||
Promotion,
|
Promotion,
|
||||||
RefreshRight,
|
RefreshRight,
|
||||||
Search,
|
Search,
|
||||||
@ -279,16 +225,11 @@ import {httpGet, httpPost} from "@/utils/http";
|
|||||||
import {useRouter} from "vue-router";
|
import {useRouter} from "vue-router";
|
||||||
import Clipboard from "clipboard";
|
import Clipboard from "clipboard";
|
||||||
import ConfigDialog from "@/components/ConfigDialog.vue";
|
import ConfigDialog from "@/components/ConfigDialog.vue";
|
||||||
import PasswordDialog from "@/components/PasswordDialog.vue";
|
|
||||||
import {checkSession} from "@/action/session";
|
import {checkSession} from "@/action/session";
|
||||||
import BindMobile from "@/components/BindMobile.vue";
|
|
||||||
import RewardVerify from "@/components/RewardVerify.vue";
|
|
||||||
import Welcome from "@/components/Welcome.vue";
|
import Welcome from "@/components/Welcome.vue";
|
||||||
import ChatMidJourney from "@/components/ChatMidJourney.vue";
|
import ChatMidJourney from "@/components/ChatMidJourney.vue";
|
||||||
|
|
||||||
const title = ref('ChatGPT-智能助手');
|
const title = ref('ChatGPT-智能助手');
|
||||||
const enableReward = ref(false) // 是否启用众筹功能
|
|
||||||
const rewardImg = ref('/images/reward.png')
|
|
||||||
const models = ref([])
|
const models = ref([])
|
||||||
const modelID = ref(0)
|
const modelID = ref(0)
|
||||||
const chatData = ref([]);
|
const chatData = ref([]);
|
||||||
@ -305,10 +246,6 @@ const roleId = ref(0)
|
|||||||
const newChatItem = ref(null);
|
const newChatItem = ref(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const showConfigDialog = ref(false);
|
const showConfigDialog = ref(false);
|
||||||
const showPasswordDialog = ref(false);
|
|
||||||
const showBindMobileDialog = ref(false);
|
|
||||||
const showRewardDialog = ref(false);
|
|
||||||
const showRewardVerifyDialog = ref(false);
|
|
||||||
const isLogin = ref(false)
|
const isLogin = ref(false)
|
||||||
const showHello = ref(true)
|
const showHello = ref(true)
|
||||||
const textInput = ref(null)
|
const textInput = ref(null)
|
||||||
@ -360,8 +297,6 @@ onMounted(() => {
|
|||||||
|
|
||||||
httpGet("/api/admin/config/get?key=system").then(res => {
|
httpGet("/api/admin/config/get?key=system").then(res => {
|
||||||
title.value = res.data.title
|
title.value = res.data.title
|
||||||
rewardImg.value = res.data.reward_img
|
|
||||||
enableReward.value = res.data.enabled_reward
|
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
ElMessage.error("获取系统配置失败:" + e.message)
|
ElMessage.error("获取系统配置失败:" + e.message)
|
||||||
})
|
})
|
||||||
@ -855,11 +790,6 @@ const searchChat = function () {
|
|||||||
chatList.value = items;
|
chatList.value = items;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateUser = function (data) {
|
|
||||||
loginUser.value.avatar = data.avatar;
|
|
||||||
loginUser.value.nickname = data.nickname;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出会话
|
// 导出会话
|
||||||
const exportChat = () => {
|
const exportChat = () => {
|
||||||
if (!activeChat.value['chat_id']) {
|
if (!activeChat.value['chat_id']) {
|
||||||
|
@ -58,6 +58,7 @@ import {isMobile} from "@/utils/libs";
|
|||||||
import {checkSession} from "@/action/session";
|
import {checkSession} from "@/action/session";
|
||||||
import {setUserToken} from "@/store/session";
|
import {setUserToken} from "@/store/session";
|
||||||
import {validateMobile} from "@/utils/validate";
|
import {validateMobile} from "@/utils/validate";
|
||||||
|
import {prevRoute} from "@/router";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const title = ref('ChatGPT-PLUS 用户登录');
|
const title = ref('ChatGPT-PLUS 用户登录');
|
||||||
@ -91,11 +92,16 @@ const login = function () {
|
|||||||
|
|
||||||
httpPost('/api/user/login', {username: username.value.trim(), password: password.value.trim()}).then((res) => {
|
httpPost('/api/user/login', {username: username.value.trim(), password: password.value.trim()}).then((res) => {
|
||||||
setUserToken(res.data)
|
setUserToken(res.data)
|
||||||
if (isMobile()) {
|
if (prevRoute.path === '') {
|
||||||
router.push('/mobile')
|
if (isMobile()) {
|
||||||
|
router.push('/mobile')
|
||||||
|
} else {
|
||||||
|
router.push('/chat')
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
router.push('/chat')
|
router.push(prevRoute.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
ElMessage.error('登录失败,' + e.message)
|
ElMessage.error('登录失败,' + e.message)
|
||||||
})
|
})
|
||||||
|
@ -1,39 +1,417 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-member" :style="{ height: winHeight + 'px' }">
|
<div class="member custom-scroll">
|
||||||
<div class="inner">
|
<div class="title">
|
||||||
<h1>会员充值中心</h1>
|
会员充值中心
|
||||||
<h2>页面正在紧锣密鼓开发中,敬请期待!</h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="inner" :style="{height: listBoxHeight + 'px'}">
|
||||||
|
|
||||||
|
<div class="user-profile">
|
||||||
|
<user-profile/>
|
||||||
|
|
||||||
|
<el-row class="user-opt" :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-button type="primary" @click="showPasswordDialog = true">修改密码</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-button type="primary" @click="showBindMobileDialog = true">绑定手机号</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-button type="primary" v-if="enableReward" @click="showRewardDialog = true">加入众筹</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-button type="primary" v-if="enableReward" @click="showRewardVerifyDialog = true">众筹核销</el-button>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="24" style="padding-top: 30px">
|
||||||
|
<el-button type="danger" round @click="logout">退出登录</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="product-box">
|
||||||
|
<div class="info">
|
||||||
|
<el-alert type="info" show-icon :closable="false" effect="dark">
|
||||||
|
<strong>说明:</strong> 成为本站会员后每月有500次对话额度,50次 AI 绘画额度,限制下月1号解除,若在期间超过次数后可单独购买点卡。
|
||||||
|
当月充值的点卡有效期可以延期到下个月底。
|
||||||
|
</el-alert>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ItemList :items="list" v-if="list.length > 0" :gap="30" :width="200">
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="product-item" :style="{width: scope.width+'px'}" @click="orderPay(scope.item)">
|
||||||
|
<div class="image-container">
|
||||||
|
<el-image :src="vipImg" fit="cover"/>
|
||||||
|
</div>
|
||||||
|
<div class="product-title">
|
||||||
|
<span class="name">{{ scope.item.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="product-info">
|
||||||
|
<div class="info-line">
|
||||||
|
<span class="label">商品原价:</span>
|
||||||
|
<span class="price">¥{{ scope.item.price }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-line">
|
||||||
|
<span class="label">促销立减:</span>
|
||||||
|
<span class="price">¥{{ scope.item.discount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-line">
|
||||||
|
<span class="label">有效期:</span>
|
||||||
|
<span class="expire" v-if="scope.item.days > 0">{{ scope.item.days }}天</span>
|
||||||
|
<span class="expire" v-else>当月有效</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ItemList>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<login-dialog :show="showLoginDialog" @hide="showLoginDialog = false"/>
|
||||||
|
|
||||||
|
<password-dialog v-if="isLogin" :show="showPasswordDialog" @hide="showPasswordDialog = false"
|
||||||
|
@logout="logout"/>
|
||||||
|
|
||||||
|
<bind-mobile v-if="isLogin" :show="showBindMobileDialog" :mobile="user.mobile"
|
||||||
|
@hide="showBindMobileDialog = false"/>
|
||||||
|
|
||||||
|
<reward-verify v-if="isLogin" :show="showRewardVerifyDialog" @hide="showRewardVerifyDialog = false"/>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="showRewardDialog"
|
||||||
|
:show-close="true"
|
||||||
|
width="400px"
|
||||||
|
title="参与众筹"
|
||||||
|
>
|
||||||
|
<el-alert type="info" :closable="false">
|
||||||
|
<div style="font-size: 14px">您好,众筹 9.9元,就可以兑换 100 次对话,以此来覆盖我们的 OpenAI
|
||||||
|
账单和服务器的费用。<strong
|
||||||
|
style="color: #f56c6c">由于本人没有开通微信支付,付款后请凭借转账单号,点击【众筹核销】按钮手动核销。</strong>
|
||||||
|
</div>
|
||||||
|
</el-alert>
|
||||||
|
<div style="text-align: center;padding-top: 10px;">
|
||||||
|
<el-image v-if="enableReward" :src="rewardImg"/>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="showPayDialog"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:show-close="true"
|
||||||
|
:width="400"
|
||||||
|
title="充值订单支付">
|
||||||
|
<div class="pay-container">
|
||||||
|
<div class="count-down">
|
||||||
|
<count-down :second="orderTimeout" @timeout="orderPay" ref="countDown"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pay-qrcode" v-loading="loading">
|
||||||
|
<el-image :src="qrcode"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tip success" v-if="text !== ''">
|
||||||
|
<el-icon>
|
||||||
|
<SuccessFilled/>
|
||||||
|
</el-icon>
|
||||||
|
<span class="text">{{ text }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tip" v-else>
|
||||||
|
<el-icon>
|
||||||
|
<InfoFilled/>
|
||||||
|
</el-icon>
|
||||||
|
<span class="text">请打开手机支付宝扫码支付</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {ref} from "vue"
|
import {onMounted, ref} from "vue"
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
import {httpGet, httpPost} from "@/utils/http";
|
||||||
|
import ItemList from "@/components/ItemList.vue";
|
||||||
|
import {InfoFilled, SuccessFilled} from "@element-plus/icons-vue";
|
||||||
|
import LoginDialog from "@/components/LoginDialog.vue";
|
||||||
|
import {checkSession} from "@/action/session";
|
||||||
|
import UserProfile from "@/components/UserProfile.vue";
|
||||||
|
import PasswordDialog from "@/components/PasswordDialog.vue";
|
||||||
|
import BindMobile from "@/components/BindMobile.vue";
|
||||||
|
import RewardVerify from "@/components/RewardVerify.vue";
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
|
import {removeUserToken} from "@/store/session";
|
||||||
|
import CountDown from "@/components/CountDown.vue";
|
||||||
|
|
||||||
|
const listBoxHeight = window.innerHeight - 97
|
||||||
|
const list = ref([])
|
||||||
|
const showLoginDialog = ref(false)
|
||||||
|
const showPayDialog = ref(false)
|
||||||
|
const elements = ref(null)
|
||||||
|
const vipImg = ref("/images/vip.png")
|
||||||
|
const enableReward = ref(false) // 是否启用众筹功能
|
||||||
|
const rewardImg = ref('/images/reward.png')
|
||||||
|
const qrcode = ref("")
|
||||||
|
const showPasswordDialog = ref(false);
|
||||||
|
const showBindMobileDialog = ref(false);
|
||||||
|
const showRewardDialog = ref(false);
|
||||||
|
const showRewardVerifyDialog = ref(false);
|
||||||
|
const text = ref("")
|
||||||
|
const user = ref(null)
|
||||||
|
const isLogin = ref(false)
|
||||||
|
const router = useRouter()
|
||||||
|
const curPayProduct = ref(null)
|
||||||
|
const activeOrderNo = ref("")
|
||||||
|
const countDown = ref(null)
|
||||||
|
const orderTimeout = ref(1800)
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkSession().then(_user => {
|
||||||
|
user.value = _user
|
||||||
|
isLogin.value = true
|
||||||
|
httpGet("/api/product/list").then((res) => {
|
||||||
|
list.value = res.data
|
||||||
|
}).catch(e => {
|
||||||
|
ElMessage.error("获取产品套餐失败:" + e.message)
|
||||||
|
})
|
||||||
|
}).catch(() => {
|
||||||
|
router.push("/login")
|
||||||
|
})
|
||||||
|
|
||||||
|
httpGet("/api/admin/config/get?key=system").then(res => {
|
||||||
|
rewardImg.value = res.data['reward_img']
|
||||||
|
enableReward.value = res.data['enabled_reward']
|
||||||
|
if (res.data['order_pay_timeout'] > 0) {
|
||||||
|
orderTimeout.value = res.data['order_pay_timeout']
|
||||||
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
ElMessage.error("获取系统配置失败:" + e.message)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const orderPay = (row) => {
|
||||||
|
if (!user.value.id) {
|
||||||
|
showLoginDialog.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (row) {
|
||||||
|
curPayProduct.value = row
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
httpPost("/api/payment/alipay/qrcode", {product_id: curPayProduct.value.id, user_id: user.value.id}).then(res => {
|
||||||
|
showPayDialog.value = true
|
||||||
|
qrcode.value = res.data['image']
|
||||||
|
activeOrderNo.value = res.data['order_no']
|
||||||
|
queryOrder(activeOrderNo.value)
|
||||||
|
loading.value = false
|
||||||
|
// 重置计数器
|
||||||
|
if (countDown.value) {
|
||||||
|
countDown.value.resetTimer()
|
||||||
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
ElMessage.error("生成支付订单失败:" + e.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryOrder = (orderNo) => {
|
||||||
|
httpPost("/api/payment/query", {order_no: orderNo}).then(res => {
|
||||||
|
if (res.data.status === 1) {
|
||||||
|
text.value = "扫码成功,请在手机上进行支付!"
|
||||||
|
queryOrder(orderNo)
|
||||||
|
} else if (res.data.status === 2) {
|
||||||
|
text.value = "支付成功,正在刷新页面"
|
||||||
|
setTimeout(() => location.reload(), 500)
|
||||||
|
} else {
|
||||||
|
// 如果当前订单没有过期,继续等待订单的下一个状态
|
||||||
|
if (activeOrderNo.value === orderNo) {
|
||||||
|
queryOrder(orderNo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
ElMessage.error("查询支付状态失败:" + e.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = function () {
|
||||||
|
httpGet('/api/user/logout').then(() => {
|
||||||
|
removeUserToken();
|
||||||
|
router.push('/login');
|
||||||
|
}).catch(() => {
|
||||||
|
ElMessage.error('注销失败!');
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const winHeight = ref(window.innerHeight)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus">
|
||||||
.page-member {
|
@import "@/assets/css/custom-scroll.styl"
|
||||||
display: flex;
|
.member {
|
||||||
justify-content: center;
|
|
||||||
align-items center
|
|
||||||
background-color: #282c34;
|
background-color: #282c34;
|
||||||
|
height 100vh
|
||||||
|
|
||||||
|
.el-dialog {
|
||||||
|
.el-dialog__body {
|
||||||
|
padding-top 10px
|
||||||
|
|
||||||
|
.pay-container {
|
||||||
|
.count-down {
|
||||||
|
display flex
|
||||||
|
justify-content center
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-qrcode {
|
||||||
|
display flex
|
||||||
|
justify-content center
|
||||||
|
|
||||||
|
.el-image {
|
||||||
|
width 360px;
|
||||||
|
height 360px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
display flex
|
||||||
|
justify-content center
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size 24px
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-size: 16px
|
||||||
|
margin-left 10px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip.success {
|
||||||
|
color #07c160
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
text-align center
|
||||||
|
background-color #25272d
|
||||||
|
font-size 24px
|
||||||
|
color #ffffff
|
||||||
|
padding 10px
|
||||||
|
border-bottom 1px solid #3c3c3c
|
||||||
|
}
|
||||||
|
|
||||||
.inner {
|
.inner {
|
||||||
text-align center
|
display flex
|
||||||
|
color #ffffff
|
||||||
|
padding 15px;
|
||||||
|
overflow-y visible
|
||||||
|
overflow-x hidden
|
||||||
|
|
||||||
h1 {
|
.user-profile {
|
||||||
color: #202020;
|
padding 10px 20px
|
||||||
font-size: 80px;
|
background-color #393F4A
|
||||||
font-weight: bold;
|
color #ffffff
|
||||||
letter-spacing: 0.1em;
|
border-radius 10px
|
||||||
text-shadow: -1px -1px 1px #111111, 2px 2px 1px #363636;
|
|
||||||
|
.el-form-item__label {
|
||||||
|
color #ffffff
|
||||||
|
justify-content start
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-opt {
|
||||||
|
.el-col {
|
||||||
|
padding 10px
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
width 100%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
|
||||||
color #ffffff;
|
.product-box {
|
||||||
font-weight: bold;
|
padding 0 10px
|
||||||
|
|
||||||
|
.info {
|
||||||
|
.el-alert__description {
|
||||||
|
font-size 14px !important
|
||||||
|
margin 0
|
||||||
|
}
|
||||||
|
padding 10px 20px
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-box {
|
||||||
|
.product-item {
|
||||||
|
border 1px solid #666666
|
||||||
|
border-radius 6px
|
||||||
|
overflow hidden
|
||||||
|
cursor pointer
|
||||||
|
transition: all 0.3s ease; /* 添加过渡效果 */
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
display flex
|
||||||
|
justify-content center
|
||||||
|
|
||||||
|
.el-image {
|
||||||
|
padding 6px
|
||||||
|
|
||||||
|
.el-image__inner {
|
||||||
|
border-radius 10px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-title {
|
||||||
|
display flex
|
||||||
|
padding 10px
|
||||||
|
|
||||||
|
.name {
|
||||||
|
width 100%
|
||||||
|
text-align center
|
||||||
|
font-size 16px
|
||||||
|
font-weight bold
|
||||||
|
color #47fff1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
padding 10px 20px
|
||||||
|
font-size 14px
|
||||||
|
color #999999
|
||||||
|
|
||||||
|
.info-line {
|
||||||
|
display flex
|
||||||
|
width 100%
|
||||||
|
padding 5px 0
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display flex
|
||||||
|
width 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
.price, .expire {
|
||||||
|
display flex
|
||||||
|
width 90px
|
||||||
|
justify-content right
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
color #f56c6c
|
||||||
|
}
|
||||||
|
|
||||||
|
.expire {
|
||||||
|
color #409eff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 0 10px rgba(71, 255, 241, 0.6); /* 添加阴影效果 */
|
||||||
|
transform: translateY(-10px); /* 向上移动10像素 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
140
web/src/views/admin/Order.vue
Normal file
140
web/src/views/admin/Order.vue
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container order" v-loading="loading">
|
||||||
|
<div class="handle-box">
|
||||||
|
<el-input v-model="query.order_no" placeholder="订单号" class="handle-input mr10"></el-input>
|
||||||
|
<el-date-picker
|
||||||
|
v-model="query.pay_time"
|
||||||
|
type="daterange"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
style="margin-right: 10px;width: 200px; position: relative;top:3px;"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" :icon="Search" @click="fetchData">搜索</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-row>
|
||||||
|
<el-table :data="items" :row-key="row => row.id" table-layout="auto">
|
||||||
|
<el-table-column prop="order_no" label="订单号"/>
|
||||||
|
<el-table-column prop="mobile" label="下单用户"/>
|
||||||
|
<el-table-column prop="subject" label="产品名称"/>
|
||||||
|
<el-table-column prop="amount" label="订单金额"/>
|
||||||
|
<el-table-column label="调用次数">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ scope.row.remark?.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-column label="支付时间">
|
||||||
|
<template #default="scope">
|
||||||
|
<span v-if="scope.row['pay_time']">{{ dateFormat(scope.row['pay_time']) }}</span>
|
||||||
|
<el-tag v-else>未支付</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="pay_way" label="支付方式"/>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-popconfirm title="确定要删除当前记录吗?" @confirm="remove(scope.row)">
|
||||||
|
<template #reference>
|
||||||
|
<el-button size="small" type="danger">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<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} from "vue";
|
||||||
|
import {httpGet, httpPost} from "@/utils/http";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
import {dateFormat, removeArrayItem} from "@/utils/libs";
|
||||||
|
import {Search} from "@element-plus/icons-vue";
|
||||||
|
|
||||||
|
// 变量定义
|
||||||
|
const items = ref([])
|
||||||
|
const query = ref({order_no: "", pay_time: []})
|
||||||
|
const total = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(15)
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
// 获取数据
|
||||||
|
const fetchData = () => {
|
||||||
|
query.value.page = page.value
|
||||||
|
query.value.page_size = pageSize.value
|
||||||
|
httpPost('/api/admin/order/list', query.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);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const remove = function (row) {
|
||||||
|
httpGet('/api/admin/order/remove?id=' + row.id).then(() => {
|
||||||
|
ElMessage.success("删除成功!")
|
||||||
|
items.value = removeArrayItem(items.value, row, (v1, v2) => {
|
||||||
|
return v1.id === v2.id
|
||||||
|
})
|
||||||
|
}).catch((e) => {
|
||||||
|
ElMessage.error("删除失败:" + e.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.order {
|
||||||
|
|
||||||
|
.handle-box {
|
||||||
|
.handle-input {
|
||||||
|
max-width 150px;
|
||||||
|
margin-right 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-box {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
display flex;
|
||||||
|
justify-content flex-end
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-select {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
226
web/src/views/admin/Product.vue
Normal file
226
web/src/views/admin/Product.vue
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container list" v-loading="loading">
|
||||||
|
|
||||||
|
<div class="handle-box">
|
||||||
|
<el-button type="primary" :icon="Plus" @click="add">新增</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-row>
|
||||||
|
<el-table :data="items" :row-key="row => row.id" table-layout="auto">
|
||||||
|
<el-table-column prop="name" label="产品名称">
|
||||||
|
<template #default="scope">
|
||||||
|
<span class="sort" :data-id="scope.row.id">{{scope.row.name}}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="price" label="产品价格"/>
|
||||||
|
<el-table-column prop="discount" label="优惠金额"/>
|
||||||
|
<el-table-column prop="days" label="有效期(天)">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag v-if="scope.row.days === 0">长期有效</el-tag>
|
||||||
|
<span v-else>{{scope.row.days}}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="calls" label="调用次数"/>
|
||||||
|
<el-table-column prop="sales" label="销量"/>
|
||||||
|
<el-table-column prop="enabled" label="启用状态">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-switch v-model="scope.row['enabled']" @change="enable(scope.row)"/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="更新时间">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ dateFormat(scope.row['updated_at']) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button size="small" type="primary" @click="edit(scope.row)">编辑</el-button>
|
||||||
|
<el-popconfirm title="确定要删除当前记录吗?" @confirm="remove(scope.row)">
|
||||||
|
<template #reference>
|
||||||
|
<el-button size="small" type="danger">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="showDialog"
|
||||||
|
:title="title"
|
||||||
|
style="width: 90%; max-width: 600px;"
|
||||||
|
>
|
||||||
|
<el-form :model="item" label-width="120px" ref="formRef" :rules="rules">
|
||||||
|
<el-form-item label="产品名称:" prop="name">
|
||||||
|
<el-input v-model="item.name" autocomplete="off"/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="产品价格:" prop="price">
|
||||||
|
<el-input v-model="item.price" autocomplete="off"/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="优惠金额:" prop="discount">
|
||||||
|
<el-input v-model="item.discount" autocomplete="off"/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="有效期:" prop="days">
|
||||||
|
<el-input v-model.number="item.days" autocomplete="off" placeholder="会员有效期(天)"/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="调用次数:" prop="days">
|
||||||
|
<el-input v-model.number="item.calls" autocomplete="off" placeholder="增加调用次数"/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="启用状态:" prop="enable">
|
||||||
|
<el-switch v-model="item.enabled"/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="showDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="save">提交</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {onMounted, reactive, ref} from "vue";
|
||||||
|
import {httpGet, httpPost} from "@/utils/http";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
import {dateFormat, removeArrayItem} from "@/utils/libs";
|
||||||
|
import {Plus} from "@element-plus/icons-vue";
|
||||||
|
import {Sortable} from "sortablejs";
|
||||||
|
|
||||||
|
// 变量定义
|
||||||
|
const items = ref([])
|
||||||
|
const item = ref({})
|
||||||
|
const showDialog = ref(false)
|
||||||
|
const title = ref("")
|
||||||
|
const rules = reactive({
|
||||||
|
name: [{required: true, message: '请输入产品名称', trigger: 'change',}],
|
||||||
|
price: [{required: true, message: '请输产品价格', trigger: 'change',}],
|
||||||
|
discount: [{required: true, message: '请输优惠金额', trigger: 'change',}],
|
||||||
|
days: [{required: true, message: '请输入有效期', trigger: 'change',}],
|
||||||
|
})
|
||||||
|
const loading = ref(true)
|
||||||
|
const formRef = ref(null)
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
httpGet('/api/admin/product/list').then((res) => {
|
||||||
|
if (res.data) {
|
||||||
|
// 初始化数据
|
||||||
|
const arr = res.data;
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
arr[i].last_used_at = dateFormat(arr[i].last_used_at)
|
||||||
|
}
|
||||||
|
items.value = arr
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}).catch(() => {
|
||||||
|
ElMessage.error("获取数据失败");
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const drawBodyWrapper = document.querySelector('.el-table__body tbody')
|
||||||
|
|
||||||
|
// 初始化拖动排序插件
|
||||||
|
Sortable.create(drawBodyWrapper, {
|
||||||
|
sort: true,
|
||||||
|
animation: 500,
|
||||||
|
onEnd({newIndex, oldIndex, from}) {
|
||||||
|
if (oldIndex === newIndex) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedData = Array.from(from.children).map(row => row.querySelector('.sort').getAttribute('data-id'));
|
||||||
|
const ids = []
|
||||||
|
const sorts = []
|
||||||
|
sortedData.forEach((id,index) => {
|
||||||
|
ids.push(parseInt(id))
|
||||||
|
sorts.push(index)
|
||||||
|
})
|
||||||
|
|
||||||
|
httpPost("/api/admin/product/sort", {ids: ids, sorts:sorts}).catch(e => {
|
||||||
|
ElMessage.error("排序失败:"+e.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const add = function () {
|
||||||
|
title.value = "新增模型"
|
||||||
|
showDialog.value = true
|
||||||
|
item.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const edit = function (row) {
|
||||||
|
title.value = "修改模型"
|
||||||
|
showDialog.value = true
|
||||||
|
item.value = row
|
||||||
|
}
|
||||||
|
|
||||||
|
const save = function () {
|
||||||
|
formRef.value.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
showDialog.value = false
|
||||||
|
item.value['price'] = parseFloat(item.value['price'])
|
||||||
|
item.value['discount'] = parseFloat(item.value['discount'])
|
||||||
|
httpPost('/api/admin/product/save', item.value).then((res) => {
|
||||||
|
ElMessage.success('操作成功!')
|
||||||
|
if (!item.value['id']) {
|
||||||
|
const newItem = res.data
|
||||||
|
items.value.push(newItem)
|
||||||
|
}
|
||||||
|
}).catch((e) => {
|
||||||
|
ElMessage.error('操作失败,' + e.message)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const enable = (row) => {
|
||||||
|
httpPost('/api/admin/product/enable', {id: row.id, enabled: row.enabled}).then(() => {
|
||||||
|
ElMessage.success("操作成功!")
|
||||||
|
}).catch(e => {
|
||||||
|
ElMessage.error("操作失败:"+e.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const remove = function (row) {
|
||||||
|
httpGet('/api/admin/product/remove?id=' + row.id).then(() => {
|
||||||
|
ElMessage.success("删除成功!")
|
||||||
|
items.value = removeArrayItem(items.value, row, (v1, v2) => {
|
||||||
|
return v1.id === v2.id
|
||||||
|
})
|
||||||
|
}).catch((e) => {
|
||||||
|
ElMessage.error("删除失败:" + e.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.list {
|
||||||
|
|
||||||
|
.opt-box {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
display flex;
|
||||||
|
justify-content flex-end
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-select {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
@ -12,6 +12,9 @@
|
|||||||
<el-form-item label="注册赠送对话次数" prop="user_init_calls">
|
<el-form-item label="注册赠送对话次数" prop="user_init_calls">
|
||||||
<el-input v-model.number="system['user_init_calls']" placeholder="新用户注册赠送对话次数"/>
|
<el-input v-model.number="system['user_init_calls']" placeholder="新用户注册赠送对话次数"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="VIP每月对话次数" prop="vip_month_calls">
|
||||||
|
<el-input v-model.number="system['vip_month_calls']" placeholder="VIP用户每月赠送对话次数"/>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="注册赠送绘图次数" prop="init_img_calls">
|
<el-form-item label="注册赠送绘图次数" prop="init_img_calls">
|
||||||
<el-input v-model.number="system['init_img_calls']" placeholder="新用户注册赠送绘图次数"/>
|
<el-input v-model.number="system['init_img_calls']" placeholder="新用户注册赠送绘图次数"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -87,6 +90,36 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="启用支付宝" prop="enabled_alipay">
|
||||||
|
<el-switch v-model="system['enabled_alipay']"/>
|
||||||
|
<el-tooltip
|
||||||
|
effect="dark"
|
||||||
|
content="是否启用支付宝支付功能,<br />请先在 config.toml 配置文件配置支付秘钥"
|
||||||
|
raw-content
|
||||||
|
placement="right"
|
||||||
|
>
|
||||||
|
<el-icon>
|
||||||
|
<InfoFilled/>
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="订单超时时间" prop="order_pay_timeout">
|
||||||
|
<div class="tip-input">
|
||||||
|
<el-input v-model.number="system['order_pay_timeout']" placeholder="单位:秒"/>
|
||||||
|
<div class="info">
|
||||||
|
<el-tooltip
|
||||||
|
effect="dark"
|
||||||
|
content="系统会定期清理超时未支付的订单<br/>默认值:900秒"
|
||||||
|
raw-content
|
||||||
|
placement="right"
|
||||||
|
>
|
||||||
|
<el-icon>
|
||||||
|
<InfoFilled/>
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="默认AI模型" prop="default_models">
|
<el-form-item label="默认AI模型" prop="default_models">
|
||||||
<template #default>
|
<template #default>
|
||||||
<div class="tip-input">
|
<div class="tip-input">
|
||||||
|
@ -11,7 +11,12 @@
|
|||||||
<el-table :data="users.items" border class="table" :row-key="row => row.id"
|
<el-table :data="users.items" border class="table" :row-key="row => row.id"
|
||||||
@selection-change="handleSelectionChange" table-layout="auto">
|
@selection-change="handleSelectionChange" table-layout="auto">
|
||||||
<el-table-column type="selection" width="38"/>
|
<el-table-column type="selection" width="38"/>
|
||||||
<el-table-column prop="mobile" label="账号"/>
|
<el-table-column prop="mobile" label="账号">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ scope.row.mobile }}</span>
|
||||||
|
<el-image v-if="scope.row.vip" :src="vipImg" style="height: 20px;position: relative; top:5px; left: 5px"/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="calls" label="剩余对话次数"/>
|
<el-table-column prop="calls" label="剩余对话次数"/>
|
||||||
<el-table-column prop="img_calls" label="剩余绘图次数"/>
|
<el-table-column prop="img_calls" label="剩余绘图次数"/>
|
||||||
<el-table-column prop="total_tokens" label="累计消耗tokens"/>
|
<el-table-column prop="total_tokens" label="累计消耗tokens"/>
|
||||||
@ -74,7 +79,7 @@
|
|||||||
<el-input v-model.number="user.calls" autocomplete="off" placeholder="0"/>
|
<el-input v-model.number="user.calls" autocomplete="off" placeholder="0"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="绘图次数:" prop="img_calls">
|
<el-form-item label="绘图次数:" prop="img_calls">
|
||||||
<el-input v-model.number="user.img_calls" autocomplete="off" placeholder="0"/>
|
<el-input v-model.number="user['img_calls']" autocomplete="off" placeholder="0"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="有效期:" prop="expired_time">
|
<el-form-item label="有效期:" prop="expired_time">
|
||||||
@ -124,6 +129,10 @@
|
|||||||
<el-form-item label="启用状态">
|
<el-form-item label="启用状态">
|
||||||
<el-switch v-model="user.status"/>
|
<el-switch v-model="user.status"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="开通VIP">
|
||||||
|
<el-switch v-model="user.vip"/>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@ -140,8 +149,8 @@
|
|||||||
width="50%"
|
width="50%"
|
||||||
>
|
>
|
||||||
<el-form label-width="100px" ref="userEditFormRef">
|
<el-form label-width="100px" ref="userEditFormRef">
|
||||||
<el-form-item label="用户名:">
|
<el-form-item label="账户:">
|
||||||
<el-input v-model="pass.username" autocomplete="off" readonly disabled/>
|
<el-input v-model="pass.mobile" autocomplete="off" readonly disabled/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="新密码:">
|
<el-form-item label="新密码:">
|
||||||
@ -168,18 +177,18 @@ import {Plus, Search} from "@element-plus/icons-vue";
|
|||||||
|
|
||||||
// 变量定义
|
// 变量定义
|
||||||
const users = ref({page: 1, page_size: 15, items: []})
|
const users = ref({page: 1, page_size: 15, items: []})
|
||||||
const query = ref({username: '', mobile: '', page: 1, page_size: 15})
|
const query = ref({mobile: '', page: 1, page_size: 15})
|
||||||
|
|
||||||
const title = ref('添加用户')
|
const title = ref('添加用户')
|
||||||
|
const vipImg = ref("/images/vip.png")
|
||||||
const add = ref(true)
|
const add = ref(true)
|
||||||
const user = ref({chat_roles: [], chat_models: []})
|
const user = ref({chat_roles: [], chat_models: []})
|
||||||
const pass = ref({username: '', password: '', id: 0})
|
const pass = ref({mobile: '', password: '', id: 0})
|
||||||
const roles = ref([])
|
const roles = ref([])
|
||||||
const models = ref([])
|
const models = ref([])
|
||||||
const showUserEditDialog = ref(false)
|
const showUserEditDialog = ref(false)
|
||||||
const showResetPassDialog = ref(false)
|
const showResetPassDialog = ref(false)
|
||||||
const rules = reactive({
|
const rules = reactive({
|
||||||
username: [{required: true, message: '请输入用户名', trigger: 'change',}],
|
|
||||||
nickname: [{required: true, message: '请输入昵称', trigger: 'change',}],
|
nickname: [{required: true, message: '请输入昵称', trigger: 'change',}],
|
||||||
password: [{required: true, message: '请输入密码', trigger: 'change',}],
|
password: [{required: true, message: '请输入密码', trigger: 'change',}],
|
||||||
mobile: [{required: true, message: '请输入手机号码', trigger: 'change',}],
|
mobile: [{required: true, message: '请输入手机号码', trigger: 'change',}],
|
||||||
@ -300,7 +309,7 @@ const handleSelectionChange = function (rows) {
|
|||||||
const resetPass = (row) => {
|
const resetPass = (row) => {
|
||||||
showResetPassDialog.value = true
|
showResetPassDialog.value = true
|
||||||
pass.value.id = row.id
|
pass.value.id = row.id
|
||||||
pass.value.username = row.username
|
pass.value.mobile = row.mobile
|
||||||
}
|
}
|
||||||
|
|
||||||
const doResetPass = () => {
|
const doResetPass = () => {
|
||||||
|
@ -78,7 +78,7 @@ import {ref} from "vue";
|
|||||||
import {httpGet, httpPost} from "@/utils/http";
|
import {httpGet, httpPost} from "@/utils/http";
|
||||||
import {showConfirmDialog, showFailToast, showSuccessToast} from "vant";
|
import {showConfirmDialog, showFailToast, showSuccessToast} from "vant";
|
||||||
import {checkSession} from "@/action/session";
|
import {checkSession} from "@/action/session";
|
||||||
import router from "@/router";
|
import {router} from "@/router";
|
||||||
import {setChatConfig} from "@/store/chat";
|
import {setChatConfig} from "@/store/chat";
|
||||||
import {removeArrayItem} from "@/utils/libs";
|
import {removeArrayItem} from "@/utils/libs";
|
||||||
import BindMobile from "@/components/mobile/BindMobile.vue";
|
import BindMobile from "@/components/mobile/BindMobile.vue";
|
||||||
|
Loading…
Reference in New Issue
Block a user