14--支付
实现
项目的初衷是用于学习后端项目框架,但一个完整的商城项目还是需要支付流程
所以我在这里选择了模拟支付回调的方法来进行实现,实际应用这个项目的时候千万不能留下模拟回调的API
在真正的企业项目应用场景,需要在这部分加入真实的电子支付(微信/支付宝)SDK以及支付密钥等配置
流程设计
- 支付回调接口 — 模拟第三方支付(微信/支付宝)通知已付款
- 订单状态推进 — 待支付 → 已支付 → (可退款 → 已退款)
- MQ 延时消息 — 超时未支付自动取消订单
- 取消/退款接口 — 用户主动取消 + 管理员退款
架构设计
用户下单 → 订单状态=待支付
│
├── 模拟支付回调 → 订单状态=已支付
│
├── 用户取消 → 订单状态=已取消
│
└── 超时未付(MQ 延时队列)→ 订单状态=已取消
超时订单取消设计
有两种实现方式
- RabbitMQ死信队列+延时插件/双队列
- 定时任务轮询
生产环境中方案1肯定是更好的解决方法,方案2的问题是延迟和资源浪费
不过我这里还是选择了方案2,因为Go语言的最大特色就是goroutine,依靠协程我们可以快速写出可用的逻辑,可以很大程度地体现Go语言的魅力之处
一. DTO结构体与查询超时订单
DTO
internal/api/handlers/dto/order.go
// MockPayCallBackRequest 模拟支付回调请求
type MockPayCallBackRequest struct {
OrderId uint `json:"order_id" binding:"required"`
}
// CancelOrderRequest 取消订单请求
type CancelOrderRequest struct {
OrderId uint `json:"order_id" binding:"required"`
}
// RefundOrderRequest 管理员退款请求
type RefundOrderRequest struct {
OrderId uint `json:"order_id" binding:"required"`
}
查询超时订单
internal/repository/order.go
// GetUnpaidOrdersOlderThan 查询超时未支付的订单
func GetUnpaidOrdersOlderThan(threshold time.Duration) ([]*model.Order, error) {
var orders []*model.Order
cutoff := time.Now().Add(-threshold)
err := DB.Where("status = ? AND updated_at < ?", 0, cutoff).Find(&orders).Error
return orders, err
}
二. 新增订单Service
internal/service/order.go
// PayCallBack 模拟订单支付回调
func PayCallBack(orderId uint) error {
// 校验订单是否存在
order, err := repository.GetOrderByID(orderId)
if err != nil {
return errors.New("order not found")
}
// 更新订单状态为已支付
if order.Status != 0 {
return errors.New("order already paid")
}
return repository.UpdateOrderStatus(orderId, 1) // 1: 已支付
}
// CancelOrder 取消订单
func CancelOrder(orderId uint, userId uint) error {
// 校验订单是否存在
order, err := repository.GetOrderByID(orderId)
if err != nil {
return errors.New("order not found")
}
// 校验订单是否属于当前用户
if order.UserId != userId {
return errors.New("permisson denied")
}
// 校验订单状态是否为未支付
if order.Status != 0 {
return errors.New("only unpay order can be canceled")
}
// 更新订单状态为已取消
return repository.UpdateOrderStatus(orderId, 2) // 2: 已取消
}
// RefundOrder 管理员退款(admin only)
func RefundOrder(orderId uint) error {
order, err := repository.GetOrderByID(orderId)
if err != nil {
return errors.New("order not found")
}
if order.Status != 1 {
return errors.New("only paid order can be refunded")
}
// 退款应退库存,这里暂不实现
return repository.UpdateOrderStatus(orderId, 3) // 3: 已退款
}
// TimeoutCancelJob 定时任务:每分钟扫一次超时未付款订单并取消
func TimeoutCancelJob() {
orders, err := repository.GetUnpaidOrdersOlderThan(time.Minute)
if err != nil {
return
}
for _, order := range orders {
if err := repository.UpdateOrderStatus(order.OrderId, 2); err != nil {
slog.Error("cancel order failed", "order_id", order.OrderId, "error", err.Error())
} else {
slog.Info("cancel order success", "order_id", order.OrderId)
}
}
}
代码逻辑都不困难,只是需要实现的Service有些多导致代码量有点大
三. 新增handler
internal/api/handlers
// MockPayCallBack 模拟支付回调接口
func MockPayCallBack(c *gin.Context) {
var req dto.MockPayCallBackRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, 400, "invalid request")
return
}
err := service.PayCallBack(req.OrderId)
if err != nil {
response.Fail(c, 500, err.Error())
return
}
response.Sucess(c, gin.H{"message": "payment confirmed"})
}
// CancelOrder 取消订单
func CancelOrder(c *gin.Context) {
var req dto.CancelOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, 400, "invalid request")
return
}
uid, _ := c.Get("uid")
err := service.CancelOrder(req.OrderId, uid.(uint))
if err != nil {
response.Fail(c, 500, err.Error())
return
}
response.Sucess(c, gin.H{"message": "order canceled"})
}
// RefundOrder 管理员退款
func RefundOrder(c *gin.Context) {
var req dto.RefundOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, 400, "invalid request")
return
}
err := service.RefundOrder(req.OrderId)
if err != nil {
response.Fail(c, 500, err.Error())
return
}
response.Sucess(c, gin.H{"message": "refund success"})
}
四. API路由和定时任务
cmd/secgo-mall/main.go
// ... 原代码 ...
func main() {
// ... MQ初始化 ...
// 定时任务:每分钟扫一次超时未付款订单并取消
go func() {
ticker := time.NewTicker(1 * time.Minute)
for range ticker.C {
// 定时任务:每分钟扫一次超时未付款订单并取消
service.TimeoutCancelJob()
}
}()
// ... Gin路由创建 ...
protected.Use(middleware.JWTAuth()) // 使用JWT鉴权中间件
{
// ... 其他路由 ...
// 取消订单
protected.POST("/order/cancel", handlers.CancelOrder)
// 管理员退款
protected.POST("/order/refund", middleware.RequireAdmin(), handlers.RefundOrder)
// 模拟支付回调
protected.POST("/pay/callback", handlers.MockPayCallBack)
}
}
// ... 原代码 ...
测试
超时检测
在先前的并发测试中我们早已生成过大量的未支付订单,所以在本次启动后定时取消任务便一次性把大量的订单取消了
{"time":"2026-04-16T22:11:12.1602708+08:00","level":"INFO","msg":"cancel order success","order_id":1}
{"time":"2026-04-16T22:11:12.1681148+08:00","level":"INFO","msg":"cancel order success","order_id":2}
{"time":"2026-04-16T22:11:12.1701911+08:00","level":"INFO","msg":"cancel order success","order_id":3}
{"time":"2026-04-16T22:11:12.1754672+08:00","level":"INFO","msg":"cancel order success","order_id":4}
{"time":"2026-04-16T22:11:12.1791053+08:00","level":"INFO","msg":"cancel order success","order_id":5}
{"time":"2026-04-16T22:11:12.1838499+08:00","level":"INFO","msg":"cancel order success","order_id":6}
{"time":"2026-04-16T22:11:12.1874092+08:00","level":"INFO","msg":"cancel order success","order_id":7}
# ...
{"time":"2026-04-16T22:11:12.6306196+08:00","level":"INFO","msg":"cancel order success","order_id":156}
{"time":"2026-04-16T22:11:12.6335715+08:00","level":"INFO","msg":"cancel order success","order_id":157}
一共取消了157条订单
下单并验证支付回调
下单
我们这里使用guest账号来进行下单,先前创建的ID3的抢购活动还处于进行中,正好用来进行测试
curl -X POST http://localhost:8080/api/seckill/purchase \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <TOKEN>" \
-d '{"seckill_id":3}'
我这里还是使用Postman
{
"code": 0,
"message": "success",
"data": {
"message": "purchase success, order is being processed"
}
}
订单号
查询一下订单号:
curl http://localhost:8080/api/order/list -H "Authorization: Bearer <TOKEN>"
{
"code": 0,
"message": "success",
"data": [
{
"order_id": 158,
"user_id": 2,
"product_id": 2,
"seckill_id": 3,
"amount": 1,
"status": 2,
"created_at": "2026-04-17T00:16:06.096+08:00",
"updated_at": "2026-04-17T00:17:37.826+08:00"
}
]
}
可以看到因为操作较慢,订单已经自动取消了
模拟支付回调
我们需要在创建订单后一分钟内快速完成支付回调
这里先把命令准备好(158之后是159)
curl -X POST http://localhost:8080/api/pay/callback \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <TOKEN>" \
-d '{"order_id":159}'
{
"code": 0,
"message": "success",
"data": {
"message": "payment confirmed"
}
}
查看一下订单详情
{
"order_id": 159,
"user_id": 2,
"product_id": 2,
"seckill_id": 3,
"amount": 1,
"status": 1,
"created_at": "2026-04-17T00:34:49.177+08:00",
"updated_at": "2026-04-17T00:35:09.687+08:00"
}
status转为1即为已支付
用户取消订单
curl -X POST http://localhost:8080/api/order/cancel \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <TOKEN>" \
-d '{"order_id":160}'
{
"code": 0,
"message": "success",
"data": {
"message": "order canceled"
}
}
订单详情
{
"order_id": 160,
"user_id": 2,
"product_id": 2,
"seckill_id": 3,
"amount": 1,
"status": 2,
"created_at": "2026-04-17T00:40:34.984+08:00",
"updated_at": "2026-04-17T00:41:15.003+08:00"
}
管理员退款
我们登录admin账户然后把159订单退款即可
curl -X POST http://localhost:8080/api/order/refund \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <TOKEN>" \
-d '{"order_id":159}'
{
"code": 0,
"message": "success",
"data": {
"message": "refund success"
}
}
回到guest查看订单详情
{
"order_id": 159,
"user_id": 2,
"product_id": 2,
"seckill_id": 3,
"amount": 1,
"status": 3,
"created_at": "2026-04-17T00:34:49.177+08:00",
"updated_at": "2026-04-17T00:46:33.139+08:00"
},
订单status=3即为已退款
边界情况
| 操作 | 对象 | 预期错误信息 |
|---|---|---|
| 取消已支付订单 | order_id=1 | only unpay order can be canceled |
| 支付已取消订单回调 | order_id=2 | order already paid |
| 退款未支付订单 | order_id=2 | only paid order can be refunded |
| 退款已退款订单 | order_id=1 | only paid order can be refunded |
// 1
{
"code": 500,
"message": "only unpay order can be canceled"
}
// 2
{
"code": 500,
"message": "order already paid"
}
// 3
{
"code": 500,
"message": "only paid order can be refunded"
}
// 4
{
"code": 500,
"message": "only paid order can be refunded"
}
普通用户取消他人订单(order_id=161 属于guest用户):
# 注册 test 用户,登录获得新TOKEN
# test 尝试取消 order_id=161
curl -X POST http://localhost:8080/api/order/cancel \
-H "Authorization: Bearer <TOKEN2>" \
-d '{"order_id":161}'
预期:permisson denied
{
"code": 500,
"message": "permisson denied"
}

浙公网安备 33010602011771号