package repository import ( "crypto/sha256" "database/sql" "encoding/hex" "errors" "fmt" "cmautobuy/admin/model" ) // ErrIdempotencyConflict 表示同一个键提交了不同的内容。 // // 这说明客户端弄错了——同一个键必须对应同一份内容。 // 内容真的变了,应该用新的 attempt_id 生成新的键。 var ErrIdempotencyConflict = errors.New("相同幂等键提交了不同内容") // 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( `SELECT request_hash, response_body FROM idempotency_keys WHERE key = ?`, 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( `INSERT INTO idempotency_keys (key, request_hash, response_body, created_at) VALUES (?, ?, ?, ?)`, key, requestHash, responseBody, model.NowISO()) if err != nil { return fmt.Errorf("保存幂等键失败: %w", err) } return nil }