| | |
| | | - **要把 bool 清回 false、或清空 string/Date 字段,必须走 Id>0 路径**:先用 `GetQcDistributionRegister(id)` 查出 tracked entity,直接改字段再 `SaveQcDistributionRegister`。用 `Id=0 + IsCharged=false` 这种方式期望"反收费"会被静默忽略。 |
| | | - **生产日志里出现 `SaveQcDistributionRegister 命中已有行`(Error 级别)** = 某个上层调用方漏做了查重或前端提交了脏 Id,**必须排查根因**。这条日志**刻意用 `LogHelper.Error`** 而不是 Info —— 因为本仓库的 `LogHelper` 没有 Warn 方法,而 Info 级别不会触发告警系统;用 Error 保证运维能第一时间收到通知。 |
| | | - **`SaveLabList` 路径(`QCDistributionInfoViewModel.ToEntity`)**:作为第一道防线,保留了自己的 `(QCDist, LabId, ProjectId)` 查重分支 —— 与 `SaveQcDistributionRegister` 里的收口形成双保险。修改这两处任一处时都要同步检查另一处。注意第一道防线命中后**只同步 `IsCharged` 和 `ModifyTime`**(基于 SaveLabList 的业务语义),第二道防线命中后走 `MergeRegisterInfo` 全字段合并(通用兜底);两者行为刻意不对称。 |
| | | - **SaveLabList 的 byId / byLabProject 字典是循环前的快照**:`QCDistributionInfoViewModel.ToEntity` 在循环开始前一次性 `WHERE QCDistributionId=? ` 预加载全部登记行进内存,构建两个字典供循环查找。这意味着**同一次 SaveLabList 请求内,前端 LabList 里如果有两条完全一致的 `(LabId, ProjectId)` 条目**,第一道防线**看不到第一次 Insert 刚产生的那一行**(字典没更新),第二次循环会继续走 Insert 分支 —— 最终落到 `SaveQcDistributionRegister` 的**第二道防线兜底**,触发 `LogHelper.Error` 并合并到已有行。这是**刻意的分层设计**:第一道防线负责 99% 的已入库重复,第二道防线负责同批提交的 race。不要试图"为了一致性"把第一道防线改成在循环内动态刷新字典 —— 那反而会放大并发窗口。 |
| | | - **`byLabProject` 的 `GroupBy.First()` 用 `OrderByDescending(p.Id)` 保证确定性**:如果数据库里历史上有未清理的重复(不应该发生,但作防御),这个顺序保证永远命中 Id 最大(最新插入)的那一条。修改这个 LINQ 表达式时必须保留 `OrderByDescending`,否则行为变得不确定。 |
| | | - **`QCDistributionRegisterInfoViewModel.ToEntityByLabCode`** 的 ProjectId 过滤是**条件的**:只有 `regInfoivewModel.ProjectId > 0` 才加入 WHERE 子句,否则退化为只按 `(QCDistributionId, LabId)` 匹配。原因:前端 `Views/Backstage/QCDistributionLabs.cshtml` 的 `openFeeWindow` / `openEMSWindow` 把 `CurrentQCDistRegisterInfo` 重置为不含 ProjectId 字段的 JSON 字面量,序列化后服务端 `regInfoivewModel.ProjectId == 0`,无条件加过滤会让 `ConfigrmFee` / `ConfigrmEMS` / `switchNextOne` 全部静默失效。**未来若前端补上 ProjectId 选择控件**,服务端会自动切换到精准匹配,不需要改服务端代码。多项目共存时当前会匹配第一条(与修复前行为一致)。 |
| | | - **`BackstageController.ImportLabs`**:是唯一绕过 `SaveQcDistributionRegister` 的写入路径(直接操作 `QCDistribution.QCDistributionRegisters` 导航集合)。它自己在源读取后用 `GroupBy(LabId, ProjectId).Select(g => g.First())` 去重;**修改 ImportLabs 时必须保留这段 GroupBy**,否则脏数据会跨分发传染。 |
| | | - **残余风险**(已知,不修复):两个管理员并发编辑同一分发时,两个独立 DbContext 的查重都可能返回空,仍有产生重复的理论窗口。本仓库不通过 DB 约束兜底,缓解办法是前端按钮防抖 + 监控 `SaveQcDistributionRegister 命中已有行` 的 Error 日志。如果日后发现并发窗口被真实触发,加唯一索引是根治方案(见"一致性"章节末尾)。 |
| | |
| | | ## 修改时的红线(针对本仓库的具体风险) |
| | | |
| | | - **`BackstageController` 是超级 Controller**:1600+ 行、几十个动作、大量 QC 业务耦合在一起。做变更时务必在动作内部就近改,**不要顺手重构或拆分文件** —— 这不是本次任务范围,且缺少测试覆盖,重构风险极高。 |
| | | - **Excel 导出的 `headers[]` 和 `cellKes[]` 是手动对齐的平行数组**:`BackstageController` 里每个导出 action(`OutputUnderTakenLabs` / `LabExcelDate` / `LabExcel` / `Quality_Excel` / `ToExcel` / ...)都用两个字符串数组靠**下标顺序对齐** —— `headers[i]` 是 Excel 第 i 列的中文标签,`cellKes[i]` 是对应的属性名(反射读 DataTable 列)。数组长度必须严格一致,语义顺序必须一一对应,**没有任何编译期/运行期检查**。历史上已经出过两次相同模式的 bug:(1) `15e82c0` 追加"是否收费"时只加到末尾忘了检查前面已有一份,导致重复列(`a3f4e4f` 已修);(2) `LabExcelDate`/`LabExcel` 长期把 `Province` 字段放在标签为"城市"的列下,admin 以为 Province 没保存(`d5a91a2` 已修)。**改这类导出的铁律**:每次修改都必须把 `headers` 和 `cellKes` 两个数组并排看一遍,逐下标核对中文标签和字段名是否语义匹配;新增列时两个数组都要加;删除列时两个数组都要删。如果下次排查的 bug 形态是"导出看不到数据"或"导出数据不对劲",**先去检查这俩数组的对齐而不是写路径**。 |
| | | - **DI 注册点单一**:所有新增 Service 类都要到 `sbcLabSystem.Framework/DependencyRegistrar.cs` 去手动注册;没有约定式自动装配。 |
| | | - **DbContext 生命周期**:`IDbContext` 是 `InstancePerLifetimeScope`,即每次 HTTP 请求共享一个 `FreseniusDBContext`。**不要在 Service 内部 `new FreseniusDBContext()`**,会绕过事务 / 变更跟踪。 |
| | | - **EF 迁移自动升级**:启动即跑 `MigrateDatabaseToLatestVersion`。本地改 migration 后第一次运行 Web 就会对你连的数据库执行 DDL —— 在连生产/共享库时要特别小心,**改 migration 前先确认 `Web.config` 里的连接串指向的是哪个环境**。 |