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

25 KiB

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 与数据库表映射

// 表名 cnc_machine → 类名 Machine
// 表名 cnc_daily_production → 类名 DailyProduction
// 表名 log_collect_raw → 类名 CollectRaw跨日志库仓储中指定库名
// 表名前缀 cnc_ / log_ 在Entity类名中去掉

3.4 控制器与路由

// 控制器名 → 路由前缀
// 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 文档注释(///)。

/// <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语句必须有行内注释。

// 日均单机产量 = 日期范围内总产量 / 天数 / 机床数
// 多天范围时Y轴单位为"件/台/天",单天为"件/台"
var days = Math.Max(1, (endDate - startDate).Days + 1);
var avgQuantity = Math.Round((decimal)totalQuantity / days / machineCount, 1);
// 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 文件顶部必须有文件头注释:

/// <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> 包装:

/// <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 完全匹配

错误示例与正确示例

// ❌ 错误:直接返回 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": [{...}, {...}] } }
// ❌ 错误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 异常处理

// 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 数据库连接管理

// 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 双库切换

// 业务库和日志库连接串不同,通过两个 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模式

[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 测试项目配置

// 每个测试项目引用对应的生产项目
// CncService.Tests → 引用 CncService、CncModels
// 使用 Moq 框架 mock 接口
// 测试数据通过 [InlineData] 或测试初始化方法提供

6.6 必测场景清单(每个方法逐项检查)

以下场景适用于所有层的每个 public 方法,开发者编写测试时必须逐项对照,确认是否需要覆盖:

A. 正常路径Happy Path

场景 说明 示例
标准输入 常规参数,期望正常返回 Create(validRequest) → 成功
边界值-最小 数值型参数传最小合法值 pageSize=1sortOrder=0
边界值-最大 数值型参数传最大合法值 pageSize=100sortOrder=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=今天 单天查询
昨日 查询昨天的数据 跨天边界正确
大范围 startDateendDate 差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] 合并:

/// <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 相关表,插入已知数据,执行测试,验证结果
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

// App_Start/UnityConfig.cs 注册依赖
container.RegisterType<IMachineService, MachineService>();
container.RegisterType<IMachineRepository, MachineRepository>();

每个 Service 通过构造函数注入依赖的 Repository

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

先跑通 登录→系统设置→车间管理 这条最小链路,再逐步扩展其他模块。