diff --git a/docs/04-后端开发规范.md b/docs/04-后端开发规范.md new file mode 100644 index 0000000..5092393 --- /dev/null +++ b/docs/04-后端开发规范.md @@ -0,0 +1,438 @@ +# CNC机床数据采集系统 - 后端开发规范 + +> 最后更新:2026-04-28 +> 适用范围:ASP.NET Web API 2 后端工程 + +--- + +## 一、技术栈 + +| 项 | 选型 | 版本 | +|----|------|------| +| IDE | Visual Studio 2017 | 15.9+ | +| 框架 | ASP.NET Web API 2 | .NET Framework 4.7.2 | +| ORM | Dapper | 最新稳定版 | +| 数据库 | MariaDB | 11.8 | +| 数据库驱动 | MySqlConnector | 最新稳定版(非Oracle的MySql.Data) | +| 测试框架 | xUnit | 最新稳定版 | +| Mock框架 | Moq | 最新稳定版 | +| JSON序列化 | Newtonsoft.Json | 12.0.3(VS2017兼容) | +| 认证 | JWT Bearer Token | 自行实现,不依赖Identity | +| 日志 | log4net | 最新稳定版 | + +--- + +## 二、解决方案结构 + +``` +E:\opencode\haoliang\ +├── CncDataSystem.sln ← VS2017 解决方案文件 +│ +├── src/ ← 源码目录 +│ ├── CncModels/ ← 数据模型层 +│ │ ├── CncModels.csproj +│ │ ├── Entity/ ← 数据库表对应的实体类 +│ │ ├── Dto/ ← API请求/响应的DTO类 +│ │ ├── Enum/ ← 枚举定义 +│ │ └── Constants/ ← 常量定义 +│ │ +│ ├── CncRepository/ ← 数据访问层 +│ │ ├── CncRepository.csproj +│ │ ├── Base/ ← 基础仓储(泛型CRUD) +│ │ └── Impl/ ← 各表的具体仓储实现 +│ │ +│ ├── CncService/ ← 业务逻辑层 +│ │ ├── CncService.csproj +│ │ ├── Interface/ ← 服务接口定义 +│ │ └── Impl/ ← 服务实现 +│ │ +│ └── CncWebApi/ ← Web API 主项目 +│ ├── CncWebApi.csproj +│ ├── App_Start/ ← WebApiConfig、FilterConfig +│ ├── Controllers/ ← API控制器 +│ ├── Filters/ ← 自定义过滤器(认证、异常) +│ ├── Infrastructure/ ← 中间件、扩展方法 +│ └── Web.config ← 数据库连接串、JWT密钥 +│ +├── tests/ ← 测试目录 +│ ├── CncModels.Tests/ ← 模型层测试 +│ ├── CncRepository.Tests/ ← 仓储层测试 +│ ├── CncService.Tests/ ← 服务层测试 +│ └── CncWebApi.Tests/ ← 控制器层测试 +│ +├── frontend/ ← 前端工程(解决方案文件夹引用,不编译) +├── docs/ ← 设计文档 +└── database/ ← 数据库脚本 +``` + +### 项目引用关系 + +``` +CncWebApi → CncService → CncRepository → CncModels + ↘ ↘ ↘ + CncModels CncModels (无依赖) +``` + +每个项目只引用自己直接依赖的项目,不跨层引用。 + +--- + +## 三、命名规范 + +### 3.1 通用规则 + +| 项 | 规范 | 示例 | +|----|------|------| +| 类名 | PascalCase | `MachineService` | +| 方法名 | PascalCase | `GetById()` | +| 参数名 | camelCase | `machineId` | +| 局部变量 | camelCase | `totalCount` | +| 私有字段 | _camelCase | `_connectionString` | +| 常量 | PascalCase 或 UPPER_SNAKE | `MaxRetryCount` | +| 枚举值 | PascalCase | `AlertType.CollectFail` | + +### 3.2 各层命名 + +| 层 | 类名后缀 | 示例 | 文件位置 | +|----|---------|------|---------| +| Entity | 无后缀 | `Machine`(对应表 `cnc_machine`) | `CncModels/Entity/` | +| 请求DTO | `Request` 后缀 | `CreateMachineRequest` | `CncModels/Dto/` | +| 响应DTO | `Response` 后缀 | `MachineListResponse` | `CncModels/Dto/` | +| 仓储接口 | `IRepository` 后缀 | `IMachineRepository` | `CncRepository/` | +| 仓储实现 | `Repository` 后缀 | `MachineRepository` | `CncRepository/Impl/` | +| 服务接口 | `IService` 后缀 | `IMachineService` | `CncService/Interface/` | +| 服务实现 | `Service` 后缀 | `MachineService` | `CncService/Impl/` | +| 控制器 | `Controller` 后缀 | `MachineController` | `CncWebApi/Controllers/` | +| 测试类 | `Tests` 后缀 | `MachineServiceTests` | `tests/CncService.Tests/` | + +### 3.3 Entity 与数据库表映射 + +```csharp +// 表名 cnc_machine → 类名 Machine +// 表名 cnc_daily_production → 类名 DailyProduction +// 表名 log_collect_raw → 类名 CollectRaw(跨日志库,仓储中指定库名) +// 表名前缀 cnc_ / log_ 在Entity类名中去掉 +``` + +### 3.4 控制器与路由 + +```csharp +// 控制器名 → 路由前缀 +// MachineController → /api/admin/machine +// DashboardController → /api/admin/dashboard +// ScreenController → /api/screen +// SysConfigController → /api/admin/sys-config + +// 路由模板统一使用属性路由 +[RoutePrefix("api/admin/machine")] +public class MachineController : ApiController +{ + [HttpGet, Route("")] + public IHttpActionResult GetList(...) { } + + [HttpGet, Route("{id:int}")] + public IHttpActionResult GetById(int id) { } + + [HttpPost, Route("")] + public IHttpActionResult Create([FromBody] CreateMachineRequest request) { } + + [HttpPut, Route("{id:int}")] + public IHttpActionResult Update(int id, [FromBody] UpdateMachineRequest request) { } + + [HttpDelete, Route("{id:int}")] + public IHttpActionResult Delete(int id) { } +} +``` + +--- + +## 四、注释规范 + +### 4.1 XML文档注释(必须) + +所有 **public 类、方法、属性、接口** 必须有 XML 文档注释(`///`)。 + +```csharp +/// +/// 机床管理服务,处理机床的CRUD操作和状态查询 +/// +public class MachineService : IMachineService +{ + /// + /// 获取机床分页列表 + /// + /// 查询条件(关键字、车间ID、在线状态等) + /// 分页结果,包含机床列表和总数 + public PagedResult GetList(MachineQuery query) + { + ... + } +} +``` + +### 4.2 行内注释(必须) + +复杂逻辑、业务规则、SQL语句必须有行内注释。 + +```csharp +// 日均单机产量 = 日期范围内总产量 / 天数 / 机床数 +// 多天范围时Y轴单位为"件/台/天",单天为"件/台" +var days = Math.Max(1, (endDate - startDate).Days + 1); +var avgQuantity = Math.Round((decimal)totalQuantity / days / machineCount, 1); +``` + +```csharp +// A-B-C-A-B场景:程序A第二次出现时创建新段记录 +// 日汇总按(machine_id, production_date, program_name)合并 +if (currentSegment.ProgramName != newProgramName) +{ + CloseSegment(currentSegment, "program_change"); + CreateSegment(machineId, newProgramName); +} +``` + +### 4.3 文件头注释(必须) + +每个 .cs 文件顶部必须有文件头注释: + +```csharp +/// +/// 机床管理控制器 +/// 对应页面:MachineListPage、MachineDetailPage +/// API文档:docs/03-API接口设计.md → 3.3 设备管理模块 +/// 数据库表:cnc_machine, cnc_worker_machine, cnc_collect_record +/// +``` + +--- + +## 五、编码规范 + +### 5.1 分层职责 + +| 层 | 职责 | 不做 | +|----|------|------| +| **Controller** | 参数校验、调用Service、包装响应格式 | 不写业务逻辑、不直接操作数据库 | +| **Service** | 业务逻辑编排、数据转换、事务管理 | 不直接写SQL、不返回IHttpActionResult | +| **Repository** | SQL编写、数据库读写、对象映射 | 不写业务判断、不知道API概念 | +| **Models** | 纯数据定义、枚举、常量 | 不包含任何逻辑 | + +### 5.2 统一响应格式 + +所有 API 返回统一的 `ApiResponse` 包装: + +```csharp +/// +/// 统一API响应格式 +/// +public class ApiResponse +{ + /// 错误码,0=成功 + public int Code { get; set; } + /// 提示信息 + public string Message { get; set; } + /// 业务数据 + public T Data { get; set; } + + public static ApiResponse Success(T data, string message = "success") + => new ApiResponse { Code = 0, Message = message, Data = data }; + + public static ApiResponse Fail(int code, string message) + => new ApiResponse { Code = code, Message = message, Data = default(T) }; +} +``` + +### 5.3 异常处理 + +```csharp +// Controller 层不 try-catch,由全局异常过滤器统一处理 +// Service 层抛出业务异常 +public class BusinessException : Exception +{ + public int Code { get; } + public BusinessException(int code, string message) : base(message) { Code = code; } +} + +// 使用示例 +if (existingMachine != null) + throw new BusinessException(40003, "设备编码已存在"); + +// 全局异常过滤器 +public class GlobalExceptionFilter : ExceptionFilterAttribute +{ + public override void OnException(HttpActionExecutedContext context) + { + if (context.Exception is BusinessException bex) + { + context.Response = ... // 返回 { code: bex.Code, message: bex.Message } + } + else + { + // 记录日志,返回 50001 + } + } +} +``` + +### 5.4 数据库连接管理 + +```csharp +// Repository 基类提供连接,每个方法 using 自动释放 +public abstract class BaseRepository +{ + private readonly string _connectionString; + + protected BaseRepository(string connectionString) + { + _connectionString = connectionString; + } + + /// + /// 创建新的数据库连接,调用方需 using 释放 + /// + protected IDbConnection CreateConnection() + { + return new MySqlConnection(_connectionString); + } +} + +// 使用示例 +public Machine GetById(int id) +{ + using (var conn = CreateConnection()) + { + return conn.QueryFirstOrDefault( + "SELECT * FROM cnc_machine WHERE id = @Id", new { Id = id }); + } +} +``` + +### 5.5 双库切换 + +```csharp +// 业务库和日志库连接串不同,通过两个 BaseRepository 子类区分 +public class BusinessRepository : BaseRepository { ... } // → cnc_business +public class LogRepository : BaseRepository { ... } // → cnc_log + +// 需要访问日志库的仓储继承 LogRepository +// 其他继承 BusinessRepository +``` + +--- + +## 六、测试规范 + +### 6.1 覆盖率要求 + +- **每个 public 方法必须有至少一个测试用例** +- **分支覆盖**:if/else 每个分支都要测到 +- **异常路径**:参数校验失败、数据不存在等异常场景必须覆盖 +- 目标:**100% 方法覆盖,≥90% 分支覆盖** + +### 6.2 测试命名 + +``` +[Method]_[Scenario]_[ExpectedResult] + +示例: +GetList_WithKeywordFilter_ReturnsFilteredMachines +GetById_WhenNotExists_ThrowsBusinessException +Create_WithDuplicateDeviceCode_Throws40003 +``` + +### 6.3 测试结构(AAA模式) + +```csharp +[Fact] +public void GetList_WithKeywordFilter_ReturnsFilteredMachines() +{ + // Arrange - 准备数据 + var mockRepo = new Mock(); + mockRepo.Setup(r => r.GetList(It.IsAny())) + .Returns(new List { ... }); + + var service = new MachineService(mockRepo.Object); + + // Act - 执行操作 + var result = service.GetList(new MachineQuery { Keyword = "西" }); + + // Assert - 验证结果 + Assert.Single(result.Items); + Assert.Contains("西", result.Items[0].Name); +} +``` + +### 6.4 各层测试策略 + +| 层 | 测试方式 | Mock对象 | +|----|---------|---------| +| **Controller** | 测试路由匹配、参数校验、响应格式 | Mock Service | +| **Service** | 测试业务逻辑、数据转换、异常抛出 | Mock Repository | +| **Repository** | 测试SQL正确性、数据映射 | 使用内存数据库或测试库 | +| **Models** | 测试属性默认值、枚举值、验证逻辑 | 无依赖 | + +### 6.5 测试项目配置 + +```csharp +// 每个测试项目引用对应的生产项目 +// CncService.Tests → 引用 CncService、CncModels +// 使用 Moq 框架 mock 接口 +// 测试数据通过 [InlineData] 或测试初始化方法提供 +``` + +--- + +## 七、依赖注入 + +使用 Unity 或手动实现简易 DI 容器(VS2017 默认不支持 .NET Core 风格的内置DI)。 + +```csharp +// App_Start/UnityConfig.cs 注册依赖 +container.RegisterType(); +container.RegisterType(); +``` + +每个 Service 通过构造函数注入依赖的 Repository: + +```csharp +public class MachineService : IMachineService +{ + private readonly IMachineRepository _machineRepo; + private readonly IWorkerRepository _workerRepo; + + public MachineService(IMachineRepository machineRepo, IWorkerRepository workerRepo) + { + _machineRepo = machineRepo; + _workerRepo = workerRepo; + } +} +``` + +--- + +## 八、Git提交规范 + +- 每完成一个模块(含测试)提交一次 +- 提交信息中文,格式:`feat(module): 描述` +- 编译+测试通过后才能提交 +- 与前端共用同一仓库 `main` 分支 + +--- + +## 九、开发顺序 + +按依赖关系从底层往上开发: + +| 步骤 | 内容 | 依赖 | +|------|------|------| +| 1 | 搭建解决方案 + 项目结构 + NuGet包 | 无 | +| 2 | CncModels(Entity + Dto + Enum) | 无 | +| 3 | CncModels.Tests | 步骤2 | +| 4 | CncRepository(基础仓储 + 连接管理) | 步骤2 | +| 5 | CncRepository.Tests(登录/系统配置/车间) | 步骤4 | +| 6 | CncService(业务逻辑) | 步骤4 | +| 7 | CncService.Tests | 步骤6 | +| 8 | CncWebApi(控制器 + 路由 + 过滤器) | 步骤6 | +| 9 | CncWebApi.Tests | 步骤8 | + +先跑通 **登录→系统设置→车间管理** 这条最小链路,再逐步扩展其他模块。