Files
go-trustlog/api/model/operation.go
ryan 88f80ffa5e feat: 新增数据库持久化模块(Persistence),实现 Cursor + Retry 双层架构
## 核心功能

### 1. 数据库持久化支持
- 新增完整的 Persistence 模块 (api/persistence/)
- 支持三种持久化策略:
  * StrategyDBOnly - 仅落库,不存证
  * StrategyDBAndTrustlog - 既落库又存证(推荐)
  * StrategyTrustlogOnly - 仅存证,不落库
- 支持多数据库:PostgreSQL, MySQL, SQLite

### 2. Cursor + Retry 双层架构
- CursorWorker:第一道防线,快速发现新记录并尝试存证
  * 增量扫描 operation 表(基于时间戳游标)
  * 默认 10 秒扫描间隔,批量处理 100 条
  * 成功更新状态,失败转入重试队列
- RetryWorker:第二道防线,处理失败记录
  * 指数退避重试(1m → 2m → 4m → 8m → 16m)
  * 默认最多重试 5 次
  * 超限自动标记为死信

### 3. 数据库表设计
- operation 表:存储操作记录,支持可空 IP 字段
- trustlog_cursor 表:Key-Value 模式,支持多游标
- trustlog_retry 表:重试队列,支持指数退避

### 4. 异步最终一致性
- 应用调用立即返回(仅落库)
- CursorWorker 异步扫描并存证
- RetryWorker 保障失败重试
- 完整的监控和死信处理机制

## 修改文件

### 核心代码(11个文件)
- api/persistence/cursor_worker.go - Cursor 工作器(新增)
- api/persistence/repository.go - 数据仓储层(新增)
- api/persistence/schema.go - 数据库 Schema(新增)
- api/persistence/strategy.go - 策略管理器(新增)
- api/persistence/client.go - 客户端封装(新增)
- api/persistence/retry_worker.go - Retry 工作器(新增)
- api/persistence/config.go - 配置管理(新增)

### 修复内部包引用(5个文件)
- api/adapter/publisher.go - 修复 internal 包引用
- api/adapter/subscriber.go - 修复 internal 包引用
- api/model/envelope.go - 修复 internal 包引用
- api/model/operation.go - 修复 internal 包引用
- api/model/record.go - 修复 internal 包引用

### 单元测试(8个文件)
- api/persistence/*_test.go - 完整的单元测试
- 测试覆盖率:28.5%
- 测试通过率:49/49 (100%)

### SQL 脚本(4个文件)
- api/persistence/sql/postgresql.sql - PostgreSQL 建表脚本
- api/persistence/sql/mysql.sql - MySQL 建表脚本
- api/persistence/sql/sqlite.sql - SQLite 建表脚本
- api/persistence/sql/test_data.sql - 测试数据

### 文档(2个文件)
- README.md - 更新主文档,新增 Persistence 使用指南
- api/persistence/README.md - 完整的 Persistence 文档
- api/persistence/sql/README.md - SQL 脚本说明

## 技术亮点

1. **充分利用 Cursor 游标表**
   - 作为任务发现队列,非简单的位置记录
   - Key-Value 模式,支持多游标并发扫描
   - 时间戳天然有序,增量扫描高效

2. **双层保障机制**
   - Cursor:正常流程,快速处理
   - Retry:异常流程,可靠重试
   - 职责分离,监控清晰

3. **可空 IP 字段支持**
   - ClientIP 和 ServerIP 使用 *string 类型
   - 支持 NULL 值,符合数据库最佳实践
   - 使用 sql.NullString 正确处理

4. **完整的监控支持**
   - 未存证记录数监控
   - Cursor 延迟监控
   - 重试队列长度监控
   - 死信队列监控

## 测试结果

-  单元测试:49/49 通过 (100%)
-  代码覆盖率:28.5%
-  编译状态:无错误
-  支持数据库:PostgreSQL, MySQL, SQLite

## Breaking Changes

无破坏性变更。Persistence 模块作为可选功能,不影响现有代码。

## 版本信息

- 版本:v2.1.0
- Go 版本要求:1.21+
- 更新日期:2025-12-23
2025-12-23 18:59:43 +08:00

582 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package model
import (
"context"
"errors"
"fmt"
"strings"
"time"
"go.yandata.net/iod/iod/go-trustlog/api/logger"
"go.yandata.net/iod/iod/go-trustlog/internal/helpers"
)
//
// ===== 操作来源类型 =====
//
// Source 表示操作来源用于区分不同系统模块IRP、DOIP
type Source string
const (
OpSourceIRP Source = "IRP"
OpSourceDOIP Source = "DOIP"
)
//
// ===== 操作类型枚举 =====
//
// Type 表示操作的具体类型。
type Type string
// DOIP 操作类型枚举。
const (
OpTypeHello Type = "Hello"
OpTypeRetrieve Type = "Retrieve"
OpTypeCreate Type = "Create"
OpTypeDelete Type = "Delete"
OpTypeUpdate Type = "Update"
OpTypeSearch Type = "Search"
OpTypeListOperations Type = "ListOperations"
)
// IRP 操作类型枚举。
const (
OpTypeOCReserved Type = "OC_RESERVED"
OpTypeOCResolution Type = "OC_RESOLUTION"
OpTypeOCGetSiteInfo Type = "OC_GET_SITEINFO"
OpTypeOCCreateHandle Type = "OC_CREATE_HANDLE"
OpTypeOCDeleteHandle Type = "OC_DELETE_HANDLE"
OpTypeOCAddValue Type = "OC_ADD_VALUE"
OpTypeOCRemoveValue Type = "OC_REMOVE_VALUE"
OpTypeOCModifyValue Type = "OC_MODIFY_VALUE"
OpTypeOCListHandle Type = "OC_LIST_HANDLE"
OpTypeOCListNA Type = "OC_LIST_NA"
OpTypeOCResolutionDOID Type = "OC_RESOLUTION_DOID"
OpTypeOCCreateDOID Type = "OC_CREATE_DOID"
OpTypeOCDeleteDOID Type = "OC_DELETE_DOID"
OpTypeOCUpdateDOID Type = "OC_UPDATE_DOID"
OpTypeOCBatchCreateDOID Type = "OC_BATCH_CREATE_DOID"
OpTypeOCResolutionDOIDRecursive Type = "OC_RESOLUTION_DOID_RECURSIVE"
OpTypeOCGetUsers Type = "OC_GET_USERS"
OpTypeOCGetRepos Type = "OC_GET_REPOS"
OpTypeOCVerifyIRS Type = "OC_VERIFY_IRS"
OpTypeOCResolveGRS Type = "OC_RESOLVE_GRS"
OpTypeOCCreateOrgGRS Type = "OC_CREATE_ORG_GRS"
OpTypeOCUpdateOrgGRS Type = "OC_UPDATE_ORG_GRS"
OpTypeOCDeleteOrgGRS Type = "OC_DELETE_ORG_GRS"
OpTypeOCSyncOrgIRSParent Type = "OC_SYNC_ORG_IRS_PARENT"
OpTypeOCUpdateOrgIRSParent Type = "OC_UPDATE_ORG_IRS_PARENT"
OpTypeOCDeleteOrgIRSParent Type = "OC_DELETE_ORG_IRS_PARENT"
OpTypeOCChallengeResponse Type = "OC_CHALLENGE_RESPONSE"
OpTypeOCVerifyChallenge Type = "OC_VERIFY_CHALLENGE"
OpTypeOCSessionSetup Type = "OC_SESSION_SETUP"
OpTypeOCSessionTerminate Type = "OC_SESSION_TERMINATE"
OpTypeOCSessionExchangeKey Type = "OC_SESSION_EXCHANGEKEY"
OpTypeOCVerifyRouter Type = "OC_VERIFY_ROUTER"
OpTypeOCQueryRouter Type = "OC_QUERY_ROUTER"
)
//
// ===== 操作类型检索工具 =====
//
// allOpTypes 存储不同来源的操作类型列表,用于快速查找和验证。
//
//nolint:gochecknoglobals // 全局常量映射用于操作类型查找
var allOpTypes = map[Source][]Type{
OpSourceDOIP: {
OpTypeHello, OpTypeRetrieve, OpTypeCreate,
OpTypeDelete, OpTypeUpdate, OpTypeSearch,
OpTypeListOperations,
},
OpSourceIRP: {
OpTypeOCReserved, OpTypeOCResolution, OpTypeOCGetSiteInfo,
OpTypeOCCreateHandle, OpTypeOCDeleteHandle, OpTypeOCAddValue,
OpTypeOCRemoveValue, OpTypeOCModifyValue, OpTypeOCListHandle,
OpTypeOCListNA, OpTypeOCResolutionDOID, OpTypeOCCreateDOID,
OpTypeOCDeleteDOID, OpTypeOCUpdateDOID, OpTypeOCBatchCreateDOID,
OpTypeOCResolutionDOIDRecursive, OpTypeOCGetUsers, OpTypeOCGetRepos,
OpTypeOCVerifyIRS, OpTypeOCResolveGRS, OpTypeOCCreateOrgGRS,
OpTypeOCUpdateOrgGRS, OpTypeOCDeleteOrgGRS, OpTypeOCSyncOrgIRSParent,
OpTypeOCUpdateOrgIRSParent, OpTypeOCDeleteOrgIRSParent,
OpTypeOCChallengeResponse, OpTypeOCVerifyChallenge,
OpTypeOCSessionSetup, OpTypeOCSessionTerminate,
OpTypeOCSessionExchangeKey, OpTypeOCVerifyRouter, OpTypeOCQueryRouter,
},
}
// GetOpTypesBySource 返回指定来源的可用操作类型列表。
func GetOpTypesBySource(source Source) []Type {
return allOpTypes[source]
}
// IsValidOpType 判断指定操作类型在给定来源下是否合法。
func IsValidOpType(source Source, opType Type) bool {
for _, t := range GetOpTypesBySource(source) {
if t == opType {
return true
}
}
return false
}
//
// ===== 操作记录结构 =====
//
// Operation 表示一次完整的操作记录。
// 用于记录系统中的操作行为,包含操作元数据、数据标识、操作者信息以及请求/响应的哈希值。
type Operation struct {
OpID string `json:"opId" validate:"max=32"`
Timestamp time.Time `json:"timestamp" validate:"required"`
OpSource Source `json:"opSource" validate:"required,oneof=IRP DOIP"`
OpType Type `json:"opType" validate:"required"`
DoPrefix string `json:"doPrefix" validate:"required,max=512"`
DoRepository string `json:"doRepository" validate:"required,max=512"`
Doid string `json:"doid" validate:"required,max=512"`
ProducerID string `json:"producerId" validate:"required,max=512"`
OpActor string `json:"opActor" validate:"max=64"`
RequestBodyHash *string `json:"requestBodyHash" validate:"omitempty,max=128"`
ResponseBodyHash *string `json:"responseBodyHash" validate:"omitempty,max=128"`
// ClientIP 客户端IP地址仅用于数据库持久化不参与存证哈希计算
ClientIP *string `json:"clientIp,omitempty" validate:"omitempty,max=32"`
// ServerIP 服务端IP地址仅用于数据库持久化不参与存证哈希计算
ServerIP *string `json:"serverIp,omitempty" validate:"omitempty,max=32"`
Ack func() bool `json:"-"`
Nack func() bool `json:"-"`
binary []byte
}
//
// ===== 构造函数 =====
//
// NewFullOperation 创建包含所有字段的完整 Operation。
// 自动完成哈希计算和字段校验,确保创建的 Operation 是完整且有效的。
func NewFullOperation(
opSource Source,
opType Type,
doPrefix, doRepository, doid string,
producerID string,
opActor string,
requestBody, responseBody interface{},
timestamp time.Time,
) (*Operation, error) {
log := logger.GetGlobalLogger()
log.Debug("Creating new full operation",
"opSource", opSource,
"opType", opType,
"doPrefix", doPrefix,
"doRepository", doRepository,
"doid", doid,
"producerID", producerID,
"opActor", opActor,
)
op := &Operation{
Timestamp: timestamp,
OpSource: opSource,
OpType: opType,
DoPrefix: doPrefix,
DoRepository: doRepository,
Doid: doid,
ProducerID: producerID,
OpActor: opActor,
}
log.Debug("Setting request body hash")
if err := op.RequestBodyFlexible(requestBody); err != nil {
log.Error("Failed to set request body hash",
"error", err,
)
return nil, err
}
log.Debug("Setting response body hash")
if err := op.ResponseBodyFlexible(responseBody); err != nil {
log.Error("Failed to set response body hash",
"error", err,
)
return nil, err
}
log.Debug("Checking and initializing operation")
if err := op.CheckAndInit(); err != nil {
log.Error("Failed to check and init operation",
"error", err,
)
return nil, err
}
log.Debug("Full operation created successfully",
"opID", op.OpID,
)
return op, nil
}
//
// ===== 接口实现 =====
//
func (o *Operation) Key() string {
return o.OpID
}
// OperationHashData 实现 HashData 接口,用于存储 Operation 的哈希计算结果。
type OperationHashData struct {
key string
hash string
}
func (o OperationHashData) Key() string {
return o.key
}
func (o OperationHashData) Hash() string {
return o.hash
}
func (o OperationHashData) Type() HashType {
return Sha256Simd
}
// DoHash 计算 Operation 的整体哈希值,用于数据完整性验证。
// 哈希基于序列化后的二进制数据计算,确保操作记录的不可篡改性。
func (o *Operation) DoHash(_ context.Context) (HashData, error) {
log := logger.GetGlobalLogger()
log.Debug("Computing hash for operation",
"opID", o.OpID,
)
hashTool := GetHashTool(Sha256Simd)
binary, err := o.MarshalBinary()
if err != nil {
log.Error("Failed to marshal operation for hash",
"error", err,
"opID", o.OpID,
)
return nil, fmt.Errorf("failed to marshal operation: %w", err)
}
log.Debug("Computing hash bytes",
"opID", o.OpID,
"binaryLength", len(binary),
)
hash, err := hashTool.HashBytes(binary)
if err != nil {
log.Error("Failed to compute hash",
"error", err,
"opID", o.OpID,
)
return nil, fmt.Errorf("failed to compute hash: %w", err)
}
log.Debug("Hash computed successfully",
"opID", o.OpID,
"hash", hash,
)
return OperationHashData{
key: o.OpID,
hash: hash,
}, nil
}
//
// ===== CBOR 序列化相关 =====
//
// operationData 用于 CBOR 序列化/反序列化的中间结构。
// 排除函数字段和缓存字段,仅包含可序列化的数据字段。
type operationData struct {
OpID *string `cbor:"opId"`
Timestamp *time.Time `cbor:"timestamp"`
OpSource *Source `cbor:"opSource"`
OpType *Type `cbor:"opType"`
DoPrefix *string `cbor:"doPrefix"`
DoRepository *string `cbor:"doRepository"`
Doid *string `cbor:"doid"`
ProducerID *string `cbor:"producerId"`
OpActor *string `cbor:"opActor"`
RequestBodyHash *string `cbor:"requestBodyHash"`
ResponseBodyHash *string `cbor:"responseBodyHash"`
}
// toOperationData 将 Operation 转换为 operationData用于序列化。
func (o *Operation) toOperationData() *operationData {
return &operationData{
OpID: &o.OpID,
Timestamp: &o.Timestamp,
OpSource: &o.OpSource,
OpType: &o.OpType,
DoPrefix: &o.DoPrefix,
DoRepository: &o.DoRepository,
Doid: &o.Doid,
ProducerID: &o.ProducerID,
OpActor: &o.OpActor,
RequestBodyHash: o.RequestBodyHash,
ResponseBodyHash: o.ResponseBodyHash,
}
}
// fromOperationData 从 operationData 填充 Operation用于反序列化。
func (o *Operation) fromOperationData(opData *operationData) {
if opData == nil {
return
}
if opData.OpID != nil {
o.OpID = *opData.OpID
}
if opData.Timestamp != nil {
o.Timestamp = *opData.Timestamp
}
if opData.OpSource != nil {
o.OpSource = *opData.OpSource
}
if opData.OpType != nil {
o.OpType = *opData.OpType
}
if opData.DoPrefix != nil {
o.DoPrefix = *opData.DoPrefix
}
if opData.DoRepository != nil {
o.DoRepository = *opData.DoRepository
}
if opData.Doid != nil {
o.Doid = *opData.Doid
}
if opData.ProducerID != nil {
o.ProducerID = *opData.ProducerID
}
if opData.OpActor != nil {
o.OpActor = *opData.OpActor
}
if opData.RequestBodyHash != nil {
hash := *opData.RequestBodyHash
o.RequestBodyHash = &hash
}
if opData.ResponseBodyHash != nil {
hash := *opData.ResponseBodyHash
o.ResponseBodyHash = &hash
}
}
// MarshalBinary 将 Operation 序列化为 CBOR 格式的二进制数据。
// 实现 encoding.BinaryMarshaler 接口。
// 使用 Canonical CBOR 编码确保序列化结果的一致性,使用缓存机制避免重复序列化。
func (o *Operation) MarshalBinary() ([]byte, error) {
log := logger.GetGlobalLogger()
log.Debug("Marshaling operation to CBOR binary",
"opID", o.OpID,
)
if o.binary != nil {
log.Debug("Using cached binary data",
"opID", o.OpID,
)
return o.binary, nil
}
opData := o.toOperationData()
log.Debug("Marshaling operation data to canonical CBOR",
"opID", o.OpID,
)
binary, err := helpers.MarshalCanonical(opData)
if err != nil {
log.Error("Failed to marshal operation to CBOR",
"error", err,
"opID", o.OpID,
)
return nil, fmt.Errorf("failed to marshal operation to CBOR: %w", err)
}
o.binary = binary
log.Debug("Operation marshaled successfully",
"opID", o.OpID,
"binaryLength", len(binary),
)
return binary, nil
}
// GetProducerID 返回 ProducerID实现 Trustlog 接口。
func (o *Operation) GetProducerID() string {
return o.ProducerID
}
// UnmarshalBinary 从 CBOR 格式的二进制数据反序列化为 Operation。
// 实现 encoding.BinaryUnmarshaler 接口。
func (o *Operation) UnmarshalBinary(data []byte) error {
log := logger.GetGlobalLogger()
log.Debug("Unmarshaling operation from CBOR binary",
"dataLength", len(data),
)
if len(data) == 0 {
log.Error("Data is empty")
return errors.New("data is empty")
}
opData := &operationData{}
log.Debug("Unmarshaling operation data from CBOR")
if err := helpers.Unmarshal(data, opData); err != nil {
log.Error("Failed to unmarshal operation from CBOR",
"error", err,
)
return fmt.Errorf("failed to unmarshal operation from CBOR: %w", err)
}
o.fromOperationData(opData)
o.binary = data
log.Debug("Operation unmarshaled successfully",
"opID", o.OpID,
)
return nil
}
//
// ===== 哈希设置方法 =====
//
// setBodyHashFlexible 根据输入数据类型计算哈希,支持 string 和 []byte。
// 使用固定的 Sha256Simd 算法。
// 如果输入为 nil 或空,则目标指针设置为 nil表示该字段未设置。
func (o *Operation) setBodyHashFlexible(data interface{}, target **string) error {
log := logger.GetGlobalLogger()
log.Debug("Setting body hash flexible",
"opID", o.OpID,
"dataType", fmt.Sprintf("%T", data),
)
if data == nil {
log.Debug("Data is nil, setting target to nil")
*target = nil
return nil
}
hashTool := GetHashTool(Sha256Simd)
var raw []byte
switch v := data.(type) {
case string:
if v == "" {
log.Debug("String data is empty, setting target to nil")
*target = nil
return nil
}
raw = []byte(v)
log.Debug("Converting string to bytes",
"stringLength", len(v),
)
case []byte:
if len(v) == 0 {
log.Debug("Byte data is empty, setting target to nil")
*target = nil
return nil
}
raw = v
log.Debug("Using byte data directly",
"byteLength", len(v),
)
default:
log.Error("Unsupported data type",
"dataType", fmt.Sprintf("%T", v),
)
return fmt.Errorf("unsupported data type %T", v)
}
log.Debug("Computing hash for body data",
"dataLength", len(raw),
)
hash, err := hashTool.HashBytes(raw)
if err != nil {
log.Error("Failed to compute hash",
"error", err,
)
return err
}
*target = &hash
log.Debug("Body hash set successfully",
"hash", hash,
)
return nil
}
// RequestBodyFlexible 设置请求体哈希值。
// 支持 string 和 []byte 类型nil 或空值会将 RequestBodyHash 设置为 nil。
func (o *Operation) RequestBodyFlexible(data interface{}) error {
return o.setBodyHashFlexible(data, &o.RequestBodyHash)
}
// ResponseBodyFlexible 设置响应体哈希值。
// 支持 string 和 []byte 类型nil 或空值会将 ResponseBodyHash 设置为 nil。
func (o *Operation) ResponseBodyFlexible(data interface{}) error {
return o.setBodyHashFlexible(data, &o.ResponseBodyHash)
}
//
// ===== 链式调用支持 =====
//
// WithRequestBody 设置请求体哈希并返回自身,支持链式调用。
func (o *Operation) WithRequestBody(data []byte) *Operation {
_ = o.RequestBodyFlexible(data)
return o
}
// WithResponseBody 设置响应体哈希并返回自身,支持链式调用。
func (o *Operation) WithResponseBody(data []byte) *Operation {
_ = o.ResponseBodyFlexible(data)
return o
}
//
// ===== 初始化与验证 =====
//
// CheckAndInit 校验并初始化 Operation。
// 自动填充缺失字段OpID、OpActor执行业务逻辑验证doid 格式),
// 字段非空验证由 validate 标签处理。
func (o *Operation) CheckAndInit() error {
log := logger.GetGlobalLogger()
log.Debug("Checking and initializing operation",
"opSource", o.OpSource,
"opType", o.OpType,
"doid", o.Doid,
)
if o.OpID == "" {
o.OpID = helpers.NewUUIDv7()
log.Debug("Generated new OpID",
"opID", o.OpID,
)
}
if o.OpActor == "" {
o.OpActor = "SYSTEM"
log.Debug("Set default OpActor to SYSTEM")
}
expectedPrefix := fmt.Sprintf("%s/%s", o.DoPrefix, o.DoRepository)
if !strings.HasPrefix(o.Doid, expectedPrefix) {
log.Error("Doid format validation failed",
"doid", o.Doid,
"expectedPrefix", expectedPrefix,
)
return fmt.Errorf("doid must start with '%s'", expectedPrefix)
}
log.Debug("Validating operation struct")
if err := helpers.GetValidator().Struct(o); err != nil {
log.Error("Operation validation failed",
"error", err,
"opID", o.OpID,
)
return err
}
log.Debug("Operation checked and initialized successfully",
"opID", o.OpID,
)
return nil
}