feat: initialize mini api skeleton
This commit is contained in:
37
.dockerignore
Normal file
37
.dockerignore
Normal file
@@ -0,0 +1,37 @@
|
||||
.git/
|
||||
.gitignore
|
||||
.gitattributes
|
||||
.gitmodules
|
||||
.github/
|
||||
.svn/
|
||||
.hg/
|
||||
.vs/
|
||||
.idea/
|
||||
.vscode/
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
**/*.csproj.user
|
||||
**/bin/
|
||||
**/obj/
|
||||
**/TestResults/
|
||||
**/coverage/
|
||||
**/artifacts/
|
||||
**/node_modules/
|
||||
**/bower_components/
|
||||
**/dist/
|
||||
**/build/
|
||||
**/.next/
|
||||
**/.nuxt/
|
||||
**/.output/
|
||||
*.log
|
||||
*.tmp
|
||||
*.temp
|
||||
*.swp
|
||||
*.bak
|
||||
*.cache
|
||||
*.pid
|
||||
*.pdb
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
13
.editorconfig
Normal file
13
.editorconfig
Normal file
@@ -0,0 +1,13 @@
|
||||
# EditorConfig
|
||||
root = true
|
||||
|
||||
[*.cs]
|
||||
dotnet_diagnostic.SA1600.severity = error
|
||||
dotnet_diagnostic.SA1601.severity = error
|
||||
dotnet_diagnostic.SA1615.severity = error
|
||||
dotnet_diagnostic.SA1629.severity = none
|
||||
dotnet_diagnostic.SA1202.severity = none
|
||||
dotnet_diagnostic.SA1200.severity = none
|
||||
dotnet_diagnostic.SA1623.severity = none
|
||||
dotnet_diagnostic.SA1111.severity = none
|
||||
dotnet_diagnostic.SA1101.severity = none
|
||||
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
.vs/
|
||||
bin/
|
||||
obj/
|
||||
**/bin/
|
||||
**/obj/
|
||||
.claude/
|
||||
*.log
|
||||
/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj.user
|
||||
|
||||
# 保留根目录 scripts 目录提交
|
||||
!scripts/
|
||||
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
[submodule "TakeoutSaaS.BuildingBlocks"]
|
||||
path = TakeoutSaaS.BuildingBlocks
|
||||
url = git@github.com:msumshk/TakeoutSaaS.BuildingBlocks.git
|
||||
[submodule "TakeoutSaaS.Docs"]
|
||||
path = TakeoutSaaS.Docs
|
||||
url = git@github.com:msumshk/TakeoutSaaS.Docs.git
|
||||
15
Directory.Build.props
Normal file
15
Directory.Build.props
Normal file
@@ -0,0 +1,15 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="$(MSBuildThisFileDirectory)stylecop.json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.IO.Packaging" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
8
NuGet.Config
Normal file
8
NuGet.Config
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="huaweicloud" value="https://repo.huaweicloud.com/repository/nuget/v3/index.json" />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
33
README.md
Normal file
33
README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# TakeoutSaaS.C-Side-Mini-Program-API
|
||||
|
||||
本仓库承载 `TakeoutSaaS.MiniApi`,面向 C 端微信小程序,路由前缀为 `/api/mini/v1`。
|
||||
|
||||
## 子模块
|
||||
|
||||
- `TakeoutSaaS.BuildingBlocks/`:共享基础组件
|
||||
- `TakeoutSaaS.Docs/`:文档、部署资料与脚本
|
||||
|
||||
## 初始化
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
dotnet restore
|
||||
dotnet build TakeoutSaaS.sln
|
||||
```
|
||||
|
||||
## 运行
|
||||
|
||||
```bash
|
||||
dotnet run --project src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj
|
||||
```
|
||||
|
||||
默认开发地址:
|
||||
|
||||
- `http://localhost:2683`
|
||||
- Swagger:`http://localhost:2683/api/docs`
|
||||
- 健康检查:`http://localhost:2683/healthz`
|
||||
|
||||
## 文档入口
|
||||
|
||||
- `TakeoutSaaS.Docs/README.md`
|
||||
- `TakeoutSaaS.Docs/Document/README.md`
|
||||
1
TakeoutSaaS.BuildingBlocks
Submodule
1
TakeoutSaaS.BuildingBlocks
Submodule
Submodule TakeoutSaaS.BuildingBlocks added at 5b07973a39
1
TakeoutSaaS.Docs
Submodule
1
TakeoutSaaS.Docs
Submodule
Submodule TakeoutSaaS.Docs added at 6680599912
114
TakeoutSaaS.sln
Normal file
114
TakeoutSaaS.sln
Normal file
@@ -0,0 +1,114 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Api", "Api", "{81034408-37C8-1011-444E-4C15C2FADA8E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.MiniApi", "src\Api\TakeoutSaaS.MiniApi\TakeoutSaaS.MiniApi.csproj", "{B2B0CCBE-B471-4282-9040-41FD4A30E368}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Application", "Application", "{22BAF98C-8415-17C4-B26A-D537657BC863}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Application", "src\Application\TakeoutSaaS.Application\TakeoutSaaS.Application.csproj", "{9ED9512C-35CC-42D7-8EA3-317DF63F2C2D}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Domain", "Domain", "{8B290487-4C16-E85E-E807-F579CBE9FC4D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Domain", "src\Domain\TakeoutSaaS.Domain\TakeoutSaaS.Domain.csproj", "{826F81CA-4E07-4C28-BB49-F32B2AF2F279}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{9048EB7F-3875-A59E-E36B-5BD4C6F2A282}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Infrastructure", "src\Infrastructure\TakeoutSaaS.Infrastructure\TakeoutSaaS.Infrastructure.csproj", "{713544AA-AD1B-45AE-9A95-D27C102C7160}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{EC447DCF-ABFA-6E24-52A5-D7FD48A5C558}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Module.Tenancy", "src\Modules\TakeoutSaaS.Module.Tenancy\TakeoutSaaS.Module.Tenancy.csproj", "{FEC3EAFC-BED6-4A4F-B956-084F41D012BB}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{B2B0CCBE-B471-4282-9040-41FD4A30E368}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B2B0CCBE-B471-4282-9040-41FD4A30E368}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B2B0CCBE-B471-4282-9040-41FD4A30E368}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B2B0CCBE-B471-4282-9040-41FD4A30E368}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B2B0CCBE-B471-4282-9040-41FD4A30E368}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B2B0CCBE-B471-4282-9040-41FD4A30E368}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B2B0CCBE-B471-4282-9040-41FD4A30E368}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B2B0CCBE-B471-4282-9040-41FD4A30E368}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B2B0CCBE-B471-4282-9040-41FD4A30E368}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B2B0CCBE-B471-4282-9040-41FD4A30E368}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B2B0CCBE-B471-4282-9040-41FD4A30E368}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B2B0CCBE-B471-4282-9040-41FD4A30E368}.Release|x86.Build.0 = Release|Any CPU
|
||||
{9ED9512C-35CC-42D7-8EA3-317DF63F2C2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9ED9512C-35CC-42D7-8EA3-317DF63F2C2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9ED9512C-35CC-42D7-8EA3-317DF63F2C2D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{9ED9512C-35CC-42D7-8EA3-317DF63F2C2D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{9ED9512C-35CC-42D7-8EA3-317DF63F2C2D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{9ED9512C-35CC-42D7-8EA3-317DF63F2C2D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{9ED9512C-35CC-42D7-8EA3-317DF63F2C2D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9ED9512C-35CC-42D7-8EA3-317DF63F2C2D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9ED9512C-35CC-42D7-8EA3-317DF63F2C2D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{9ED9512C-35CC-42D7-8EA3-317DF63F2C2D}.Release|x64.Build.0 = Release|Any CPU
|
||||
{9ED9512C-35CC-42D7-8EA3-317DF63F2C2D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{9ED9512C-35CC-42D7-8EA3-317DF63F2C2D}.Release|x86.Build.0 = Release|Any CPU
|
||||
{826F81CA-4E07-4C28-BB49-F32B2AF2F279}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{826F81CA-4E07-4C28-BB49-F32B2AF2F279}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{826F81CA-4E07-4C28-BB49-F32B2AF2F279}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{826F81CA-4E07-4C28-BB49-F32B2AF2F279}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{826F81CA-4E07-4C28-BB49-F32B2AF2F279}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{826F81CA-4E07-4C28-BB49-F32B2AF2F279}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{826F81CA-4E07-4C28-BB49-F32B2AF2F279}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{826F81CA-4E07-4C28-BB49-F32B2AF2F279}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{826F81CA-4E07-4C28-BB49-F32B2AF2F279}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{826F81CA-4E07-4C28-BB49-F32B2AF2F279}.Release|x64.Build.0 = Release|Any CPU
|
||||
{826F81CA-4E07-4C28-BB49-F32B2AF2F279}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{826F81CA-4E07-4C28-BB49-F32B2AF2F279}.Release|x86.Build.0 = Release|Any CPU
|
||||
{713544AA-AD1B-45AE-9A95-D27C102C7160}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{713544AA-AD1B-45AE-9A95-D27C102C7160}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{713544AA-AD1B-45AE-9A95-D27C102C7160}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{713544AA-AD1B-45AE-9A95-D27C102C7160}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{713544AA-AD1B-45AE-9A95-D27C102C7160}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{713544AA-AD1B-45AE-9A95-D27C102C7160}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{713544AA-AD1B-45AE-9A95-D27C102C7160}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{713544AA-AD1B-45AE-9A95-D27C102C7160}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{713544AA-AD1B-45AE-9A95-D27C102C7160}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{713544AA-AD1B-45AE-9A95-D27C102C7160}.Release|x64.Build.0 = Release|Any CPU
|
||||
{713544AA-AD1B-45AE-9A95-D27C102C7160}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{713544AA-AD1B-45AE-9A95-D27C102C7160}.Release|x86.Build.0 = Release|Any CPU
|
||||
{FEC3EAFC-BED6-4A4F-B956-084F41D012BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FEC3EAFC-BED6-4A4F-B956-084F41D012BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FEC3EAFC-BED6-4A4F-B956-084F41D012BB}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{FEC3EAFC-BED6-4A4F-B956-084F41D012BB}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{FEC3EAFC-BED6-4A4F-B956-084F41D012BB}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{FEC3EAFC-BED6-4A4F-B956-084F41D012BB}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{FEC3EAFC-BED6-4A4F-B956-084F41D012BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FEC3EAFC-BED6-4A4F-B956-084F41D012BB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FEC3EAFC-BED6-4A4F-B956-084F41D012BB}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{FEC3EAFC-BED6-4A4F-B956-084F41D012BB}.Release|x64.Build.0 = Release|Any CPU
|
||||
{FEC3EAFC-BED6-4A4F-B956-084F41D012BB}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{FEC3EAFC-BED6-4A4F-B956-084F41D012BB}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{81034408-37C8-1011-444E-4C15C2FADA8E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{B2B0CCBE-B471-4282-9040-41FD4A30E368} = {81034408-37C8-1011-444E-4C15C2FADA8E}
|
||||
{22BAF98C-8415-17C4-B26A-D537657BC863} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{9ED9512C-35CC-42D7-8EA3-317DF63F2C2D} = {22BAF98C-8415-17C4-B26A-D537657BC863}
|
||||
{8B290487-4C16-E85E-E807-F579CBE9FC4D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{826F81CA-4E07-4C28-BB49-F32B2AF2F279} = {8B290487-4C16-E85E-E807-F579CBE9FC4D}
|
||||
{9048EB7F-3875-A59E-E36B-5BD4C6F2A282} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{713544AA-AD1B-45AE-9A95-D27C102C7160} = {9048EB7F-3875-A59E-E36B-5BD4C6F2A282}
|
||||
{EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{FEC3EAFC-BED6-4A4F-B956-084F41D012BB} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
5
src/Api/Directory.Build.props
Normal file
5
src/Api/Directory.Build.props
Normal file
@@ -0,0 +1,5 @@
|
||||
<Project>
|
||||
<ItemGroup>
|
||||
<Using Include="Asp.Versioning" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,30 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Mini.Bootstrap;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.MiniApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 小程序端启动引导接口。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/mini/v{version:apiVersion}/bootstrap")]
|
||||
public sealed class BootstrapController(ISender sender) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 返回当前服务基础能力与运行环境信息。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>引导信息。</returns>
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(typeof(ApiResponse<MiniBootstrapResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<MiniBootstrapResponse>> GetAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await sender.Send(new GetMiniBootstrapQuery(), cancellationToken);
|
||||
return ApiResponse<MiniBootstrapResponse>.Ok(response);
|
||||
}
|
||||
}
|
||||
33
src/Api/TakeoutSaaS.MiniApi/Controllers/HealthController.cs
Normal file
33
src/Api/TakeoutSaaS.MiniApi/Controllers/HealthController.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.MiniApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 小程序端健康检查。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/mini/v{version:apiVersion}/health")]
|
||||
public sealed class HealthController : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取服务健康状态。
|
||||
/// </summary>
|
||||
/// <returns>健康状态。</returns>
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
public ApiResponse<object> Get()
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
status = "OK",
|
||||
service = "TakeoutSaaS.MiniApi",
|
||||
time = DateTime.UtcNow
|
||||
};
|
||||
|
||||
return ApiResponse<object>.Ok(payload);
|
||||
}
|
||||
}
|
||||
6
src/Api/TakeoutSaaS.MiniApi/Dockerfile
Normal file
6
src/Api/TakeoutSaaS.MiniApi/Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
||||
WORKDIR /app
|
||||
COPY publish/ .
|
||||
EXPOSE 7803
|
||||
ENV ASPNETCORE_URLS=http://+:7803
|
||||
ENTRYPOINT ["dotnet", "TakeoutSaaS.MiniApi.dll"]
|
||||
148
src/Api/TakeoutSaaS.MiniApi/Program.cs
Normal file
148
src/Api/TakeoutSaaS.MiniApi/Program.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using Microsoft.AspNetCore.Cors.Infrastructure;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenTelemetry.Metrics;
|
||||
using OpenTelemetry.Resources;
|
||||
using OpenTelemetry.Trace;
|
||||
using Serilog;
|
||||
using TakeoutSaaS.Application.App.Extensions;
|
||||
using TakeoutSaaS.Infrastructure.App.Extensions;
|
||||
using TakeoutSaaS.Module.Tenancy.Extensions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Ids;
|
||||
using TakeoutSaaS.Shared.Web.Extensions;
|
||||
using TakeoutSaaS.Shared.Web.Swagger;
|
||||
using TakeoutSaaS.Shared.Kernel.Ids;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
const string logTemplate = "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] [TraceId:{TraceId}] [SpanId:{SpanId}] [Service:{Service}] {SourceContext} {Message:lj}{NewLine}{Exception}";
|
||||
var isDevelopment = builder.Environment.IsDevelopment();
|
||||
|
||||
builder.Host.UseSerilog((_, _, configuration) =>
|
||||
{
|
||||
configuration
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.WithProperty("Service", "TakeoutSaaS.MiniApi")
|
||||
.WriteTo.Console(outputTemplate: logTemplate)
|
||||
.WriteTo.File(
|
||||
"logs/mini-api-.log",
|
||||
rollingInterval: RollingInterval.Day,
|
||||
retainedFileCountLimit: 7,
|
||||
shared: true,
|
||||
outputTemplate: logTemplate);
|
||||
});
|
||||
|
||||
builder.Services.AddSharedWebCore();
|
||||
if (isDevelopment)
|
||||
{
|
||||
builder.Services.AddSharedSwagger(options =>
|
||||
{
|
||||
options.Title = "外卖SaaS - 小程序端";
|
||||
options.Description = "小程序 API 文档";
|
||||
options.EnableAuthorization = false;
|
||||
});
|
||||
}
|
||||
|
||||
builder.Services.AddAppInfrastructure(builder.Configuration);
|
||||
builder.Services.AddAppApplication();
|
||||
builder.Services.AddTenantResolution(builder.Configuration);
|
||||
builder.Services.AddHealthChecks();
|
||||
builder.Services.AddOptions<IdGeneratorOptions>()
|
||||
.Bind(builder.Configuration.GetSection(IdGeneratorOptions.SectionName))
|
||||
.ValidateDataAnnotations();
|
||||
builder.Services.AddSingleton<IIdGenerator>(provider =>
|
||||
{
|
||||
var options = provider.GetRequiredService<IOptions<IdGeneratorOptions>>().Value;
|
||||
return new SnowflakeIdGenerator(options.WorkerId, options.DatacenterId);
|
||||
});
|
||||
|
||||
var otelSection = builder.Configuration.GetSection("Otel");
|
||||
var otelEndpoint = otelSection.GetValue<string>("Endpoint");
|
||||
var useConsoleExporter = otelSection.GetValue<bool?>("UseConsoleExporter") ?? isDevelopment;
|
||||
builder.Services.AddOpenTelemetry()
|
||||
.ConfigureResource(resource => resource.AddService(
|
||||
serviceName: "TakeoutSaaS.MiniApi",
|
||||
serviceVersion: "1.0.0",
|
||||
serviceInstanceId: Environment.MachineName))
|
||||
.WithTracing(tracing =>
|
||||
{
|
||||
tracing
|
||||
.SetSampler(new ParentBasedSampler(new AlwaysOnSampler()))
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddHttpClientInstrumentation();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(otelEndpoint))
|
||||
{
|
||||
tracing.AddOtlpExporter(exporter => exporter.Endpoint = new Uri(otelEndpoint));
|
||||
}
|
||||
|
||||
if (useConsoleExporter)
|
||||
{
|
||||
tracing.AddConsoleExporter();
|
||||
}
|
||||
})
|
||||
.WithMetrics(metrics =>
|
||||
{
|
||||
metrics
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddHttpClientInstrumentation()
|
||||
.AddRuntimeInstrumentation()
|
||||
.AddPrometheusExporter();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(otelEndpoint))
|
||||
{
|
||||
metrics.AddOtlpExporter(exporter => exporter.Endpoint = new Uri(otelEndpoint));
|
||||
}
|
||||
|
||||
if (useConsoleExporter)
|
||||
{
|
||||
metrics.AddConsoleExporter();
|
||||
}
|
||||
});
|
||||
|
||||
var miniOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:Mini");
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("MiniApiCors", policy =>
|
||||
{
|
||||
ConfigureCorsPolicy(policy, miniOrigins);
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
app.UseCors("MiniApiCors");
|
||||
app.UseTenantResolution();
|
||||
app.UseSharedWebCore();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSharedSwagger();
|
||||
}
|
||||
|
||||
app.MapHealthChecks("/healthz");
|
||||
app.MapPrometheusScrapingEndpoint();
|
||||
app.MapControllers();
|
||||
app.Run();
|
||||
|
||||
static string[] ResolveCorsOrigins(IConfiguration configuration, string sectionKey)
|
||||
{
|
||||
var origins = configuration.GetSection(sectionKey).Get<string[]>();
|
||||
return origins?
|
||||
.Where(origin => !string.IsNullOrWhiteSpace(origin))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray() ?? [];
|
||||
}
|
||||
|
||||
static void ConfigureCorsPolicy(CorsPolicyBuilder policy, string[] origins)
|
||||
{
|
||||
if (origins.Length == 0)
|
||||
{
|
||||
policy.AllowAnyOrigin();
|
||||
}
|
||||
else
|
||||
{
|
||||
policy.WithOrigins(origins);
|
||||
}
|
||||
|
||||
policy
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
}
|
||||
14
src/Api/TakeoutSaaS.MiniApi/Properties/launchSettings.json
Normal file
14
src/Api/TakeoutSaaS.MiniApi/Properties/launchSettings.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"TakeoutSaaS.MiniApi": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "api/docs",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "http://localhost:2683"
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj
Normal file
28
src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj
Normal file
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerfileContext>../../..</DockerfileContext>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.14.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.14.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.14.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.14.0" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.14.0" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.14.0" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.14.0-beta.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Kernel\TakeoutSaaS.Shared.Kernel.csproj" />
|
||||
<ProjectReference Include="..\..\..\TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Web\TakeoutSaaS.Shared.Web.csproj" />
|
||||
<ProjectReference Include="..\..\Application\TakeoutSaaS.Application\TakeoutSaaS.Application.csproj" />
|
||||
<ProjectReference Include="..\..\Infrastructure\TakeoutSaaS.Infrastructure\TakeoutSaaS.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Tenancy\TakeoutSaaS.Module.Tenancy.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
9
src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json
Normal file
9
src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Cors": {
|
||||
"Mini": []
|
||||
},
|
||||
"Otel": {
|
||||
"Endpoint": "",
|
||||
"UseConsoleExporter": true
|
||||
}
|
||||
}
|
||||
9
src/Api/TakeoutSaaS.MiniApi/appsettings.Production.json
Normal file
9
src/Api/TakeoutSaaS.MiniApi/appsettings.Production.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Cors": {
|
||||
"Mini": []
|
||||
},
|
||||
"Otel": {
|
||||
"Endpoint": "",
|
||||
"UseConsoleExporter": false
|
||||
}
|
||||
}
|
||||
23
src/Api/TakeoutSaaS.MiniApi/appsettings.json
Normal file
23
src/Api/TakeoutSaaS.MiniApi/appsettings.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"AllowedHosts": "*",
|
||||
"IdGenerator": {
|
||||
"WorkerId": 0,
|
||||
"DatacenterId": 0
|
||||
},
|
||||
"Tenancy": {
|
||||
"TenantIdHeaderName": "X-Tenant-Id",
|
||||
"TenantCodeHeaderName": "X-Tenant-Code",
|
||||
"IgnoredPaths": [
|
||||
"/healthz",
|
||||
"/api/mini/v1/health"
|
||||
],
|
||||
"ThrowIfUnresolved": false
|
||||
},
|
||||
"Cors": {
|
||||
"Mini": []
|
||||
},
|
||||
"Otel": {
|
||||
"Endpoint": "",
|
||||
"UseConsoleExporter": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using SharedValidationException = TakeoutSaaS.Shared.Abstractions.Exceptions.ValidationException;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Common.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// MediatR 校验行为。
|
||||
/// </summary>
|
||||
/// <typeparam name="TRequest">请求类型。</typeparam>
|
||||
/// <typeparam name="TResponse">响应类型。</typeparam>
|
||||
public sealed class ValidationBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators)
|
||||
: IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : notnull
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<TResponse> Handle(
|
||||
TRequest request,
|
||||
RequestHandlerDelegate<TResponse> next,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!validators.Any())
|
||||
{
|
||||
return await next();
|
||||
}
|
||||
|
||||
var context = new ValidationContext<TRequest>(request);
|
||||
var validationResults = await Task.WhenAll(validators.Select(validator => validator.ValidateAsync(context, cancellationToken)));
|
||||
var failures = validationResults
|
||||
.SelectMany(result => result.Errors)
|
||||
.Where(error => error is not null)
|
||||
.GroupBy(error => error.PropertyName)
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
group => group.Select(error => error.ErrorMessage).Distinct().ToArray());
|
||||
|
||||
if (failures.Count > 0)
|
||||
{
|
||||
throw new SharedValidationException(failures);
|
||||
}
|
||||
|
||||
return await next();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Reflection;
|
||||
using TakeoutSaaS.Application.App.Common.Behaviors;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// 业务应用层服务注册。
|
||||
/// </summary>
|
||||
public static class AppApplicationServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册应用层处理器与验证管道。
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合。</param>
|
||||
/// <returns>服务集合。</returns>
|
||||
public static IServiceCollection AddAppApplication(this IServiceCollection services)
|
||||
{
|
||||
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
|
||||
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
|
||||
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using MediatR;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Mini.Bootstrap;
|
||||
|
||||
/// <summary>
|
||||
/// 获取小程序端启动引导信息。
|
||||
/// </summary>
|
||||
public sealed record GetMiniBootstrapQuery : IRequest<MiniBootstrapResponse>;
|
||||
@@ -0,0 +1,33 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Mini.Bootstrap;
|
||||
|
||||
/// <summary>
|
||||
/// 小程序端启动引导查询处理器。
|
||||
/// </summary>
|
||||
public sealed class GetMiniBootstrapQueryHandler(
|
||||
IHostEnvironment environment,
|
||||
ITenantContextAccessor tenantContextAccessor)
|
||||
: IRequestHandler<GetMiniBootstrapQuery, MiniBootstrapResponse>
|
||||
{
|
||||
private static readonly string[] Scenes = ["Delivery", "Pickup", "DineIn"];
|
||||
private static readonly string[] Channels = ["WeChatMiniProgram"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<MiniBootstrapResponse> Handle(GetMiniBootstrapQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var response = new MiniBootstrapResponse
|
||||
{
|
||||
Service = "TakeoutSaaS.MiniApi",
|
||||
Environment = environment.EnvironmentName,
|
||||
ServerTime = DateTime.UtcNow,
|
||||
SupportedScenes = Scenes,
|
||||
SupportedChannels = Channels,
|
||||
TenantCode = tenantContextAccessor.Current?.TenantCode
|
||||
};
|
||||
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace TakeoutSaaS.Application.App.Mini.Bootstrap;
|
||||
|
||||
/// <summary>
|
||||
/// 小程序端启动引导响应。
|
||||
/// </summary>
|
||||
public sealed record MiniBootstrapResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 服务标识。
|
||||
/// </summary>
|
||||
public string Service { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 当前环境名称。
|
||||
/// </summary>
|
||||
public string Environment { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 服务端时间。
|
||||
/// </summary>
|
||||
public DateTime ServerTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 支持的履约场景。
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> SupportedScenes { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 支持的渠道。
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> SupportedChannels { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 当前请求解析到的租户编码。
|
||||
/// </summary>
|
||||
public string? TenantCode { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MediatR" Version="14.0.0" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\Domain\TakeoutSaaS.Domain\TakeoutSaaS.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
12
src/Domain/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj
Normal file
12
src/Domain/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj
Normal file
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.App.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// 基础设施服务注册。
|
||||
/// </summary>
|
||||
public static class AppServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册骨架所需的基础设施能力。
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合。</param>
|
||||
/// <param name="configuration">配置源。</param>
|
||||
/// <returns>服务集合。</returns>
|
||||
public static IServiceCollection AddAppInfrastructure(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
_ = configuration;
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Application\TakeoutSaaS.Application\TakeoutSaaS.Application.csproj" />
|
||||
<ProjectReference Include="..\..\Domain\TakeoutSaaS.Domain\TakeoutSaaS.Domain.csproj" />
|
||||
<ProjectReference Include="..\..\..\TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,40 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Module.Tenancy.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// 多租户服务注册与应用扩展。
|
||||
/// </summary>
|
||||
public static class TenantServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册轻量多租户服务。
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合。</param>
|
||||
/// <param name="configuration">配置源。</param>
|
||||
/// <returns>服务集合。</returns>
|
||||
public static IServiceCollection AddTenantResolution(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.TryAddSingleton<ITenantContextAccessor, TenantContextAccessor>();
|
||||
services.TryAddScoped<ITenantProvider, TenantProvider>();
|
||||
services.TryAddScoped<ITenantCodeResolver, NoOpTenantCodeResolver>();
|
||||
|
||||
services.AddOptions<TenantResolutionOptions>()
|
||||
.Bind(configuration.GetSection("Tenancy"))
|
||||
.ValidateDataAnnotations();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用租户解析中间件。
|
||||
/// </summary>
|
||||
/// <param name="app">应用实例。</param>
|
||||
/// <returns>应用实例。</returns>
|
||||
public static IApplicationBuilder UseTenantResolution(this IApplicationBuilder app)
|
||||
=> app.UseMiddleware<TenantResolutionMiddleware>();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace TakeoutSaaS.Module.Tenancy;
|
||||
|
||||
/// <summary>
|
||||
/// 租户编码解析器。
|
||||
/// </summary>
|
||||
public interface ITenantCodeResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据租户编码解析租户 ID。
|
||||
/// </summary>
|
||||
/// <param name="code">租户编码。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>租户 ID;未找到时返回 <c>null</c>。</returns>
|
||||
Task<long?> ResolveAsync(string code, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace TakeoutSaaS.Module.Tenancy;
|
||||
|
||||
/// <summary>
|
||||
/// 默认空实现,不做数据库或远程租户解析。
|
||||
/// </summary>
|
||||
public sealed class NoOpTenantCodeResolver : ITenantCodeResolver
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Task<long?> ResolveAsync(string code, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = code;
|
||||
_ = cancellationToken;
|
||||
return Task.FromResult<long?>(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,33 @@
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Module.Tenancy;
|
||||
|
||||
/// <summary>
|
||||
/// 基于 <see cref="AsyncLocal{T}"/> 的租户上下文访问器。
|
||||
/// </summary>
|
||||
public sealed class TenantContextAccessor : ITenantContextAccessor
|
||||
{
|
||||
private static readonly AsyncLocal<TenantContextHolder?> Holder = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public TenantContext? Current
|
||||
{
|
||||
get => Holder.Value?.Context;
|
||||
set
|
||||
{
|
||||
if (Holder.Value != null)
|
||||
{
|
||||
Holder.Value.Context = value;
|
||||
}
|
||||
else if (value != null)
|
||||
{
|
||||
Holder.Value = new TenantContextHolder { Context = value };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TenantContextHolder
|
||||
{
|
||||
public TenantContext? Context { get; set; }
|
||||
}
|
||||
}
|
||||
13
src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs
Normal file
13
src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Module.Tenancy;
|
||||
|
||||
/// <summary>
|
||||
/// 默认租户提供者。
|
||||
/// </summary>
|
||||
public sealed class TenantProvider(ITenantContextAccessor tenantContextAccessor) : ITenantProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public long GetCurrentTenantId()
|
||||
=> tenantContextAccessor.Current?.TenantId ?? 0;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Module.Tenancy;
|
||||
|
||||
/// <summary>
|
||||
/// 轻量多租户解析中间件。
|
||||
/// </summary>
|
||||
public sealed class TenantResolutionMiddleware(
|
||||
RequestDelegate next,
|
||||
ILogger<TenantResolutionMiddleware> logger,
|
||||
ITenantContextAccessor tenantContextAccessor,
|
||||
IOptionsMonitor<TenantResolutionOptions> optionsMonitor)
|
||||
{
|
||||
/// <summary>
|
||||
/// 执行租户解析并写入上下文。
|
||||
/// </summary>
|
||||
/// <param name="context">HTTP 上下文。</param>
|
||||
/// <param name="tenantCodeResolver">租户编码解析器。</param>
|
||||
/// <returns>任务。</returns>
|
||||
public async Task InvokeAsync(HttpContext context, ITenantCodeResolver tenantCodeResolver)
|
||||
{
|
||||
var options = optionsMonitor.CurrentValue ?? new TenantResolutionOptions();
|
||||
if (ShouldSkip(context.Request.Path, options))
|
||||
{
|
||||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var tenantContext = await ResolveTenantAsync(context, options, tenantCodeResolver);
|
||||
tenantContextAccessor.Current = tenantContext;
|
||||
context.Items[TenantConstants.HttpContextItemKey] = tenantContext;
|
||||
|
||||
if (!tenantContext.IsResolved && options.ThrowIfUnresolved)
|
||||
{
|
||||
logger.LogDebug("未能解析租户:{Path}", context.Request.Path);
|
||||
var response = ApiResponse.Error(ErrorCodes.BadRequest, "缺少租户标识");
|
||||
context.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
await context.Response.WriteAsJsonAsync(response, cancellationToken: context.RequestAborted);
|
||||
tenantContextAccessor.Current = null;
|
||||
context.Items.Remove(TenantConstants.HttpContextItemKey);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await next(context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
tenantContextAccessor.Current = null;
|
||||
context.Items.Remove(TenantConstants.HttpContextItemKey);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldSkip(PathString path, TenantResolutionOptions options)
|
||||
{
|
||||
if (!path.HasValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return options.IgnoredPaths.Any(ignore =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ignore))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return path.StartsWithSegments(ignore, StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<TenantContext> ResolveTenantAsync(
|
||||
HttpContext context,
|
||||
TenantResolutionOptions options,
|
||||
ITenantCodeResolver tenantCodeResolver)
|
||||
{
|
||||
var request = context.Request;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(options.TenantIdHeaderName) &&
|
||||
request.Headers.TryGetValue(options.TenantIdHeaderName, out var tenantIdHeader) &&
|
||||
long.TryParse(tenantIdHeader.FirstOrDefault(), out var tenantId) &&
|
||||
tenantId > 0)
|
||||
{
|
||||
return new TenantContext(tenantId, null, $"header:{options.TenantIdHeaderName}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(options.TenantCodeHeaderName) &&
|
||||
request.Headers.TryGetValue(options.TenantCodeHeaderName, out var tenantCodeHeader))
|
||||
{
|
||||
var tenantCode = tenantCodeHeader.FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(tenantCode))
|
||||
{
|
||||
var resolvedTenantId = await tenantCodeResolver.ResolveAsync(tenantCode, context.RequestAborted);
|
||||
return new TenantContext(resolvedTenantId ?? 0, tenantCode, $"header:{options.TenantCodeHeaderName}");
|
||||
}
|
||||
}
|
||||
|
||||
return TenantContext.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace TakeoutSaaS.Module.Tenancy;
|
||||
|
||||
/// <summary>
|
||||
/// 多租户解析配置。
|
||||
/// </summary>
|
||||
public sealed class TenantResolutionOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Header 租户 ID 名称。
|
||||
/// </summary>
|
||||
public string TenantIdHeaderName { get; set; } = "X-Tenant-Id";
|
||||
|
||||
/// <summary>
|
||||
/// Header 租户编码名称。
|
||||
/// </summary>
|
||||
public string TenantCodeHeaderName { get; set; } = "X-Tenant-Code";
|
||||
|
||||
/// <summary>
|
||||
/// 跳过解析的路径列表。
|
||||
/// </summary>
|
||||
public ISet<string> IgnoredPaths { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"/healthz",
|
||||
"/api/mini/v1/health"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 未解析租户时是否立即返回 400。
|
||||
/// </summary>
|
||||
public bool ThrowIfUnresolved { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 对外只读的忽略路径视图。
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<string> IgnoredPathItems => new ReadOnlyCollection<string>(IgnoredPaths.ToList());
|
||||
}
|
||||
7
stylecop.json
Normal file
7
stylecop.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"settings": {
|
||||
"documentationRules": {
|
||||
"documentationCulture": "zh-CN"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user