# XXX 项目复刻开发说明 > 目的:基于当前项目快速开发一个“风格完全一致”的新项目。 > 使用方式:先完整阅读本文,再按“复刻步骤清单”执行。 > 本文最高优先级:出入参模型、分页列表、出入参约定、路由约定、服务约定。后续新增功能如果与历史存量代码存在轻微写法差异,新增代码优先遵循本文,不再继续扩散不一致风格。 ## 1. 项目定位与技术基线 - 运行时:`.NET 8`(`global.json` 固定为 `8.0.303`)。 - 架构风格:`Fake` 模块化 + 分层架构(API / Application / Repositories / Domain)。 - 容器与日志:`Autofac` + `Serilog`。 - 配置中心:`Consul`(启动时加载公共与业务配置)。 - ORM:`SqlSugar`(MySQL)。 - 缓存:`FreeRedis`。 - 消息:`RabbitMQ`(事件总线)。 - 序列化:`Newtonsoft.Json`(统一驼峰、DateTime/Long 转换)。 ## 2. 解决方案结构(必须保持) `XXX.sln` 当前核心项目: - `src/XXX.Api`:Web 入口、鉴权、Swagger、CORS、过滤器、路由暴露。 - `src/XXX.Application`:业务编排层(Service、HostService)。 - `src/XXX.Domain`:共享枚举、常量、转换器、通用 DTO、实体、仓储接口 - `src/XXX.Repositories`:仓储实现、数据库访问、基础设施 建议新项目保持同样的目录和分层职责,不要把 Repository 逻辑放到 Application,也不要把业务规则放到 API 层。 ## 3. 复刻时必须优先统一的五类约定 这一节是本文最重要的部分。后续开发是否能“照着文档就写出风格一致的代码”,主要取决于这五类约定是否被严格执行。 ### 3.1 出入参模型分层约定 当前项目真实风格是“所有 DTO 都放 Domain”,而是按“写模型”和“读模型”分层: - 出参入参放 `Application/Dtos/<领域>/`。 - 跨模块复用、通用基类、公共查询基类放 `Domainon/Dtos/`。 - Entity 只用于持久化,不直接作为 API 出参返回给前端。 命名建议按当前项目风格收敛: - 写操作入参:`CreateXxxInput`、`UpdateXxxInput`、`SaveXxxInput`、`CheckXxxInput`、`DeleteXxxInput`。 - 分页查询入参:`XxxPaginatedInput` 或 `XxxPaginatedInput`。 - 详情出参:`XxxOutput`。 - 分页列表出参:`XxxPaginatedOutput` 或 `XxxListOutput`。 - 树形出参:`XxxTreeListOutput`。 - 下拉出参:`XxxDropdownOutput` / `XxxDropDownItem`。 落地规则: - 同一个领域内,命名风格必须保持一致,不要同时出现 `AppListOutput`、`AppListItemDto` 两种并存写法。 - “保存类”接口的入参一律使用明确 Input 模型,不要直接收多个散参数。 - 列表、树、级联、下拉等只读接口,允许直接返回 Dto,这是当前项目主流风格。 - 集合类型属性统一显式初始化,例如 `= new();`,避免空引用分支扩散。 ### 3.2 分页列表约定 分页是后续最容易写散的部分,必须统一。 统一契约: - 分页入参必须继承 `PagedQueryInput`。 - `PagedQueryInput` 统一只提供 `PageIndex`、`PageSize` 两个基础字段。 - 分页返回值统一使用 `PagedResult`。 - 分页查询最终统一通过 `ToPagedListAsync(input.PageIndex, input.PageSize)` 收口。 - 分页列表默认按 `UpdateTime` 或明确业务时间字段倒序,不允许无序分页。 当前项目基线可直接复用: ```csharp public class PagedQueryInput { public int PageIndex { get; set; } public int PageSize { get; set; } } ``` ```csharp public static async Task> ToPagedListAsync( this ISugarQueryable query, int pageIndex, int pageSize) where TEntity : class { RefAsync totalCount = 0; var items = await query.ToPageListAsync(pageIndex, pageSize, totalCount); return new PagedResult(totalCount, items); } ``` 分页接口统一要求: - 简单下拉、树形、级联不要伪装成分页,直接返回 `List`。 - 复杂筛选分页优先 `POST` + 一个完整 Input 模型,不要拆成很多 Query 参数。 - 除非确实是动态列场景,否则分页出参必须使用强类型模型,不要默认返回 `Dictionary`。 - 动态列分页属于例外能力,只能在“字段配置驱动展示列”的场景使用。 ### 3.3 出入参约定 新增接口时,优先按以下规则写: - `GET`:仅用于详情、下拉、字典、简单查询,参数来自 Query。 - `POST`:用于分页查询、复杂筛选、保存、校验、导入导出等。 - `PUT`:用于明确语义的更新动作,项目里已有使用,但不是主流;是否使用要在同一领域内保持一致。 - `DELETE`:仅在该领域已经采用 REST 风格时继续沿用;否则统一使用 `POST("Xxx/Delete")` 或 `POST("Xxx/DeleteByIds")`。 参数组织约定: - 简单标量参数用 `[Required]` 标记必填。 - 可选筛选字段统一使用可空类型或 `string?`。 - 保存型接口尽量只接收一个 Input 对象。 - 批量操作参数命名统一为 `ids`、`items`、`list` 之一,不要同义混写。 - 枚举字段优先直接使用枚举类型,不要在 Application 层传播魔法数字。 返回值约定: - 详情:返回单个 `XxxOutput` / `XxxValueObject`。 - 列表:返回 `List`。 - 分页:返回 `PagedResult`。 - 保存新增:优先返回主键 `int` 或 `long`。 - 修改、删除、校验:优先返回 `bool`。 - 有业务语义的失败统一抛 `BusinessException`。 ### 3.4 路由约定 路由必须显式声明,不能依赖默认约定路由。 统一规则: - 路由模式统一为:`<领域>/<动作>`。 - 路由使用 PascalCase,与当前项目风格保持一致。 - 方法名保留 `Async` 后缀,但路由名不要再继续扩散 `Async` 后缀。 - 新增代码优先在方法上直接写完整业务路由,例如 `[HttpPost("ServiceProvider/PaginatedList")]`。 - 如果某个 Service 已使用类级别 `[Route("App")]`,则该类内所有 Action 都保持同一套路,不要一半类路由、一半全路径。 推荐动作命名: - 分页:`/PaginatedList` - 树形:`/GetTreeList` - 下拉:`/GetDropdownList` - 详情:`/GetById` 或 `/GetXxxById` - 新增:`/Create` 或 `/Add` - 修改:`/Update` - 删除:`/Delete` / `/DeleteByIds` - 保存型合并动作:`/Save` - 批量动作:`/BatchXxx` ### 3.5 服务约定 服务层风格统一,才能保证后续代码一眼能看懂。 `ApplicationService` 职责: - 对外暴露 API。 - 做参数校验、业务校验、权限入口控制。 - 组织事务边界。 - 调用 Repository、其他 Service、缓存、事件、外部 HTTP。 - 做输入输出对象转换。 `Repository` 职责: - 封装数据库读写。 - 封装分页、投影、子查询、多表查询。 - 对外提供稳定查询方法,不把 SqlSugar 细节扩散到上层。 禁止事项: - 不在 API 层写业务逻辑。 - 不把 Entity 直接返回前端。 - 不在 Service 中大段拼接复杂多表查询。 - 不把事务边界拆散到多个 Controller / Action 外部调用中。 推荐服务拆分方式: - 一个领域一个主服务,例如 `InterfaceService`、`ServiceProviderService`。 - 独立子职责可拆成并列服务,例如 `ServiceProviderAuthService`、`ServiceProviderAccountService`。 - HostService、Forward 流程、过滤器、缓存辅助逻辑不要混进普通 CRUD 服务。 依赖注入约定: - 当前项目同时存在构造函数注入和 `LazyServiceProvider.GetRequiredService()` 两种风格。 - 新增类时可以二选一,但同一个类内不要混用。 - 如果是复刻某个现有服务,优先延续该服务当前风格,不要做无必要切换。 ## 4. 全局编码约定(从现有代码提炼) ### 4.1 命名与组织 - 项目命名采用:`<产品名>.<层名>`,例如 `XXX.Application`。 - 实体、仓储接口目录放在 `Domain/<领域>Aggregate` - 业务服务放在 `Application/Services/<领域>/`。 - 仓储实现放在 `Repositories/<领域>/`。 - 只读投影模型放在 `Domain/Dtos/<领域>/`。 - 写操作 DTO 放在 `Application/Dtos/<领域>/`。 ### 4.2 API 风格 - 业务服务类继承 `ApplicationService`,直接作为控制器暴露。 - 默认全局鉴权,匿名访问需要明确放开。 - 统一使用 Swagger 分组标记 `ApiExplorerSettings(GroupName = "...")`。 - 错误处理使用 `BusinessException` 体现业务语义。 ### 4.3 数据访问风格 - 仓储实现继承 `BasicRepository`。 - 查询统一基于 `AsQueryable()` + `WhereIF` + `Select()`。 - 分页统一返回 `PagedResult`。 - 多表/子查询场景通过 `LeftJoin`、`SqlFunc.Subqueryable<>()` 等方式收口在 Repository 内部。 ### 4.4 事务与一致性 - 涉及多表写入(主表 + 参数 + 关联表)必须显式事务。 - 统一写法: ```csharp using var ts = TransacationHelper.GetReadCommitted(); // 1. 校验 // 2. 持久化 // 3. 清缓存 / 发事件 ts.Complete(); ``` - 事务粒度按“一个完整业务动作”控制,不要半截提交。 ## 5. 启动链路与模块装配(复刻重点) ### 5.1 API 启动(`XXX.Api/Program.cs`) 必须保留的关键顺序: 1. `CreateSlimBuilder(args)`。 2. `AddConsul("idwms/common")` 与业务配置节点。 3. `UseAutofac().UseSerilog()`。 4. `AddApplication()`。 5. `AddServiceDefaults()`。 6. `Build()` 后 `InitializeApplication()` 再 `Run()`。 ### 5.2 API 模块(`XXXApiModule`) 模块中应保留这些能力: - 全局 Claim 映射(`FakeClaimTypes.UserId`)。 - `ApplicationServices2Controller<...>()` 自动控制器映射。 - 全局授权策略(默认全部接口需登录)。 - JSON 序列化配置(驼峰 + 自定义 converter)。 - Swagger + 审计。 - CORS 从 `App:CorsOrigins` 读取。 - 代理头配置(支持 Nginx 获取真实 IP)。 - 中间件顺序:异常处理 -> 默认端点 -> PathBase -> Swagger -> CORS -> Routing -> Auth -> ForwardedHeaders -> Controllers。 ### 5.3 Application 模块 必须包含: - AutoMapper Profile 扫描。 - Options 绑定(如 `InterfaceRelay`)。 - HostService 注册。 - Redis 初始化(缺配置直接抛异常)。 - 预热逻辑(如 `NatashaInitializer.Preheating()`)。 - 命名 HttpClient(统一超时与 SSL 策略)。 ### 5.4 Repositories 模块 必须包含: - SqlSugar DbContext 注册(连接串来自 `ConnectionStrings:Default`)。 - `IBasicRepository<>` 泛型仓储注册。 - 其他基础设施配置(例如 ElasticSearch Options)。 ## 6. 配置规范(新项目必须对齐) `src/XXX.Api/appsettings.json` 仅保留轻量入口配置,核心配置走 Consul。 建议保持同结构: - `UseLocalConfigs` - `Consul:Address` - `Consul:Token` 同时在 Consul 准备以下节点(按本项目习惯): - 公共节点:`idwms/common` - 业务节点:`idwms/<你的业务名>` 并确保这些节在配置中心可用: - `App:Urls` - `App:CorsOrigins` - `ConnectionStrings:Default` - `Redis` - `ElasticSearch` - `Serilog` - 业务 Options(如 `InterfaceRelay`) ## 7. 业务代码写法模板(建议直接复用) ### 7.1 分页列表模板 推荐把“分页入参 + 分页出参 + Repository 查询 + Application 暴露”整套一起按下面方式写。 ```csharp public class XxxPaginatedInputObjectValue : PagedQueryInput { public string? Keyword { get; set; } public int? Status { get; set; } } public class XxxPaginatedOutputValueObject { public int Id { get; set; } public string Name { get; set; } public DateTime UpdateTime { get; set; } } ``` ```csharp public interface IXxxRepository : IBasicRepository { Task> GetPaginatedListAsync( XxxPaginatedInputObjectValue input); } public class XxxRepository : BasicRepository, IXxxRepository, IScopedDependency { public async Task> GetPaginatedListAsync( XxxPaginatedInputObjectValue input) { var query = AsQueryable(); query.WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), x => x.Name.Contains(input.Keyword!)) .WhereIF(input.Status != null, x => x.Status == input.Status); return await query .Select() .OrderByDescending(x => x.UpdateTime) .ToPagedListAsync(input.PageIndex, input.PageSize); } } ``` ```csharp [ApiExplorerSettings(GroupName = "示例分组")] public class XxxService : ApplicationService { private IXxxRepository XxxRepository => LazyServiceProvider.GetRequiredService(); [HttpPost("Xxx/PaginatedList")] public async Task> GetPaginatedListAsync( XxxPaginatedInputObjectValue input) { return await XxxRepository.GetPaginatedListAsync(input); } } ``` ### 7.2 保存类模板 ```csharp public class SaveXxxInput { public OptionType OptionType { get; set; } public int? Id { get; set; } public string Name { get; set; } public List Items { get; set; } = new(); } ``` ```csharp [ApiExplorerSettings(GroupName = "示例分组")] public class XxxService : ApplicationService { private IXxxRepository XxxRepository => LazyServiceProvider.GetRequiredService(); [HttpPost("Xxx/Save")] public async Task SaveAsync(SaveXxxInput input) { if (input.OptionType == OptionType.删除) { await XxxRepository.SoftDeleteAsync(x => x.Id == input.Id); return input.Id ?? 0; } using var ts = TransacationHelper.GetReadCommitted(); // 1. 业务校验 // 2. 主表保存 // 3. 明细保存 // 4. 清缓存 / 发事件 ts.Complete(); return input.Id ?? 0; } } ``` ### 7.3 路由模板 新增领域时,优先按下面这组动作名铺开,后续别人一看就知道接口在哪: - `Xxx/PaginatedList` - `Xxx/GetById` - `Xxx/GetDropdownList` - `Xxx/Create` - `Xxx/Update` - `Xxx/DeleteByIds` - `Xxx/Save` 规则: - 只要用了 `Save`,就让 `Save` 明确承担新增/修改/删除的统一入口,不要同时再出现语义重复的 `Submit`、`Edit`、`DoSave`。 - 只要用了 `PaginatedList`,同领域内就不要再出现 `GetList`、`GetPageList`、`GetPagedData` 多套命名并存。 ## 8. 复刻步骤清单(按顺序) 1. 复制当前解决方案结构,替换项目名前缀(建议全量替换 `XXX`)。 2. 保持 `global.json`、`Directory.Build.props`、`common.props` 不变或等价。 3. 按现有模式创建各层 `Module` 并配置 `[DependsOn]`。 4. 在 API `Program.cs` 保留 Consul、Serilog、Autofac、Fake 初始化顺序。 5. 完成 `appsettings.json` 与 Consul 节点准备。 6. 先建 `ApplicatiDomainon` 公共枚举和分页基类,再建 `Contracts DTO`,再建 `Repositories ValueObjects`,最后写 `Repository` 与 `Application Service`。 7. 先统一路由命名和分页契约,再批量开发业务接口,避免边写边变风格。 8. 所有多表写入逻辑补齐事务。 9. 补齐 AutoMapper Profile。 10. 通过 AppHost 本地启动联调,再单启 API 验证。 11. 重点检查:鉴权默认开启、CORS、序列化格式、路径基址、代理头、日志落库/落 ES。