diff --git a/client/src/types/api.ts b/client/src/types/api.ts index 2e9c14a3..359edfba 100644 --- a/client/src/types/api.ts +++ b/client/src/types/api.ts @@ -264,12 +264,13 @@ export interface OilCostComparisonRequestDto { } export enum CostType { - Charging = 1, - Maintenance = 2, - Insurance = 3, - Parking = 4, - Repair = 5, - Other = 6 + Charging = 0, + Maintenance = 1, + Insurance = 2, + Other = 3, + Toll = 4, + Parking = 5, + Repair = 6 } export enum GasolineGrade { diff --git a/client/src/views/electric-vehicle/costs/index.vue b/client/src/views/electric-vehicle/costs/index.vue index c6ff65a9..07676c28 100644 --- a/client/src/views/electric-vehicle/costs/index.vue +++ b/client/src/views/electric-vehicle/costs/index.vue @@ -118,11 +118,12 @@ placeholder="请选择类型" style="width: 100%" > - - - - - + + + + + + @@ -219,12 +220,13 @@ const formRules: FormRules = { const getCostTypeName = (type: number) => { const map = { - 1: "充电", - 2: "保养", - 3: "保险", - 4: "停车", - 5: "维修", - 6: "其他" + 0: "充电", + 1: "保养", + 2: "保险", + 3: "其他", + 4: "过路费", + 5: "停车", + 6: "维修" }; return map[type] || "未知"; }; @@ -286,7 +288,7 @@ const handleCreate = () => { costDate: new Date().toISOString().split("T")[0], amount: 0, isBelongToSelf: true, - costType: 2, + costType: 1, vehicleId: vehicles.value.length === 1 ? vehicles.value[0].id : undefined, remark: "" }); diff --git a/sql/18-migrate-electric-vehicle-cost-type.sql b/sql/18-migrate-electric-vehicle-cost-type.sql new file mode 100644 index 00000000..bbd61f22 --- /dev/null +++ b/sql/18-migrate-electric-vehicle-cost-type.sql @@ -0,0 +1,77 @@ +-- ============================================================ +-- 迁移脚本: 18-migrate-electric-vehicle-cost-type.sql +-- 描述: 统一 AppElectricVehicleCost 表的 CostType 枚举值为 0-based +-- 日期: 2026-04-28 +-- ============================================================ +-- +-- 背景说明: +-- 旧前端 CostType 使用 1-based 枚举: +-- 1=充电, 2=保养, 3=保险, 4=停车, 5=维修, 6=其他 +-- +-- 后端原来使用 0-based 枚举: +-- 0=充电, 1=保养, 2=保险, 3=其他 +-- +-- 新的统一 0-based 枚举: +-- 0=充电, 1=保养, 2=保险, 3=其他, 4=过路费, 5=停车, 6=维修 +-- +-- 数据库中可能存在的情况: +-- 1. 如果后端直接存储了旧前端传来的 1-based 值,则数据库中有 1-6 +-- 2. 如果后端使用自己的 0-based 枚举,则数据库中有 0-3 +-- +-- 迁移策略(安全、幂等): +-- - 对于旧前端 1-based 值(4→停车, 5→维修, 6→其他),映射到新的 0-based 值 +-- - 对于后端 0-based 值(0-3),保持不变 +-- - 使用 CASE WHEN 确保只迁移需要迁移的值 +-- ============================================================ + +-- 先备份(如果还没备份的话,建议在执行前手动备份 DFApp.db) + +-- 步骤1: 查看当前数据分布(仅用于调试,注释掉以避免影响自动化执行) +-- SELECT CostType, COUNT(*) as cnt FROM AppElectricVehicleCost GROUP BY CostType ORDER BY CostType; + +-- 步骤2: 如果数据库中有旧前端 1-based 的值,需要迁移 +-- 注意:必须先处理值 4/5/6,再处理值 1,避免冲突 +-- 使用临时值 -1/-2/-3 避免迁移过程中的主键冲突 + +-- 旧 4 (停车) → 新 5 (停车) +UPDATE AppElectricVehicleCost +SET CostType = -1 +WHERE CostType = 4 + AND EXISTS (SELECT 1 FROM AppElectricVehicleCost WHERE CostType = 4); + +-- 旧 5 (维修) → 新 6 (维修) +UPDATE AppElectricVehicleCost +SET CostType = -2 +WHERE CostType = 5 + AND EXISTS (SELECT 1 FROM AppElectricVehicleCost WHERE CostType = 5); + +-- 旧 6 (其他) → 新 3 (其他) +UPDATE AppElectricVehicleCost +SET CostType = -3 +WHERE CostType = 6 + AND EXISTS (SELECT 1 FROM AppElectricVehicleCost WHERE CostType = 6); + +-- 旧 1 (充电, 前端1-based) → 新 0 (充电) +-- 只有当数据库中同时存在 CostType=0 和 CostType=1 时, +-- 说明数据库混合了两种编码方式,此时 CostType=1 是旧前端的充电记录 +-- 如果数据库中只有 CostType=1 没有 CostType=0,说明后端直接存了前端值,1=充电 +-- 如果数据库中只有 CostType=0 没有 CostType=1,说明后端用了自己的枚举,无需迁移 +UPDATE AppElectricVehicleCost +SET CostType = -4 +WHERE CostType = 1 + AND NOT EXISTS ( + -- 排除:如果 CostType=1 对应的是后端枚举的"保养"(即后端自己创建的记录) + -- 判断依据:如果数据库中存在 CostType=0(后端枚举的充电),说明后端使用了自己的枚举 + -- 此时 CostType=1 是后端的"保养",不应迁移 + SELECT 1 FROM AppElectricVehicleCost WHERE CostType = 0 + ); + +-- 步骤3: 将临时值更新为最终目标值 +UPDATE AppElectricVehicleCost SET CostType = 5 WHERE CostType = -1; +UPDATE AppElectricVehicleCost SET CostType = 6 WHERE CostType = -2; +UPDATE AppElectricVehicleCost SET CostType = 3 WHERE CostType = -3; +UPDATE AppElectricVehicleCost SET CostType = 0 WHERE CostType = -4; + +-- 步骤4: 验证迁移结果 +-- SELECT CostType, COUNT(*) as cnt FROM AppElectricVehicleCost GROUP BY CostType ORDER BY CostType; +-- 预期结果:CostType 值应全部在 0-6 范围内,不存在负数或超出范围的值 diff --git a/src/DFApp.Web/Background/LotteryResultJob.cs b/src/DFApp.Web/Background/LotteryResultJob.cs index bd0a31f8..97c209e0 100644 --- a/src/DFApp.Web/Background/LotteryResultJob.cs +++ b/src/DFApp.Web/Background/LotteryResultJob.cs @@ -172,7 +172,8 @@ private async Task ProcessResultsIndividually(List } LotteryResult entity = _mapper.MapToEntityFromExternalResultItem(item); - await _lotteryResultRepository.InsertAsync(entity); + // 使用 InsertReturnIdAsync 获取自增 Id,InsertAsync 不会回填自增主键 + entity.Id = await _lotteryResultRepository.InsertReturnIdAsync(entity); successCount++; // 同时写入该条结果对应的奖级信息 diff --git a/src/DFApp.Web/Background/RssMirrorFetchJob.cs b/src/DFApp.Web/Background/RssMirrorFetchJob.cs index 087c6e08..9c575e8d 100644 --- a/src/DFApp.Web/Background/RssMirrorFetchJob.cs +++ b/src/DFApp.Web/Background/RssMirrorFetchJob.cs @@ -157,10 +157,10 @@ private async Task FetchRssSource(RssSource source) } } - // 逐个插入镜像条目(确保自增ID回填) + // 逐个插入镜像条目,确保自增 Id 回填 foreach (var item in newItems) { - await _rssMirrorItemRepository.InsertAsync(item); + item.Id = await _rssMirrorItemRepository.InsertReturnIdAsync(item); } // 插入分词数据 diff --git a/src/DFApp.Web/Data/ISqlSugarRepository.cs b/src/DFApp.Web/Data/ISqlSugarRepository.cs index 27a40394..a84b163c 100644 --- a/src/DFApp.Web/Data/ISqlSugarRepository.cs +++ b/src/DFApp.Web/Data/ISqlSugarRepository.cs @@ -85,6 +85,13 @@ namespace DFApp.Web.Data; /// 插入的行数 Task InsertAsync(T entity); + /// + /// 插入实体并返回自增主键 ID + /// + /// 实体 + /// 自增主键 ID + Task InsertReturnIdAsync(T entity); + /// /// 批量插入实体 /// diff --git a/src/DFApp.Web/Data/SqlSugarRepository.cs b/src/DFApp.Web/Data/SqlSugarRepository.cs index 8473a831..70e7bf05 100644 --- a/src/DFApp.Web/Data/SqlSugarRepository.cs +++ b/src/DFApp.Web/Data/SqlSugarRepository.cs @@ -152,6 +152,16 @@ public async Task InsertAsync(T entity) return await _db.Insertable(entity).ExecuteCommandAsync(); } + /// + /// 插入实体并返回自增主键 ID + /// + /// 实体 + /// 自增主键 ID + public async Task InsertReturnIdAsync(T entity) + { + return await _db.Insertable(entity).ExecuteReturnBigIdentityAsync(); + } + /// /// 批量插入实体 /// diff --git a/src/DFApp.Web/Domain/ElectricVehicle/ElectricVehicleEnums.cs b/src/DFApp.Web/Domain/ElectricVehicle/ElectricVehicleEnums.cs index e787f3b6..45fb1dfa 100644 --- a/src/DFApp.Web/Domain/ElectricVehicle/ElectricVehicleEnums.cs +++ b/src/DFApp.Web/Domain/ElectricVehicle/ElectricVehicleEnums.cs @@ -23,7 +23,22 @@ public enum CostType /// /// 其他费用 /// - Other + Other = 3, + + /// + /// 过路费 + /// + Toll = 4, + + /// + /// 停车费 + /// + Parking = 5, + + /// + /// 维修费 + /// + Repair = 6 } /// diff --git a/src/DFApp.Web/Services/Aria2/Aria2Manager.cs b/src/DFApp.Web/Services/Aria2/Aria2Manager.cs index 0b866265..cd1b08c3 100644 --- a/src/DFApp.Web/Services/Aria2/Aria2Manager.cs +++ b/src/DFApp.Web/Services/Aria2/Aria2Manager.cs @@ -182,8 +182,8 @@ private async Task SaveTellStatusResultAsync(ResponseBase response) var filesItemRepository = scope.ServiceProvider.GetRequiredService>(); var urisItemRepository = scope.ServiceProvider.GetRequiredService>(); - // 保存主记录 - await resultRepository.InsertAsync(tellStatusResult); + // 保存主记录,使用 InsertReturnIdAsync 获取自增 Id + tellStatusResult.Id = await resultRepository.InsertReturnIdAsync(tellStatusResult); // 解析并保存文件列表 if (resultElement.TryGetProperty("files", out var filesElement) && filesElement.ValueKind == JsonValueKind.Array) @@ -201,7 +201,8 @@ private async Task SaveTellStatusResultAsync(ResponseBase response) Selected = fileElement.TryGetProperty("selected", out var selected) ? GetBoolValue(selected) : null }; - await filesItemRepository.InsertAsync(filesItem); + // 使用 InsertReturnIdAsync 获取自增 Id,用于子表外键关联 + filesItem.Id = (int)await filesItemRepository.InsertReturnIdAsync(filesItem); _logger.LogInformation(" 文件[{Index}]: {Path}, 长度: {Length}, 已完成: {CompletedLength}", filesItem.Index, filesItem.Path, filesItem.Length, filesItem.CompletedLength); diff --git a/src/DFApp.Web/Services/Lottery/LotteryDataFetchService.cs b/src/DFApp.Web/Services/Lottery/LotteryDataFetchService.cs index b0f73ced..e70f8041 100644 --- a/src/DFApp.Web/Services/Lottery/LotteryDataFetchService.cs +++ b/src/DFApp.Web/Services/Lottery/LotteryDataFetchService.cs @@ -145,8 +145,11 @@ public async Task FetchLotteryData(LotteryDataFetch results.Add(lotteryResult); } - // 先保存 LotteryResult - await _lotteryResultRepository.InsertAsync(results); + // 逐条保存 LotteryResult,确保自增 Id 正确回填 + foreach (var result in results) + { + result.Id = await _lotteryResultRepository.InsertReturnIdAsync(result); + } response.SavedCount = results.Count; // 保存关联的 Prizegrades diff --git a/src/DFApp.Web/Services/Media/ExternalLinkService.cs b/src/DFApp.Web/Services/Media/ExternalLinkService.cs index 3c1a3cb7..f9ec1f0a 100644 --- a/src/DFApp.Web/Services/Media/ExternalLinkService.cs +++ b/src/DFApp.Web/Services/Media/ExternalLinkService.cs @@ -182,7 +182,8 @@ public Task GetExternalLink() IsExternalLinkGenerated = true, IsDownloadCompleted = true, }; - await mediaInfoRepository.InsertAsync(zipMediaInfo); + // 使用 InsertReturnIdAsync 获取自增 Id,InsertAsync 不会回填自增主键 + zipMediaInfo.Id = await mediaInfoRepository.InsertReturnIdAsync(zipMediaInfo); temp.Add(zipMediaInfo); } @@ -211,7 +212,8 @@ public Task GetExternalLink() }); } - await externalLinkRepository.InsertAsync(mediaExternalLink); + // 使用 InsertReturnIdAsync 获取自增 Id,InsertAsync 不会回填自增主键 + mediaExternalLink.Id = await externalLinkRepository.InsertReturnIdAsync(mediaExternalLink); // 插入后获取外链 ID,为子记录赋值并批量插入 foreach (var item in mediaExternalLinkMediaIds)