# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## 项目概述 `sbcLabSystem` 是一个基于 **ASP.NET MVC 4 + Entity Framework 6 + SQL Server** 的实验室质控(QC)管理系统,部署目标为 IIS + .NET Framework **v4.8**。业务围绕**实验室质控(QC)发放 / 登记 / 答卷 / 审批 / 统计 / 邮件通知**展开,主要用户为检验实验室管理员。 没有 README,没有 docs 目录,没有已有的 CLAUDE.md —— 本文件是首份给 Claude 的项目说明。 ## 解决方案结构 一个 `sbcLabSystem.sln` 包含多个工程,分层依赖如下(顶层 Web 项目 → 下层类库): ``` sbcLabSystem (ASP.NET MVC Web 项目, 入口 Global.asax.cs) ├─ sbcLabSystem.Framework (Autofac DI 注册: DependencyRegistrar.cs) ├─ sbcLabSystem.Service (业务服务层: Account/QC/Task/Common) ├─ sbcLabSystem.Data (EF6 DbContext + Domain 实体 + Migrations) ├─ sbcLabSystem.Import (WinForms 导入/迁移工具, 基于老 edmx 模型) └─ EmailService (Windows Service, 邮件发送) 外部引用: ..\..\Common\PalGain\PalGain.Core (自研基础框架: IoC, 任务, 缓存, 仓储) ..\..\Common\WCF_BatchService\trunk\...Framework.Utility ``` 依赖关键点: - `PalGain.Core` 位于解决方案外的 `..\..\Common\PalGain\` —— 克隆仓库时需要同时拉取该路径,否则无法编译。`WCF_BatchService` 同理。 - `sbcLabSystem.Framework.DependencyRegistrar` 通过 `PalGainEngine.Instance.Register()` 在 `Application_Start` 自动注册到 Autofac 容器,业务服务通过构造器注入使用。 ## 常用命令 本项目是 .NET Framework(**不是 .NET Core/.NET 5+**),因此使用 MSBuild,而不是 `dotnet build`。 ### 还原与构建(Windows / Visual Studio 2019 环境) ```bash # 还原 NuGet 包(.NET Framework 项目必须用 nuget.exe,不能用 dotnet restore) nuget restore sbcLabSystem.sln # 用 MSBuild 构建整个解决方案(Debug) msbuild sbcLabSystem.sln /p:Configuration=Debug /p:Platform="Any CPU" # 只构建 Web 项目 msbuild sbcLabSystem\sbcLabSystem.csproj /p:Configuration=Debug ``` > 首选在 Visual Studio 2019+ 中直接打开 `sbcLabSystem.sln` 进行 F5 调试 —— 项目依赖老版 packages(EF 6.1.3、Autofac 3.5.2、MVC 4.0)并且引用了位于解决方案外的相对路径工程,命令行构建对环境要求严格。 ### 运行 - Web 项目通过 IIS Express(VS F5)或部署到 IIS 运行;入口是 `sbcLabSystem/Global.asax.cs`。 - `sbcLabSystem.Import` 是 WinForms 工具(`Program.cs` + `Form1.cs`),用于老系统数据迁移,**与线上 Web 运行无关**。 ### 数据库迁移(EF6 Code First) 数据库通过 EF6 Code First Migrations 管理。启动时 `ContextBuilder.InitDataBase()` 注册 `MigrateDatabaseToLatestVersion` —— 应用启动会自动把数据库升级到最新 migration。 新增迁移(Package Manager Console,默认项目选 `sbcLabSystem.Data`): ```powershell Add-Migration -ProjectName sbcLabSystem.Data -StartUpProjectName sbcLabSystem Update-Database -ProjectName sbcLabSystem.Data -StartUpProjectName sbcLabSystem ``` 连接字符串在 `sbcLabSystem/Web.config` 的 `` 节,名称 `FreseniusDBContext`。**切勿把开发库密码提交到仓库以外的公开位置。** ### 测试 仓库中**没有单测工程**。新增变更时若需要验证,只能通过跑起 Web 项目手测 / SQL 验证,或在 `sbcLabSystem.Import` 里写一次性脚本。提交完成声明前务必说明「无自动化测试覆盖」,避免假阳性成功。 ## 架构关键事实 阅读多文件才能看出的"大图",先在这里固化: ### 1. 请求管线与 DI 装配 1. `MvcApplication.Application_Start`(`sbcLabSystem/Global.asax.cs`)顺序做四件事: - `PalGainEngine.Instance.Register()` —— 扫描所有 `IDependencyRegistrar` 实现,用 Autofac 构建容器,并把 `AutofacDependencyResolver` 装到 MVC。 - `ContextBuilder.InitDataBase()` —— 启用 EF 自动迁移。 - MVC 标配三件套 `AreaRegistration / FilterConfig / RouteConfig / BundleConfig`。 - `TaskManager.Instance.Initialize(); Start();` —— 启动 PalGain 的后台任务调度,`sbcLabSystem.Service/Task/` 下的计划任务(如 `SendEmailTaskService`、`KeepAliveTask`)在这里跑起来。 2. `sbcLabSystem.Framework/DependencyRegistrar.cs` 是唯一的服务注册点: - `IDbContext` → `FreseniusDBContext`(`InstancePerLifetimeScope`,即每请求一个 DbContext)。 - `IRespository<>` → `EfRepository<>`(泛型仓储,来自 `PalGain.Core`)。 - 业务服务按需注册:`AccountService`、`QCService`、`ScheduleTaskService` 等。**新增 Service 时必须在此处注册**,否则 Controller 构造注入会抛 `DependencyResolutionException`。 ### 2. 数据层(`sbcLabSystem.Data`) - `DBContext/FreseniusDBContext.cs` 是**当前系统唯一在用的 DbContext**。里面用 Fluent API 定义了 QC 相关的三组外键关系(`QCDistribution ↔ QCDistributionRegisterInfo ↔ LabInfo`,以及 `QCDistribution ↔ StandAnswer` 的 1:1 外键映射为 `QCDistInfoId`)。 - `Domain/` 下按业务域分子目录:`Account`(UserInfo/RoleInfo/ResourceInfo/UserRequestInfo)、`Backstage`(QC 发放/登记、审批、邮件 SMTP、标准答案、结果占比等)、`Config`(LocationInfo)。所有实体继承自 `PalGain.Core.BaseEntity`。 - `Migrations/` 下的迁移是**线上真实历史**(最早 2016-12,长期演进)。**不要 rebase/squash 已应用的 migration**;只能追加 `Add-Migration`。 - `sbcLabSystem.Import` 工程里同时存在**另一套 edmx 模型**(`sbcLabSystem.edmx`、`hds123001_db.edmx`)和大量 POCO —— 这是给老数据导入工具用的 DB-First 产物,**与 Web 项目运行时的 Code First 模型是两条平行的独立链路**。修改 Web 业务时不要去改 `sbcLabSystem.Import` 下的实体。 ### 3. 业务服务层(`sbcLabSystem.Service`) - 按域拆子目录:`Account/AccountService.cs`、`QC/QCService.cs`(1100+ 行,核心质控发放逻辑)、`QC/AnswerService.cs`、`Task/SendEmailTaskService.cs` 等。 - `ExcelUtil.cs` + `sbcLabSystem/Excel/*.xlsx` 模板配合 —— 多处 Controller 通过模板 Excel 生成下发/回收文档,修改模板时需同步确认 `ExcelUtil` 的列映射。 - 邮件发送有两条路径:**Web 进程内**通过 `SendEmailTaskService`(PalGain 任务)周期执行;**独立** `EmailService` 工程是一个可部署的 Windows Service,用途是离线批量发邮件 —— 改邮件逻辑前先确认要改的是哪一条。 ### 4. Web 层(`sbcLabSystem`) - **Controllers**:`AccountController`(登录/权限)、`BackstageController`(**全系统后台主入口,1600+ 行,质控发放/登记/审批/答卷/统计/邮件/打印都塞在这一个 Controller 里**)、`HomeController`、`PdfViewController`、`UserUIController`。 - `BaseController` 做了两件事:把默认 `Json()` 结果改成 `JsonNetResult`(即 Json.NET 序列化,替代 MS 内置的 `JavaScriptSerializer`,以便处理循环引用与日期格式),以及用 iTextSharp 把 HTML 渲染成 PDF(`RenderToPDF` 被 `PdfViewController` 和 `BackstageController` 打印相关动作复用)。**新增控制器若需要返回 JSON 或者 HTML→PDF 能力,务必继承 `BaseController` 而非 `Controller`。** - **Models/Backstage/** 全是 ViewModel(不是实体),命名后缀为 `ViewModel` 或 `PageViewModel`。Controller 动作的入参/出参应走 ViewModel,不要直接暴露 EF 实体。 - **Reports/** 里有 `.rdlc` 报表文件(`EmsInfo.rdlc`、`EnvelopeInfo.rdlc` 等),通过 `PrintLabInfo` / `PrintEnvelope` 等动作渲染,参数由 `DataSet1.xsd` 定义。改报表列时三件套(`.rdlc` / `.xsd` / 传入的 ViewModel)必须同步。 ### 5. 配置与打包 - `Setup1` 工程是 Visual Studio 老式 Installer 项目,用于生成 Web 安装包,日常开发基本不用动。 - `UpgradeLog*.htm` 是旧版 VS 升级项目留下的报告,可忽略。 - `.gitignore` 已屏蔽 `bin/`、`obj/`、`packages/`,但仓库里依旧保留了 `packages/` 目录 —— 那是老项目直接把 NuGet 包签入的历史遗留,不要当作缺失。 ## QCDistributionRegisterInfo 保存契约 本仓库**没有** `(QCDistributionId, LabId, ProjectId)` 的数据库唯一索引(产品决定不动 DDL),防重复登记完全靠业务层。约定如下: - **唯一的保存入口**:所有保存 `QCDistributionRegisterInfo` 的路径都必须经过 `QCService.SaveQcDistributionRegister`。新写任何涉及登记表写入的代码,**不要**直接 `_qcDistributionRegisters.Insert`、不要绕开此方法。 - **Id==0 的语义**:该方法把 `Id==0` 视为"新增"请求,进入前会按 `(QCDistributionId, LabId, ProjectId)` 做查重: - 查不到 → 正常 Insert。 - 查到 → 打一条 `LogHelper.Error` 日志(强制告警),按 `MergeRegisterInfo` 规则**合并**到既有行后 UPDATE;同时把入参 `qcDistributionRegister.Id` 回写为 `existing.Id`,便于调用方后续引用。 - **MergeRegisterInfo 的合并规则(刻意不对称)**: - `string` 字段:`!string.IsNullOrEmpty(source)` 才覆盖(保留既有内容;空串也不覆盖,因为 `QCDistributionRegisterInfoViewModel.ToEntity` 里的 `entity.EMSNo = "" ?? ...` 这类 force-set 代码会把 null 变成 "", 若按 `!= null` 判断就会清空既有 `EMSNo`)。 - `DateTime?` 字段:`HasValue` 才覆盖。 - `DateTime ModifyTime`:非默认值才覆盖(调用方都用 `DateTime.Now`,天然非默认)。 - `bool` 字段:**只能 `false → true`,不能 `true → false`**。这是为了防止一次 Id==0 的脏 Save 把既有的 `IsCharged=1` 意外清零。 - `QCDistributionId / LabId / ProjectId`:业务唯一键,不动。 - **要把 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` 里的连接串指向的是哪个环境**。 - **隐式契约变更**:修改 ViewModel / Service 公共方法的返回结构、Map key 格式、枚举含义等,必须 grep 所有调用方与对应 `.cshtml` 视图(视图里常用 `@Model.XXX` 直接绑定),见全局 CLAUDE.md「隐式契约变更必须追踪所有调用方」。 - **Web.config 连接字符串包含明文口令**:现有代码已经把数据库口令以明文方式写在 `Web.config` 中。修改时**不要进一步扩大暴露面**(例如写进日志、复制到示例文件、贴进评审意见或 commit message)。若用户明确要求整改加密,再独立开任务处理。 ## 语言与沟通约定 代码注释与 UI 文案使用中文;对话回复使用中文(见全局设置)。方法名、类名、枚举值等标识符保持英文原样。