实现七月迭代公共技术基础
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m20s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m20s
This commit is contained in:
124
pkg/idempotency/fingerprint.go
Normal file
124
pkg/idempotency/fingerprint.go
Normal file
@@ -0,0 +1,124 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user