保存资源项目、所有者和操作人,接口从会话记录归属,普通更新保留创建信息。 新增三库第十一版迁移、同项目绑定约束和 SQLite 触发器保护,补充迁移回滚重试、仓储、接口及备份恢复测试。 本次整理验证:后端构建、静态检查、关联改造针对性测试和 CLI 包测试通过。 范围说明:项目成员权限及其他资源归属尚未完成;本次未重新执行外部数据库实库、独立程序及生产部署验收。
86 lines
4.3 KiB
Go
86 lines
4.3 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
)
|
|
|
|
// SQLite 不重建业务表,以触发器实现引用约束;即使连接关闭外键开关也不能绕过。
|
|
// 触发器作为完整语句执行,不能用迁移文件的分号拆分器切开触发器主体。
|
|
func installSQLiteResourceConstraints(ctx context.Context, tx *sql.Tx) error {
|
|
for _, resource := range []struct{ table, column string }{{"domains", "domain_id"}, {"servers", "server_id"}} {
|
|
var invalid int
|
|
if err := tx.QueryRowContext(ctx, fmt.Sprintf(`SELECT COUNT(*) FROM %s r LEFT JOIN projects p ON p.id = r.project_id WHERE p.id IS NULL`, resource.table)).Scan(&invalid); err != nil {
|
|
return err
|
|
}
|
|
if invalid != 0 {
|
|
return fmt.Errorf("%s 引用了不存在的项目,拒绝迁移", resource.table)
|
|
}
|
|
query := fmt.Sprintf(`SELECT COUNT(*) FROM domain_servers b LEFT JOIN %s r
|
|
ON r.id = b.%s AND r.project_id = b.project_id WHERE r.id IS NULL`, resource.table, resource.column)
|
|
if err := tx.QueryRowContext(ctx, query).Scan(&invalid); err != nil {
|
|
return err
|
|
}
|
|
if invalid != 0 {
|
|
return fmt.Errorf("历史域名服务器关联存在无效的 %s 引用,拒绝迁移,请先核查原数据", resource.table)
|
|
}
|
|
statements := []string{
|
|
fmt.Sprintf(`CREATE TRIGGER scert_%[1]s_replace_guard BEFORE INSERT ON %[1]s
|
|
WHEN EXISTS (SELECT 1 FROM domain_servers WHERE %[2]s = NEW.id)
|
|
BEGIN SELECT RAISE(ABORT, '资源仍有部署关联,不能替换'); END`, resource.table, resource.column),
|
|
fmt.Sprintf(`CREATE TRIGGER scert_%[1]s_project_insert BEFORE INSERT ON %[1]s
|
|
WHEN NOT EXISTS (SELECT 1 FROM projects WHERE id = NEW.project_id)
|
|
BEGIN SELECT RAISE(ABORT, '资源项目不存在'); END`, resource.table),
|
|
fmt.Sprintf(`CREATE TRIGGER scert_%[1]s_project_update BEFORE UPDATE OF project_id, id ON %[1]s
|
|
WHEN NOT EXISTS (SELECT 1 FROM projects WHERE id = NEW.project_id)
|
|
OR EXISTS (SELECT 1 FROM domain_servers WHERE %[2]s = OLD.id
|
|
AND (project_id != NEW.project_id OR %[2]s != NEW.id))
|
|
BEGIN SELECT RAISE(ABORT, '资源项目不存在或仍有部署关联'); END`, resource.table, resource.column),
|
|
fmt.Sprintf(`CREATE TRIGGER scert_%[1]s_binding_delete BEFORE DELETE ON %[1]s
|
|
WHEN EXISTS (SELECT 1 FROM domain_servers WHERE %[2]s = OLD.id)
|
|
BEGIN SELECT RAISE(ABORT, '资源仍有部署关联'); END`, resource.table, resource.column),
|
|
}
|
|
for _, statement := range statements {
|
|
if _, err := tx.ExecContext(ctx, statement); err != nil {
|
|
return fmt.Errorf("建立资源项目约束失败:%w", err)
|
|
}
|
|
}
|
|
}
|
|
// REPLACE 的隐式删除在关闭 recursive_triggers 时不触发删除保护,需保护唯一域名冲突。
|
|
for _, event := range []string{"INSERT", "UPDATE"} {
|
|
statement := fmt.Sprintf(`CREATE TRIGGER scert_domain_name_replace_%s BEFORE %s ON domains
|
|
WHEN EXISTS (SELECT 1 FROM domains d JOIN domain_servers b ON b.domain_id = d.id
|
|
WHERE d.name = NEW.name AND d.id != NEW.id)
|
|
BEGIN SELECT RAISE(ABORT, '同名域名仍有部署关联,不能替换'); END`, event, event)
|
|
if _, err := tx.ExecContext(ctx, statement); err != nil {
|
|
return fmt.Errorf("建立域名替换保护失败:%w", err)
|
|
}
|
|
}
|
|
for _, event := range []string{"INSERT", "UPDATE"} {
|
|
statement := fmt.Sprintf(`CREATE TRIGGER scert_binding_project_%s BEFORE %s ON domain_servers
|
|
WHEN NOT EXISTS (SELECT 1 FROM domains WHERE id = NEW.domain_id AND project_id = NEW.project_id)
|
|
OR NOT EXISTS (SELECT 1 FROM servers WHERE id = NEW.server_id AND project_id = NEW.project_id)
|
|
BEGIN SELECT RAISE(ABORT, '域名与服务器必须存在且属于同一项目'); END`, event, event)
|
|
if _, err := tx.ExecContext(ctx, statement); err != nil {
|
|
return fmt.Errorf("建立部署关联约束失败:%w", err)
|
|
}
|
|
}
|
|
for _, event := range []string{"DELETE", "UPDATE OF id"} {
|
|
name := "delete"
|
|
condition := ""
|
|
if event != "DELETE" {
|
|
name = "update"
|
|
condition = "OLD.id != NEW.id AND "
|
|
}
|
|
statement := fmt.Sprintf(`CREATE TRIGGER scert_project_resource_%s BEFORE %s ON projects
|
|
WHEN %s(EXISTS (SELECT 1 FROM domains WHERE project_id = OLD.id)
|
|
OR EXISTS (SELECT 1 FROM servers WHERE project_id = OLD.id))
|
|
BEGIN SELECT RAISE(ABORT, '项目仍被资源引用'); END`, name, event, condition)
|
|
if _, err := tx.ExecContext(ctx, statement); err != nil {
|
|
return fmt.Errorf("建立项目引用保护失败:%w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|