14--支付

实现

项目的初衷是用于学习后端项目框架,但一个完整的商城项目还是需要支付流程

所以我在这里选择了模拟支付回调的方法来进行实现,实际应用这个项目的时候千万不能留下模拟回调的API

在真正的企业项目应用场景,需要在这部分加入真实的电子支付(微信/支付宝)SDK以及支付密钥等配置

流程设计

  1. 支付回调接口 — 模拟第三方支付(微信/支付宝)通知已付款
  2. 订单状态推进 — 待支付 → 已支付 → (可退款 → 已退款)
  3. MQ 延时消息 — 超时未支付自动取消订单
  4. 取消/退款接口 — 用户主动取消 + 管理员退款

架构设计

用户下单 → 订单状态=待支付
    │
    ├── 模拟支付回调 → 订单状态=已支付
    │
    ├── 用户取消   → 订单状态=已取消
    │
    └── 超时未付(MQ 延时队列)→ 订单状态=已取消

超时订单取消设计

有两种实现方式

  1. RabbitMQ死信队列+延时插件/双队列
  2. 定时任务轮询

生产环境中方案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"
}
posted @ 2026-04-17 17:47  Chuan81  阅读(29)  评论(0)    收藏  举报