feat: fixed bug for wechat bot to parse transactions. enable user to exchange reward with img_calls

This commit is contained in:
RockYang 2024-01-03 11:15:54 +08:00
parent ff4515bbf1
commit b5641be30d
15 changed files with 166 additions and 55 deletions

View File

@ -13,10 +13,11 @@ ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了
* 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI * 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI
绘画函数插件。 绘画函数插件。
## 关于部署镜像申明 ## 最新版本一键部署脚本
由于目前部署人数越来越多,本人的阿里云镜像仓库流量不够支撑大家使用了。所以从 v3.2.0 版本开始,一键部署脚本和部署镜像将只提供给 **[付费技术交流群]** 内用户使用。 ```shell
代码依旧是全部开源的,大家可自行编译打包镜像。 bash -c "$(curl -fsSL https://img.r9it.com/tmp/install-v3.2.3-8b588904ef.sh)"
```
## 功能截图 ## 功能截图

View File

@ -159,6 +159,8 @@ type SystemConfig struct {
EnabledMsg bool `json:"enabled_msg"` // 是否启用短信验证码服务 EnabledMsg bool `json:"enabled_msg"` // 是否启用短信验证码服务
RewardImg string `json:"reward_img"` // 众筹收款二维码地址 RewardImg string `json:"reward_img"` // 众筹收款二维码地址
EnabledReward bool `json:"enabled_reward"` // 启用众筹功能 EnabledReward bool `json:"enabled_reward"` // 启用众筹功能
ChatCallPrice float64 `json:"chat_call_price"` // 对话单次调用费用
ImgCallPrice float64 `json:"img_call_price"` // 绘图单次调用费用
EnabledAlipay bool `json:"enabled_alipay"` // 是否启用支付宝支付通道 EnabledAlipay bool `json:"enabled_alipay"` // 是否启用支付宝支付通道
OrderPayTimeout int `json:"order_pay_timeout"` //订单支付超时时间 OrderPayTimeout int `json:"order_pay_timeout"` //订单支付超时时间
DefaultModels []string `json:"default_models"` // 默认开通的 AI 模型 DefaultModels []string `json:"default_models"` // 默认开通的 AI 模型
@ -166,4 +168,5 @@ type SystemConfig struct {
InviteChatCalls int `json:"invite_chat_calls"` // 邀请用户注册奖励对话次数 InviteChatCalls int `json:"invite_chat_calls"` // 邀请用户注册奖励对话次数
InviteImgCalls int `json:"invite_img_calls"` // 邀请用户注册奖励绘图次数 InviteImgCalls int `json:"invite_img_calls"` // 邀请用户注册奖励绘图次数
ForceInvite bool `json:"force_invite"` // 是否强制必须使用邀请码才能注册 ForceInvite bool `json:"force_invite"` // 是否强制必须使用邀请码才能注册
} }

View File

@ -55,3 +55,16 @@ func (h *RewardHandler) List(c *gin.Context) {
resp.SUCCESS(c, rewards) 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)
}

View File

@ -4,20 +4,24 @@ import (
"chatplus/core" "chatplus/core"
"chatplus/core/types" "chatplus/core/types"
"chatplus/store/model" "chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils" "chatplus/utils"
"chatplus/utils/resp" "chatplus/utils/resp"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"math"
"strings" "strings"
"sync"
) )
type RewardHandler struct { type RewardHandler struct {
BaseHandler BaseHandler
db *gorm.DB db *gorm.DB
lock sync.Mutex
} }
func NewRewardHandler(server *core.AppServer, db *gorm.DB) *RewardHandler { func NewRewardHandler(server *core.AppServer, db *gorm.DB) *RewardHandler {
h := RewardHandler{db: db} h := RewardHandler{db: db, lock: sync.Mutex{}}
h.App = server h.App = server
return &h return &h
} }
@ -26,15 +30,25 @@ func NewRewardHandler(server *core.AppServer, db *gorm.DB) *RewardHandler {
func (h *RewardHandler) Verify(c *gin.Context) { func (h *RewardHandler) Verify(c *gin.Context) {
var data struct { var data struct {
TxId string `json:"tx_id"` TxId string `json:"tx_id"`
Type string `json:"type"`
} }
if err := c.ShouldBindJSON(&data); err != nil { if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return return
} }
user, err := utils.GetLoginUser(c, h.db)
if err != nil {
resp.HACKER(c)
return
}
// 移除转账单号中间的空格,防止有人复制的时候多复制了空格 // 移除转账单号中间的空格,防止有人复制的时候多复制了空格
data.TxId = strings.ReplaceAll(data.TxId, " ", "") data.TxId = strings.ReplaceAll(data.TxId, " ", "")
h.lock.Lock()
defer h.lock.Unlock()
var item model.Reward var item model.Reward
res := h.db.Where("tx_id = ?", data.TxId).First(&item) res := h.db.Where("tx_id = ?", data.TxId).First(&item)
if res.Error != nil { if res.Error != nil {
@ -47,15 +61,17 @@ func (h *RewardHandler) Verify(c *gin.Context) {
return return
} }
user, err := utils.GetLoginUser(c, h.db)
if err != nil {
resp.HACKER(c)
return
}
tx := h.db.Begin() tx := h.db.Begin()
calls := (item.Amount + 0.1) * 10 exchange := vo.RewardExchange{}
res = h.db.Model(&user).UpdateColumn("calls", gorm.Expr("calls + ?", calls)) 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 { if res.Error != nil {
resp.ERROR(c, "更新数据库失败!") resp.ERROR(c, "更新数据库失败!")
return return
@ -64,6 +80,7 @@ func (h *RewardHandler) Verify(c *gin.Context) {
// 更新核销状态 // 更新核销状态
item.Status = true item.Status = true
item.UserId = user.Id item.UserId = user.Id
item.Exchange = utils.JsonEncode(exchange)
res = h.db.Updates(&item) res = h.db.Updates(&item)
if res.Error != nil { if res.Error != nil {
tx.Rollback() tx.Rollback()

View File

@ -57,13 +57,7 @@ func main() {
if configFile == "" { if configFile == "" {
configFile = "config.toml" configFile = "config.toml"
} }
var debug bool debug, _ := strconv.ParseBool(os.Getenv("APP_DEBUG"))
debugEnv := os.Getenv("DEBUG")
if debugEnv == "" {
debug = true
} else {
debug, _ = strconv.ParseBool(os.Getenv("DEBUG"))
}
logger.Info("Loading config file: ", configFile) logger.Info("Loading config file: ", configFile)
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
@ -282,6 +276,7 @@ func main() {
fx.Invoke(func(s *core.AppServer, h *admin.RewardHandler) { fx.Invoke(func(s *core.AppServer, h *admin.RewardHandler) {
group := s.Engine.Group("/api/admin/reward/") group := s.Engine.Group("/api/admin/reward/")
group.GET("list", h.List) group.GET("list", h.List)
group.GET("remove", h.Remove)
}), }),
fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) { fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) {
group := s.Engine.Group("/api/admin/dashboard/") group := s.Engine.Group("/api/admin/dashboard/")

View File

@ -6,6 +6,8 @@ import (
"github.com/eatmoreapple/openwechat" "github.com/eatmoreapple/openwechat"
"github.com/skip2/go-qrcode" "github.com/skip2/go-qrcode"
"gorm.io/gorm" "gorm.io/gorm"
"os"
"strconv"
) )
// 微信收款机器人 // 微信收款机器人
@ -34,8 +36,13 @@ func (b *Bot) Run() error {
} }
// scan code login callback // scan code login callback
b.bot.UUIDCallback = b.qrCodeCallBack b.bot.UUIDCallback = b.qrCodeCallBack
debug, err := strconv.ParseBool(os.Getenv("APP_DEBUG"))
err := b.bot.Login() if debug {
reloadStorage := openwechat.NewJsonFileHotReloadStorage("storage.json")
err = b.bot.HotLogin(reloadStorage, true)
} else {
err = b.bot.Login()
}
if err != nil { if err != nil {
return err return err
} }
@ -56,8 +63,8 @@ func (b *Bot) messageHandler(msg *openwechat.Message) {
msg.MsgType == openwechat.MsgTypeApp || msg.MsgType == openwechat.MsgTypeApp ||
msg.AppMsgType == openwechat.AppMsgTypeUrl { msg.AppMsgType == openwechat.AppMsgTypeUrl {
// 解析支付金额 // 解析支付金额
message, err := parseTransactionMessage(msg.Content) message := parseTransactionMessage(msg.Content)
if err == nil { if message.Url != "" {
transaction := extractTransaction(message) transaction := extractTransaction(message)
logger.Infof("解析到收款信息:%+v", transaction) logger.Infof("解析到收款信息:%+v", transaction)
var item model.Reward var item model.Reward

View File

@ -2,17 +2,15 @@ package wx
import ( import (
"encoding/xml" "encoding/xml"
"net/url"
"strconv" "strconv"
"strings" "strings"
) )
// Message 转账消息 // Message 转账消息
type Message struct { type Message struct {
XMLName xml.Name `xml:"msg"` Des string
AppMsg struct { Url string
Des string `xml:"des"`
Url string `xml:"url"`
} `xml:"appmsg"`
} }
// Transaction 解析后的交易信息 // Transaction 解析后的交易信息
@ -23,20 +21,40 @@ type Transaction struct {
} }
// 解析微信转账消息 // 解析微信转账消息
func parseTransactionMessage(xmlData string) (*Message, error) { func parseTransactionMessage(xmlData string) *Message {
var msg Message decoder := xml.NewDecoder(strings.NewReader(xmlData))
if err := xml.Unmarshal([]byte(xmlData), &msg); err != nil { message := Message{}
return nil, err 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 { func extractTransaction(message *Message) Transaction {
var tx = Transaction{} var tx = Transaction{}
// 导出交易金额和备注 // 导出交易金额和备注
lines := strings.Split(message.AppMsg.Des, "\n") lines := strings.Split(message.Des, "\n")
for _, line := range lines { for _, line := range lines {
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
if len(line) == 0 { if len(line) == 0 {
@ -59,10 +77,9 @@ func extractTransaction(message *Message) Transaction {
} }
// 解析交易 ID // 解析交易 ID
index := strings.Index(message.AppMsg.Url, "trans_id=") parse, err := url.Parse(message.Url)
if index != -1 { if err == nil {
end := strings.LastIndex(message.AppMsg.Url, "&") tx.TransId = parse.Query().Get("id")
tx.TransId = strings.TrimSpace(message.AppMsg.Url[index+9 : end])
} }
return tx return tx
} }

View File

@ -4,9 +4,10 @@ package model
type Reward struct { type Reward struct {
BaseModel BaseModel
UserId uint // 用户 ID UserId uint // 用户 ID
TxId string // 交易ID TxId string // 交易ID
Amount float64 // 打赏金额 Amount float64 // 打赏金额
Remark string // 打赏备注 Remark string // 打赏备注
Status bool // 核销状态 Status bool // 核销状态
Exchange string // 众筹兑换详情JSON
} }

View File

@ -2,10 +2,16 @@ package vo
type Reward struct { type Reward struct {
BaseVo BaseVo
UserId uint `json:"user_id"` // 用户 ID UserId uint `json:"user_id"` // 用户 ID
Username string `json:"username"` Username string `json:"username"`
TxId string `json:"tx_id"` // 交易ID TxId string `json:"tx_id"` // 交易ID
Amount float64 `json:"amount"` // 打赏金额 Amount float64 `json:"amount"` // 打赏金额
Remark string `json:"remark"` // 打赏备注 Remark string `json:"remark"` // 打赏备注
Status bool `json:"status"` // 核销状态 Status bool `json:"status"` // 核销状态
Exchange RewardExchange `json:"exchange"`
}
type RewardExchange struct {
Calls int `json:"calls"`
ImgCalls int `json:"img_calls"`
} }

View File

@ -1,5 +1,5 @@
package main package main
func main() { func main() {
} }

View File

@ -1 +1,2 @@
ALTER TABLE `chatgpt_users` ADD `nickname` VARCHAR(30) NOT NULL COMMENT '昵称' AFTER `mobile`; 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`;

View File

@ -13,10 +13,19 @@
</el-alert> </el-alert>
<el-form :model="form"> <el-form :model="form">
<el-form-item label=""> <el-form-item label="转账单号">
<el-input v-model="form.tx_id"/> <el-input v-model="form.tx_id"/>
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-form :model="form">
<el-form-item label="兑换类别">
<el-radio-group v-model="form.type">
<el-radio label="chat" border>对话聊天</el-radio>
<el-radio label="img" border>AI绘图</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</div> </div>
<template #footer> <template #footer>
@ -46,6 +55,7 @@ const showDialog = computed(() => {
const title = ref('众筹码核销') const title = ref('众筹码核销')
const form = ref({ const form = ref({
tx_id: '', tx_id: '',
type: 'chat'
}) })
const emits = defineEmits(['hide']); const emits = defineEmits(['hide']);

View File

@ -142,7 +142,7 @@ const routes = [
path: '/admin/reward', path: '/admin/reward',
name: 'admin-reward', name: 'admin-reward',
meta: {title: '众筹管理'}, meta: {title: '众筹管理'},
component: () => import('@/views/admin/RewardList.vue'), component: () => import('@/views/admin/Reward.vue'),
}, },
{ {
path: '/admin/loginLog', path: '/admin/loginLog',

View File

@ -21,6 +21,25 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="兑换详情">
<template #default="scope">
<el-tag v-if="scope.row['exchange']['calls'] > 0">聊天{{ scope.row['exchange']['calls'] }}</el-tag>
<el-tag v-else-if="scope.row['exchange']['img_calls'] > 0" type="success">
绘图{{ scope.row['exchange']['img_calls'] }}
</el-tag>
</template>
</el-table-column>
<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-table>
</el-row> </el-row>
@ -31,7 +50,7 @@
import {ref} from "vue"; import {ref} from "vue";
import {httpGet} from "@/utils/http"; import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {dateFormat} from "@/utils/libs"; import {dateFormat, removeArrayItem} from "@/utils/libs";
// //
const items = ref([]) const items = ref([])
@ -52,6 +71,16 @@ httpGet('/api/admin/reward/list').then((res) => {
ElMessage.error("获取数据失败"); 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)
})
}
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>

View File

@ -71,6 +71,15 @@
</el-tooltip> </el-tooltip>
</el-form-item> </el-form-item>
<div v-if="system['enabled_reward']">
<el-form-item label="单次对话价格" prop="chat_call_price">
<el-input v-model="system['chat_call_price']" placeholder="众筹金额跟对话次数的兑换比例"/>
</el-form-item>
<el-form-item label="单次绘图价格" prop="img_call_price">
<el-input v-model="system['img_call_price']" placeholder="众筹金额跟绘图次数的兑换比例"/>
</el-form-item>
</div>
<el-form-item label="收款二维码" prop="reward_img"> <el-form-item label="收款二维码" prop="reward_img">
<el-input v-model="system['reward_img']" placeholder="众筹收款二维码地址"> <el-input v-model="system['reward_img']" placeholder="众筹收款二维码地址">
<template #append> <template #append>
@ -312,6 +321,8 @@ const save = function (key) {
if (key === 'system') { if (key === 'system') {
systemFormRef.value.validate((valid) => { systemFormRef.value.validate((valid) => {
if (valid) { if (valid) {
system.value['img_call_price'] = parseFloat(system.value['img_call_price']) ?? 0
system.value['chat_call_price'] = parseFloat(system.value['chat_call_price']) ?? 0
httpPost('/api/admin/config/update', {key: key, config: system.value}).then(() => { httpPost('/api/admin/config/update', {key: key, config: system.value}).then(() => {
ElMessage.success("操作成功!") ElMessage.success("操作成功!")
}).catch(e => { }).catch(e => {