// 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 } }