mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-09-19 17:56:39 +08:00
feat: 增加订单倒计时组件,自动清理过期未支付订单
This commit is contained in:
parent
b589102be8
commit
414c1de963
@ -145,5 +145,6 @@ type SystemConfig struct {
|
|||||||
EnabledFunction bool `json:"enabled_function"` // 启用 API 函数功能
|
EnabledFunction bool `json:"enabled_function"` // 启用 API 函数功能
|
||||||
EnabledReward bool `json:"enabled_reward"` // 启用众筹功能
|
EnabledReward bool `json:"enabled_reward"` // 启用众筹功能
|
||||||
EnabledAlipay bool `json:"enabled_alipay"` // 是否启用支付宝支付通道
|
EnabledAlipay bool `json:"enabled_alipay"` // 是否启用支付宝支付通道
|
||||||
|
OrderPayTimeout int `json:"order_pay_timeout"` //订单支付超时时间
|
||||||
DefaultModels []string `json:"default_models"` // 默认开通的 AI 模型
|
DefaultModels []string `json:"default_models"` // 默认开通的 AI 模型
|
||||||
}
|
}
|
||||||
|
@ -40,9 +40,25 @@ func (e *XXLJobExecutor) Run() error {
|
|||||||
|
|
||||||
// ClearOrder 清理未支付的订单,如果没有抛出异常则表示执行成功
|
// ClearOrder 清理未支付的订单,如果没有抛出异常则表示执行成功
|
||||||
func (e *XXLJobExecutor) ClearOrder(cxt context.Context, param *xxl.RunReq) (msg string) {
|
func (e *XXLJobExecutor) ClearOrder(cxt context.Context, param *xxl.RunReq) (msg string) {
|
||||||
timeout := time.Now().Unix() - 600
|
logger.Debug("执行清理未支付订单...")
|
||||||
|
var sysConfig model.Config
|
||||||
|
res := e.db.Where("marker", "system").First(&sysConfig)
|
||||||
|
if res.Error != nil {
|
||||||
|
return "error with get system config: " + res.Error.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
var config types.SystemConfig
|
||||||
|
err := utils.JsonDecode(sysConfig.Config, &config)
|
||||||
|
if err != nil {
|
||||||
|
return "error with decode system config: " + err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.OrderPayTimeout == 0 { // 默认未支付订单的生命周期为 30 分钟
|
||||||
|
config.OrderPayTimeout = 1800
|
||||||
|
}
|
||||||
|
timeout := time.Now().Unix() - int64(config.OrderPayTimeout)
|
||||||
start := utils.Stamp2str(timeout)
|
start := utils.Stamp2str(timeout)
|
||||||
res := e.db.Where("status != ? AND created_at < ?", types.OrderPaidSuccess, start).Delete(&model.Order{})
|
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)
|
return fmt.Sprintf("Clear order successfully, affect rows: %d", res.RowsAffected)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,13 +74,13 @@ func (e *XXLJobExecutor) ResetVipCalls(cxt context.Context, param *xxl.RunReq) (
|
|||||||
var sysConfig model.Config
|
var sysConfig model.Config
|
||||||
res = e.db.Where("marker", "system").First(&sysConfig)
|
res = e.db.Where("marker", "system").First(&sysConfig)
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
panic(res.Error)
|
return "error with get system config: " + res.Error.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
var config types.SystemConfig
|
var config types.SystemConfig
|
||||||
err := utils.JsonDecode(sysConfig.Config, &config)
|
err := utils.JsonDecode(sysConfig.Config, &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return "error with decode system config: " + err.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取本月月初时间
|
// 获取本月月初时间
|
||||||
@ -115,9 +131,9 @@ func (e *XXLJobExecutor) ResetVipCalls(cxt context.Context, param *xxl.RunReq) (
|
|||||||
type customLogger struct{}
|
type customLogger struct{}
|
||||||
|
|
||||||
func (l *customLogger) Info(format string, a ...interface{}) {
|
func (l *customLogger) Info(format string, a ...interface{}) {
|
||||||
logger.Debug(format, a)
|
logger.Debugf(format, a...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *customLogger) Error(format string, a ...interface{}) {
|
func (l *customLogger) Error(format string, a ...interface{}) {
|
||||||
logger.Error(format, a)
|
logger.Errorf(format, a...)
|
||||||
}
|
}
|
||||||
|
97
web/src/components/CountDown.vue
Normal file
97
web/src/components/CountDown.vue
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 倒计时组件 -->
|
||||||
|
<div class="countdown">
|
||||||
|
<el-tag size="large" :type="type">{{ timerStr }}</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
import {onMounted, ref, watch} from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
second: Number,
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(['timeout']);
|
||||||
|
const counter = ref(props.second)
|
||||||
|
const timerStr = ref("")
|
||||||
|
const handler = ref(null)
|
||||||
|
|
||||||
|
watch(() => props.second, (newVal) => {
|
||||||
|
counter.value = newVal
|
||||||
|
resetTimer()
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
resetTimer()
|
||||||
|
})
|
||||||
|
|
||||||
|
const resetTimer = () => {
|
||||||
|
if (handler.value) {
|
||||||
|
clearInterval(handler.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
counter.value = props.second
|
||||||
|
formatTimer(counter.value)
|
||||||
|
handler.value = setInterval(() => {
|
||||||
|
formatTimer(counter.value)
|
||||||
|
if (counter.value === 0) {
|
||||||
|
clearInterval(handler.value)
|
||||||
|
emits("timeout")
|
||||||
|
}
|
||||||
|
counter.value--
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTimer = (secs) => {
|
||||||
|
const timer = []
|
||||||
|
let hour, min
|
||||||
|
// 计算小时
|
||||||
|
if (secs > 3600) {
|
||||||
|
hour = Math.floor(secs / 3600)
|
||||||
|
if (hour < 10) {
|
||||||
|
hour = "0" + hour
|
||||||
|
}
|
||||||
|
secs = secs % 3600
|
||||||
|
timer.push(hour + " 时 ")
|
||||||
|
} else {
|
||||||
|
timer.push("00 时 ")
|
||||||
|
}
|
||||||
|
// 计算分钟
|
||||||
|
if (secs > 60) {
|
||||||
|
min = Math.floor(secs / 60)
|
||||||
|
if (min < 10) {
|
||||||
|
min = "0" + min
|
||||||
|
}
|
||||||
|
secs = secs % 60
|
||||||
|
timer.push(min + " 分 ")
|
||||||
|
} else {
|
||||||
|
timer.push("00 分 ")
|
||||||
|
}
|
||||||
|
// 计算秒数
|
||||||
|
if (secs < 10) {
|
||||||
|
secs = "0" + secs
|
||||||
|
}
|
||||||
|
timer.push(secs + " 秒")
|
||||||
|
timerStr.value = timer.join("")
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({resetTimer})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.countdown {
|
||||||
|
display flex
|
||||||
|
|
||||||
|
.el-tag--large {
|
||||||
|
.el-tag__content {
|
||||||
|
font-size 14px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
@ -100,7 +100,11 @@
|
|||||||
:width="400"
|
:width="400"
|
||||||
title="充值订单支付">
|
title="充值订单支付">
|
||||||
<div class="pay-container">
|
<div class="pay-container">
|
||||||
<div class="pay-qrcode">
|
<div class="count-down">
|
||||||
|
<count-down :second="orderTimeout" @timeout="orderPay" ref="countDown"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pay-qrcode" v-loading="loading">
|
||||||
<el-image :src="qrcode"/>
|
<el-image :src="qrcode"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -135,6 +139,7 @@ import BindMobile from "@/components/BindMobile.vue";
|
|||||||
import RewardVerify from "@/components/RewardVerify.vue";
|
import RewardVerify from "@/components/RewardVerify.vue";
|
||||||
import {useRouter} from "vue-router";
|
import {useRouter} from "vue-router";
|
||||||
import {removeUserToken} from "@/store/session";
|
import {removeUserToken} from "@/store/session";
|
||||||
|
import CountDown from "@/components/CountDown.vue";
|
||||||
|
|
||||||
const listBoxHeight = window.innerHeight - 97
|
const listBoxHeight = window.innerHeight - 97
|
||||||
const list = ref([])
|
const list = ref([])
|
||||||
@ -153,6 +158,11 @@ const text = ref("")
|
|||||||
const user = ref(null)
|
const user = ref(null)
|
||||||
const isLogin = ref(false)
|
const isLogin = ref(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const curPayProduct = ref(null)
|
||||||
|
const activeOrderNo = ref("")
|
||||||
|
const countDown = ref(null)
|
||||||
|
const orderTimeout = ref(1800)
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -171,6 +181,9 @@ onMounted(() => {
|
|||||||
httpGet("/api/admin/config/get?key=system").then(res => {
|
httpGet("/api/admin/config/get?key=system").then(res => {
|
||||||
rewardImg.value = res.data['reward_img']
|
rewardImg.value = res.data['reward_img']
|
||||||
enableReward.value = res.data['enabled_reward']
|
enableReward.value = res.data['enabled_reward']
|
||||||
|
if (res.data['order_pay_timeout'] > 0) {
|
||||||
|
orderTimeout.value = res.data['order_pay_timeout']
|
||||||
|
}
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
ElMessage.error("获取系统配置失败:" + e.message)
|
ElMessage.error("获取系统配置失败:" + e.message)
|
||||||
})
|
})
|
||||||
@ -181,10 +194,20 @@ const orderPay = (row) => {
|
|||||||
showLoginDialog.value = true
|
showLoginDialog.value = true
|
||||||
return
|
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
|
showPayDialog.value = true
|
||||||
qrcode.value = res.data['image']
|
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 => {
|
}).catch(e => {
|
||||||
ElMessage.error("生成支付订单失败:" + e.message)
|
ElMessage.error("生成支付订单失败:" + e.message)
|
||||||
})
|
})
|
||||||
@ -199,8 +222,11 @@ const queryOrder = (orderNo) => {
|
|||||||
text.value = "支付成功,正在刷新页面"
|
text.value = "支付成功,正在刷新页面"
|
||||||
setTimeout(() => location.reload(), 500)
|
setTimeout(() => location.reload(), 500)
|
||||||
} else {
|
} else {
|
||||||
|
// 如果当前订单没有过期,继续等待订单的下一个状态
|
||||||
|
if (activeOrderNo.value === orderNo) {
|
||||||
queryOrder(orderNo)
|
queryOrder(orderNo)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
ElMessage.error("查询支付状态失败:" + e.message)
|
ElMessage.error("查询支付状态失败:" + e.message)
|
||||||
})
|
})
|
||||||
@ -225,9 +251,14 @@ const logout = function () {
|
|||||||
|
|
||||||
.el-dialog {
|
.el-dialog {
|
||||||
.el-dialog__body {
|
.el-dialog__body {
|
||||||
padding-top 0
|
padding-top 10px
|
||||||
|
|
||||||
.pay-container {
|
.pay-container {
|
||||||
|
.count-down {
|
||||||
|
display flex
|
||||||
|
justify-content center
|
||||||
|
}
|
||||||
|
|
||||||
.pay-qrcode {
|
.pay-qrcode {
|
||||||
display flex
|
display flex
|
||||||
justify-content center
|
justify-content center
|
||||||
|
@ -103,6 +103,23 @@
|
|||||||
</el-icon>
|
</el-icon>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="订单超时时间" prop="order_pay_timeout">
|
||||||
|
<div class="tip-input">
|
||||||
|
<el-input v-model.number="system['order_pay_timeout']" placeholder="单位:秒"/>
|
||||||
|
<div class="info">
|
||||||
|
<el-tooltip
|
||||||
|
effect="dark"
|
||||||
|
content="系统会定期清理超时未支付的订单<br/>默认值:900秒"
|
||||||
|
raw-content
|
||||||
|
placement="right"
|
||||||
|
>
|
||||||
|
<el-icon>
|
||||||
|
<InfoFilled/>
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="默认AI模型" prop="default_models">
|
<el-form-item label="默认AI模型" prop="default_models">
|
||||||
<template #default>
|
<template #default>
|
||||||
<div class="tip-input">
|
<div class="tip-input">
|
||||||
|
Loading…
Reference in New Issue
Block a user