admin/开发复刻说明.md
2026-04-15 15:25:38 +08:00

436 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<T>`。
- 分页查询最终统一通过 `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<PagedResult<TEntity>> ToPagedListAsync<TEntity>(
this ISugarQueryable<TEntity> query,
int pageIndex,
int pageSize) where TEntity : class
{
RefAsync<int> totalCount = 0;
var items = await query.ToPageListAsync(pageIndex, pageSize, totalCount);
return new PagedResult<TEntity>(totalCount, items);
}
```
分页接口统一要求:
- 简单下拉、树形、级联不要伪装成分页,直接返回 `List<T>`。
- 复杂筛选分页优先 `POST` + 一个完整 Input 模型,不要拆成很多 Query 参数。
- 除非确实是动态列场景,否则分页出参必须使用强类型模型,不要默认返回 `Dictionary<string, object?>`。
- 动态列分页属于例外能力,只能在“字段配置驱动展示列”的场景使用。
### 3.3 出入参约定
新增接口时,优先按以下规则写:
- `GET`:仅用于详情、下拉、字典、简单查询,参数来自 Query。
- `POST`:用于分页查询、复杂筛选、保存、校验、导入导出等。
- `PUT`:用于明确语义的更新动作,项目里已有使用,但不是主流;是否使用要在同一领域内保持一致。
- `DELETE`:仅在该领域已经采用 REST 风格时继续沿用;否则统一使用 `POST("Xxx/Delete")` 或 `POST("Xxx/DeleteByIds")`。
参数组织约定:
- 简单标量参数用 `[Required]` 标记必填。
- 可选筛选字段统一使用可空类型或 `string?`。
- 保存型接口尽量只接收一个 Input 对象。
- 批量操作参数命名统一为 `ids`、`items`、`list` 之一,不要同义混写。
- 枚举字段优先直接使用枚举类型,不要在 Application 层传播魔法数字。
返回值约定:
- 详情:返回单个 `XxxOutput` / `XxxValueObject`。
- 列表:返回 `List<T>`。
- 分页:返回 `PagedResult<T>`。
- 保存新增:优先返回主键 `int` 或 `long`。
- 修改、删除、校验:优先返回 `bool`。
- 有业务语义的失败统一抛 `BusinessException`。
### 3.4 路由约定
路由必须显式声明,不能依赖默认约定路由。
统一规则:
- 路由模式统一为:`<领域>/<动作>`。
- 路由使用 PascalCase,与当前项目风格保持一致。
- 方法名保留 `Async` 后缀,但路由名不要再继续扩散 `Async` 后缀。
- 新增代码优先在方法上直接写完整业务路由,例如 `[HttpPost("ServiceProvider/PaginatedList")]`。
- 如果某个 Service 已使用类级别 `[Route("App")]`,则该类内所有 Action 都保持同一套路,不要一半类路由、一半全路径。
推荐动作命名:
- 分页:`<Domain>/PaginatedList`
- 树形:`<Domain>/GetTreeList`
- 下拉:`<Domain>/GetDropdownList`
- 详情:`<Domain>/GetById` 或 `<Domain>/GetXxxById`
- 新增:`<Domain>/Create` 或 `<Domain>/Add`
- 修改:`<Domain>/Update`
- 删除:`<Domain>/Delete` / `<Domain>/DeleteByIds`
- 保存型合并动作:`<Domain>/Save`
- 批量动作:`<Domain>/BatchXxx`
### 3.5 服务约定
服务层风格统一,才能保证后续代码一眼能看懂。
`ApplicationService` 职责:
- 对外暴露 API。
- 做参数校验、业务校验、权限入口控制。
- 组织事务边界。
- 调用 Repository、其他 Service、缓存、事件、外部 HTTP。
- 做输入输出对象转换。
`Repository` 职责:
- 封装数据库读写。
- 封装分页、投影、子查询、多表查询。
- 对外提供稳定查询方法,不把 SqlSugar 细节扩散到上层。
禁止事项:
- 不在 API 层写业务逻辑。
- 不把 Entity 直接返回前端。
- 不在 Service 中大段拼接复杂多表查询。
- 不把事务边界拆散到多个 Controller / Action 外部调用中。
推荐服务拆分方式:
- 一个领域一个主服务,例如 `InterfaceService`、`ServiceProviderService`。
- 独立子职责可拆成并列服务,例如 `ServiceProviderAuthService`、`ServiceProviderAccountService`。
- HostService、Forward 流程、过滤器、缓存辅助逻辑不要混进普通 CRUD 服务。
依赖注入约定:
- 当前项目同时存在构造函数注入和 `LazyServiceProvider.GetRequiredService<T>()` 两种风格。
- 新增类时可以二选一,但同一个类内不要混用。
- 如果是复刻某个现有服务,优先延续该服务当前风格,不要做无必要切换。
## 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<TEntity>`。
- 查询统一基于 `AsQueryable()` + `WhereIF` + `Select<DTO>()`。
- 分页统一返回 `PagedResult<T>`。
- 多表/子查询场景通过 `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<YourApiModule>()`。
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<XxxEntity>
{
Task<PagedResult<XxxPaginatedOutputValueObject>> GetPaginatedListAsync(
XxxPaginatedInputObjectValue input);
}
public class XxxRepository : BasicRepository<XxxEntity>, IXxxRepository, IScopedDependency
{
public async Task<PagedResult<XxxPaginatedOutputValueObject>> 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<XxxPaginatedOutputValueObject>()
.OrderByDescending(x => x.UpdateTime)
.ToPagedListAsync(input.PageIndex, input.PageSize);
}
}
```
```csharp
[ApiExplorerSettings(GroupName = "示例分组")]
public class XxxService : ApplicationService
{
private IXxxRepository XxxRepository => LazyServiceProvider.GetRequiredService<IXxxRepository>();
[HttpPost("Xxx/PaginatedList")]
public async Task<PagedResult<XxxPaginatedOutputValueObject>> 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<SaveXxxItemInput> Items { get; set; } = new();
}
```
```csharp
[ApiExplorerSettings(GroupName = "示例分组")]
public class XxxService : ApplicationService
{
private IXxxRepository XxxRepository => LazyServiceProvider.GetRequiredService<IXxxRepository>();
[HttpPost("Xxx/Save")]
public async Task<int> 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。