You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
haoliang-net/docs/04-后端开发规范.md

714 lines
25 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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.3VS2017兼容 |
| 认证 | 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
/// <summary>
/// 机床管理服务处理机床的CRUD操作和状态查询
/// </summary>
public class MachineService : IMachineService
{
/// <summary>
/// 获取机床分页列表
/// </summary>
/// <param name="query">查询条件关键字、车间ID、在线状态等</param>
/// <returns>分页结果,包含机床列表和总数</returns>
public PagedResult<MachineListItem> 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
/// <summary>
/// 机床管理控制器
/// 对应页面MachineListPage、MachineDetailPage
/// API文档docs/03-API接口设计.md → 3.3 设备管理模块
/// 数据库表cnc_machine, cnc_worker_machine, cnc_collect_record
/// </summary>
```
---
## 五、编码规范
### 5.1 分层职责
| 层 | 职责 | 不做 |
|----|------|------|
| **Controller** | 参数校验、调用Service、包装响应格式 | 不写业务逻辑、不直接操作数据库 |
| **Service** | 业务逻辑编排、数据转换、事务管理 | 不直接写SQL、不返回IHttpActionResult |
| **Repository** | SQL编写、数据库读写、对象映射 | 不写业务判断、不知道API概念 |
| **Models** | 纯数据定义、枚举、常量 | 不包含任何逻辑 |
### 5.2 统一响应格式
所有 API 返回统一的 `ApiResponse<T>` 包装:
```csharp
/// <summary>
/// 统一API响应格式
/// </summary>
public class ApiResponse<T>
{
/// <summary>错误码0=成功</summary>
public int Code { get; set; }
/// <summary>提示信息</summary>
public string Message { get; set; }
/// <summary>业务数据</summary>
public T Data { get; set; }
public static ApiResponse<T> Success(T data, string message = "success")
=> new ApiResponse<T> { Code = 0, Message = message, Data = data };
public static ApiResponse<T> Fail(int code, string message)
=> new ApiResponse<T> { Code = code, Message = message, Data = default(T) };
}
```
### 5.2.1 Mock数据结构对齐强制
> **核心原则:后端 API 返回的 `data` 内部结构必须严格匹配 Mock 文件中的定义。前端代码按 Mock 结构取值,后端返回格式不一致将导致前端取不到数据。**
#### 规则说明
1. **列表接口**Mock 返回 `{ code: 0, data: { items: [...], total: N, page: N, pageSize: N } }`,后端必须返回 `ApiResponse<PagedResult<T>>``PagedResult` 包含 `items/total/page/pageSize` 四个字段
2. **非分页列表接口**如车间下拉、品牌下拉等Mock 返回 `{ code: 0, data: { items: [...] } }`,后端必须返回 `ApiResponse<object>` 并将列表包装为 `new { items = list }`**禁止**直接返回 `ApiResponse<List<T>>`
3. **单对象接口**如详情、统计Mock 返回 `{ code: 0, data: { ... } }`,后端返回 `ApiResponse<T>` 即可
4. **写操作接口**(新增/编辑/删除Mock 返回 `{ code: 0, message: "success", data: null }``{ code: 0, data: { id: N } }`,后端保持一致
5. **字段名必须与 Mock 一致**:经 CamelCase 序列化后的 JSON 字段名必须与 Mock 中的 key 完全匹配
#### 错误示例与正确示例
```csharp
// ❌ 错误:直接返回 Listdata 变成数组,前端 r.data.items 为 undefined
return Ok(ApiResponse<List<WorkshopListItem>>.Success(result));
// JSON: { "code": 0, "data": [{...}, {...}] }
// ✅ 正确:包装为 items 对象,与 Mock 的 { data: { items: [...] } } 一致
return Ok(ApiResponse<object>.Success(new { items = result }));
// JSON: { "code": 0, "data": { "items": [{...}, {...}] } }
```
```csharp
// ❌ 错误PagedResult 的 Items 字段不匹配(虽然是 PagedResult
// 如果前端期望 { items, total, page, pageSize }PagedResult 字段名必须完全匹配
// 当前 PagedResult<T> 已定义为 Items/Total/Page/PageSize经 CamelCase 序列化为 items/total/page/pageSize ✅
// ✅ 分页列表直接返回 PagedResult 即可
return Ok(ApiResponse<PagedResult<MachineListItem>>.Success(result));
// JSON: { "code": 0, "data": { "items": [...], "total": 160, "page": 1, "pageSize": 20 } }
```
#### 检查方法
每个接口开发完成后,必须:
1. 打开对应的 Mock 文件(`frontend/mock/*.ts`),找到该 URL 的 `response` 定义
2. 确认 Mock 中 `data` 的结构(是 `{ items: [...] }` 还是 `{ ... }` 或是 `null`
3. 确认后端 `ApiResponse<T>` 中的 `T` 能序列化出相同结构
4. 如有差异,调整后端返回格式以匹配 Mock
### 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;
}
/// <summary>
/// 创建新的数据库连接,调用方需 using 释放
/// </summary>
protected IDbConnection CreateConnection()
{
return new MySqlConnection(_connectionString);
}
}
// 使用示例
public Machine GetById(int id)
{
using (var conn = CreateConnection())
{
return conn.QueryFirstOrDefault<Machine>(
"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 方法必须有至少一个测试用例**
- 目标:**100% 方法覆盖≥95% 分支覆盖**
- 内部 private 方法通过 public 方法间接覆盖,不单独测试
### 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<IMachineRepository>();
mockRepo.Setup(r => r.GetList(It.IsAny<MachineQuery>()))
.Returns(new List<Machine> { ... });
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正确性、数据映射 | 使用测试数据库真实MariaDB |
| **Models** | 测试属性默认值、枚举值、验证逻辑 | 无依赖 |
### 6.5 测试项目配置
```csharp
// 每个测试项目引用对应的生产项目
// CncService.Tests → 引用 CncService、CncModels
// 使用 Moq 框架 mock 接口
// 测试数据通过 [InlineData] 或测试初始化方法提供
```
---
### 6.6 必测场景清单(每个方法逐项检查)
以下场景**适用于所有层的每个 public 方法**,开发者编写测试时必须逐项对照,确认是否需要覆盖:
#### A. 正常路径Happy Path
| 场景 | 说明 | 示例 |
|------|------|------|
| 标准输入 | 常规参数,期望正常返回 | `Create(validRequest)` → 成功 |
| 边界值-最小 | 数值型参数传最小合法值 | `pageSize=1`、`sortOrder=0` |
| 边界值-最大 | 数值型参数传最大合法值 | `pageSize=100`、`sortOrder=99` |
| 边界值-零 | 数值型参数传0 | `collectInterval=0`(如果业务允许) |
| 边界值-空集合 | 查询结果为空列表 | `GetList(不存在的关键词)``items=[], total=0` |
| 边界值-单条数据 | 查询结果恰好1条 | `GetList(精确匹配)``items=[1条]` |
| 边界值-满页 | 查询结果恰好等于pageSize | 分页边界不丢数据 |
| 边界值-跨页 | 查询结果超过pageSize翻页正确 | 第1页20条、第2页从第21条开始 |
| 字符串-最大长度 | 字符串参数传maxlength边界 | `name` 传100字符 |
| 字符串-含特殊字符 | 中文、括号、横杠、空格 | `"A栋二期"`、`"西-1.8"` |
#### B. 参数校验异常(输入不合法)
| 场景 | 说明 | 示例 |
|------|------|------|
| null 参数 | 必填参数传 null | `Create(null)` → 抛出 ArgumentNullException |
| 空字符串 | 必填字符串传 `""` | `name=""` → 校验失败 |
| 纯空格 | 字符串参数传 `" "` | `name=" "` → 视为空 |
| 超长字符串 | 超过 maxlength | `name` 传201字符 → 校验失败 |
| 负数 | 应为正整数的参数传负数 | `collectInterval=-1` → 校验失败 |
| 超范围数值 | 超出业务允许范围 | `sortOrder=100` → 校验失败 |
| 非法格式 | IP地址、URL格式不正确 | `ipAddress="abc"` → 校验失败 |
| 缺失必填字段 | 请求体缺少必填字段 | 只有 `name` 没有 `deviceCode` |
| 类型不匹配 | 数字字段传字符串等 | pageSize="abc"API层拦截 |
#### C. 业务规则异常(输入合法但违反业务约束)
| 场景 | 说明 | 示例 |
|------|------|------|
| 唯一性冲突 | 唯一键重复 | `Create(已存在的deviceCode)` → 40003 |
| 外键不存在 | 关联实体不存在 | `Create(workshopId=999)` → 外键校验失败 |
| 状态不允许操作 | 当前状态下不能执行该操作 | 删除有关联机床的车间 → 40001 |
| 资源不存在 | 操作的目标ID不存在 | `Update(id=99999)` → 40002 |
| 资源已停用 | 操作已停用的资源 | 编辑已停用的品牌(如果业务限制) |
| 跨天日期逻辑 | 开始日期 > 结束日期 | `startDate=4月30日, endDate=4月28日` → 校验失败 |
| 密码校验 | 旧密码不正确 | `changePassword(错误旧密码)` → 40001 |
| 自引用冲突 | 不能删除自己 | 不能删除正在使用的Token如果适用 |
#### D. 数据一致性(并发和状态)
| 场景 | 说明 | 示例 |
|------|------|------|
| 并发修改 | 两人同时编辑同一条记录 | 以最后一次为准,不报错 |
| 先查后改一致性 | 查出来时有数据,改的时候数据已被删 | 返回40002而非抛空引用异常 |
| 事务完整性 | 多表操作部分失败时全部回滚 | 创建机床+绑定工人,工人绑定失败时机床也回滚 |
| 批量操作部分失败 | 批量处理中部分成功部分失败 | 全部失败或全部成功,不允许部分成功 |
#### E. 边界日期和时间
| 场景 | 说明 | 示例 |
|------|------|------|
| 今日 | `startDate=endDate=今天` | 单天查询 |
| 昨日 | 查询昨天的数据 | 跨天边界正确 |
| 大范围 | `startDate``endDate` 差90天 | 不报错SQL性能可接受 |
| 跨月 | 查询范围跨越月份分区 | 分区表查询正确4月30日~5月2日 |
| 日期为null | 可选日期参数不传 | 使用默认值(今日) |
#### F. 安全性
| 场景 | 说明 | 示例 |
|------|------|------|
| SQL注入 | 参数含SQL片段 | `keyword="' OR 1=1 --"` → 不影响查询 |
| XSS | 参数含脚本标签 | `name="<script>alert(1)</script>"` → 原样存储不执行 |
| 敏感字段脱敏 | Token/密码字段返回时遮蔽 | `api_token` 返回 `"********"` |
| 无Token访问 | 未登录访问管理后台API | 返回 40101 |
| 无效Token | Token过期或伪造 | 返回 40101 |
| 大屏免认证 | `/api/screen/**` 不需要Token | 正常返回数据 |
#### G. 分页参数
| 场景 | 说明 | 示例 |
|------|------|------|
| 默认分页 | 不传page/pageSize | 使用默认值 page=1, pageSize=20 |
| 超大页码 | `page=99999` 无数据 | 返回 `items=[], total=原始总数` |
| page=0 或负数 | 非法页码 | 自动修正为 page=1 |
| pageSize超大 | `pageSize=9999` | 限制最大100 |
| pageSize=0 或负数 | 非法pageSize | 自动修正为默认20 |
---
### 6.7 测试场景检查模板
每个方法编写测试前,填写此检查清单,标注 ✅已覆盖 / ❌不适用:
```
方法MachineService.Create(CreateMachineRequest request)
A. 正常路径
✅ 标准输入 → 成功返回新建ID
✅ 字符串含中文和特殊字符 → 正常存储
❌ 边界值-最大长度在DTO校验测试中覆盖
B. 参数校验异常
✅ request为null → ArgumentNullException
✅ deviceCode为空 → 校验失败
✅ name为纯空格 → 校验失败
✅ ipAddress格式错误 → 校验失败
C. 业务规则异常
✅ deviceCode已存在 → BusinessException 40003
✅ workshopId不存在 → BusinessException 40002
✅ collectAddressId不存在 → BusinessException 40002
✅ brandId不存在 → BusinessException 40002
D. 数据一致性
✅ 事务回滚(机床创建成功但后续操作失败)
E. 日期时间
❌ 不涉及
F. 安全性
✅ deviceCode含SQL注入片段 → 不影响
G. 分页
❌ 不涉及(非查询方法)
```
### 6.8 参数化测试(减少重复代码)
对于同一方法的多种输入组合,使用 `[Theory]` + `[InlineData]` 合并:
```csharp
/// <summary>
/// 参数校验:必填字段为空或空白时必须抛出异常
/// 覆盖场景null、空字符串、纯空格
/// </summary>
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Create_WithBlankName_ThrowsArgumentException(string name)
{
var request = new CreateWorkshopRequest { Name = name, SortOrder = 1 };
var ex = Assert.Throws<ArgumentException>(() => _service.Create(request));
Assert.Contains("车间名称", ex.Message);
}
/// <summary>
/// 唯一性校验不同车间名称各创建一次均成功重复名称抛40003
/// 覆盖场景:正常创建、重复名称
/// </summary>
[Fact]
public void Create_WithDuplicateName_Throws40003()
{
_mockRepo.Setup(r => r.GetByName("A栋")).Returns((Workshop)null);
_mockRepo.Setup(r => r.GetByName("B栋")).Returns((Workshop)null);
_mockRepo.Setup(r => r.GetByName("A栋")).Returns(new Workshop { Name = "A栋" });
// 第一次创建成功
_service.Create(new CreateWorkshopRequest { Name = "A栋", SortOrder = 1 });
// 第二次创建同名 → 异常
var ex = Assert.Throws<BusinessException>(() =>
_service.Create(new CreateWorkshopRequest { Name = "A栋", SortOrder = 2 }));
Assert.Equal(40003, ex.Code);
}
```
### 6.9 Repository 层测试(真实数据库)
Repository 测试使用**真实 MariaDB 测试库**,不走 Mock
```
测试库cnc_business_test / cnc_log_test
原则:每个测试方法前 TRUNCATE 相关表,插入已知数据,执行测试,验证结果
```
```csharp
public class MachineRepositoryTests : IDisposable
{
private readonly MySqlConnection _conn;
private readonly MachineRepository _repo;
public MachineRepositoryTests()
{
_conn = new MySqlConnection("Server=localhost;Database=cnc_business_test;Uid=root;Pwd=root;");
_conn.Open();
_repo = new MachineRepository(_conn);
// 清空测试表,插入基础数据
CleanTestData();
}
[Fact]
public void GetById_ExistingMachine_ReturnsCorrectEntity()
{
// Arrange
_conn.Execute("INSERT INTO cnc_machine (id, device_code, name, ...) VALUES (1, 'test_001', '测试机床', ...)");
// Act
var machine = _repo.GetById(1);
// Assert
Assert.NotNull(machine);
Assert.Equal("test_001", machine.DeviceCode);
Assert.Equal("测试机床", machine.Name);
}
[Fact]
public void GetById_NotExists_ReturnsNull()
{
var machine = _repo.GetById(99999);
Assert.Null(machine);
}
public void Dispose()
{
CleanTestData();
_conn?.Close();
}
}
```
### 6.10 禁止的测试行为
| 禁止 | 原因 |
|------|------|
| `Assert.True(true)` | 无意义的断言,自欺欺人 |
| 只测正常路径不测异常 | 隐藏生产环境Bug |
| 测试方法间有执行顺序依赖 | 并行执行时全部失败 |
| Mock了被测方法本身 | 测了个寂寞 |
| 用 `// TODO: 补充测试` 跳过 | 不允许提交未完成的测试 |
| 删除测试来"通过"构建 | 禁止,必须修复 |
| 忽略 `[Fact(Skip="...")]` | 禁止跳过测试 |
---
## 七、依赖注入
使用 Unity 或手动实现简易 DI 容器VS2017 默认不支持 .NET Core 风格的内置DI
```csharp
// App_Start/UnityConfig.cs 注册依赖
container.RegisterType<IMachineService, MachineService>();
container.RegisterType<IMachineRepository, MachineRepository>();
```
每个 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 | CncModelsEntity + 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 |
先跑通 **登录→系统设置→车间管理** 这条最小链路,再逐步扩展其他模块。