From 848a10c3a8973dfd0b644409ceaa344983332c8d Mon Sep 17 00:00:00 2001 From: RockYang Date: Mon, 6 Nov 2023 17:55:46 +0800 Subject: [PATCH 01/12] feat: integrated Alipay payment module --- api/.gitignore | 1 + api/core/types/config.go | 24 ++ api/core/types/order.go | 17 ++ api/go.mod | 8 + api/go.sum | 14 ++ api/handler/admin/order_handler.go | 93 ++++++++ api/handler/admin/product_handler.go | 144 ++++++++++++ api/handler/order_handler.go | 57 +++++ api/handler/payment_handler.go | 259 ++++++++++++++++++++++ api/handler/product_handler.go | 44 ++++ api/main.go | 48 ++++ api/service/payment/alipay_service.go | 142 ++++++++++++ api/service/snowflake.go | 56 +++++ api/service/xxl_job_service.go | 123 ++++++++++ api/store/model/order.go | 21 ++ api/store/model/product.go | 14 ++ api/store/model/user.go | 2 + api/store/vo/order.go | 18 ++ api/store/vo/product.go | 13 ++ api/store/vo/user.go | 2 + api/utils/common.go | 52 +++++ database/update-v3.1.8.sql | 60 +++++ web/src/components/admin/AdminSidebar.vue | 10 + web/src/router.js | 12 + web/src/views/admin/Order.vue | 139 ++++++++++++ web/src/views/admin/Product.vue | 226 +++++++++++++++++++ 26 files changed, 1599 insertions(+) create mode 100644 api/core/types/order.go create mode 100644 api/handler/admin/order_handler.go create mode 100644 api/handler/admin/product_handler.go create mode 100644 api/handler/order_handler.go create mode 100644 api/handler/payment_handler.go create mode 100644 api/handler/product_handler.go create mode 100644 api/service/payment/alipay_service.go create mode 100644 api/service/snowflake.go create mode 100644 api/service/xxl_job_service.go create mode 100644 api/store/model/order.go create mode 100644 api/store/model/product.go create mode 100644 api/store/vo/order.go create mode 100644 api/store/vo/product.go create mode 100644 database/update-v3.1.8.sql create mode 100644 web/src/views/admin/Order.vue create mode 100644 web/src/views/admin/Product.vue diff --git a/api/.gitignore b/api/.gitignore index 7502cf69..bd063e1f 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -18,3 +18,4 @@ data config.toml static/upload storage.json +certs/alipay/* diff --git a/api/core/types/config.go b/api/core/types/config.go index 3e39a20f..49f1ea6c 100644 --- a/api/core/types/config.go +++ b/api/core/types/config.go @@ -21,6 +21,9 @@ type AppConfig struct { MjConfig MidJourneyConfig // mj 绘画配置 WeChatBot bool // 是否启用微信机器人 SdConfig StableDiffusionConfig // sd 绘画配置 + + XXLConfig XXLConfig + AlipayConfig AlipayConfig } type ChatPlusApiConfig struct { @@ -57,6 +60,27 @@ type AliYunSmsConfig struct { CodeTempId string // 验证码短信模板 ID } +type AlipayConfig struct { + Enabled bool // 是否启用支付宝服务 + Company string // 公司名称 + UserId string // 支付宝用户 ID + AppId string // 支付宝 AppID + PrivateKey string // 用户私钥文件路径 + PublicKey string // 用户公钥文件路径 + AlipayPublicKey string // 支付宝公钥文件路径 + RootCert string // Root 秘钥路径 + ReturnURL string // 支付成功返回 URL + NotifyURL string // 异步通知回调 +} + +type XXLConfig struct { // XXL 任务调度配置 + ServerAddr string + AccessToken string + ExecutorIp string + ExecutorPort string + RegistryKey string +} + type RedisConfig struct { Host string Port int diff --git a/api/core/types/order.go b/api/core/types/order.go new file mode 100644 index 00000000..433580a1 --- /dev/null +++ b/api/core/types/order.go @@ -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"` +} diff --git a/api/go.mod b/api/go.mod index 2795d11c..a4113b18 100644 --- a/api/go.mod +++ b/api/go.mod @@ -18,12 +18,15 @@ require ( github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 github.com/qiniu/go-sdk/v7 v7.17.1 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e + github.com/smartwalle/alipay/v3 v3.2.15 github.com/syndtr/goleveldb v1.0.0 go.uber.org/zap v1.23.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gorm.io/driver/mysql v1.4.7 ) +require github.com/xxl-job/xxl-job-executor-go v1.2.0 + require ( github.com/andybalholm/brotli v1.0.4 // indirect github.com/bytedance/sonic v1.9.1 // indirect @@ -34,6 +37,7 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gaukas/godicttls v0.0.3 // indirect + github.com/go-basic/ipv4 v1.0.0 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/goccy/go-json v0.10.2 // indirect @@ -49,6 +53,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/sha256-simd v1.0.1 // indirect + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/onsi/ginkgo/v2 v2.10.0 // indirect github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect @@ -59,6 +64,9 @@ require ( github.com/refraction-networking/utls v1.3.2 // indirect github.com/rs/xid v1.5.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/smartwalle/ncrypto v1.0.2 // indirect + github.com/smartwalle/ngx v1.0.6 // indirect + github.com/smartwalle/nsign v1.0.8 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect go.uber.org/dig v1.16.1 // indirect golang.org/x/arch v0.3.0 // indirect diff --git a/api/go.sum b/api/go.sum index 108f5b59..6bc673ed 100644 --- a/api/go.sum +++ b/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-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-basic/ipv4 v1.0.0 h1:gjyFAa1USC1hhXTkPOwBWDPfMcUaIM+tvo1XzV9EZxs= +github.com/go-basic/ipv4 v1.0.0/go.mod h1:etLBnaxbidQfuqE6wgZQfs38nEWNmzALkxDZe4xY8Dg= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -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.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -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/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= +github.com/smartwalle/alipay/v3 v3.2.15 h1:3fvFJnINKKAOXHR/Iv20k1Z7KJ+nOh3oK214lELPqG8= +github.com/smartwalle/alipay/v3 v3.2.15/go.mod h1:niTNB609KyUYuAx9Bex/MawEjv2yPx4XOjxSAkqmGjE= +github.com/smartwalle/ncrypto v1.0.2 h1:pTAhCqtPCMhpOwFXX+EcMdR6PNzruBNoGQrN2S1GbGI= +github.com/smartwalle/ncrypto v1.0.2/go.mod h1:Dwlp6sfeNaPMnOxMNayMTacvC5JGEVln3CVdiVDgbBk= +github.com/smartwalle/ngx v1.0.6 h1:JPNqNOIj+2nxxFtrSkJO+vKJfeNUSEQueck/Wworjps= +github.com/smartwalle/ngx v1.0.6/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0= +github.com/smartwalle/nsign v1.0.8 h1:78KWtwKPrdt4Xsn+tNEBVxaTLIJBX9YRX0ZSrMUeuHo= +github.com/smartwalle/nsign v1.0.8/go.mod h1:eY6I4CJlyNdVMP+t6z1H6Jpd4m5/V+8xi44ufSTxXgc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -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/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/xxl-job/xxl-job-executor-go v1.2.0 h1:MTl2DpwrK2+hNjRRks2k7vB3oy+3onqm9OaSarneeLQ= +github.com/xxl-job/xxl-job-executor-go v1.2.0/go.mod h1:bUFhz/5Irp9zkdYk5MxhQcDDT6LlZrI8+rv5mHtQ1mo= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= diff --git a/api/handler/admin/order_handler.go b/api/handler/admin/order_handler.go new file mode 100644 index 00000000..4ec4537a --- /dev/null +++ b/api/handler/admin/order_handler.go @@ -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) +} diff --git a/api/handler/admin/product_handler.go b/api/handler/admin/product_handler.go new file mode 100644 index 00000000..2c6d8df4 --- /dev/null +++ b/api/handler/admin/product_handler.go @@ -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) +} diff --git a/api/handler/order_handler.go b/api/handler/order_handler.go new file mode 100644 index 00000000..35d9e7ee --- /dev/null +++ b/api/handler/order_handler.go @@ -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)) +} diff --git a/api/handler/payment_handler.go b/api/handler/payment_handler.go new file mode 100644 index 00000000..e329f926 --- /dev/null +++ b/api/handler/payment_handler.go @@ -0,0 +1,259 @@ +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" +) + +// 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 := h.App.Config.AlipayConfig.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) { + var data struct { + ProductId uint `json:"product_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 + } + user, err := utils.GetLoginUser(c, h.db) + if err != nil { + resp.NotAuth(c) + 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, + 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") +} diff --git a/api/handler/product_handler.go b/api/handler/product_handler.go new file mode 100644 index 00000000..9bc7988a --- /dev/null +++ b/api/handler/product_handler.go @@ -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) +} diff --git a/api/main.go b/api/main.go index 139fedd7..953793eb 100644 --- a/api/main.go +++ b/api/main.go @@ -11,6 +11,7 @@ import ( "chatplus/service/fun" "chatplus/service/mj" "chatplus/service/oss" + "chatplus/service/payment" "chatplus/service/sd" "chatplus/service/wx" "chatplus/store" @@ -96,6 +97,10 @@ func main() { fx.Provide(store.NewLevelDB), fx.Provide(store.NewRedisClient), + fx.Provide(func() embed.FS { + return xdbFS + }), + // 创建 Ip2Region 查询对象 fx.Provide(func() (*xdb.Searcher, error) { file, err := xdbFS.Open("res/ip2region.xdb") @@ -124,6 +129,9 @@ func main() { fx.Provide(handler.NewMidJourneyHandler), fx.Provide(handler.NewChatModelHandler), fx.Provide(handler.NewSdJobHandler), + fx.Provide(handler.NewPaymentHandler), + fx.Provide(handler.NewOrderHandler), + fx.Provide(handler.NewProductHandler), fx.Provide(admin.NewConfigHandler), fx.Provide(admin.NewAdminHandler), @@ -133,6 +141,8 @@ func main() { fx.Provide(admin.NewRewardHandler), fx.Provide(admin.NewDashboardHandler), fx.Provide(admin.NewChatModelHandler), + fx.Provide(admin.NewProductHandler), + fx.Provide(admin.NewOrderHandler), // 创建服务 fx.Provide(service.NewAliYunSmsService), @@ -181,6 +191,16 @@ func main() { }() } }), + + fx.Provide(payment.NewAlipayService), + fx.Provide(service.NewSnowflake), + fx.Provide(service.NewXXLJobExecutor), + fx.Invoke(func(exec *service.XXLJobExecutor) { + go func() { + log.Fatal(exec.Run()) + }() + }), + // 注册路由 fx.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) { group := s.Engine.Group("/api/role/") @@ -296,6 +316,34 @@ func main() { group.POST("sort", h.Sort) group.GET("remove", h.Remove) }), + fx.Invoke(func(s *core.AppServer, h *handler.PaymentHandler) { + group := s.Engine.Group("/api/payment/") + group.GET("alipay", h.Alipay) + group.POST("query", h.OrderQuery) + group.POST("alipay/qrcode", h.AlipayQrcode) + group.POST("alipay/notify", h.AlipayNotify) + }), + fx.Invoke(func(s *core.AppServer, h *admin.ProductHandler) { + group := s.Engine.Group("/api/admin/product/") + group.POST("save", h.Save) + group.GET("list", h.List) + group.POST("enable", h.Enable) + group.POST("sort", h.Sort) + group.GET("remove", h.Remove) + }), + fx.Invoke(func(s *core.AppServer, h *admin.OrderHandler) { + group := s.Engine.Group("/api/admin/order/") + group.POST("list", h.List) + group.GET("remove", h.Remove) + }), + fx.Invoke(func(s *core.AppServer, h *handler.OrderHandler) { + group := s.Engine.Group("/api/order/") + group.POST("list", h.List) + }), + fx.Invoke(func(s *core.AppServer, h *handler.ProductHandler) { + group := s.Engine.Group("/api/product/") + group.GET("list", h.List) + }), fx.Invoke(func(s *core.AppServer, db *gorm.DB) { err := s.Run(db) diff --git a/api/service/payment/alipay_service.go b/api/service/payment/alipay_service.go new file mode 100644 index 00000000..64ab8aa3 --- /dev/null +++ b/api/service/payment/alipay_service.go @@ -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, err + } + + xClient, err := alipay.New(config.AppId, priKey, true) + 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 alipay CertPublicKey: %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 AlipayCertPublicKey: %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 +} diff --git a/api/service/snowflake.go b/api/service/snowflake.go new file mode 100644 index 00000000..66416bef --- /dev/null +++ b/api/service/snowflake.go @@ -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 +} diff --git a/api/service/xxl_job_service.go b/api/service/xxl_job_service.go new file mode 100644 index 00000000..1822429b --- /dev/null +++ b/api/service/xxl_job_service.go @@ -0,0 +1,123 @@ +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 { + 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) { + timeout := time.Now().Unix() - 600 + 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 { + panic(res.Error) + } + + var config types.SystemConfig + err := utils.JsonDecode(sysConfig.Config, &config) + if err != nil { + panic(err) + } + + // 获取本月月初时间 + 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.Debug(format, a) +} + +func (l *customLogger) Error(format string, a ...interface{}) { + logger.Error(format, a) +} diff --git a/api/store/model/order.go b/api/store/model/order.go new file mode 100644 index 00000000..65d5580f --- /dev/null +++ b/api/store/model/order.go @@ -0,0 +1,21 @@ +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 + DeletedAt gorm.DeletedAt +} diff --git a/api/store/model/product.go b/api/store/model/product.go new file mode 100644 index 00000000..040f715a --- /dev/null +++ b/api/store/model/product.go @@ -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 +} diff --git a/api/store/model/user.go b/api/store/model/user.go index ccbf6442..ab35a748 100644 --- a/api/store/model/user.go +++ b/api/store/model/user.go @@ -16,4 +16,6 @@ type User struct { Status bool `gorm:"default:true"` // 当前状态 LastLoginAt int64 // 最后登录时间 LastLoginIp string // 最后登录 IP + Vip bool // 是否 VIP 会员 + Tokens int } diff --git a/api/store/vo/order.go b/api/store/vo/order.go new file mode 100644 index 00000000..f7d6bc3c --- /dev/null +++ b/api/store/vo/order.go @@ -0,0 +1,18 @@ +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"` + Remark types.OrderRemark `json:"remark"` +} diff --git a/api/store/vo/product.go b/api/store/vo/product.go new file mode 100644 index 00000000..b0a867d3 --- /dev/null +++ b/api/store/vo/product.go @@ -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"` +} diff --git a/api/store/vo/user.go b/api/store/vo/user.go index 8c291a96..05f413b9 100644 --- a/api/store/vo/user.go +++ b/api/store/vo/user.go @@ -17,4 +17,6 @@ type User struct { Status bool `json:"status"` // 当前状态 LastLoginAt int64 `json:"last_login_at"` // 最后登录时间 LastLoginIp string `json:"last_login_ip"` // 最后登录 IP + Vip bool `json:"vip"` + Tokens int `json:"token"` // 当月消耗的 fee } diff --git a/api/utils/common.go b/api/utils/common.go index 5147d470..748dbc8c 100644 --- a/api/utils/common.go +++ b/api/utils/common.go @@ -1,8 +1,16 @@ package utils import ( + "bytes" "encoding/json" "fmt" + "github.com/nfnt/resize" + "github.com/skip2/go-qrcode" + "image" + "image/color" + "image/draw" + "image/jpeg" + "io" "reflect" "strconv" "strings" @@ -150,3 +158,47 @@ func ForceCovert(src any, dst interface{}) error { } 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 +} diff --git a/database/update-v3.1.8.sql b/database/update-v3.1.8.sql new file mode 100644 index 00000000..fbdbf498 --- /dev/null +++ b/database/update-v3.1.8.sql @@ -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; \ No newline at end of file diff --git a/web/src/components/admin/AdminSidebar.vue b/web/src/components/admin/AdminSidebar.vue index e8eaa26e..cbd2c5e5 100644 --- a/web/src/components/admin/AdminSidebar.vue +++ b/web/src/components/admin/AdminSidebar.vue @@ -96,6 +96,16 @@ const items = [ index: '/admin/chat/model', title: '语言模型', }, + { + icon: 'recharge', + index: '/admin/product', + title: '充值产品', + }, + { + icon: 'order', + index: '/admin/order', + title: '充值订单', + }, { icon: 'reward', index: '/admin/reward', diff --git a/web/src/router.js b/web/src/router.js index b21f0129..6b4ec273 100644 --- a/web/src/router.js +++ b/web/src/router.js @@ -126,6 +126,18 @@ const routes = [ meta: {title: '语言模型'}, 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', name: 'admin-reward', diff --git a/web/src/views/admin/Order.vue b/web/src/views/admin/Order.vue new file mode 100644 index 00000000..a7c86e7e --- /dev/null +++ b/web/src/views/admin/Order.vue @@ -0,0 +1,139 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/admin/Product.vue b/web/src/views/admin/Product.vue new file mode 100644 index 00000000..0543a7a1 --- /dev/null +++ b/web/src/views/admin/Product.vue @@ -0,0 +1,226 @@ + + + + + \ No newline at end of file From 4a81826d19258db2c1606022840fe0d0b016aa20 Mon Sep 17 00:00:00 2001 From: RockYang Date: Tue, 7 Nov 2023 12:02:16 +0800 Subject: [PATCH 02/12] feat: adjust table styles for markdown --- web/src/components/ChatReply.vue | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/web/src/components/ChatReply.vue b/web/src/components/ChatReply.vue index 0726a0c3..7c8b6bfd 100644 --- a/web/src/components/ChatReply.vue +++ b/web/src/components/ChatReply.vue @@ -125,6 +125,31 @@ export default defineComponent({ p:first-child { 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 + } + } } From d0b8d666e4e2b06b285146c69827ec1dec2d19ee Mon Sep 17 00:00:00 2001 From: RockYang Date: Tue, 7 Nov 2023 18:10:28 +0800 Subject: [PATCH 03/12] feat: finish payment page layout --- api/core/config.go | 7 +- api/core/types/config.go | 3 +- api/res/img/alipay.jpg | Bin 0 -> 15537 bytes api/service/payment/alipay_service.go | 10 +- api/service/sd/service.go | 2 +- web/public/images/alipay.jpg | Bin 0 -> 15537 bytes web/public/images/vip.png | Bin 0 -> 1930 bytes web/src/views/Member.vue | 187 +++++++++++++++++++++++--- 8 files changed, 178 insertions(+), 31 deletions(-) create mode 100644 api/res/img/alipay.jpg create mode 100644 web/public/images/alipay.jpg create mode 100644 web/public/images/vip.png diff --git a/api/core/config.go b/api/core/config.go index 95c8953a..77722515 100644 --- a/api/core/config.go +++ b/api/core/config.go @@ -33,9 +33,10 @@ func NewDefaultConfig() *types.AppConfig { BasePath: "./static/upload", }, }, - MjConfig: types.MidJourneyConfig{Enabled: false}, - SdConfig: types.StableDiffusionConfig{Enabled: false, Txt2ImgJsonPath: "res/text2img.json"}, - WeChatBot: false, + MjConfig: types.MidJourneyConfig{Enabled: false}, + SdConfig: types.StableDiffusionConfig{Enabled: false, Txt2ImgJsonPath: "res/text2img.json"}, + WeChatBot: false, + AlipayConfig: types.AlipayConfig{SandBox: true}, } } diff --git a/api/core/types/config.go b/api/core/types/config.go index 49f1ea6c..221399e3 100644 --- a/api/core/types/config.go +++ b/api/core/types/config.go @@ -61,7 +61,8 @@ type AliYunSmsConfig struct { } type AlipayConfig struct { - Enabled bool // 是否启用支付宝服务 + Enabled bool // 是否启用该服务 + SandBox bool // 是否沙箱环境 Company string // 公司名称 UserId string // 支付宝用户 ID AppId string // 支付宝 AppID diff --git a/api/res/img/alipay.jpg b/api/res/img/alipay.jpg new file mode 100644 index 0000000000000000000000000000000000000000..af7b406831559d80b153585986176497b053ef15 GIT binary patch literal 15537 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 literal 0 HcmV?d00001 diff --git a/api/service/payment/alipay_service.go b/api/service/payment/alipay_service.go index 64ab8aa3..d33d5032 100644 --- a/api/service/payment/alipay_service.go +++ b/api/service/payment/alipay_service.go @@ -20,27 +20,27 @@ var logger = logger2.GetLogger() func NewAlipayService(appConfig *types.AppConfig) (*AlipayService, error) { config := appConfig.AlipayConfig if !config.Enabled { - logger.Info("Disabled alipay service") + logger.Info("Disabled Alipay service") return nil, nil } priKey, err := readKey(config.PrivateKey) if err != nil { - return nil, err + return nil, fmt.Errorf("error with read App Private key: %v", err) } - xClient, err := alipay.New(config.AppId, priKey, true) + 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 alipay CertPublicKey: %v", err) + 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 AlipayCertPublicKey: %v", err) + return nil, fmt.Errorf("error with loading Alipay PublicKey: %v", err) } return &AlipayService{config: &config, client: xClient}, nil diff --git a/api/service/sd/service.go b/api/service/sd/service.go index ed7ff531..def9940a 100644 --- a/api/service/sd/service.go +++ b/api/service/sd/service.go @@ -81,7 +81,7 @@ func (s *Service) Run() { // PushTask 推送任务到队列 func (s *Service) PushTask(task types.SdTask) { - logger.Infof("add a new MidJourney Task: %+v", task) + logger.Infof("add a new Stable Diffusion Task: %+v", task) s.taskQueue.RPush(task) } diff --git a/web/public/images/alipay.jpg b/web/public/images/alipay.jpg new file mode 100644 index 0000000000000000000000000000000000000000..af7b406831559d80b153585986176497b053ef15 GIT binary patch literal 15537 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 literal 0 HcmV?d00001 diff --git a/web/public/images/vip.png b/web/public/images/vip.png new file mode 100644 index 0000000000000000000000000000000000000000..e7374cfaff0793fe753149c1e2aae4db863f83ff GIT binary patch literal 1930 zcmZ`)cRU+-6b~U%d$d+H*IK=DTH3l8Nvs$#LJ4v-M5<81c@Avur-u%4Z=leeIz0XT?LR(9SDvJUD011>0(wWam zUmqC6r=8Q=c6=rf=4@>asP0o);wwU7<|qu9e`3MDsQ`e;brjMJ6a93pus>MkwqjRH zf8V;F95KhC3<-SiDwX)FP{9M$0TAV*8R`4kG6hfMC#QgzPZQiL0UZ#e%t(5dZ!+X3 za9X)JEh|+)ia>ad4T^~JTwCbsgF$kLx1cQxg?n-S-;)vaRo_Yf?(&VsEkr-BSy;$S z`v0!1&|gYNGsF>L=Z&jdx&p$0FQEuWaM1Z7gVv}^gpKYBsKQ86hbqZJ1qUpVwvOdU z>yG#~=Ukzr>vhoVnyw!Q=h6=WpQ&EK0_XE9~0fK zm@V>SzN(7WbKauDa!i(V#=*I7WJ--TB(DW@TD=ltZukb!D1YmWg8gpdcXB;R&exOr z$bV)?mf-D*ZzOyWrcd=SSIx-d;=(0aXzDi+8KU;kzy+t?@#KV=!`Q%HU-4UlD_dok zKxk@Ei_7`;GRp1Pb_~x-Xjpqr4q5M8yoOG0bzeEN_a>jFzeSpaOBL5d&|}W)>RJzJ z)$(x|BgKryVfcXG)Pl~MSJEdEEm8=@5aG2jqz=56k4%NVlqi~fduk^8DAhfkjp0JD zI1+Dg=}$NgEK!p6P1dQQhKBA(R)5-@S$XF~vbof|ysoXd)cKTgR z+2=je^XcWviI?iN@27W-9X#57Y@MjdwW~E-dA-}_fnbNKnXAYmgraa4s?e=Lzv`}> z;kE#FzOTIW4e;Z=0}CfVz&|M_w{|AQM?;XoLxN)^({mNI?vFDCW~)oo|{ z+D@P#0OBU*lnx1=bo*R_`^FTq_AY&Gs}wci00F#mUwjuARl}7wdMEFJzt>TzCruX6 z=1E+?cOu)yPND^)vg^MgSMF`UDmafvYd~M-rOkv4t@BLUC=!`vD~&csHr9$7yJ{ix z>vT57Te^0Dnw8V~?37fg|F<06EVWyra8O3`bc2E&FQ+`)BkO^)7Y2ntE~RJ0Yr3v; z9&yzDHZ3boF3@JffD|BzfZTna!nh=groQm7@519N>(QKK z7|6kDjB~o-S!GP!Pmj-JP%K zlowKTj-E6Ie20VX7TTbplth{0^|4@f9;?TMf(FhUeHB1@(S#CPY6CBq(d$Z{M%Db z8csvrPwtI8Y#k?^94pI?8lJ0Qs0zLv598ipJ(cgb4D@kMhA<3U-T zdaC{Hk-;TA;Fe{G41+ycAD>yYAH8%fo?kdz(#G4J4($BvhN6-7-U}j{=yDRp%d-D- zI^f;O**iNOU%EXTcsj$CYk$pGNJld|1x$z0FLXl@j2~r3j*lSu$8xW`>&RXi^I?Ql z+X)GmO~u%xTkCXygP$T&BxoO+b^zv-m;gr?x%{y-L8C<9Z!ulwD0up2KMEDGWCplsJwHhKf z6Fa$dJ#jaAj15xiPWFD7pSb&fvXAOPRkp_fSKyQ!7|>){!V2v=lxWb4lD91LJ?TD!BW`5~i_(c&6| zFeT*e@zA*nzF{9S+K$|exj6gsKczT-xDgY2G4FF=c{k9==Iay|l43>oa@d ztz2i(jYOyyDYaHnX#dhD^76A(x&K>ELS{DVCj$BQp*U>8QVM^c0#KG{WVJay@n587 Bop=BM literal 0 HcmV?d00001 diff --git a/web/src/views/Member.vue b/web/src/views/Member.vue index cf8eb528..7b283bae 100644 --- a/web/src/views/Member.vue +++ b/web/src/views/Member.vue @@ -1,39 +1,184 @@ - \ No newline at end of file diff --git a/web/src/views/Member.vue b/web/src/views/Member.vue index eada7e7f..445f1ff9 100644 --- a/web/src/views/Member.vue +++ b/web/src/views/Member.vue @@ -5,40 +5,46 @@
-
- - 说明: 成为本站会员后每月有500次对话额度,50次 AI 绘画额度,限制下月1号解除,若在期间超过次数后可单独购买点卡。 - 当月充值的点卡有效期可以延期到下个月底。 - + - - + +
@@ -81,6 +87,7 @@ import LoginDialog from "@/components/LoginDialog.vue"; import {checkSession} from "@/action/session"; import {arrayContains, removeArrayItem, substr} from "@/utils/libs"; import router from "@/router"; +import UserProfile from "@/components/UserProfile.vue"; const listBoxHeight = window.innerHeight - 97 const list = ref([]) @@ -187,88 +194,105 @@ const queryOrder = (orderNo) => { } .inner { + display flex color #ffffff padding 15px; overflow-y visible overflow-x hidden - .info { - .el-alert__description { - font-size 14px !important - margin 0 - } + .user-profile { padding 10px 20px + background-color #393F4A + color #ffffff + border-radius 10px + + .el-form-item__label { + color #ffffff + justify-content start + } } - .list-box { - .product-item { - border 1px solid #666666 - border-radius 6px - overflow hidden - cursor pointer - transition: all 0.3s ease; /* 添加过渡效果 */ + .product-box { + padding 0 10px - .image-container { - display flex - justify-content center + .info { + .el-alert__description { + font-size 14px !important + margin 0 + } + padding 10px 20px + } - .el-image { - padding 6px + .list-box { + .product-item { + border 1px solid #666666 + border-radius 6px + overflow hidden + cursor pointer + transition: all 0.3s ease; /* 添加过渡效果 */ - .el-image__inner { - border-radius 10px + .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 { + .product-title { display flex - width 100% - padding 5px 0 + padding 10px - .label { + .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 + } } - .price, .expire { - display flex - width 80px - 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像素 */ + &:hover { + box-shadow: 0 0 10px rgba(71, 255, 241, 0.6); /* 添加阴影效果 */ + transform: translateY(-10px); /* 向上移动10像素 */ + } } } } From ef87487f604f110352d8bc03f439e0339590c961 Mon Sep 17 00:00:00 2001 From: RockYang Date: Wed, 8 Nov 2023 21:14:09 +0800 Subject: [PATCH 07/12] fix: fix bug for token expired with QiNiu oss upload file --- api/service/oss/qiniu_oss.go | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/api/service/oss/qiniu_oss.go b/api/service/oss/qiniu_oss.go index 0c1a1fc1..fbdb229c 100644 --- a/api/service/oss/qiniu_oss.go +++ b/api/service/oss/qiniu_oss.go @@ -15,12 +15,13 @@ import ( ) type QinNiuOss struct { - config *types.QiNiuOssConfig - token string - uploader *storage.FormUploader - manager *storage.BucketManager - proxyURL string - dir string + config *types.QiNiuOssConfig + mac *qbox.Mac + putPolicy storage.PutPolicy + uploader *storage.FormUploader + manager *storage.BucketManager + proxyURL string + dir string } func NewQiNiuOss(appConfig *types.AppConfig) QinNiuOss { @@ -38,12 +39,13 @@ func NewQiNiuOss(appConfig *types.AppConfig) QinNiuOss { Scope: config.Bucket, } return QinNiuOss{ - config: config, - token: putPolicy.UploadToken(mac), - uploader: formUploader, - manager: storage.NewBucketManager(mac, &storeConfig), - proxyURL: appConfig.ProxyURL, - dir: "chatgpt-plus", + config: config, + mac: mac, + putPolicy: putPolicy, + uploader: formUploader, + manager: storage.NewBucketManager(mac, &storeConfig), + proxyURL: appConfig.ProxyURL, + dir: "chatgpt-plus", } } @@ -65,7 +67,7 @@ func (s QinNiuOss) PutFile(ctx *gin.Context, name string) (string, error) { // 上传文件 ret := storage.PutRet{} extra := storage.PutExtra{} - err = s.uploader.Put(ctx, &ret, s.token, key, src, file.Size, &extra) + err = s.uploader.Put(ctx, &ret, s.putPolicy.UploadToken(s.mac), key, src, file.Size, &extra) if err != nil { return "", err } @@ -93,7 +95,7 @@ func (s QinNiuOss) PutImg(imageURL string, useProxy bool) (string, error) { ret := storage.PutRet{} extra := storage.PutExtra{} // 上传文件字节数据 - err = s.uploader.Put(context.Background(), &ret, s.token, key, bytes.NewReader(imageData), int64(len(imageData)), &extra) + err = s.uploader.Put(context.Background(), &ret, s.putPolicy.UploadToken(s.mac), key, bytes.NewReader(imageData), int64(len(imageData)), &extra) if err != nil { return "", err } From df367e0d4754b028ee50c52d17788289131e9949 Mon Sep 17 00:00:00 2001 From: RockYang Date: Thu, 9 Nov 2023 16:56:44 +0800 Subject: [PATCH 08/12] feat: check if the user's chat quota is gt than current chat model required before starting a conversation --- api/handler/chatimpl/azure_handler.go | 3 +- api/handler/chatimpl/baidu_handler.go | 4 +- api/handler/chatimpl/chat_handler.go | 15 +++- api/handler/chatimpl/chatglm_handler.go | 4 +- api/handler/chatimpl/openai_handler.go | 3 +- api/handler/chatimpl/xunfei_handler.go | 4 +- web/src/components/ConfigDialog.vue | 91 ++++--------------- web/src/components/LoginDialog.vue | 18 ++-- web/src/components/UserProfile.vue | 56 +++++++++--- web/src/main.js | 2 +- web/src/router.js | 4 +- web/src/views/ChatApps.vue | 5 +- web/src/views/ChatPlus.vue | 71 +-------------- web/src/views/Login.vue | 12 ++- web/src/views/Member.vue | 114 +++++++++++++++++++----- web/src/views/mobile/ChatList.vue | 2 +- 16 files changed, 200 insertions(+), 208 deletions(-) diff --git a/api/handler/chatimpl/azure_handler.go b/api/handler/chatimpl/azure_handler.go index 297fa91a..e800544d 100644 --- a/api/handler/chatimpl/azure_handler.go +++ b/api/handler/chatimpl/azure_handler.go @@ -242,8 +242,7 @@ func (h *ChatHandler) sendAzureMessage( } // 更新用户信息 - h.db.Model(&model.User{}).Where("id = ?", userVo.Id). - UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens)) + h.incUserTokenFee(userVo.Id, totalTokens) } // 保存当前会话 diff --git a/api/handler/chatimpl/baidu_handler.go b/api/handler/chatimpl/baidu_handler.go index 1f8007c5..1ca49e57 100644 --- a/api/handler/chatimpl/baidu_handler.go +++ b/api/handler/chatimpl/baidu_handler.go @@ -9,7 +9,6 @@ import ( "context" "encoding/json" "fmt" - "gorm.io/gorm" "io" "net/http" "strings" @@ -184,8 +183,7 @@ func (h *ChatHandler) sendBaiduMessage( logger.Error("failed to save reply history message: ", res.Error) } // 更新用户信息 - h.db.Model(&model.User{}).Where("id = ?", userVo.Id). - UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens)) + h.incUserTokenFee(userVo.Id, totalTokens) } // 保存当前会话 diff --git a/api/handler/chatimpl/chat_handler.go b/api/handler/chatimpl/chat_handler.go index d56d00aa..c1480c2c 100644 --- a/api/handler/chatimpl/chat_handler.go +++ b/api/handler/chatimpl/chat_handler.go @@ -187,8 +187,14 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio return nil } + if userVo.Calls < session.Model.Weight { + utils.ReplyMessage(ws, fmt.Sprintf("您当前剩余对话次数(%d)已不足以支付当前模型的单次对话需要消耗的对话额度(%d)!", userVo.Calls, session.Model.Weight)) + utils.ReplyMessage(ws, ErrImg) + return nil + } + if userVo.Calls <= 0 && userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" { - utils.ReplyMessage(ws, "您的对话次数已经用尽,请联系管理员或者点击左下角菜单加入众筹获得100次对话!") + utils.ReplyMessage(ws, "您的对话次数已经用尽,请联系管理员或者充值点卡继续对话!") utils.ReplyMessage(ws, ErrImg) return nil } @@ -477,3 +483,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)) } } + +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)) +} diff --git a/api/handler/chatimpl/chatglm_handler.go b/api/handler/chatimpl/chatglm_handler.go index cfd154d5..3f680cac 100644 --- a/api/handler/chatimpl/chatglm_handler.go +++ b/api/handler/chatimpl/chatglm_handler.go @@ -10,7 +10,6 @@ import ( "encoding/json" "fmt" "github.com/golang-jwt/jwt/v5" - "gorm.io/gorm" "io" "strings" "time" @@ -164,8 +163,7 @@ func (h *ChatHandler) sendChatGLMMessage( logger.Error("failed to save reply history message: ", res.Error) } // 更新用户信息 - h.db.Model(&model.User{}).Where("id = ?", userVo.Id). - UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens)) + h.incUserTokenFee(userVo.Id, totalTokens) } // 保存当前会话 diff --git a/api/handler/chatimpl/openai_handler.go b/api/handler/chatimpl/openai_handler.go index a6ed62be..69a2b85e 100644 --- a/api/handler/chatimpl/openai_handler.go +++ b/api/handler/chatimpl/openai_handler.go @@ -241,8 +241,7 @@ func (h *ChatHandler) sendOpenAiMessage( } // 更新用户信息 - h.db.Model(&model.User{}).Where("id = ?", userVo.Id). - UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens)) + h.incUserTokenFee(userVo.Id, totalTokens) } // 保存当前会话 diff --git a/api/handler/chatimpl/xunfei_handler.go b/api/handler/chatimpl/xunfei_handler.go index 19710568..8169aeb4 100644 --- a/api/handler/chatimpl/xunfei_handler.go +++ b/api/handler/chatimpl/xunfei_handler.go @@ -12,7 +12,6 @@ import ( "encoding/json" "fmt" "github.com/gorilla/websocket" - "gorm.io/gorm" "io" "net/http" "net/url" @@ -227,8 +226,7 @@ func (h *ChatHandler) sendXunFeiMessage( logger.Error("failed to save reply history message: ", res.Error) } // 更新用户信息 - h.db.Model(&model.User{}).Where("id = ?", userVo.Id). - UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens)) + h.incUserTokenFee(userVo.Id, totalTokens) } // 保存当前会话 diff --git a/web/src/components/ConfigDialog.vue b/web/src/components/ConfigDialog.vue index 4ca1f568..4fed0a0e 100644 --- a/web/src/components/ConfigDialog.vue +++ b/web/src/components/ConfigDialog.vue @@ -5,55 +5,30 @@ :close-on-click-modal="true" :before-close="close" style="max-width: 600px" - title="用户设置" + title="账户信息" > - - @@ -63,6 +38,7 @@ 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"; // eslint-disable-next-line no-undef const props = defineProps({ @@ -74,7 +50,7 @@ const props = defineProps({ const showDialog = computed(() => { return props.show }) -const form = ref({ +const user = ref({ username: '', nickname: '', avatar: '', @@ -87,50 +63,15 @@ const form = ref({ onMounted(() => { // 获取最新用户信息 httpGet('/api/user/profile').then(res => { - form.value = res.data - form.value.chat_config.api_keys = res.data.chat_config.api_keys ?? {OpenAI: "", Azure: "", ChatGLM: ""} + 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) }); }) -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 -const emits = defineEmits(['hide', 'update-user']); -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 emits = defineEmits(['hide']); const close = function () { emits('hide', false); } diff --git a/web/src/components/LoginDialog.vue b/web/src/components/LoginDialog.vue index 1effc4c5..3499046f 100644 --- a/web/src/components/LoginDialog.vue +++ b/web/src/components/LoginDialog.vue @@ -9,7 +9,7 @@ title="用户登录" >
- + @@ -33,12 +33,12 @@
@@ -90,9 +90,17 @@ const close = function () { border-radius 20px .label { + padding-top 3px + .el-icon { - font-size 16px + position relative + font-size 20px margin-right 6px + top 4px + } + + span { + font-size 16px } } diff --git a/web/src/components/UserProfile.vue b/web/src/components/UserProfile.vue index 7e5f8bff..2ba963b3 100644 --- a/web/src/components/UserProfile.vue +++ b/web/src/components/UserProfile.vue @@ -32,6 +32,20 @@ {{ dateFormat(user['expired_time']) }} + + + + + + + + + + + + + 保存 + @@ -43,6 +57,7 @@ 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({ username: '', @@ -55,13 +70,17 @@ const user = ref({ }) onMounted(() => { - // 获取最新用户信息 - 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: ""} + 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 => { - ElMessage.error("获取用户信息失败:" + e.message) - }); + console.log(e) + }) }) const afterRead = (file) => { @@ -74,14 +93,7 @@ const afterRead = (file) => { // 执行上传操作 httpPost('/api/upload', formData).then((res) => { user.value.avatar = res.data - httpPost('/api/user/profile/update', user.value).then(() => { - ElMessage.success({ - message: '更新成功', - duration: 500, - }) - }).catch((e) => { - ElMessage.error('更新失败:' + e.message) - }) + ElMessage.success({message: "上传成功", duration: 500}) }).catch((e) => { ElMessage.error('图片上传失败:' + e.message) }) @@ -91,6 +103,14 @@ const afterRead = (file) => { }, }); }; + +const save = () => { + httpPost('/api/user/profile/update', user.value).then(() => { + ElMessage.success({message: '更新成功', duration: 500}) + }).catch((e) => { + ElMessage.error('更新失败:' + e.message) + }) +} \ No newline at end of file diff --git a/web/src/main.js b/web/src/main.js index 5db4c31e..3119148a 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -32,7 +32,7 @@ import { TextEllipsis, Uploader } from "vant"; -import router from "@/router"; +import {router} from "@/router"; import 'v3-waterfall/dist/style.css' import V3waterfall from "v3-waterfall"; diff --git a/web/src/router.js b/web/src/router.js index 6b4ec273..200b0ec1 100644 --- a/web/src/router.js +++ b/web/src/router.js @@ -226,12 +226,14 @@ const router = createRouter({ routes: routes, }) +let prevRoute = null // dynamic change the title when router change router.beforeEach((to, from, next) => { if (to.meta.title) { document.title = `${to.meta.title} | ${process.env.VUE_APP_TITLE}` } + prevRoute = from next() }) -export default router; \ No newline at end of file +export {router, prevRoute}; \ No newline at end of file diff --git a/web/src/views/ChatApps.vue b/web/src/views/ChatApps.vue index 57f1bcbc..78d21188 100644 --- a/web/src/views/ChatApps.vue +++ b/web/src/views/ChatApps.vue @@ -40,7 +40,7 @@ - \ No newline at end of file diff --git a/web/src/components/UserProfile.vue b/web/src/components/UserProfile.vue index 2ba963b3..e803c89d 100644 --- a/web/src/components/UserProfile.vue +++ b/web/src/components/UserProfile.vue @@ -16,6 +16,14 @@ {{ user.mobile }} + + + {{ user['calls'] }} @@ -60,6 +68,7 @@ import {dateFormat} from "@/utils/libs"; import {checkSession} from "@/action/session"; const user = ref({ + vip: false, username: '', nickname: '', avatar: '', @@ -68,6 +77,7 @@ const user = ref({ tokens: 0, chat_config: {api_keys: {OpenAI: "", Azure: "", ChatGLM: ""}} }) +const vipImg = ref("/images/vip.png") onMounted(() => { checkSession().then(() => { diff --git a/web/src/views/ChatPlus.vue b/web/src/views/ChatPlus.vue index 525e4bb9..977dc899 100644 --- a/web/src/views/ChatPlus.vue +++ b/web/src/views/ChatPlus.vue @@ -194,8 +194,7 @@ - + diff --git a/web/src/views/Member.vue b/web/src/views/Member.vue index 87384567..b67ea5bc 100644 --- a/web/src/views/Member.vue +++ b/web/src/views/Member.vue @@ -10,16 +10,20 @@ - 修改密码 + 修改密码 - 绑定手机号 + 绑定手机号 - 加入众筹 + 加入众筹 - 众筹核销 + 众筹核销 + + + + 退出登录 @@ -67,7 +71,7 @@ - @@ -81,7 +85,7 @@
您好,众筹 9.9元,就可以兑换 100 次对话,以此来覆盖我们的 OpenAI 账单和服务器的费用。由于本人没有开通微信支付,付款后请凭借转账单号进入核销【众筹核销】菜单手动核销。 + style="color: #f56c6c">由于本人没有开通微信支付,付款后请凭借转账单号,点击【众筹核销】按钮手动核销。
@@ -94,7 +98,7 @@ :close-on-click-modal="false" :show-close="true" :width="400" - title="用户登录"> + title="充值订单支付">
@@ -130,6 +134,7 @@ 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"; const listBoxHeight = window.innerHeight - 97 const list = ref([]) @@ -201,6 +206,15 @@ const queryOrder = (orderNo) => { }) } +const logout = function () { + httpGet('/api/user/logout').then(() => { + removeUserToken(); + router.push('/login'); + }).catch(() => { + ElMessage.error('注销失败!'); + }) +} + \ No newline at end of file diff --git a/web/src/views/Member.vue b/web/src/views/Member.vue index b67ea5bc..33a07740 100644 --- a/web/src/views/Member.vue +++ b/web/src/views/Member.vue @@ -100,7 +100,11 @@ :width="400" title="充值订单支付">
-
+
+ +
+ +
@@ -135,6 +139,7 @@ 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([]) @@ -153,6 +158,11 @@ 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(() => { @@ -171,6 +181,9 @@ onMounted(() => { 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) }) @@ -181,10 +194,20 @@ const orderPay = (row) => { showLoginDialog.value = true return } - httpPost("/api/payment/alipay/qrcode", {product_id: row.id, user_id: user.value.id}).then(res => { + 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'] - queryOrder(res.data['order_no']) + activeOrderNo.value = res.data['order_no'] + queryOrder(activeOrderNo.value) + loading.value = false + // 重置计数器 + if (countDown.value) { + countDown.value.resetTimer() + } }).catch(e => { ElMessage.error("生成支付订单失败:" + e.message) }) @@ -199,7 +222,10 @@ const queryOrder = (orderNo) => { text.value = "支付成功,正在刷新页面" setTimeout(() => location.reload(), 500) } else { - queryOrder(orderNo) + // 如果当前订单没有过期,继续等待订单的下一个状态 + if (activeOrderNo.value === orderNo) { + queryOrder(orderNo) + } } }).catch(e => { ElMessage.error("查询支付状态失败:" + e.message) @@ -225,9 +251,14 @@ const logout = function () { .el-dialog { .el-dialog__body { - padding-top 0 + padding-top 10px .pay-container { + .count-down { + display flex + justify-content center + } + .pay-qrcode { display flex justify-content center diff --git a/web/src/views/admin/SysConfig.vue b/web/src/views/admin/SysConfig.vue index 3a612692..b9e7e776 100644 --- a/web/src/views/admin/SysConfig.vue +++ b/web/src/views/admin/SysConfig.vue @@ -103,6 +103,23 @@ + +
+ +
+ + + + + +
+
+