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
224 lines
6.3 KiB
Go
224 lines
6.3 KiB
Go
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"
|
|
}
|