编辑 | blame | 历史 | 原始文档

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 环境)

# 还原 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<FreseniusDBContext, Configuration> —— 应用启动会自动把数据库升级到最新 migration。

新增迁移(Package Manager Console,默认项目选 sbcLabSystem.Data):

Add-Migration <MigrationName> -ProjectName sbcLabSystem.Data -StartUpProjectName sbcLabSystem
Update-Database -ProjectName sbcLabSystem.Data -StartUpProjectName sbcLabSystem

连接字符串在 sbcLabSystem/Web.config<connectionStrings> 节,名称 FreseniusDBContext。**切勿把开发库密码提交到仓库以外的公开位置。**

测试

仓库中**没有单测工程**。新增变更时若需要验证,只能通过跑起 Web 项目手测 / SQL 验证,或在 sbcLabSystem.Import 里写一次性脚本。提交完成声明前务必说明「无自动化测试覆盖」,避免假阳性成功。

架构关键事实

阅读多文件才能看出的"大图",先在这里固化:

1. 请求管线与 DI 装配

  1. MvcApplication.Application_StartsbcLabSystem/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/ 下的计划任务(如 SendEmailTaskServiceKeepAliveTask)在这里跑起来。
  1. sbcLabSystem.Framework/DependencyRegistrar.cs 是唯一的服务注册点:
  • IDbContextFreseniusDBContextInstancePerLifetimeScope,即每请求一个 DbContext)。
  • IRespository<>EfRepository<>(泛型仓储,来自 PalGain.Core)。
  • 业务服务按需注册:AccountServiceQCServiceScheduleTaskService 等。**新增 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.edmxhds123001_db.edmx)和大量 POCO —— 这是给老数据导入工具用的 DB-First 产物,**与 Web 项目运行时的 Code First 模型是两条平行的独立链路**。修改 Web 业务时不要去改 sbcLabSystem.Import 下的实体。

3. 业务服务层(sbcLabSystem.Service

  • 按域拆子目录:Account/AccountService.csQC/QCService.cs(1100+ 行,核心质控发放逻辑)、QC/AnswerService.csTask/SendEmailTaskService.cs 等。
  • ExcelUtil.cs + sbcLabSystem/Excel/*.xlsx 模板配合 —— 多处 Controller 通过模板 Excel 生成下发/回收文档,修改模板时需同步确认 ExcelUtil 的列映射。
  • 邮件发送有两条路径:**Web 进程内**通过 SendEmailTaskService(PalGain 任务)周期执行;**独立** EmailService 工程是一个可部署的 Windows Service,用途是离线批量发邮件 —— 改邮件逻辑前先确认要改的是哪一条。

4. Web 层(sbcLabSystem

  • ControllersAccountController(登录/权限)、BackstageController(**全系统后台主入口,1600+ 行,质控发放/登记/审批/答卷/统计/邮件/打印都塞在这一个 Controller 里**)、HomeControllerPdfViewControllerUserUIController
  • BaseController 做了两件事:把默认 Json() 结果改成 JsonNetResult(即 Json.NET 序列化,替代 MS 内置的 JavaScriptSerializer,以便处理循环引用与日期格式),以及用 iTextSharp 把 HTML 渲染成 PDF(RenderToPDFPdfViewControllerBackstageController 打印相关动作复用)。**新增控制器若需要返回 JSON 或者 HTML→PDF 能力,务必继承 BaseController 而非 Controller。**
  • Models/Backstage/ 全是 ViewModel(不是实体),命名后缀为 ViewModelPageViewModel。Controller 动作的入参/出参应走 ViewModel,不要直接暴露 EF 实体。
  • Reports/ 里有 .rdlc 报表文件(EmsInfo.rdlcEnvelopeInfo.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 里的收口形成双保险。修改这两处任一处时都要同步检查另一处。注意第一道防线命中后**只同步 IsChargedModifyTime**(基于 SaveLabList 的业务语义),第二道防线命中后走 MergeRegisterInfo 全字段合并(通用兜底);两者行为刻意不对称。
  • QCDistributionRegisterInfoViewModel.ToEntityByLabCode 的 ProjectId 过滤是**条件的**:只有 regInfoivewModel.ProjectId > 0 才加入 WHERE 子句,否则退化为只按 (QCDistributionId, LabId) 匹配。原因:前端 Views/Backstage/QCDistributionLabs.cshtmlopenFeeWindow / openEMSWindowCurrentQCDistRegisterInfo 重置为不含 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 业务耦合在一起。做变更时务必在动作内部就近改,**不要顺手重构或拆分文件** —— 这不是本次任务范围,且缺少测试覆盖,重构风险极高。
  • DI 注册点单一:所有新增 Service 类都要到 sbcLabSystem.Framework/DependencyRegistrar.cs 去手动注册;没有约定式自动装配。
  • DbContext 生命周期IDbContextInstancePerLifetimeScope,即每次 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 文案使用中文;对话回复使用中文(见全局设置)。方法名、类名、枚举值等标识符保持英文原样。