今天闲逛的时候,看到掘金上面有一个订单30分钟未支付,自动取消的一个需求。这个需求平时也会在美团的外卖下单、拼多多等下单的时候可以看到,但是之前一直也没有思考过,怎么去实现这个需求。所以,今天就把这个需求实现一遍。
准备
- 使用语言 golang
了解需求
这个需求是属于开发中的延时任务的一个功能。平常开发中会遇到的生成了订单但是没有支付,30分钟以后订单就会自动关闭。
思路
使用数据库轮序技术。该方案通常是在小型项目中使用,即通过开启一个线程定时的去扫描数据库,通过订单时间来判断是否有超时的订单,然后进行修改和删除等操作。
需要考虑的方面
- 定义订单超时时间:需要确定订单超时时间,例如订单30分钟未支付则自动取消。
- 监听订单状态变化:需要在程序中监听订单状态变化,例如订单状态为待支付,且下单时间超过订单超时时间,则自动取消订单。
- 更新订单状态:需要在程序中更新订单状态,将订单状态从待支付修改为取消。
简单思路
- 在数据库中创建订单表,包含订单id、下单时间和订单状态等字段。
- 创建一个goroutine用于监听订单状态变化,每隔一段时间查询数据库中所有待支付订单,判断是否超时,如果超时则将订单状态修改为取消。
- 在程序启动时启动该goroutine,保持一直运行。
- 在用户下单时,在数据库中插入新订单记录,将订单状态设置为待支付。
- 当用户支付成功后,在数据库中将订单状态修改为已支付。
技术实现
package main
import (
"database/sql"
"fmt"
"time"
_ "github.com/go-sql-driver/mysql"
)
type Order struct {
ID int
CreatedAt time.Time
Status string
PayTimeout time.Duration
}
func main() {
// 连接数据库
db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local")
if err != nil {
panic(any(err))
}
defer db.Close()
// 创建订单表
if _, err := db.Exec(`
CREATE TABLE IF NOT EXISTS orders (
id int(11) NOT NULL AUTO_INCREMENT,
created_at datetime NOT NULL,
status varchar(10) NOT NULL DEFAULT '',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`); err != nil {
panic(any(err))
}
// 定义订单超时时间为30分钟
// payTimeout := 30 * time.Minute
payTimeout := 1 * time.Minute
// 启动goroutine监听订单状态变化
go func() {
for {
// 查询所有待支付订单
rows, err := db.Query("SELECT id, created_at FROM orders WHERE status = 'pending'")
if err != nil {
fmt.Println("Failed to query orders:", err)
continue
}
// 遍历订单记录
for rows.Next() {
var orderID int
var createdAt time.Time
if err := rows.Scan(&orderID, &createdAt); err != nil {
fmt.Println("Failed to scan order:", err)
continue
}
// 判断订单是否超时
if time.Since(createdAt) >= payTimeout {
// 更新订单状态为取消
if _, err := db.Exec("UPDATE orders SET status = 'cancelled' WHERE id = ?", orderID); err != nil {
fmt.Println("Failed to update order:", err)
continue
}
fmt.Printf("Order %d cancelled\n", orderID)
}
}
// 关闭rows
rows.Close()
// 休眠5秒钟
time.Sleep(5 * time.Second)
}
}()
// 用户下单
orderID := placeOrder(db)
// 模拟支付
fmt.Println("Paying for order", orderID)
time.Sleep(10 * time.Minute)
// 查询订单状态
order := getOrder(db, orderID)
fmt.Println("Order status:", order.Status)
}
// 下单
func placeOrder(db *sql.DB) int {
now := time.Now()
// 插入订单记录
result, err := db.Exec("INSERT INTO orders (created_at, status) VALUES (?, 'pending')", now)
if err != nil {
panic(any(err))
}
orderID, err := result.LastInsertId()
if err != nil {
panic(any(err))
}
fmt.Println("Placed order", orderID)
return int(orderID)
}
// 查询订单
func getOrder(db *sql.DB, orderID int) *Order {
row := db.QueryRow("SELECT id, created_at, status FROM orders WHERE id = ?", orderID)
var order Order
if err := row.Scan(&order.ID, &order.CreatedAt, &order.Status); err != nil {
panic(any(err))
}
return &order
}
其他方案
使用 Redis 和 Golang 实现订单 30 分钟未支付自动取消的功能(后期会补充这边文章)