Files

238 lines
8.0 KiB
Go
Executable File
Raw Permalink 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 store
import (
"context"
"database/sql"
"fmt"
"os"
"time"
_ "modernc.org/sqlite"
)
// LedgerStore 账本存储接口(只增不改,独立数据库)
type LedgerStore interface {
AppendLedger(ctx context.Context, prevHash, payloadHash, entryHash, signature string, eventID int64) error
GetLastLedgerEntry(ctx context.Context) (*LedgerEntry, error)
CountLedger(ctx context.Context) (int64, error)
GetLedgerEntry(ctx context.Context, id int64) (*LedgerEntry, error)
ListLedger(ctx context.Context, limit int) ([]*LedgerEntry, error)
// HasLedgerEvent 按 eventID(订单ID)查询账本是否已有记录(崩溃恢复去重用)
HasLedgerEvent(ctx context.Context, eventID int64) (bool, error)
CreateSnapshot(ctx context.Context, date string, lastHash, signature string) error
GetLatestSnapshot(ctx context.Context) (*Snapshot, error)
ListSnapshots(ctx context.Context, limit int) ([]*Snapshot, error)
Close() error
}
// LedgerDB 独立账本数据库:只存 audit_ledger + daily_snapshots
// 文件权限 0600,WAL 模式,触发器防 UPDATE/DELETE
type LedgerDB struct {
db *sql.DB
}
// NewLedgerDB 打开/创建独立账本数据库
func NewLedgerDB(path string) (*LedgerDB, error) {
// 先创建空文件以确保我们可以设置权限
fd, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return nil, fmt.Errorf("创建账本数据库文件失败: %w", err)
}
fd.Close()
// 强制设置权限为 0600(只有文件所有者可读写)
if err := os.Chmod(path, 0600); err != nil {
return nil, fmt.Errorf("设置账本数据库权限失败: %w", err)
}
// 打开连接:WAL 模式防崩溃 + 严格外键
db, err := sql.Open("sqlite", path+"?_journal_mode=WAL&_busy_timeout=10000&_synchronous=FULL")
if err != nil {
return nil, fmt.Errorf("打开账本数据库失败: %w", err)
}
// 连接池:账本写入频率低,单连接更安全
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
l := &LedgerDB{db: db}
if err := l.migrate(); err != nil {
db.Close()
return nil, err
}
return l, nil
}
// migrate 创建账本表 + 触发器(只增不改)
func (l *LedgerDB) migrate() error {
stmts := []string{
`CREATE TABLE IF NOT EXISTS audit_ledger (
id INTEGER PRIMARY KEY AUTOINCREMENT,
prev_hash TEXT NOT NULL,
payload_hash TEXT NOT NULL,
entry_hash TEXT NOT NULL,
signature TEXT NOT NULL,
event_id INTEGER,
created_at TEXT NOT NULL
)`,
// 禁止 UPDATE — 任何修改账本条目的尝试都被拒绝
`CREATE TRIGGER IF NOT EXISTS trg_ledger_no_update BEFORE UPDATE ON audit_ledger
BEGIN SELECT RAISE(ABORT, 'audit_ledger is append-only: UPDATE denied'); END`,
// 禁止 DELETE — 任何删除账本条目的尝试都被拒绝
`CREATE TRIGGER IF NOT EXISTS trg_ledger_no_delete BEFORE DELETE ON audit_ledger
BEGIN SELECT RAISE(ABORT, 'audit_ledger is append-only: DELETE denied'); END`,
`CREATE TABLE IF NOT EXISTS daily_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
snapshot_date TEXT NOT NULL UNIQUE,
last_hash TEXT NOT NULL,
signature TEXT NOT NULL,
created_at TEXT NOT NULL
)`,
// 快照同样防篡改
`CREATE TRIGGER IF NOT EXISTS trg_snapshot_no_update BEFORE UPDATE ON daily_snapshots
BEGIN SELECT RAISE(ABORT, 'daily_snapshots is append-only: UPDATE denied'); END`,
`CREATE TRIGGER IF NOT EXISTS trg_snapshot_no_delete BEFORE DELETE ON daily_snapshots
BEGIN SELECT RAISE(ABORT, 'daily_snapshots is append-only: DELETE denied'); END`,
// 启用 WAL 模式(持久化)
`PRAGMA journal_mode=WAL`,
// 同步模式 FULL — 每次写入都等待数据落盘
`PRAGMA synchronous=FULL`,
}
for _, stmt := range stmts {
if _, err := l.db.Exec(stmt); err != nil {
return fmt.Errorf("账本数据库迁移失败: %w", err)
}
}
return nil
}
// ---------- 账本操作(只增不改) ----------
func (l *LedgerDB) AppendLedger(ctx context.Context, prevHash, payloadHash, entryHash, signature string, eventID int64) error {
_, err := l.db.ExecContext(ctx,
`INSERT INTO audit_ledger (prev_hash, payload_hash, entry_hash, signature, event_id, created_at)
VALUES (?, ?, ?, ?, ?, ?)`,
prevHash, payloadHash, entryHash, signature, eventID, nowStr())
return err
}
func (l *LedgerDB) GetLastLedgerEntry(ctx context.Context) (*LedgerEntry, error) {
row := l.db.QueryRowContext(ctx,
`SELECT id, prev_hash, payload_hash, entry_hash, signature, event_id, created_at
FROM audit_ledger ORDER BY id DESC LIMIT 1`)
var e LedgerEntry
var created string
if err := row.Scan(&e.ID, &e.PrevHash, &e.PayloadHash, &e.EntryHash, &e.Signature, &e.EventID, &created); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
e.CreatedAt, _ = time.Parse(time.RFC3339Nano, created)
return &e, nil
}
func (l *LedgerDB) CountLedger(ctx context.Context) (int64, error) {
var n int64
if err := l.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM audit_ledger`).Scan(&n); err != nil {
return 0, err
}
return n, nil
}
func (l *LedgerDB) GetLedgerEntry(ctx context.Context, id int64) (*LedgerEntry, error) {
row := l.db.QueryRowContext(ctx,
`SELECT id, prev_hash, payload_hash, entry_hash, signature, event_id, created_at
FROM audit_ledger WHERE id=?`, id)
var e LedgerEntry
var created string
if err := row.Scan(&e.ID, &e.PrevHash, &e.PayloadHash, &e.EntryHash, &e.Signature, &e.EventID, &created); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
e.CreatedAt, _ = time.Parse(time.RFC3339Nano, created)
return &e, nil
}
// HasLedgerEvent 按 eventID(订单ID)查询账本是否已有记录(崩溃恢复去重用)
func (l *LedgerDB) HasLedgerEvent(ctx context.Context, eventID int64) (bool, error) {
var n int64
if err := l.db.QueryRowContext(ctx,
`SELECT COUNT(*) FROM audit_ledger WHERE event_id=?`, eventID).Scan(&n); err != nil {
return false, err
}
return n > 0, nil
}
func (l *LedgerDB) ListLedger(ctx context.Context, limit int) ([]*LedgerEntry, error) {
rows, err := l.db.QueryContext(ctx,
`SELECT id, prev_hash, payload_hash, entry_hash, signature, event_id, created_at
FROM audit_ledger ORDER BY id DESC LIMIT ?`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
out := []*LedgerEntry{}
for rows.Next() {
var e LedgerEntry
var created string
if err := rows.Scan(&e.ID, &e.PrevHash, &e.PayloadHash, &e.EntryHash, &e.Signature, &e.EventID, &created); err != nil {
return nil, err
}
e.CreatedAt, _ = time.Parse(time.RFC3339Nano, created)
out = append(out, &e)
}
return out, rows.Err()
}
// ---------- 快照操作 ----------
func (l *LedgerDB) ListSnapshots(ctx context.Context, limit int) ([]*Snapshot, error) {
rows, err := l.db.QueryContext(ctx,
`SELECT id, snapshot_date, last_hash, signature, created_at FROM daily_snapshots ORDER BY id DESC LIMIT ?`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
out := []*Snapshot{}
for rows.Next() {
var sn Snapshot
var created string
if err := rows.Scan(&sn.ID, &sn.SnapshotDate, &sn.LastHash, &sn.Signature, &created); err != nil {
return nil, err
}
sn.CreatedAt, _ = time.Parse(time.RFC3339Nano, created)
out = append(out, &sn)
}
return out, rows.Err()
}
func (l *LedgerDB) CreateSnapshot(ctx context.Context, date string, lastHash, signature string) error {
_, err := l.db.ExecContext(ctx,
`INSERT INTO daily_snapshots (snapshot_date, last_hash, signature, created_at) VALUES (?, ?, ?, ?)`,
date, lastHash, signature, nowStr())
return err
}
func (l *LedgerDB) GetLatestSnapshot(ctx context.Context) (*Snapshot, error) {
row := l.db.QueryRowContext(ctx,
`SELECT id, snapshot_date, last_hash, signature, created_at FROM daily_snapshots ORDER BY id DESC LIMIT 1`)
var sn Snapshot
var created string
if err := row.Scan(&sn.ID, &sn.SnapshotDate, &sn.LastHash, &sn.Signature, &created); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
sn.CreatedAt, _ = time.Parse(time.RFC3339Nano, created)
return &sn, nil
}
// Close 关闭账本数据库
func (l *LedgerDB) Close() error {
return l.db.Close()
}