From b5641be30d7d46cc767d659d245480ecb9cc73cc Mon Sep 17 00:00:00 2001 From: RockYang Date: Wed, 3 Jan 2024 11:15:54 +0800 Subject: [PATCH] feat: fixed bug for wechat bot to parse transactions. enable user to exchange reward with img_calls --- README.md | 7 +-- api/core/types/config.go | 3 ++ api/handler/admin/reward_handler.go | 13 +++++ api/handler/reward_handler.go | 37 ++++++++++---- api/main.go | 9 +--- api/service/wx/bot.go | 15 ++++-- api/service/wx/tranaction.go | 49 +++++++++++++------ api/store/model/reward.go | 11 +++-- api/store/vo/reward.go | 18 ++++--- api/test/test.go | 2 +- database/update-v3.2.4.sql | 1 + web/src/components/RewardVerify.vue | 12 ++++- web/src/router.js | 2 +- .../admin/{RewardList.vue => Reward.vue} | 31 +++++++++++- web/src/views/admin/SysConfig.vue | 11 +++++ 15 files changed, 166 insertions(+), 55 deletions(-) rename web/src/views/admin/{RewardList.vue => Reward.vue} (58%) diff --git a/README.md b/README.md index bbdbc515..8b85243d 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,11 @@ ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了 * 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI 绘画函数插件。 -## 关于部署镜像申明 +## 最新版本一键部署脚本 -由于目前部署人数越来越多,本人的阿里云镜像仓库流量不够支撑大家使用了。所以从 v3.2.0 版本开始,一键部署脚本和部署镜像将只提供给 **[付费技术交流群]** 内用户使用。 -代码依旧是全部开源的,大家可自行编译打包镜像。 +```shell +bash -c "$(curl -fsSL https://img.r9it.com/tmp/install-v3.2.3-8b588904ef.sh)" +``` ## 功能截图 diff --git a/api/core/types/config.go b/api/core/types/config.go index a1827ba3..026a7954 100644 --- a/api/core/types/config.go +++ b/api/core/types/config.go @@ -159,6 +159,8 @@ type SystemConfig struct { EnabledMsg bool `json:"enabled_msg"` // 是否启用短信验证码服务 RewardImg string `json:"reward_img"` // 众筹收款二维码地址 EnabledReward bool `json:"enabled_reward"` // 启用众筹功能 + ChatCallPrice float64 `json:"chat_call_price"` // 对话单次调用费用 + ImgCallPrice float64 `json:"img_call_price"` // 绘图单次调用费用 EnabledAlipay bool `json:"enabled_alipay"` // 是否启用支付宝支付通道 OrderPayTimeout int `json:"order_pay_timeout"` //订单支付超时时间 DefaultModels []string `json:"default_models"` // 默认开通的 AI 模型 @@ -166,4 +168,5 @@ type SystemConfig struct { InviteChatCalls int `json:"invite_chat_calls"` // 邀请用户注册奖励对话次数 InviteImgCalls int `json:"invite_img_calls"` // 邀请用户注册奖励绘图次数 ForceInvite bool `json:"force_invite"` // 是否强制必须使用邀请码才能注册 + } diff --git a/api/handler/admin/reward_handler.go b/api/handler/admin/reward_handler.go index b0470c37..61f65696 100644 --- a/api/handler/admin/reward_handler.go +++ b/api/handler/admin/reward_handler.go @@ -55,3 +55,16 @@ func (h *RewardHandler) List(c *gin.Context) { resp.SUCCESS(c, rewards) } + +func (h *RewardHandler) Remove(c *gin.Context) { + id := h.GetInt(c, "id", 0) + + if id > 0 { + res := h.db.Where("id = ?", id).Delete(&model.Reward{}) + if res.Error != nil { + resp.ERROR(c, "更新数据库失败!") + return + } + } + resp.SUCCESS(c) +} diff --git a/api/handler/reward_handler.go b/api/handler/reward_handler.go index ed603d30..8fc36905 100644 --- a/api/handler/reward_handler.go +++ b/api/handler/reward_handler.go @@ -4,20 +4,24 @@ 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" + "math" "strings" + "sync" ) type RewardHandler struct { BaseHandler - db *gorm.DB + db *gorm.DB + lock sync.Mutex } func NewRewardHandler(server *core.AppServer, db *gorm.DB) *RewardHandler { - h := RewardHandler{db: db} + h := RewardHandler{db: db, lock: sync.Mutex{}} h.App = server return &h } @@ -26,15 +30,25 @@ func NewRewardHandler(server *core.AppServer, db *gorm.DB) *RewardHandler { func (h *RewardHandler) Verify(c *gin.Context) { var data struct { TxId string `json:"tx_id"` + Type string `json:"type"` } if err := c.ShouldBindJSON(&data); err != nil { resp.ERROR(c, types.InvalidArgs) return } + user, err := utils.GetLoginUser(c, h.db) + if err != nil { + resp.HACKER(c) + return + } + // 移除转账单号中间的空格,防止有人复制的时候多复制了空格 data.TxId = strings.ReplaceAll(data.TxId, " ", "") + h.lock.Lock() + defer h.lock.Unlock() + var item model.Reward res := h.db.Where("tx_id = ?", data.TxId).First(&item) if res.Error != nil { @@ -47,15 +61,17 @@ func (h *RewardHandler) Verify(c *gin.Context) { return } - user, err := utils.GetLoginUser(c, h.db) - if err != nil { - resp.HACKER(c) - return - } - tx := h.db.Begin() - calls := (item.Amount + 0.1) * 10 - res = h.db.Model(&user).UpdateColumn("calls", gorm.Expr("calls + ?", calls)) + exchange := vo.RewardExchange{} + if data.Type == "chat" { + calls := math.Ceil(item.Amount / h.App.SysConfig.ChatCallPrice) + exchange.Calls = int(calls) + res = h.db.Model(&user).UpdateColumn("calls", gorm.Expr("calls + ?", calls)) + } else if data.Type == "img" { + calls := math.Ceil(item.Amount / h.App.SysConfig.ImgCallPrice) + exchange.ImgCalls = int(calls) + res = h.db.Model(&user).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", calls)) + } if res.Error != nil { resp.ERROR(c, "更新数据库失败!") return @@ -64,6 +80,7 @@ func (h *RewardHandler) Verify(c *gin.Context) { // 更新核销状态 item.Status = true item.UserId = user.Id + item.Exchange = utils.JsonEncode(exchange) res = h.db.Updates(&item) if res.Error != nil { tx.Rollback() diff --git a/api/main.go b/api/main.go index 94d9040a..f1713bc7 100644 --- a/api/main.go +++ b/api/main.go @@ -57,13 +57,7 @@ func main() { if configFile == "" { configFile = "config.toml" } - var debug bool - debugEnv := os.Getenv("DEBUG") - if debugEnv == "" { - debug = true - } else { - debug, _ = strconv.ParseBool(os.Getenv("DEBUG")) - } + debug, _ := strconv.ParseBool(os.Getenv("APP_DEBUG")) logger.Info("Loading config file: ", configFile) defer func() { if err := recover(); err != nil { @@ -282,6 +276,7 @@ func main() { fx.Invoke(func(s *core.AppServer, h *admin.RewardHandler) { group := s.Engine.Group("/api/admin/reward/") group.GET("list", h.List) + group.GET("remove", h.Remove) }), fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) { group := s.Engine.Group("/api/admin/dashboard/") diff --git a/api/service/wx/bot.go b/api/service/wx/bot.go index a5564999..0feb097c 100644 --- a/api/service/wx/bot.go +++ b/api/service/wx/bot.go @@ -6,6 +6,8 @@ import ( "github.com/eatmoreapple/openwechat" "github.com/skip2/go-qrcode" "gorm.io/gorm" + "os" + "strconv" ) // 微信收款机器人 @@ -34,8 +36,13 @@ func (b *Bot) Run() error { } // scan code login callback b.bot.UUIDCallback = b.qrCodeCallBack - - err := b.bot.Login() + debug, err := strconv.ParseBool(os.Getenv("APP_DEBUG")) + if debug { + reloadStorage := openwechat.NewJsonFileHotReloadStorage("storage.json") + err = b.bot.HotLogin(reloadStorage, true) + } else { + err = b.bot.Login() + } if err != nil { return err } @@ -56,8 +63,8 @@ func (b *Bot) messageHandler(msg *openwechat.Message) { msg.MsgType == openwechat.MsgTypeApp || msg.AppMsgType == openwechat.AppMsgTypeUrl { // 解析支付金额 - message, err := parseTransactionMessage(msg.Content) - if err == nil { + message := parseTransactionMessage(msg.Content) + if message.Url != "" { transaction := extractTransaction(message) logger.Infof("解析到收款信息:%+v", transaction) var item model.Reward diff --git a/api/service/wx/tranaction.go b/api/service/wx/tranaction.go index ee06a9e3..0b1bca4d 100644 --- a/api/service/wx/tranaction.go +++ b/api/service/wx/tranaction.go @@ -2,17 +2,15 @@ package wx import ( "encoding/xml" + "net/url" "strconv" "strings" ) // Message 转账消息 type Message struct { - XMLName xml.Name `xml:"msg"` - AppMsg struct { - Des string `xml:"des"` - Url string `xml:"url"` - } `xml:"appmsg"` + Des string + Url string } // Transaction 解析后的交易信息 @@ -23,20 +21,40 @@ type Transaction struct { } // 解析微信转账消息 -func parseTransactionMessage(xmlData string) (*Message, error) { - var msg Message - if err := xml.Unmarshal([]byte(xmlData), &msg); err != nil { - return nil, err - } +func parseTransactionMessage(xmlData string) *Message { + decoder := xml.NewDecoder(strings.NewReader(xmlData)) + message := Message{} + for { + token, err := decoder.Token() + if err != nil { + break + } - return &msg, nil + switch se := token.(type) { + case xml.StartElement: + var value string + if se.Name.Local == "des" && message.Des == "" { + if err := decoder.DecodeElement(&value, &se); err == nil { + message.Des = strings.TrimSpace(value) + } + break + } + if se.Name.Local == "weapp_path" && !strings.Contains(message.Url, "customerDetails.html") { + if err := decoder.DecodeElement(&value, &se); err == nil { + message.Url = strings.TrimSpace(value) + } + break + } + } + } + return &message } // 导出交易信息 func extractTransaction(message *Message) Transaction { var tx = Transaction{} // 导出交易金额和备注 - lines := strings.Split(message.AppMsg.Des, "\n") + lines := strings.Split(message.Des, "\n") for _, line := range lines { line = strings.TrimSpace(line) if len(line) == 0 { @@ -59,10 +77,9 @@ func extractTransaction(message *Message) Transaction { } // 解析交易 ID - index := strings.Index(message.AppMsg.Url, "trans_id=") - if index != -1 { - end := strings.LastIndex(message.AppMsg.Url, "&") - tx.TransId = strings.TrimSpace(message.AppMsg.Url[index+9 : end]) + parse, err := url.Parse(message.Url) + if err == nil { + tx.TransId = parse.Query().Get("id") } return tx } diff --git a/api/store/model/reward.go b/api/store/model/reward.go index a035d4d1..43b9c8ce 100644 --- a/api/store/model/reward.go +++ b/api/store/model/reward.go @@ -4,9 +4,10 @@ package model type Reward struct { BaseModel - UserId uint // 用户 ID - TxId string // 交易ID - Amount float64 // 打赏金额 - Remark string // 打赏备注 - Status bool // 核销状态 + UserId uint // 用户 ID + TxId string // 交易ID + Amount float64 // 打赏金额 + Remark string // 打赏备注 + Status bool // 核销状态 + Exchange string // 众筹兑换详情,JSON } diff --git a/api/store/vo/reward.go b/api/store/vo/reward.go index 1aa06cd0..cb6490e1 100644 --- a/api/store/vo/reward.go +++ b/api/store/vo/reward.go @@ -2,10 +2,16 @@ package vo type Reward struct { BaseVo - UserId uint `json:"user_id"` // 用户 ID - Username string `json:"username"` - TxId string `json:"tx_id"` // 交易ID - Amount float64 `json:"amount"` // 打赏金额 - Remark string `json:"remark"` // 打赏备注 - Status bool `json:"status"` // 核销状态 + UserId uint `json:"user_id"` // 用户 ID + Username string `json:"username"` + TxId string `json:"tx_id"` // 交易ID + Amount float64 `json:"amount"` // 打赏金额 + Remark string `json:"remark"` // 打赏备注 + Status bool `json:"status"` // 核销状态 + Exchange RewardExchange `json:"exchange"` +} + +type RewardExchange struct { + Calls int `json:"calls"` + ImgCalls int `json:"img_calls"` } diff --git a/api/test/test.go b/api/test/test.go index 79058077..fe7f767d 100644 --- a/api/test/test.go +++ b/api/test/test.go @@ -1,5 +1,5 @@ package main func main() { - + } diff --git a/database/update-v3.2.4.sql b/database/update-v3.2.4.sql index 32f38023..87642d1a 100644 --- a/database/update-v3.2.4.sql +++ b/database/update-v3.2.4.sql @@ -1 +1,2 @@ ALTER TABLE `chatgpt_users` ADD `nickname` VARCHAR(30) NOT NULL COMMENT '昵称' AFTER `mobile`; +ALTER TABLE `chatgpt_rewards` ADD `exchange` VARCHAR(255) NOT NULL COMMENT '兑换详情(json)' AFTER `status`; diff --git a/web/src/components/RewardVerify.vue b/web/src/components/RewardVerify.vue index 17e583f9..141e8c06 100644 --- a/web/src/components/RewardVerify.vue +++ b/web/src/components/RewardVerify.vue @@ -13,10 +13,19 @@ - + + + + + + 对话聊天 + AI绘图 + + + + + + + + + + + @@ -31,7 +50,7 @@ import {ref} from "vue"; import {httpGet} from "@/utils/http"; import {ElMessage} from "element-plus"; -import {dateFormat} from "@/utils/libs"; +import {dateFormat, removeArrayItem} from "@/utils/libs"; // 变量定义 const items = ref([]) @@ -52,6 +71,16 @@ httpGet('/api/admin/reward/list').then((res) => { ElMessage.error("获取数据失败"); }) +const remove = function (row) { + httpGet('/api/admin/reward/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) + }) +}