All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m20s
125 lines
3.5 KiB
Go
125 lines
3.5 KiB
Go
// Package idempotency 提供创建类命令的幂等身份与请求指纹公共契约。
|
||
package idempotency
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"reflect"
|
||
"strings"
|
||
|
||
"github.com/bytedance/sonic"
|
||
)
|
||
|
||
const (
|
||
// FingerprintAlgorithmV1 是请求指纹算法的首个稳定版本。
|
||
FingerprintAlgorithmV1 = "sha256-canonical-json-v1"
|
||
)
|
||
|
||
// Scope 表示请求 ID 的幂等作用域。
|
||
type Scope struct {
|
||
Subject string `json:"subject"`
|
||
Operation string `json:"operation"`
|
||
}
|
||
|
||
// FingerprintValue 表示带算法版本的请求指纹。
|
||
type FingerprintValue struct {
|
||
Algorithm string `json:"algorithm"`
|
||
Value string `json:"value"`
|
||
}
|
||
|
||
// Record 表示业务模块自行持久化的幂等事实最小投影。
|
||
type Record struct {
|
||
Scope Scope `json:"scope"`
|
||
RequestID string `json:"request_id"`
|
||
Fingerprint FingerprintValue `json:"fingerprint"`
|
||
}
|
||
|
||
// ReplayKind 表示已有事实与本次命令的关系。
|
||
type ReplayKind string
|
||
|
||
const (
|
||
// ReplaySame 表示相同命令重放,应返回原业务结果。
|
||
ReplaySame ReplayKind = "same"
|
||
// ReplayConflict 表示同作用域、同请求 ID 携带了不同业务内容。
|
||
ReplayConflict ReplayKind = "conflict"
|
||
// ReplayUnrelated 表示不是同一幂等作用域内的命令。
|
||
ReplayUnrelated ReplayKind = "unrelated"
|
||
)
|
||
|
||
var volatileFieldNames = map[string]struct{}{
|
||
"authorization": {},
|
||
"cookie": {},
|
||
"nonce": {},
|
||
"sign": {},
|
||
"signature": {},
|
||
"timestamp": {},
|
||
"token": {},
|
||
}
|
||
|
||
// Fingerprint 对影响业务结果的规范化字段计算稳定指纹。
|
||
//
|
||
// 调用方仍应优先传入专用命令 DTO;本函数会递归排除约定的易变传输字段,
|
||
// 并去除字符串首尾空白,避免签名、令牌或对象字段顺序污染业务身份。
|
||
func Fingerprint(command any) (FingerprintValue, error) {
|
||
raw, err := sonic.ConfigStd.Marshal(command)
|
||
if err != nil {
|
||
return FingerprintValue{}, err
|
||
}
|
||
var value any
|
||
if err := sonic.Unmarshal(raw, &value); err != nil {
|
||
return FingerprintValue{}, err
|
||
}
|
||
normalized := normalize(value)
|
||
canonical, err := sonic.ConfigStd.Marshal(normalized)
|
||
if err != nil {
|
||
return FingerprintValue{}, err
|
||
}
|
||
sum := sha256.Sum256(canonical)
|
||
return FingerprintValue{
|
||
Algorithm: FingerprintAlgorithmV1,
|
||
Value: hex.EncodeToString(sum[:]),
|
||
}, nil
|
||
}
|
||
|
||
// ValidateScope 校验幂等作用域和稳定请求 ID 是否完整。
|
||
func ValidateScope(scope Scope, requestID string) bool {
|
||
return strings.TrimSpace(scope.Subject) != "" &&
|
||
strings.TrimSpace(scope.Operation) != "" &&
|
||
strings.TrimSpace(requestID) != ""
|
||
}
|
||
|
||
// Classify 比较已有幂等事实与本次命令。
|
||
func Classify(existing Record, scope Scope, requestID string, fingerprint FingerprintValue) ReplayKind {
|
||
if existing.Scope != scope || existing.RequestID != requestID {
|
||
return ReplayUnrelated
|
||
}
|
||
if reflect.DeepEqual(existing.Fingerprint, fingerprint) {
|
||
return ReplaySame
|
||
}
|
||
return ReplayConflict
|
||
}
|
||
|
||
func normalize(value any) any {
|
||
switch typed := value.(type) {
|
||
case map[string]any:
|
||
result := make(map[string]any, len(typed))
|
||
for key, item := range typed {
|
||
if _, volatile := volatileFieldNames[strings.ToLower(strings.TrimSpace(key))]; volatile {
|
||
continue
|
||
}
|
||
result[key] = normalize(item)
|
||
}
|
||
return result
|
||
case []any:
|
||
result := make([]any, len(typed))
|
||
for index, item := range typed {
|
||
result[index] = normalize(item)
|
||
}
|
||
return result
|
||
case string:
|
||
return strings.TrimSpace(typed)
|
||
default:
|
||
return value
|
||
}
|
||
}
|