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:
223
supply-api/internal/repository/foreign_key_validator.go
Normal file
223
supply-api/internal/repository/foreign_key_validator.go
Normal 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"
|
||||
}
|
||||
Reference in New Issue
Block a user