diff --git a/AGENTS.md b/AGENTS.md index e6446a2b..e3bcc2d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,7 +46,7 @@ ``` DFApp/ ← 仓库根目录 ├── AGENTS.md ← 本文件 -├── DFApp.Web/ ← 后端项目 +├── src/DFApp.Web/ ← 后端项目 ├── DFApp.LotteryProxy/ ← 彩票代理服务 ├── test/DFApp.Web.Tests/ ← 单元测试 ├── client/ ← 前端项目(Vue 3) @@ -55,7 +55,7 @@ DFApp/ ← 仓库根目录 ``` ### 后端结构(轻量级单体架构) -- `DFApp.Web/` ← 唯一后端项目 +- `src/DFApp.Web/` ← 唯一后端项目 - `Domain/` - 实体和自定义基类 - `Services/` - 应用服务 - `Controllers/` - API 控制器(路由模式:`/api/app/{kebab-case-entity}`) diff --git a/sql/16-add-missing-concurrency-stamp.sql b/sql/16-add-missing-concurrency-stamp.sql new file mode 100644 index 00000000..3dee3699 --- /dev/null +++ b/sql/16-add-missing-concurrency-stamp.sql @@ -0,0 +1,81 @@ +-- ============================================================= +-- 为缺失 ConcurrencyStamp 列的表补充该列 +-- 幂等安全版:可安全地重复执行 +-- +-- 说明: +-- AppRssWordSegment、AppRssSubscriptionDownloads、AppMediaExternalLinkMediaIds +-- 三个表的对应实体(RssWordSegment、RssSubscriptionDownload、 +-- MediaExternalLinkMediaIds)继承自 EntityBase,后者定义了 +-- ConcurrencyStamp 列,但建表时遗漏了该列,导致查询时报错: +-- "no such column: ConcurrencyStamp" +-- +-- 幂等性原理: +-- SQLite 不支持 ALTER TABLE ADD COLUMN IF NOT EXISTS 语法, +-- 通过 pragma_table_info 检查列是否存在,仅对缺失的列生成 +-- ALTER TABLE 语句,使用 .output 重定向 + .read 执行的方式。 +-- ============================================================= + +.bail on +.headers off + +-- ============================================================ +-- 第一部分:检查当前状态 +-- ============================================================ + +SELECT '=== ConcurrencyStamp 列迁移前状态检查 ==='; + +SELECT 'AppRssWordSegment:' || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AppRssWordSegment') WHERE name = 'ConcurrencyStamp') > 0 THEN ' 已存在' ELSE ' 缺失' END; + +SELECT 'AppRssSubscriptionDownloads:' || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AppRssSubscriptionDownloads') WHERE name = 'ConcurrencyStamp') > 0 THEN ' 已存在' ELSE ' 缺失' END; + +SELECT 'AppMediaExternalLinkMediaIds:' || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AppMediaExternalLinkMediaIds') WHERE name = 'ConcurrencyStamp') > 0 THEN ' 已存在' ELSE ' 缺失' END; + + +-- ============================================================ +-- 第二部分:动态生成并执行 ALTER TABLE 语句 +-- 仅对不存在的列生成 ALTER TABLE,已存在的列自动跳过 +-- ============================================================ + +.output /tmp/_migration_16_steps.sql + +-- AppRssWordSegment(RssWordSegment : CreationAuditedEntity → EntityBase) +SELECT 'ALTER TABLE "AppRssWordSegment" ADD COLUMN "ConcurrencyStamp" TEXT NOT NULL DEFAULT '''';' +WHERE (SELECT COUNT(*) FROM pragma_table_info('AppRssWordSegment') WHERE name = 'ConcurrencyStamp') = 0; + +-- AppRssSubscriptionDownloads(RssSubscriptionDownload : CreationAuditedEntity → EntityBase) +SELECT 'ALTER TABLE "AppRssSubscriptionDownloads" ADD COLUMN "ConcurrencyStamp" TEXT NOT NULL DEFAULT '''';' +WHERE (SELECT COUNT(*) FROM pragma_table_info('AppRssSubscriptionDownloads') WHERE name = 'ConcurrencyStamp') = 0; + +-- AppMediaExternalLinkMediaIds(MediaExternalLinkMediaIds : EntityBase) +SELECT 'ALTER TABLE "AppMediaExternalLinkMediaIds" ADD COLUMN "ConcurrencyStamp" TEXT NOT NULL DEFAULT '''';' +WHERE (SELECT COUNT(*) FROM pragma_table_info('AppMediaExternalLinkMediaIds') WHERE name = 'ConcurrencyStamp') = 0; + +.output stdout + +-- 执行动态生成的 ALTER TABLE 语句 +-- 如果所有列都已存在,临时文件为空,.read 不会执行任何操作 +.read /tmp/_migration_16_steps.sql + +-- 清理临时文件 +.shell rm -f /tmp/_migration_16_steps.sql + + +-- ============================================================ +-- 第三部分:迁移后验证 +-- ============================================================ + +SELECT '=== 迁移后验证 ==='; + +SELECT 'AppRssWordSegment:' || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AppRssWordSegment') WHERE name = 'ConcurrencyStamp') > 0 THEN ' ✅ConcurrencyStamp' ELSE ' ❌ConcurrencyStamp缺失' END; + +SELECT 'AppRssSubscriptionDownloads:' || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AppRssSubscriptionDownloads') WHERE name = 'ConcurrencyStamp') > 0 THEN ' ✅ConcurrencyStamp' ELSE ' ❌ConcurrencyStamp缺失' END; + +SELECT 'AppMediaExternalLinkMediaIds:' || + CASE WHEN (SELECT COUNT(*) FROM pragma_table_info('AppMediaExternalLinkMediaIds') WHERE name = 'ConcurrencyStamp') > 0 THEN ' ✅ConcurrencyStamp' ELSE ' ❌ConcurrencyStamp缺失' END; + +SELECT '✅ ConcurrencyStamp 列迁移完成(幂等安全,可重复执行)'; diff --git a/src/DFApp.Web/Controllers/ExternalLinkController.cs b/src/DFApp.Web/Controllers/ExternalLinkController.cs index 37469d5f..2cb14468 100644 --- a/src/DFApp.Web/Controllers/ExternalLinkController.cs +++ b/src/DFApp.Web/Controllers/ExternalLinkController.cs @@ -56,6 +56,17 @@ public async Task GetAsync([FromRoute] long id) return Success(result); } + /// + /// 生成外链(后台任务) + /// + [HttpGet("external-link")] + [Permission(DFAppPermissions.Medias.Create)] + public async Task GetExternalLinkAsync() + { + var result = await _externalLinkService.GetExternalLink(); + return Success(result); + } + /// /// 分页获取外链列表 /// diff --git a/src/DFApp.Web/Program.cs b/src/DFApp.Web/Program.cs index 7530dfd4..1e626bec 100644 --- a/src/DFApp.Web/Program.cs +++ b/src/DFApp.Web/Program.cs @@ -87,6 +87,7 @@ public async static Task Main(string[] args) // 注册密码哈希服务(无状态,使用 Transient) builder.Services.AddTransient(); + builder.Services.AddTransient(); // 注册油价刷新器(无状态,使用 Transient) builder.Services.AddTransient(); @@ -232,7 +233,7 @@ public async static Task Main(string[] args) }); }); - Quartz.Logging.LogProvider.IsDisabled = true; + // Quartz.Logging.LogProvider.IsDisabled = true; builder.Services.AddQuartz(q => { // GasolinePriceRefreshJob — 每晚21:00执行