diff --git a/sql/02-migrate-abpusers-table.sql b/sql/02-migrate-abpusers-table.sql index de8f831f..646f7eb5 100644 --- a/sql/02-migrate-abpusers-table.sql +++ b/sql/02-migrate-abpusers-table.sql @@ -1,9 +1,10 @@ -- ============================================================ --- AbpUsers 表精简迁移脚本 +-- AbpUsers 表精简迁移脚本(幂等安全版) -- 用途:移除 ABP Framework 遗留的冗余字段,使表结构与新 User 实体一致 -- 前置条件:已备份 DFApp.db,应用已停止运行 --- 执行方式:sqlite3 DFApp.db < sql/migrate-abpusers-table.sql +-- 执行方式:sqlite3 DFApp.db < sql/02-migrate-abpusers-table.sql -- 注意:此操作不可逆!请务必先备份数据库! +-- 幂等性:如果迁移已完成,脚本会安全终止,不会重复执行 -- ============================================================ -- -- 背景: @@ -18,14 +19,23 @@ -- 2. 从旧表复制未软删除的数据到新表(排除 IsDeleted=1 的记录) -- 3. 删除旧表 -- 4. 将新表重命名为 AbpUsers +-- +-- 安全机制: +-- - .bail on:遇到错误时立即终止,防止 INSERT 失败后继续执行 DROP TABLE +-- - 迁移状态检查:检测 IsDeleted 列是否存在,已完成迁移时安全退出 +-- - DROP TABLE IF EXISTS:清理上次失败运行遗留的临时表 -- ============================================================ +-- 关键安全设置:遇到错误时终止脚本执行 +-- 防止 INSERT 失败后脚本继续执行 DROP TABLE 导致数据丢失 +.bail on + -- ============================================================ -- 第一部分:前置检查 -- ============================================================ --- 确认 AbpUsers 表存在 +-- 1.1 确认 AbpUsers 表存在 SELECT '正在检查 AbpUsers 表是否存在...' AS step; SELECT CASE WHEN COUNT(*) > 0 THEN '✅ AbpUsers 表存在,可以继续' @@ -33,13 +43,27 @@ SELECT CASE END AS result FROM sqlite_master WHERE type = 'table' AND name = 'AbpUsers'; --- 查看当前表结构 +-- 1.2 查看当前表结构 SELECT '=== 当前 AbpUsers 表结构 ===' AS section; -SELECT name AS ColumnName, type AS DataType, `notnull` AS NotNull +SELECT name AS ColumnName, type AS DataType, "notnull" AS "NotNull" FROM pragma_table_info('AbpUsers') ORDER BY cid; --- 查看当前数据量(含已软删除的) +-- 1.3 检查迁移状态 +-- 通过 pragma_table_info 查询列信息,不直接引用 IsDeleted 列,因此总是安全的 +SELECT '=== 迁移状态检查 ===' AS section; +SELECT CASE + WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpUsers') WHERE name = 'IsDeleted') > 0 + THEN '🔄 IsDeleted 列存在,需要执行数据迁移' + ELSE '⏭️ IsDeleted 列不存在,迁移已完成。脚本将安全终止。' +END AS migration_status; + +-- 1.4 查看当前数据量(含已软删除的) +-- 此查询引用 IsDeleted 列作为安全守卫: +-- - 如果 IsDeleted 列存在 → 正常输出统计数据 +-- - 如果 IsDeleted 列不存在 → 产生 "no such column: IsDeleted" 错误 +-- 配合 .bail on 指令,脚本将在此安全终止 +-- 后续的事务迁移操作(含 DROP TABLE)不会被执行,保护数据安全 SELECT '=== 当前数据统计 ===' AS section; SELECT COUNT(*) AS TotalRows, @@ -50,8 +74,12 @@ FROM AbpUsers; -- ============================================================ -- 第二部分:数据迁移(事务保护) +-- 仅在 IsDeleted 列存在时才会执行到这里(否则已在上方终止) -- ============================================================ +-- 清理上次失败运行遗留的临时表(幂等性保障) +DROP TABLE IF EXISTS _AbpUsers_new; + BEGIN TRANSACTION; -- 备份提示(仅输出提醒,SQLite 不支持自动备份) @@ -127,7 +155,7 @@ COMMIT; -- 验证新表结构(应只有 10 列) SELECT '=== 新 AbpUsers 表结构 ===' AS section; -SELECT name AS ColumnName, type AS DataType, `notnull` AS NotNull +SELECT name AS ColumnName, type AS DataType, "notnull" AS "NotNull" FROM pragma_table_info('AbpUsers') ORDER BY cid; diff --git a/sql/04-add-missing-audit-columns.sql b/sql/04-add-missing-audit-columns.sql index 9b3273cc..a4f23143 100644 --- a/sql/04-add-missing-audit-columns.sql +++ b/sql/04-add-missing-audit-columns.sql @@ -1,31 +1,149 @@ +-- ============================================================= -- 修复 AuditedEntity 派生实体缺失的审计列(CreatorId, LastModifierId) --- 部分 AuditedEntity 派生实体的数据库表在迁移时遗漏了审计列 +-- 幂等安全版:可安全地重复执行 +-- +-- 说明:部分 AuditedEntity 派生实体的数据库表在迁移时遗漏了审计列 +-- +-- 注意:AppPermissionGrants 表的审计列不在此脚本中添加。 +-- 该表由脚本 06 (06-migrate-to-app-permission-grants.sql) 创建, +-- 创建时已包含 CreatorId、LastModificationTime、LastModifierId 列。 +-- 之前此脚本引用了尚未创建的 AppPermissionGrants 表导致报错。 +-- +-- 幂等性原理: +-- SQLite 不支持 ALTER TABLE ADD COLUMN IF NOT EXISTS 语法, +-- 也不支持 SQL 中的 IF/ELSE 条件执行 DDL 语句。 +-- 本脚本使用以下技巧实现幂等性: +-- 1. 通过 SELECT 'ALTER TABLE ...' WHERE (列不存在) 仅输出需要执行的语句 +-- 2. 将输出重定向到临时文件(.output) +-- 3. 通过 .read 执行临时文件中的语句 +-- 已存在的列不会生成对应语句,从而避免 "duplicate column name" 错误。 +-- ============================================================= + +.bail on +.headers off + +-- ============================================================ +-- 第一部分:检查当前状态 +-- ============================================================ + +SELECT '=== 审计列迁移前状态检查 ==='; + +-- AppMediaInfo +SELECT 'AppMediaInfo:' || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AppMediaInfo') WHERE name = 'CreatorId') > 0 THEN ' CreatorId=已存在' ELSE ' CreatorId=缺失' END || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AppMediaInfo') WHERE name = 'LastModifierId') > 0 THEN ', LastModifierId=已存在' ELSE ', LastModifierId=缺失' END; + +-- AppRssSubscriptions +SELECT 'AppRssSubscriptions:' || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AppRssSubscriptions') WHERE name = 'CreatorId') > 0 THEN ' CreatorId=已存在' ELSE ' CreatorId=缺失' END || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AppRssSubscriptions') WHERE name = 'LastModifierId') > 0 THEN ', LastModifierId=已存在' ELSE ', LastModifierId=缺失' END; + +-- AppRssMirrorItem +SELECT 'AppRssMirrorItem:' || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AppRssMirrorItem') WHERE name = 'CreatorId') > 0 THEN ' CreatorId=已存在' ELSE ' CreatorId=缺失' END || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AppRssMirrorItem') WHERE name = 'LastModifierId') > 0 THEN ', LastModifierId=已存在' ELSE ', LastModifierId=缺失' END; + +-- AbpRoleClaims +SELECT 'AbpRoleClaims:' || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpRoleClaims') WHERE name = 'CreationTime') > 0 THEN ' CreationTime=已存在' ELSE ' CreationTime=缺失' END || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpRoleClaims') WHERE name = 'CreatorId') > 0 THEN ', CreatorId=已存在' ELSE ', CreatorId=缺失' END || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpRoleClaims') WHERE name = 'LastModificationTime') > 0 THEN ', LastModificationTime=已存在' ELSE ', LastModificationTime=缺失' END || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpRoleClaims') WHERE name = 'LastModifierId') > 0 THEN ', LastModifierId=已存在' ELSE ', LastModifierId=缺失' END; + +-- AbpRoles +SELECT 'AbpRoles:' || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpRoles') WHERE name = 'CreatorId') > 0 THEN ' CreatorId=已存在' ELSE ' CreatorId=缺失' END || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpRoles') WHERE name = 'LastModificationTime') > 0 THEN ', LastModificationTime=已存在' ELSE ', LastModificationTime=缺失' END || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpRoles') WHERE name = 'LastModifierId') > 0 THEN ', LastModifierId=已存在' ELSE ', LastModifierId=缺失' END; + + +-- ============================================================ +-- 第二部分:动态生成并执行 ALTER TABLE 语句 +-- 仅对不存在的列生成 ALTER TABLE,已存在的列自动跳过 +-- ============================================================ + +.output /tmp/_migration_04_steps.sql -- AppMediaInfo(MediaInfo : AuditedEntity) -ALTER TABLE "AppMediaInfo" ADD COLUMN "CreatorId" TEXT NULL; -ALTER TABLE "AppMediaInfo" ADD COLUMN "LastModifierId" TEXT NULL; +SELECT 'ALTER TABLE "AppMediaInfo" ADD COLUMN "CreatorId" TEXT NULL;' +WHERE (SELECT COUNT(*) FROM pragma_table_info('AppMediaInfo') WHERE name = 'CreatorId') = 0; + +SELECT 'ALTER TABLE "AppMediaInfo" ADD COLUMN "LastModifierId" TEXT NULL;' +WHERE (SELECT COUNT(*) FROM pragma_table_info('AppMediaInfo') WHERE name = 'LastModifierId') = 0; -- AppRssSubscriptions(RssSubscription : AuditedEntity) -- CreatorId 已存在,仅补充 LastModifierId -ALTER TABLE "AppRssSubscriptions" ADD COLUMN "LastModifierId" TEXT NULL; +SELECT 'ALTER TABLE "AppRssSubscriptions" ADD COLUMN "LastModifierId" TEXT NULL;' +WHERE (SELECT COUNT(*) FROM pragma_table_info('AppRssSubscriptions') WHERE name = 'LastModifierId') = 0; -- AppRssMirrorItem(RssMirrorItem : AuditedEntity) -ALTER TABLE "AppRssMirrorItem" ADD COLUMN "CreatorId" TEXT NULL; -ALTER TABLE "AppRssMirrorItem" ADD COLUMN "LastModifierId" TEXT NULL; +SELECT 'ALTER TABLE "AppRssMirrorItem" ADD COLUMN "CreatorId" TEXT NULL;' +WHERE (SELECT COUNT(*) FROM pragma_table_info('AppRssMirrorItem') WHERE name = 'CreatorId') = 0; + +SELECT 'ALTER TABLE "AppRssMirrorItem" ADD COLUMN "LastModifierId" TEXT NULL;' +WHERE (SELECT COUNT(*) FROM pragma_table_info('AppRssMirrorItem') WHERE name = 'LastModifierId') = 0; -- AbpRoleClaims(RoleClaim : AuditedEntity) -ALTER TABLE "AbpRoleClaims" ADD COLUMN "CreationTime" TEXT NOT NULL DEFAULT '0001-01-01 00:00:00'; -ALTER TABLE "AbpRoleClaims" ADD COLUMN "CreatorId" TEXT NULL; -ALTER TABLE "AbpRoleClaims" ADD COLUMN "LastModificationTime" TEXT NULL; -ALTER TABLE "AbpRoleClaims" ADD COLUMN "LastModifierId" TEXT NULL; +SELECT 'ALTER TABLE "AbpRoleClaims" ADD COLUMN "CreationTime" TEXT NOT NULL DEFAULT ''0001-01-01 00:00:00'';' +WHERE (SELECT COUNT(*) FROM pragma_table_info('AbpRoleClaims') WHERE name = 'CreationTime') = 0; + +SELECT 'ALTER TABLE "AbpRoleClaims" ADD COLUMN "CreatorId" TEXT NULL;' +WHERE (SELECT COUNT(*) FROM pragma_table_info('AbpRoleClaims') WHERE name = 'CreatorId') = 0; + +SELECT 'ALTER TABLE "AbpRoleClaims" ADD COLUMN "LastModificationTime" TEXT NULL;' +WHERE (SELECT COUNT(*) FROM pragma_table_info('AbpRoleClaims') WHERE name = 'LastModificationTime') = 0; --- AppPermissionGrants(PermissionGrant : AuditedEntity) -ALTER TABLE "AppPermissionGrants" ADD COLUMN "CreatorId" TEXT NULL; -ALTER TABLE "AppPermissionGrants" ADD COLUMN "LastModificationTime" TEXT NULL; -ALTER TABLE "AppPermissionGrants" ADD COLUMN "LastModifierId" TEXT NULL; +SELECT 'ALTER TABLE "AbpRoleClaims" ADD COLUMN "LastModifierId" TEXT NULL;' +WHERE (SELECT COUNT(*) FROM pragma_table_info('AbpRoleClaims') WHERE name = 'LastModifierId') = 0; -- AbpRoles(Role : AuditedEntity) -- CreationTime 已存在,仅补充 CreatorId 和修改审计列 -ALTER TABLE "AbpRoles" ADD COLUMN "CreatorId" TEXT NULL; -ALTER TABLE "AbpRoles" ADD COLUMN "LastModificationTime" TEXT NULL; -ALTER TABLE "AbpRoles" ADD COLUMN "LastModifierId" TEXT NULL; +SELECT 'ALTER TABLE "AbpRoles" ADD COLUMN "CreatorId" TEXT NULL;' +WHERE (SELECT COUNT(*) FROM pragma_table_info('AbpRoles') WHERE name = 'CreatorId') = 0; + +SELECT 'ALTER TABLE "AbpRoles" ADD COLUMN "LastModificationTime" TEXT NULL;' +WHERE (SELECT COUNT(*) FROM pragma_table_info('AbpRoles') WHERE name = 'LastModificationTime') = 0; + +SELECT 'ALTER TABLE "AbpRoles" ADD COLUMN "LastModifierId" TEXT NULL;' +WHERE (SELECT COUNT(*) FROM pragma_table_info('AbpRoles') WHERE name = 'LastModifierId') = 0; + +.output stdout + +-- 执行动态生成的 ALTER TABLE 语句 +-- 如果所有列都已存在,临时文件为空,.read 不会执行任何操作 +.read /tmp/_migration_04_steps.sql + +-- 清理临时文件 +.shell rm -f /tmp/_migration_04_steps.sql + + +-- ============================================================ +-- 第三部分:迁移后验证 +-- ============================================================ + +SELECT '=== 迁移后验证 ==='; + +SELECT 'AppMediaInfo:' || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AppMediaInfo') WHERE name = 'CreatorId') > 0 THEN ' ✅CreatorId' ELSE ' ❌CreatorId缺失' END || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AppMediaInfo') WHERE name = 'LastModifierId') > 0 THEN ' ✅LastModifierId' ELSE ' ❌LastModifierId缺失' END; + +SELECT 'AppRssSubscriptions:' || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AppRssSubscriptions') WHERE name = 'CreatorId') > 0 THEN ' ✅CreatorId' ELSE ' ❌CreatorId缺失' END || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AppRssSubscriptions') WHERE name = 'LastModifierId') > 0 THEN ' ✅LastModifierId' ELSE ' ❌LastModifierId缺失' END; + +SELECT 'AppRssMirrorItem:' || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AppRssMirrorItem') WHERE name = 'CreatorId') > 0 THEN ' ✅CreatorId' ELSE ' ❌CreatorId缺失' END || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AppRssMirrorItem') WHERE name = 'LastModifierId') > 0 THEN ' ✅LastModifierId' ELSE ' ❌LastModifierId缺失' END; + +SELECT 'AbpRoleClaims:' || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpRoleClaims') WHERE name = 'CreationTime') > 0 THEN ' ✅CreationTime' ELSE ' ❌CreationTime缺失' END || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpRoleClaims') WHERE name = 'CreatorId') > 0 THEN ' ✅CreatorId' ELSE ' ❌CreatorId缺失' END || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpRoleClaims') WHERE name = 'LastModificationTime') > 0 THEN ' ✅LastModificationTime' ELSE ' ❌LastModificationTime缺失' END || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpRoleClaims') WHERE name = 'LastModifierId') > 0 THEN ' ✅LastModifierId' ELSE ' ❌LastModifierId缺失' END; + +SELECT 'AbpRoles:' || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpRoles') WHERE name = 'CreatorId') > 0 THEN ' ✅CreatorId' ELSE ' ❌CreatorId缺失' END || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpRoles') WHERE name = 'LastModificationTime') > 0 THEN ' ✅LastModificationTime' ELSE ' ❌LastModificationTime缺失' END || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpRoles') WHERE name = 'LastModifierId') > 0 THEN ' ✅LastModifierId' ELSE ' ❌LastModifierId缺失' END; + +SELECT '✅ 审计列迁移完成(幂等安全,可重复执行)'; diff --git a/sql/05-fix-guid-case-migration.sql b/sql/05-fix-guid-case-migration.sql index bbaa364c..6f4efdd3 100644 --- a/sql/05-fix-guid-case-migration.sql +++ b/sql/05-fix-guid-case-migration.sql @@ -117,9 +117,9 @@ UPDATE AppLotteryPrizegrades SET CreatorId = LOWER(CreatorId) WHERE CreatorId IS -- 列:CreatorId UPDATE AppLotteryResult SET CreatorId = LOWER(CreatorId) WHERE CreatorId IS NOT NULL AND CreatorId != LOWER(CreatorId); --- 20. AppPermissionGrants(应用权限授予,ProviderKey 可能是 Guid 或角色名) --- 列:ProviderKey(仅当 ProviderKey 符合 Guid 格式时转换) -UPDATE AppPermissionGrants SET ProviderKey = LOWER(ProviderKey) WHERE ProviderKey LIKE '%-%-%-%-%' AND ProviderKey != LOWER(ProviderKey); +-- 20. AppPermissionGrants 已移至脚本 06 处理 +-- 原因:AppPermissionGrants 表在脚本 06 中创建,脚本 05 执行时该表尚不存在 +-- 脚本 06 在数据迁移时已统一使用 lower(ProviderKey),并在迁移完成后清理残留大写 Guid -- 21. AppMediaExternalLink(媒体外链) -- 列:CreatorId, LastModifierId diff --git a/sql/06-migrate-to-app-permission-grants.sql b/sql/06-migrate-to-app-permission-grants.sql index 7d531cdb..d3fb7494 100644 --- a/sql/06-migrate-to-app-permission-grants.sql +++ b/sql/06-migrate-to-app-permission-grants.sql @@ -51,6 +51,11 @@ AND EXISTS ( WHERE UPPER(r.Id) = UPPER(AppPermissionGrants.ProviderKey) ); +-- 5.5 清理残留的大写 Guid 格式 ProviderKey(从脚本 05 移入) +-- 确保所有 Guid 格式的 ProviderKey 统一为小写(角色名等非 Guid 值不受影响) +UPDATE AppPermissionGrants SET ProviderKey = LOWER(ProviderKey) +WHERE ProviderKey LIKE '%-%-%-%-%' AND ProviderKey != LOWER(ProviderKey); + -- 6. 验证迁移结果 SELECT '=== 迁移结果统计 ===' AS info; SELECT '旧表总数' AS label, COUNT(*) AS cnt FROM AbpPermissionGrants diff --git a/sql/08-verify-identity-data.sql b/sql/08-verify-identity-data.sql index c9891811..9adae86b 100644 --- a/sql/08-verify-identity-data.sql +++ b/sql/08-verify-identity-data.sql @@ -42,39 +42,27 @@ SELECT COUNT(*) AS '禁用用户数' FROM AbpUsers WHERE IsActive = 0; -- 1.2 软删除检查 -- 迁移脚本 migrate-abpusers-table.sql 会移除 IsDeleted 列, -- 如果该列仍存在则检查是否有残留的软删除数据 +-- 注意:SQLite 会解析整个语句,不能在 CASE 中条件引用已删除的列, +-- 因此只能通过 pragma_table_info 检查列是否存在 SELECT '--- 软删除检查 ---' AS section; SELECT CASE - WHEN COUNT(*) = 0 THEN '⚠️ IsDeleted 列不存在(已执行表结构迁移,此项跳过)' - ELSE '检查软删除数据...' -END AS status -FROM pragma_table_info('AbpUsers') WHERE name = 'IsDeleted'; + WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpUsers') WHERE name = 'IsDeleted') = 0 + THEN '✅ IsDeleted 列已移除,无软删除数据' + ELSE '⚠️ IsDeleted 列仍存在,请检查是否有软删除用户残留' +END AS '软删除验证结果'; -SELECT COUNT(*) AS '软删除用户数(IsDeleted=1)' -FROM AbpUsers -WHERE name = 'IsDeleted' AND (SELECT COUNT(*) FROM pragma_table_info('AbpUsers') WHERE name = 'IsDeleted') > 0 -AND IsDeleted = 1; - --- 使用子查询方式避免列不存在时报错 -SELECT - CASE - WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpUsers') WHERE name = 'IsDeleted') = 0 - THEN '✅ IsDeleted 列已移除,无软删除数据' - WHEN (SELECT COUNT(*) FROM AbpUsers WHERE IsDeleted = 1) = 0 - THEN '✅ 无软删除用户' - ELSE '❌ 仍有软删除用户残留,数量: ' || (SELECT COUNT(*) FROM AbpUsers WHERE IsDeleted = 1) - END AS '软删除验证结果'; -- 1.3 多租户检查 -- 迁移脚本会移除 TenantId 列,如果该列仍存在则检查是否有多租户数据残留 +-- 注意:SQLite 会解析整个语句,不能在 CASE 中条件引用已删除的列, +-- 因此只能通过 pragma_table_info 检查列是否存在 SELECT '--- 多租户检查 ---' AS section; -SELECT - CASE - WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpUsers') WHERE name = 'TenantId') = 0 - THEN '✅ TenantId 列已移除,无多租户数据' - WHEN (SELECT COUNT(*) FROM AbpUsers WHERE TenantId IS NOT NULL) = 0 - THEN '✅ 所有用户 TenantId 均为 NULL,无多租户数据' - ELSE '❌ 存在多租户用户数据,数量: ' || (SELECT COUNT(*) FROM AbpUsers WHERE TenantId IS NOT NULL) - END AS '多租户验证结果'; +SELECT CASE + WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpUsers') WHERE name = 'TenantId') = 0 + THEN '✅ TenantId 列已移除,无多租户数据' + ELSE '⚠️ TenantId 列仍存在,请检查是否有多租户数据残留' +END AS '多租户验证结果'; + -- 1.4 密码哈希检查 -- 所有用户都应设置密码哈希 @@ -371,6 +359,8 @@ SELECT )) AS '重复用户名数'; -- 8.3 迁移状态检查 +-- 注意:SQLite 会解析整个语句,不能在 CASE 中条件引用已删除的列, +-- 因此只通过 pragma_table_info 检查列是否存在 SELECT '--- 迁移状态检查 ---' AS section; SELECT CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpUsers') WHERE name = 'IsDeleted') = 0 @@ -379,12 +369,10 @@ SELECT END AS '表结构迁移状态', CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpUsers') WHERE name = 'TenantId') = 0 THEN '✅ 已清理' - ELSE CASE WHEN (SELECT COUNT(*) FROM AbpUsers WHERE TenantId IS NOT NULL) = 0 - THEN '✅ 已清理(列存在但值为空)' - ELSE '❌ 存在多租户数据' - END + ELSE '⚠️ TenantId 列仍存在,请检查是否有多租户数据残留' END AS '多租户数据清理状态'; + -- 8.4 最终验证结论 SELECT '' AS blank; SELECT '===== 最终验证结论 =====' AS step; @@ -407,13 +395,11 @@ SELECT CASE SELECT UserName FROM AbpUsers GROUP BY UserName HAVING COUNT(*) > 1 )) > 0 THEN '❌ 数据完整性存在问题:存在重复用户名' - -- 迁移残留检查(仅在列仍存在时才报错) + -- 迁移残留检查(仅检查列是否仍存在,不引用已删除的列) WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpUsers') WHERE name = 'IsDeleted') > 0 - AND (SELECT COUNT(*) FROM AbpUsers WHERE IsDeleted = 1) > 0 - THEN '❌ 数据完整性存在问题:存在软删除用户残留' + THEN '❌ 数据完整性存在问题:IsDeleted 列仍存在,迁移未完成' WHEN (SELECT COUNT(*) FROM pragma_table_info('AbpUsers') WHERE name = 'TenantId') > 0 - AND (SELECT COUNT(*) FROM AbpUsers WHERE TenantId IS NOT NULL) > 0 - THEN '❌ 数据完整性存在问题:存在多租户数据残留' + THEN '❌ 数据完整性存在问题:TenantId 列仍存在,迁移未完成' -- 所有检查通过 ELSE '✅ 所有数据验证通过,数据完整性良好' END AS '验证结论'; diff --git a/sql/10-add-disk-space-check.sql b/sql/10-add-disk-space-check.sql index 6b330d61..4b235c1f 100644 --- a/sql/10-add-disk-space-check.sql +++ b/sql/10-add-disk-space-check.sql @@ -1,2 +1,58 @@ +-- ============================================================= -- 添加磁盘空间暂存字段到订阅下载表 -ALTER TABLE "AppRssSubscriptionDownloads" ADD COLUMN "IsPendingDueToLowDiskSpace" INTEGER NOT NULL DEFAULT 0; +-- 幂等安全版:可安全地重复执行 +-- +-- 说明:为 AppRssSubscriptionDownloads 表添加 IsPendingDueToLowDiskSpace 列, +-- 用于标记因磁盘空间不足而暂缓下载的任务。 +-- +-- 幂等性原理: +-- SQLite 不支持 ALTER TABLE ADD COLUMN IF NOT EXISTS 语法, +-- 本脚本使用与 04-add-missing-audit-columns.sql 相同的技巧: +-- 1. 通过 pragma_table_info 检查列是否存在 +-- 2. 仅在列不存在时生成 ALTER TABLE 语句到临时文件 +-- 3. 通过 .read 执行临时文件中的语句 +-- ============================================================= + +.bail on +.headers off + +-- ============================================================ +-- 第一部分:检查当前状态 +-- ============================================================ + +SELECT '=== 磁盘空间暂存字段迁移前状态检查 ==='; + +SELECT 'AppRssSubscriptionDownloads:' || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AppRssSubscriptionDownloads') WHERE name = 'IsPendingDueToLowDiskSpace') > 0 THEN ' IsPendingDueToLowDiskSpace=已存在' ELSE ' IsPendingDueToLowDiskSpace=缺失' END; + + +-- ============================================================ +-- 第二部分:动态生成并执行 ALTER TABLE 语句 +-- 仅对不存在的列生成 ALTER TABLE,已存在的列自动跳过 +-- ============================================================ + +.output /tmp/_migration_10_steps.sql + +SELECT 'ALTER TABLE "AppRssSubscriptionDownloads" ADD COLUMN "IsPendingDueToLowDiskSpace" INTEGER NOT NULL DEFAULT 0;' +WHERE (SELECT COUNT(*) FROM pragma_table_info('AppRssSubscriptionDownloads') WHERE name = 'IsPendingDueToLowDiskSpace') = 0; + +.output stdout + +-- 执行动态生成的 ALTER TABLE 语句 +-- 如果列已存在,临时文件为空,.read 不会执行任何操作 +.read /tmp/_migration_10_steps.sql + +-- 清理临时文件 +.shell rm -f /tmp/_migration_10_steps.sql + + +-- ============================================================ +-- 第三部分:迁移后验证 +-- ============================================================ + +SELECT '=== 迁移后验证 ==='; + +SELECT 'AppRssSubscriptionDownloads:' || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AppRssSubscriptionDownloads') WHERE name = 'IsPendingDueToLowDiskSpace') > 0 THEN ' ✅IsPendingDueToLowDiskSpace' ELSE ' ❌IsPendingDueToLowDiskSpace缺失' END; + +SELECT '✅ 磁盘空间暂存字段迁移完成(幂等安全,可重复执行)'; diff --git a/sql/14-fix-default-password.sql b/sql/14-fix-default-password.sql new file mode 100644 index 00000000..6d7ca8d2 --- /dev/null +++ b/sql/14-fix-default-password.sql @@ -0,0 +1,12 @@ +-- 修复默认密码哈希值 +-- 使用与 PasswordHasher.cs (PBKDF2-HMAC-SHA256, 16字节盐, 10000次迭代, 32字节哈希) 相同的算法生成 +-- 密码: qwe123# + +UPDATE AbpUsers +SET PasswordHash = 'ad1UIl5Y6YqFeRC+5ixQnPy9cW3wjY0QVFT25NiRu/DQmle5JZ+mJSxScxfOOsWV' +WHERE PasswordHash IS NULL OR PasswordHash = '8rZB1hd/U7b290OS9NGoVwQ13WanO9EfDHjqNzTQGsyIriXgmxg3dfAoaMCpP9pz'; + +-- 输出更新的用户数量 +SELECT COUNT(*) AS UpdatedCount +FROM AbpUsers +WHERE PasswordHash = 'ad1UIl5Y6YqFeRC+5ixQnPy9cW3wjY0QVFT25NiRu/DQmle5JZ+mJSxScxfOOsWV'; diff --git a/sql/15-grant-admin-new-permissions.sql b/sql/15-grant-admin-new-permissions.sql new file mode 100644 index 00000000..88ed911c --- /dev/null +++ b/sql/15-grant-admin-new-permissions.sql @@ -0,0 +1,99 @@ +-- 为 admin 角色补齐脚本 11 之后新增的权限 +-- 背景:脚本 11 (11-grant-admin-all-permissions.sql) 之后,DFAppPermissions.cs 中 +-- 新增了 UserManagement、RoleManagement、PermissionGrantManagement、 +-- UserRoleManagement、FileFilter、RssSubscription 共 6 个权限组 24 个权限, +-- 需要为 admin 角色补齐授予 +-- 日期:2026-04-26 +-- +-- 注意:使用 INSERT OR IGNORE 确保幂等性,重复执行不会出错 +-- 不指定 Id 字段(自增) + +-- ======================================== +-- UserManagement 用户管理(5个权限) +-- ======================================== +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.UserManagement', 'Role', 'admin'); + +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.UserManagement.Create', 'Role', 'admin'); + +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.UserManagement.Update', 'Role', 'admin'); + +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.UserManagement.Delete', 'Role', 'admin'); + +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.UserManagement.ChangePassword', 'Role', 'admin'); + +-- ======================================== +-- RoleManagement 角色管理(4个权限) +-- ======================================== +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.RoleManagement', 'Role', 'admin'); + +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.RoleManagement.Create', 'Role', 'admin'); + +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.RoleManagement.Update', 'Role', 'admin'); + +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.RoleManagement.Delete', 'Role', 'admin'); + +-- ======================================== +-- PermissionGrantManagement 权限授予管理(3个权限) +-- ======================================== +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.PermissionGrantManagement', 'Role', 'admin'); + +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.PermissionGrantManagement.Grant', 'Role', 'admin'); + +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.PermissionGrantManagement.Revoke', 'Role', 'admin'); + +-- ======================================== +-- UserRoleManagement 用户角色管理(3个权限) +-- ======================================== +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.UserRoleManagement', 'Role', 'admin'); + +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.UserRoleManagement.Assign', 'Role', 'admin'); + +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.UserRoleManagement.Remove', 'Role', 'admin'); + +-- ======================================== +-- FileFilter 文件过滤(4个权限) +-- ======================================== +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.FileFilter', 'Role', 'admin'); + +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.FileFilter.Create', 'Role', 'admin'); + +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.FileFilter.Edit', 'Role', 'admin'); + +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.FileFilter.Delete', 'Role', 'admin'); + +-- ======================================== +-- RssSubscription RSS订阅(5个权限) +-- ======================================== +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.RssSubscription', 'Role', 'admin'); + +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.RssSubscription.Create', 'Role', 'admin'); + +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.RssSubscription.Update', 'Role', 'admin'); + +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.RssSubscription.Delete', 'Role', 'admin'); + +INSERT OR IGNORE INTO AppPermissionGrants (PermissionName, ProviderType, ProviderKey) +VALUES ('DFApp.RssSubscription.Download', 'Role', 'admin'); diff --git a/src/DFApp.Web/Domain/Aria2/Aria2RpcClient.cs b/src/DFApp.Web/Domain/Aria2/Aria2RpcClient.cs index 0a7d2497..7c8d907f 100644 --- a/src/DFApp.Web/Domain/Aria2/Aria2RpcClient.cs +++ b/src/DFApp.Web/Domain/Aria2/Aria2RpcClient.cs @@ -5,7 +5,9 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; +using DFApp.Web.Data.Configuration; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace DFApp.Aria2; @@ -17,8 +19,11 @@ public class Aria2RpcClient { private readonly HttpClient _httpClient; private readonly IConfiguration _configuration; + private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger _logger; + private const string ModuleName = "DFApp.Aria2.Aria2RpcClient"; + // JSON 序列化选项:不区分大小写,使用驼峰命名 private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { @@ -26,27 +31,52 @@ public class Aria2RpcClient PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - public Aria2RpcClient(HttpClient httpClient, IConfiguration configuration, ILogger logger) + public Aria2RpcClient( + HttpClient httpClient, + IConfiguration configuration, + IServiceScopeFactory scopeFactory, + ILogger logger) { _httpClient = httpClient; _configuration = configuration; + _scopeFactory = scopeFactory; _logger = logger; } /// - /// 获取 RPC URL + /// 从数据库获取 RPC URL,读取失败时回退到 IConfiguration 或默认值 /// - private string GetRpcUrl() + private async Task GetRpcUrlAsync() { - return _configuration["Aria2:RpcUrl"] ?? "http://localhost:6800/jsonrpc"; + try + { + using var scope = _scopeFactory.CreateScope(); + var configRepo = scope.ServiceProvider.GetRequiredService(); + return await configRepo.GetConfigurationInfoValue("aria2rpc", ModuleName); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "从数据库读取 aria2rpc 配置失败,使用 IConfiguration 兜底值"); + return _configuration["Aria2:RpcUrl"] ?? "http://localhost:6800/jsonrpc"; + } } /// - /// 获取 RPC 密钥 + /// 从数据库获取 RPC 密钥,读取失败时回退到 IConfiguration 或空字符串 /// - private string GetSecret() + private async Task GetSecretAsync() { - return _configuration["Aria2:Secret"] ?? string.Empty; + try + { + using var scope = _scopeFactory.CreateScope(); + var configRepo = scope.ServiceProvider.GetRequiredService(); + return await configRepo.GetConfigurationInfoValue("aria2secret", ModuleName); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "从数据库读取 aria2secret 配置失败,使用 IConfiguration 兜底值"); + return _configuration["Aria2:Secret"] ?? string.Empty; + } } /// @@ -56,8 +86,8 @@ private async Task SendRequestAsync(string method, List parameter { try { - var rpcUrl = GetRpcUrl(); - var rpcToken = GetSecret(); + var rpcUrl = await GetRpcUrlAsync(); + var rpcToken = await GetSecretAsync(); // 添加 token 到参数 if (!string.IsNullOrWhiteSpace(rpcToken)) @@ -97,7 +127,7 @@ private async Task SendRequestAsync(string method, List parameter } catch (Exception ex) { - _logger.LogError(ex, "调用 Aria2 RPC 失败: {Method}, URL: {Url}", method, GetRpcUrl()); + _logger.LogError(ex, "调用 Aria2 RPC 失败: {Method}", method); throw; } } diff --git a/src/DFApp.Web/Program.cs b/src/DFApp.Web/Program.cs index 0a76860b..92dd7da2 100644 --- a/src/DFApp.Web/Program.cs +++ b/src/DFApp.Web/Program.cs @@ -276,8 +276,6 @@ public async static Task Main(string[] args) app.UseDeveloperExceptionPage(); } - app.UseStaticFiles(); - app.UseRouting(); if (!env.IsDevelopment())