feat: add sign check for PC QR code payment

This commit is contained in:
RockYang 2024-05-22 17:47:53 +08:00
parent 4bba77ab47
commit f755bdccae
8 changed files with 95 additions and 20 deletions

View File

@ -3,6 +3,7 @@
* 功能优化:当数据库更新失败的时候记录错误日志 * 功能优化:当数据库更新失败的时候记录错误日志
* 功能优化:聊天输入框会随着输入内容的增多自动调整高度 * 功能优化:聊天输入框会随着输入内容的增多自动调整高度
* Bug修复修复移动端聊天页面模型切换不生效的Bug * Bug修复修复移动端聊天页面模型切换不生效的Bug
* 功能优化给PC端扫码支付增加签名验证和有效期验证
## v4.0.7 ## v4.0.7

View File

@ -76,6 +76,7 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
func (h *ApiKeyHandler) List(c *gin.Context) { func (h *ApiKeyHandler) List(c *gin.Context) {
status := h.GetBool(c, "status") status := h.GetBool(c, "status")
t := h.GetTrim(c, "type") t := h.GetTrim(c, "type")
platform := h.GetTrim(c, "platform")
session := h.DB.Session(&gorm.Session{}) session := h.DB.Session(&gorm.Session{})
if status { if status {
@ -84,6 +85,9 @@ func (h *ApiKeyHandler) List(c *gin.Context) {
if t != "" { if t != "" {
session = session.Where("type", t) session = session.Where("type", t)
} }
if platform != "" {
session = session.Where("platform", platform)
}
var items []model.ApiKey var items []model.ApiKey
var keys = make([]vo.ApiKey, 0) var keys = make([]vo.ApiKey, 0)

View File

@ -89,9 +89,13 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
func (h *ChatModelHandler) List(c *gin.Context) { func (h *ChatModelHandler) List(c *gin.Context) {
session := h.DB.Session(&gorm.Session{}) session := h.DB.Session(&gorm.Session{})
enable := h.GetBool(c, "enable") enable := h.GetBool(c, "enable")
platform := h.GetTrim(c, "platform")
if enable { if enable {
session = session.Where("enabled", enable) session = session.Where("enabled", enable)
} }
if platform != "" {
session = session.Where("platform", platform)
}
var items []model.ChatModel var items []model.ChatModel
var cms = make([]vo.ChatModel, 0) var cms = make([]vo.ChatModel, 0)
res := session.Order("sort_num ASC").Find(&items) res := session.Order("sort_num ASC").Find(&items)

View File

@ -8,6 +8,9 @@ package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"embed"
"encoding/base64"
"fmt"
"geekai/core" "geekai/core"
"geekai/core/types" "geekai/core/types"
"geekai/service" "geekai/service"
@ -15,9 +18,6 @@ import (
"geekai/store/model" "geekai/store/model"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"embed"
"encoding/base64"
"fmt"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
"math" "math"
"net/http" "net/http"
@ -44,6 +44,7 @@ type PaymentHandler struct {
snowflake *service.Snowflake snowflake *service.Snowflake
fs embed.FS fs embed.FS
lock sync.Mutex lock sync.Mutex
signKey string // 用来签名的随机秘钥
} }
func NewPaymentHandler( func NewPaymentHandler(
@ -65,12 +66,27 @@ func NewPaymentHandler(
App: server, App: server,
DB: db, DB: db,
}, },
signKey: utils.RandString(32),
} }
} }
func (h *PaymentHandler) DoPay(c *gin.Context) { func (h *PaymentHandler) DoPay(c *gin.Context) {
orderNo := h.GetTrim(c, "order_no") orderNo := h.GetTrim(c, "order_no")
payWay := h.GetTrim(c, "pay_way") payWay := h.GetTrim(c, "pay_way")
t := h.GetInt(c, "t", 0)
sign := h.GetTrim(c, "sign")
signStr := fmt.Sprintf("%s-%s-%d-%s", orderNo, payWay, t, h.signKey)
newSign := utils.Sha256(signStr)
if newSign != sign {
resp.ERROR(c, "订单签名错误!")
return
}
// 检查二维码是否过期
if time.Now().Unix()-int64(t) > 30 {
resp.ERROR(c, "支付二维码已过期,请重新生成!")
return
}
if orderNo == "" { if orderNo == "" {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
@ -273,8 +289,10 @@ func (h *PaymentHandler) PayQrcode(c *gin.Context) {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
} }
timestamp := time.Now().Unix()
imageURL := fmt.Sprintf("%s://%s/api/payment/doPay?order_no=%s&pay_way=%s", parse.Scheme, parse.Host, orderNo, data.PayWay) signStr := fmt.Sprintf("%s-%s-%d-%s", orderNo, data.PayWay, timestamp, h.signKey)
sign := utils.Sha256(signStr)
imageURL := fmt.Sprintf("%s://%s/api/payment/doPay?order_no=%s&pay_way=%s&t=%d&sign=%s", parse.Scheme, parse.Host, orderNo, data.PayWay, timestamp, sign)
imgData, err := utils.GenQrcode(imageURL, 400, file) imgData, err := utils.GenQrcode(imageURL, 400, file)
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
@ -333,6 +351,7 @@ func (h *PaymentHandler) Mobile(c *gin.Context) {
ReturnURL: returnURL, ReturnURL: returnURL,
CallbackURL: returnURL, CallbackURL: returnURL,
WapName: "极客学长", WapName: "极客学长",
Type: "WAP",
} }
r, err := h.huPiPayService.Pay(params) r, err := h.huPiPayService.Pay(params)
if err != nil { if err != nil {

View File

@ -49,6 +49,8 @@ type HuPiPayReq struct {
CallbackURL string `json:"callback_url"` CallbackURL string `json:"callback_url"`
Time string `json:"time"` Time string `json:"time"`
NonceStr string `json:"nonce_str"` NonceStr string `json:"nonce_str"`
Type string `json:"type"`
WapUrl string `json:"wap_url"`
} }
type HuPiResp struct { type HuPiResp struct {

View File

@ -2,6 +2,24 @@
<div class="container list" v-loading="loading"> <div class="container list" v-loading="loading">
<div class="handle-box"> <div class="handle-box">
<el-select v-model="query.platform" placeholder="平台" class="handle-input">
<el-option
v-for="item in platforms"
:key="item.value"
:label="item.name"
:value="item.value"
/>
</el-select>
<el-select v-model="query.type" placeholder="类型" class="handle-input">
<el-option
v-for="item in types"
:key="item.value"
:label="item.name"
:value="item.value"
/>
</el-select>
<el-button :icon="Search" @click="fetchData">搜索</el-button>
<el-button type="primary" :icon="Plus" @click="add">新增</el-button> <el-button type="primary" :icon="Plus" @click="add">新增</el-button>
<a href="https://api.chat-plus.net" target="_blank" style="margin-left: 10px"> <a href="https://api.chat-plus.net" target="_blank" style="margin-left: 10px">
<el-button type="success" :icon="ShoppingCart" @click="add" plain>购买API-KEY</el-button> <el-button type="success" :icon="ShoppingCart" @click="add" plain>购买API-KEY</el-button>
@ -127,11 +145,12 @@ import {onMounted, onUnmounted, reactive, ref} from "vue";
import {httpGet, httpPost} from "@/utils/http"; import {httpGet, httpPost} from "@/utils/http";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {dateFormat, removeArrayItem, substr} from "@/utils/libs"; import {dateFormat, removeArrayItem, substr} from "@/utils/libs";
import {DocumentCopy, Plus, ShoppingCart, InfoFilled} from "@element-plus/icons-vue"; import {DocumentCopy, Plus, ShoppingCart, Search} from "@element-plus/icons-vue";
import ClipboardJS from "clipboard"; import ClipboardJS from "clipboard";
// //
const items = ref([]) const items = ref([])
const query = ref({type: '',platform:''})
const item = ref({}) const item = ref({})
const showDialog = ref(false) const showDialog = ref(false)
const rules = reactive({ const rules = reactive({
@ -166,6 +185,8 @@ onMounted(() => {
}).catch(e =>{ }).catch(e =>{
ElMessage.error("获取配置失败:"+e.message) ElMessage.error("获取配置失败:"+e.message)
}) })
fetchData()
}) })
onUnmounted(() => { onUnmounted(() => {
@ -173,7 +194,9 @@ onUnmounted(() => {
}) })
// //
httpGet('/api/admin/apikey/list').then((res) => {
const fetchData = () => {
httpGet('/api/admin/apikey/list', query.value).then((res) => {
if (res.data) { if (res.data) {
// //
const arr = res.data; const arr = res.data;
@ -186,6 +209,7 @@ httpGet('/api/admin/apikey/list').then((res) => {
}).catch(() => { }).catch(() => {
ElMessage.error("获取数据失败"); ElMessage.error("获取数据失败");
}) })
}
const add = function () { const add = function () {
showDialog.value = true showDialog.value = true
@ -264,6 +288,10 @@ const changeType = (value) => {
.list { .list {
.handle-box { .handle-box {
margin-bottom 20px margin-bottom 20px
.handle-input {
max-width 150px;
margin-right 10px;
}
} }
.copy-key { .copy-key {

View File

@ -2,6 +2,16 @@
<div class="container model-list" v-loading="loading"> <div class="container model-list" v-loading="loading">
<div class="handle-box"> <div class="handle-box">
<el-select v-model="query.platform" placeholder="平台" class="handle-input">
<el-option
v-for="item in platforms"
:key="item.value"
:label="item.name"
:value="item.value"
/>
</el-select>
<el-button :icon="Search" @click="fetchData">搜索</el-button>
<el-button type="primary" :icon="Plus" @click="add">新增</el-button> <el-button type="primary" :icon="Plus" @click="add">新增</el-button>
</div> </div>
@ -198,12 +208,13 @@ import {onMounted, onUnmounted, reactive, ref} from "vue";
import {httpGet, httpPost} from "@/utils/http"; import {httpGet, httpPost} from "@/utils/http";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {dateFormat, removeArrayItem, substr} from "@/utils/libs"; import {dateFormat, removeArrayItem, substr} from "@/utils/libs";
import {DocumentCopy, InfoFilled, Plus} from "@element-plus/icons-vue"; import {DocumentCopy, InfoFilled, Plus,Search} from "@element-plus/icons-vue";
import {Sortable} from "sortablejs"; import {Sortable} from "sortablejs";
import ClipboardJS from "clipboard"; import ClipboardJS from "clipboard";
// //
const items = ref([]) const items = ref([])
const query = ref({platform:''})
const item = ref({}) const item = ref({})
const showDialog = ref(false) const showDialog = ref(false)
const title = ref("") const title = ref("")
@ -226,7 +237,7 @@ httpGet('/api/admin/apikey/list?status=true&type=chat').then(res => {
// //
const fetchData = () => { const fetchData = () => {
httpGet('/api/admin/model/list').then((res) => { httpGet('/api/admin/model/list', query.value).then((res) => {
if (res.data) { if (res.data) {
// //
const arr = res.data; const arr = res.data;
@ -350,6 +361,10 @@ const remove = function (row) {
.handle-box { .handle-box {
margin-bottom 20px margin-bottom 20px
.handle-input {
max-width 150px;
margin-right 10px;
}
} }
.cell { .cell {

View File

@ -354,6 +354,8 @@ const doResetPass = () => {
.pagination { .pagination {
padding 20px 0 padding 20px 0
display flex display flex
justify-content right
width 100%
} }
.el-select { .el-select {