From e29739f628d93e9cf14ada688edfd5b78a64ebbe Mon Sep 17 00:00:00 2001 From: df123 Date: Mon, 27 Apr 2026 08:58:02 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix(docs):=20=E4=BF=AE=E6=AD=A3=20AGENTS.md?= =?UTF-8?q?=20=E4=B8=AD=E5=90=8E=E7=AB=AF=E9=A1=B9=E7=9B=AE=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将目录结构图中 DFApp.Web/ 修正为 src/DFApp.Web/ - 将后端结构描述中 DFApp.Web/ 修正为 src/DFApp.Web/ - 与实际项目结构及 .sln 文件引用保持一致 --- AGENTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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}`) From 5608e1fcfaf22bb1bd27b712387b2ade730ec737 Mon Sep 17 00:00:00 2001 From: df123 Date: Mon, 27 Apr 2026 09:00:08 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=E4=B8=BA=20AppRssWordSegment?= =?UTF-8?q?=E3=80=81AppRssSubscriptionDownloads=E3=80=81AppMediaExternalLi?= =?UTF-8?q?nkMediaIds=20=E8=A1=A8=E6=B7=BB=E5=8A=A0=E7=BC=BA=E5=A4=B1?= =?UTF-8?q?=E7=9A=84=20ConcurrencyStamp=20=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建 sql/16-add-missing-concurrency-stamp.sql 迁移脚本 - 修复 rss-mirror-items 页面 500 错误(根因:SQLite 报 no such column: ConcurrencyStamp) - 幂等脚本,可安全重复执行 - 同时修复 AppRssSubscriptionDownloads 和 AppMediaExternalLinkMediaIds 的相同问题 --- sql/16-add-missing-concurrency-stamp.sql | 81 ++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 sql/16-add-missing-concurrency-stamp.sql 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 列迁移完成(幂等安全,可重复执行)'; From 26330ac0e39efb83cd810365e38d4648b709143e Mon Sep 17 00:00:00 2001 From: df123 Date: Mon, 27 Apr 2026 20:50:00 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=A4=96?= =?UTF-8?q?=E9=93=BE=E7=94=9F=E6=88=90=E6=8E=A5=E5=8F=A3=EF=BC=88=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E4=BB=BB=E5=8A=A1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/DFApp.Web/Controllers/ExternalLinkController.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) 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); + } + /// /// 分页获取外链列表 /// From 91fc39ac58903b61d7c4bc34006fc8632e8bcf22 Mon Sep 17 00:00:00 2001 From: df123 Date: Mon, 27 Apr 2026 21:22:30 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=BD=A9=E7=A5=A8?= =?UTF-8?q?=E5=BC=80=E5=A5=96=E7=BB=93=E6=9E=9C=E5=AE=9A=E6=97=B6=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=97=A0=E6=B3=95=E6=89=A7=E8=A1=8C=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LotteryMapper 未注册到 DI 容器,从构造函数注入改为直接实例化 - 恢复 Quartz 框架级日志输出,避免 Job 实例化失败等错误被静默吞掉 --- src/DFApp.Web/Background/LotteryResultJob.cs | 5 ++--- src/DFApp.Web/Program.cs | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/DFApp.Web/Background/LotteryResultJob.cs b/src/DFApp.Web/Background/LotteryResultJob.cs index c9208f72..98ed35a4 100644 --- a/src/DFApp.Web/Background/LotteryResultJob.cs +++ b/src/DFApp.Web/Background/LotteryResultJob.cs @@ -24,7 +24,8 @@ public class LotteryResultJob : IJob private readonly ISqlSugarReadOnlyRepository _resultReadOnly; private readonly ISqlSugarRepository _lotteryPrizegradesRepository; private readonly ISqlSugarReadOnlyRepository _prizegradesReadOnly; - private readonly LotteryMapper _mapper; + // LotteryMapper 未注册到 DI 容器,直接实例化使用 + private readonly LotteryMapper _mapper = new(); private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _configuration; private readonly ILogger _logger; @@ -34,7 +35,6 @@ public LotteryResultJob( ISqlSugarReadOnlyRepository resultReadOnly, ISqlSugarRepository lotteryPrizegradesRepository, ISqlSugarReadOnlyRepository prizegradesReadOnly, - LotteryMapper mapper, IHttpClientFactory httpClientFactory, IConfiguration configuration, ILogger logger) @@ -43,7 +43,6 @@ public LotteryResultJob( _resultReadOnly = resultReadOnly; _lotteryPrizegradesRepository = lotteryPrizegradesRepository; _prizegradesReadOnly = prizegradesReadOnly; - _mapper = mapper; _httpClientFactory = httpClientFactory; _configuration = configuration; _logger = logger; diff --git a/src/DFApp.Web/Program.cs b/src/DFApp.Web/Program.cs index 7530dfd4..a359de46 100644 --- a/src/DFApp.Web/Program.cs +++ b/src/DFApp.Web/Program.cs @@ -232,7 +232,8 @@ public async static Task Main(string[] args) }); }); - Quartz.Logging.LogProvider.IsDisabled = true; + // 禁用 Quartz 日志会导致 Job 实例化失败等框架级错误被静默吞掉,保持日志开启以便排查问题 + // Quartz.Logging.LogProvider.IsDisabled = true; builder.Services.AddQuartz(q => { // GasolinePriceRefreshJob — 每晚21:00执行 From b3013d25be54277da762a5d89cc73d6465027827 Mon Sep 17 00:00:00 2001 From: df123 Date: Mon, 27 Apr 2026 21:26:33 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20=E6=B3=A8=E5=86=8C=20LotteryMapper?= =?UTF-8?q?=20=E5=88=B0=20DI=20=E5=AE=B9=E5=99=A8=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=BD=A9=E7=A5=A8=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E6=89=A7=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/DFApp.Web/Background/LotteryResultJob.cs | 5 +++-- src/DFApp.Web/Program.cs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/DFApp.Web/Background/LotteryResultJob.cs b/src/DFApp.Web/Background/LotteryResultJob.cs index 98ed35a4..c9208f72 100644 --- a/src/DFApp.Web/Background/LotteryResultJob.cs +++ b/src/DFApp.Web/Background/LotteryResultJob.cs @@ -24,8 +24,7 @@ public class LotteryResultJob : IJob private readonly ISqlSugarReadOnlyRepository _resultReadOnly; private readonly ISqlSugarRepository _lotteryPrizegradesRepository; private readonly ISqlSugarReadOnlyRepository _prizegradesReadOnly; - // LotteryMapper 未注册到 DI 容器,直接实例化使用 - private readonly LotteryMapper _mapper = new(); + private readonly LotteryMapper _mapper; private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _configuration; private readonly ILogger _logger; @@ -35,6 +34,7 @@ public LotteryResultJob( ISqlSugarReadOnlyRepository resultReadOnly, ISqlSugarRepository lotteryPrizegradesRepository, ISqlSugarReadOnlyRepository prizegradesReadOnly, + LotteryMapper mapper, IHttpClientFactory httpClientFactory, IConfiguration configuration, ILogger logger) @@ -43,6 +43,7 @@ public LotteryResultJob( _resultReadOnly = resultReadOnly; _lotteryPrizegradesRepository = lotteryPrizegradesRepository; _prizegradesReadOnly = prizegradesReadOnly; + _mapper = mapper; _httpClientFactory = httpClientFactory; _configuration = configuration; _logger = logger; diff --git a/src/DFApp.Web/Program.cs b/src/DFApp.Web/Program.cs index a359de46..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,6 @@ public async static Task Main(string[] args) }); }); - // 禁用 Quartz 日志会导致 Job 实例化失败等框架级错误被静默吞掉,保持日志开启以便排查问题 // Quartz.Logging.LogProvider.IsDisabled = true; builder.Services.AddQuartz(q => {