2026-08-06 17:04:49 +08:00
|
|
|
package repository
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"database/sql"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
|
|
"cmautobuy/admin/model"
|
2026-08-10 02:21:31 +08:00
|
|
|
"github.com/go-sql-driver/mysql"
|
2026-08-06 17:04:49 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ErrIdempotencyConflict 表示同一个键提交了不同的内容。
|
|
|
|
|
//
|
|
|
|
|
// 这说明客户端弄错了——同一个键必须对应同一份内容。
|
|
|
|
|
// 内容真的变了,应该用新的 attempt_id 生成新的键。
|
2026-08-10 02:21:31 +08:00
|
|
|
var (
|
|
|
|
|
ErrIdempotencyConflict = errors.New("相同幂等键提交了不同内容")
|
|
|
|
|
ErrIdempotencyAlreadySaved = errors.New("幂等键已被并发请求保存")
|
|
|
|
|
)
|
2026-08-06 17:04:49 +08:00
|
|
|
|
|
|
|
|
// HashRequest 计算请求体的哈希,用来判断"同一个键"配的是不是"同一份内容"。
|
|
|
|
|
func HashRequest(body []byte) string {
|
|
|
|
|
sum := sha256.Sum256(body)
|
|
|
|
|
return hex.EncodeToString(sum[:])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LookupIdempotent 查这个键是不是已经处理过。
|
|
|
|
|
//
|
|
|
|
|
// 三种结果:
|
|
|
|
|
// - 处理过且内容一致 → 返回上次的响应体和 true,**调用方直接原样返回,不要重复落库**
|
|
|
|
|
// - 处理过但内容不同 → 返回 ErrIdempotencyConflict,调用方回 409
|
|
|
|
|
// - 没处理过 → 返回 ("", false, nil),调用方正常处理
|
|
|
|
|
func LookupIdempotent(q Execer, key, requestHash string) (string, bool, error) {
|
|
|
|
|
var storedHash, storedBody string
|
|
|
|
|
err := q.QueryRow(
|
2026-08-10 02:21:31 +08:00
|
|
|
"SELECT request_hash, response_body FROM idempotency_keys WHERE `key` = ?",
|
2026-08-06 17:04:49 +08:00
|
|
|
key).Scan(&storedHash, &storedBody)
|
|
|
|
|
|
|
|
|
|
if err == sql.ErrNoRows {
|
|
|
|
|
return "", false, nil
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", false, fmt.Errorf("查询幂等键失败: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if storedHash != requestHash {
|
|
|
|
|
return "", false, ErrIdempotencyConflict
|
|
|
|
|
}
|
|
|
|
|
return storedBody, true, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SaveIdempotent 记下这个键处理过了,以及当时返回了什么。
|
|
|
|
|
//
|
|
|
|
|
// `[必须]` 必须和业务写入在**同一个事务**里。
|
|
|
|
|
// 分开写的话,业务写成功但幂等记录没写上,客户端重试就会被重复处理。
|
|
|
|
|
func SaveIdempotent(q Execer, key, requestHash, responseBody string) error {
|
|
|
|
|
_, err := q.Exec(
|
2026-08-10 02:21:31 +08:00
|
|
|
"INSERT INTO idempotency_keys (`key`, request_hash, response_body, created_at)\n"+
|
|
|
|
|
" VALUES (?, ?, ?, ?)",
|
2026-08-06 17:04:49 +08:00
|
|
|
key, requestHash, responseBody, model.NowISO())
|
|
|
|
|
if err != nil {
|
2026-08-10 02:21:31 +08:00
|
|
|
var mysqlErr *mysql.MySQLError
|
|
|
|
|
if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
|
|
|
|
|
return ErrIdempotencyAlreadySaved
|
|
|
|
|
}
|
2026-08-06 17:04:49 +08:00
|
|
|
return fmt.Errorf("保存幂等键失败: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|