feat: 初始化ForeignKeyValidator和CompensationProcessor

P0-07: 批量补偿处理器
- 添加NewCompensationProcessor构造函数
- 添加NoOpCompensationStats实现
- 添加defaultCompensationExecutor placeholder实现
- 在main.go中初始化CompensationProcessor

P0-09: 外键校验器
- 修改ForeignKeyValidator使用pgxpool替代sql.DB
- 在main.go中初始化ForeignKeyValidator
- 在创建账户前调用ValidateSupplyAccountOwner
- 在创建套餐前调用ValidatePackageSupplyAccount
- SupplyAPI添加fkValidator字段

修改的文件:
- cmd/supply-api/main.go: 初始化组件
- internal/httpapi/supply_api.go: 添加外键校验
- internal/domain/compensation.go: 添加构造函数和Stats实现
- internal/repository/foreign_key_validator.go: 改用pgxpool
This commit is contained in:
Your Name
2026-04-08 19:00:06 +08:00
parent 2f0011b118
commit 40ab7cf851
4 changed files with 540 additions and 6 deletions

View File

@@ -0,0 +1,223 @@
package repository
import (
"context"
"fmt"
"github.com/jackc/pgx/v5/pgxpool"
)
// ==================== P0-09 外键约束策略 ====================
// 问题:跨域模型缺少外键约束策略声明
// 修复方案:应用层外键 + 定期一致性校验
// ForeignKeyValidator 应用层外键校验器
type ForeignKeyValidator struct {
pool *pgxpool.Pool
}
// NewForeignKeyValidator 创建外键校验器
func NewForeignKeyValidator(pool *pgxpool.Pool) *ForeignKeyValidator {
return &ForeignKeyValidator{pool: pool}
}
// ValidateReference 验证引用完整性
func (v *ForeignKeyValidator) ValidateReference(ctx context.Context, ref ReferenceCheck) error {
var exists bool
err := v.pool.QueryRow(ctx, ref.CheckSQL, ref.Args...).Scan(&exists)
if err != nil {
return fmt.Errorf("failed to check reference: %w", err)
}
if !exists {
return ErrReferencedEntityNotFound
}
return nil
}
// ReferenceCheck 引用检查
type ReferenceCheck struct {
TableName string
FieldName string
FieldValue interface{}
CheckSQL string
Args []interface{}
}
// ErrReferencedEntityNotFound 引用实体不存在
var ErrReferencedEntityNotFound = fmt.Errorf("referenced entity not found")
// ValidateSupplyAccountOwner 校验供应账号所属用户存在
func (v *ForeignKeyValidator) ValidateSupplyAccountOwner(ctx context.Context, userID int64) error {
return v.ValidateReference(ctx, ReferenceCheck{
TableName: "iam_users",
FieldName: "id",
FieldValue: userID,
CheckSQL: "SELECT EXISTS(SELECT 1 FROM iam_users WHERE id = $1)",
Args: []interface{}{userID},
})
}
// ValidatePackageSupplyAccount 校验套餐所属供应账号存在
func (v *ForeignKeyValidator) ValidatePackageSupplyAccount(ctx context.Context, accountID int64) error {
return v.ValidateReference(ctx, ReferenceCheck{
TableName: "supply_accounts",
FieldName: "id",
FieldValue: accountID,
CheckSQL: "SELECT EXISTS(SELECT 1 FROM supply_accounts WHERE id = $1)",
Args: []interface{}{accountID},
})
}
// ValidateOrderSupplyAccount 校验订单所属供应账号存在
func (v *ForeignKeyValidator) ValidateOrderSupplyAccount(ctx context.Context, accountID int64) error {
return v.ValidateReference(ctx, ReferenceCheck{
TableName: "supply_accounts",
FieldName: "id",
FieldValue: accountID,
CheckSQL: "SELECT EXISTS(SELECT 1 FROM supply_accounts WHERE id = $1)",
Args: []interface{}{accountID},
})
}
// ValidateOrderSupplyPackage 校验订单所属套餐存在
func (v *ForeignKeyValidator) ValidateOrderSupplyPackage(ctx context.Context, packageID int64) error {
return v.ValidateReference(ctx, ReferenceCheck{
TableName: "supply_packages",
FieldName: "id",
FieldValue: packageID,
CheckSQL: "SELECT EXISTS(SELECT 1 FROM supply_packages WHERE id = $1)",
Args: []interface{}{packageID},
})
}
// ValidateBillingAccount 校验账户所属租户存在
func (v *ForeignKeyValidator) ValidateBillingAccount(ctx context.Context, tenantID int64) error {
return v.ValidateReference(ctx, ReferenceCheck{
TableName: "core_tenants",
FieldName: "id",
FieldValue: tenantID,
CheckSQL: "SELECT EXISTS(SELECT 1 FROM core_tenants WHERE id = $1)",
Args: []interface{}{tenantID},
})
}
// OrphanRecordCheck 孤立记录检查结果
type OrphanRecordCheck struct {
TableName string
FieldName string
Count int64
}
// orphanCheckSQL 孤立检查SQL
type orphanCheckSQL struct {
TableName string
FieldName string
SQL string
}
// CheckOrphanRecords 执行孤立记录检查
func (v *ForeignKeyValidator) CheckOrphanRecords(ctx context.Context) ([]OrphanRecordCheck, error) {
checks := []orphanCheckSQL{
// 检查孤立的supply_accounts
{
TableName: "supply_accounts",
FieldName: "user_id",
SQL: `SELECT COUNT(*) FROM supply_accounts sa WHERE NOT EXISTS (SELECT 1 FROM iam_users WHERE id = sa.user_id)`,
},
// 检查孤立的supply_packages
{
TableName: "supply_packages",
FieldName: "supply_account_id",
SQL: `SELECT COUNT(*) FROM supply_packages sp WHERE NOT EXISTS (SELECT 1 FROM supply_accounts WHERE id = sp.supply_account_id)`,
},
// 检查孤立的supply_orders (supply_account_id)
{
TableName: "supply_orders",
FieldName: "supply_account_id",
SQL: `SELECT COUNT(*) FROM supply_orders so WHERE NOT EXISTS (SELECT 1 FROM supply_accounts WHERE id = so.supply_account_id)`,
},
// 检查孤立的supply_orders (supply_package_id)
{
TableName: "supply_orders",
FieldName: "supply_package_id",
SQL: `SELECT COUNT(*) FROM supply_orders so WHERE NOT EXISTS (SELECT 1 FROM supply_packages WHERE id = so.supply_package_id)`,
},
}
var results []OrphanRecordCheck
for _, check := range checks {
var count int64
err := v.pool.QueryRow(ctx, check.SQL).Scan(&count)
if err != nil {
return nil, fmt.Errorf("failed to check orphans for %s.%s: %w", check.TableName, check.FieldName, err)
}
if count > 0 {
results = append(results, OrphanRecordCheck{
TableName: check.TableName,
FieldName: check.FieldName,
Count: count,
})
}
}
return results, nil
}
// ForeignKeyPolicy 外键策略定义
type ForeignKeyPolicy struct {
// 保留物理外键的表
PhysicalFKTables []string
// 使用应用层外键的表
ApplicationFKTables []string
// 无外键的表
NoFKTables []string
}
// GetDefaultForeignKeyPolicy 获取默认外键策略
func GetDefaultForeignKeyPolicy() *ForeignKeyPolicy {
return &ForeignKeyPolicy{
// 核心实体表保留物理外键
PhysicalFKTables: []string{
"core_tenants",
"core_projects",
"iam_users",
"billing_accounts",
},
// 高频写入表使用应用层外键
ApplicationFKTables: []string{
"supply_accounts",
"supply_packages",
"supply_orders",
"supply_usage_records",
"supply_settlements",
},
// 审计/日志表无外键
NoFKTables: []string{
"audit_events",
"outbox_events",
"outbox_dead_letter",
"supply_idempotency_record",
"supply_batch_compensation",
},
}
}
// GetPolicyForTable 获取表的外键策略
func (p *ForeignKeyPolicy) GetPolicyForTable(tableName string) string {
for _, t := range p.PhysicalFKTables {
if t == tableName {
return "physical"
}
}
for _, t := range p.ApplicationFKTables {
if t == tableName {
return "application"
}
}
for _, t := range p.NoFKTables {
if t == tableName {
return "none"
}
}
return "unknown"
}