chore(build): 添加基础构建配置和版本管理

- 新增 .dockerignore 文件,忽略多种临时及中间文件
- 新增 .gitattributes 文件,配置文本文件换行及合并行为
- 新增详细的 .gitignore 文件,排除多种开发及生成文件
- 新增 VS Code C# 代码片段,提升开发效率
- 添加 Directory.Build.props,统一 MSBuild 配置和代码分析规则
- 添加空的 Directory.Build.targets,预留构建任务扩展位置
- 添加 Directory.Packages.props,实现依赖包版本集中管理和声明
This commit is contained in:
sam 2026-02-11 12:58:54 +08:00
commit e24925e1ed
113 changed files with 12163 additions and 0 deletions

25
Backend/.dockerignore Normal file
View File

@ -0,0 +1,25 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

63
Backend/.gitattributes vendored Normal file
View File

@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

398
Backend/.gitignore vendored Normal file
View File

@ -0,0 +1,398 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
# Exception: allow frontend scripts bin directory
!src/frontend/scripts/**/bin/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
# Only ignore NuGet packages folders at project root level, not frontend workspace packages
/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Nuget personal access tokens and Credentials
# nuget.config
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/csharp.code-snippets
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
.idea/
*.sln.iml
/.vs
# Internal packages build outputs (generated by unbuild --stub, contains absolute paths)
src/frontend/internal/**/dist/
src/frontend/packages/**/dist/
src/frontend/scripts/**/dist/

625
Backend/.vscode/csharp.code-snippets vendored Normal file
View File

@ -0,0 +1,625 @@
{
"PostProcessor Class": {
"prefix": "postproc",
"body": [
"sealed class ${1:MyProcessor} : IPostProcessor<${2:My}Request, ${2:My}Response>",
"{",
" public Task PostProcessAsync(${2:My}Request req, ${2:My}Response res, HttpContext ctx, IReadOnlyCollection<ValidationFailure> fails, CancellationToken c)",
" {",
" $0",
" }",
"}"
],
"description": "post-processor"
},
"Test Class": {
"prefix": "tstclass",
"body": [
"namespace Tests;",
"",
"public class ${1:My}Tests : TestClass<${2:App}Fixture>",
"{",
" public ${1:My}Tests(${2:App}Fixture f, ITestOutputHelper o) : base(f, o) { }",
"",
" [Fact]",
" public async Task ${3:Name_Of_The_Test}()",
" {",
" $0",
" }",
"}"
],
"description": "test class"
},
"Endpoint with Request Only": {
"prefix": "epreq",
"body": [
"sealed class ${1:My}Request",
"{",
"",
"}",
"",
"sealed class ${1:My}Endpoint : Endpoint<${1:My}Request>",
"{",
" public override void Configure()",
" {",
" ${2:Post}(\"${3:route-pattern}\");",
" AllowAnonymous();",
" }",
"",
" public override async Task HandleAsync(${1:My}Request r, CancellationToken c)",
" {",
" $0",
" }",
"}"
],
"description": "endpoint with request only"
},
"NetCorePal Command": {
"prefix": "ncpcmd",
"body": [
"public record ${1:My}Command() : ICommand;",
"",
"public class ${1:My}CommandValidator : AbstractValidator<${1:My}Command>",
"{",
" public ${1:My}CommandValidator()",
" {",
" // 添加验证规则示例:",
" // RuleFor(x => x.Property).NotEmpty();",
" }",
"}",
"",
"public class ${1:My}CommandHandler : ICommandHandler<${1:My}Command>",
"{",
" public async Task Handle(",
" ${1:My}Command request, ",
" CancellationToken cancellationToken)",
" {",
" // 实现业务逻辑",
" throw new NotImplementedException();",
" }",
"}"
],
"description": "创建命令"
},
"NetCorePal Command with Response": {
"prefix": "ncpcmdres",
"body": [
"public record ${1:My}Command() : ICommand<${1:My}CommandResponse>;",
"",
"public record ${1:My}CommandResponse();",
"",
"public class ${1:My}CommandValidator : AbstractValidator<${1:My}Command>",
"{",
" public ${1:My}CommandValidator()",
" {",
" // 添加验证规则示例:",
" // RuleFor(x => x.Property).NotEmpty();",
" }",
"}",
"",
"public class ${1:My}CommandHandler : ICommandHandler<${1:My}Command, ${1:My}CommandResponse>",
"{",
" public async Task<${1:My}CommandResponse> Handle(",
" ${1:My}Command request,",
" CancellationToken cancellationToken)",
" {",
" // 实现业务逻辑",
" throw new NotImplementedException();",
" }",
"}"
],
"description": "创建命令(含返回值)"
},
"Endpoint Request & Response DTOs": {
"prefix": "epdto",
"body": [
"sealed class ${1:My}Request",
"{",
" $0",
"}",
"",
"sealed class ${1:My}Response",
"{",
"",
"}"
],
"description": "endpoint request & response dtos"
},
"NetCorePal Aggregate Root": {
"prefix": "ncpar",
"body": [
"public partial record ${1:My}Id : IInt64StronglyTypedId;",
"",
"public class ${1:My} : Entity<${1:My}Id>, IAggregateRoot",
"{",
" protected ${1:My}() { }",
"}"
],
"description": "创建聚合根"
},
"Test Fixture": {
"prefix": "tstfixture",
"body": [
"namespace Tests;",
"",
"public class ${1:App}Fixture : TestFixture<Program>",
"{",
" public ${1:App}Fixture(IMessageSink s) : base(s) { }",
"",
" protected override Task SetupAsync()",
" {",
" $0",
" }",
"",
" protected override void ConfigureServices(IServiceCollection s)",
" {",
"",
" }",
"",
" protected override Task TearDownAsync()",
" {",
"",
" }",
"}"
],
"description": "test fixture"
},
"Event Handler": {
"prefix": "evnt",
"body": [
"sealed class ${1:MyEvent} : IEvent",
"{",
"",
"}",
"",
"sealed class ${1:MyEvent}Handler : IEventHandler<${1:MyEvent}>",
"{",
" public Task HandleAsync(${1:MyEvent} e, CancellationToken c)",
" {",
" $0",
" }",
"}"
],
"description": "event handler"
},
"NetCorePal Repository": {
"prefix": "ncprepo",
"body": [
"public interface I${1:My}Repository : IRepository<${1:My}, ${1:My}Id>;",
"",
"public class ${1:My}Repository(ApplicationDbContext context) ",
" : RepositoryBase<${1:My}, ${1:My}Id, ApplicationDbContext>(context), ",
" I${1:My}Repository",
"{",
"}"
],
"description": "创建仓储"
},
"Endpoint Data": {
"prefix": "epdat",
"body": [
"static class ${1:My}Data",
"{",
" $0",
"}"
],
"description": "endpoint data"
},
"Command Handler with Result": {
"prefix": "cmdres",
"body": [
"sealed class ${1:MyCommand} : ICommand<${1:MyCommand}Result>",
"{",
"",
"}",
"",
"sealed class ${1:MyCommand}Result",
"{",
"",
"}",
"",
"sealed class ${1:MyCommand}Handler : ICommandHandler<${1:MyCommand}, ${1:MyCommand}Result>",
"{",
" public Task<${1:MyCommand}Result> ExecuteAsync(${1:MyCommand} cmd, CancellationToken c)",
" {",
" $0",
" }",
"}"
],
"description": "command handler with result"
},
"Command Handler": {
"prefix": "cmd",
"body": [
"sealed class ${1:MyCommand} : ICommand",
"{",
"",
"}",
"",
"sealed class ${1:MyCommand}Handler : ICommandHandler<${1:MyCommand}>",
"{",
" public Task ExecuteAsync(${1:MyCommand} cmd, CancellationToken c)",
" {",
" $0",
" }",
"}"
],
"description": "command handler"
},
"Endpoint Validator": {
"prefix": "epval",
"body": [
"sealed class ${1:My}Validator : Validator<${1:My}Request>",
"{",
" public ${1:My}Validator()",
" {",
" $0",
" }",
"}"
],
"description": "endpoint validator"
},
"Global Pre-processor": {
"prefix": "preproc_g",
"body": [
"sealed class ${1:MyProcessor} : IGlobalPreProcessor",
"{",
" public Task PreProcessAsync(object r, HttpContext ctx, List<ValidationFailure> fails, CancellationToken c)",
" {",
" $0",
" }",
"}"
],
"description": "global pre-processor"
},
"Endpoint with Response Only": {
"prefix": "epres",
"body": [
"sealed class ${1:My}Response",
"{",
"",
"}",
"",
"sealed class ${1:My}Endpoint : EndpointWithoutRequest<${1:My}Response>",
"{",
" public override void Configure()",
" {",
" ${2:Get}(\"${3:route-pattern}\");",
" AllowAnonymous();",
" }",
"",
" public override async Task HandleAsync(CancellationToken c)",
" {",
" $0",
" }",
"}"
],
"description": "endpoint with response only"
},
"NetCorePal Integration Event": {
"prefix": "ncpie",
"body": [
"public record ${1:MyCreated}IntegrationEvent();",
"",
"public class ${1:MyCreated}IntegrationEventHandler(IMediator mediator) : IIntegrationEventHandler<${1:MyCreated}IntegrationEvent>",
"{",
" public Task HandleAsync(${1:MyCreated}IntegrationEvent eventData, CancellationToken cancellationToken = default)",
" {",
" // var cmd = new ${1:MyCreated}Command(eventData.Id);",
" // return mediator.Send(cmd, cancellationToken);",
" throw new NotImplementedException();",
" }",
"}"
],
"description": "创建集成事件与事件处理器"
},
"NetCorePal Domain Event Handler": {
"prefix": "ncpdeh",
"body": [
"public class ${1:MyCreated}DomainEventHandler(IMediator mediator) ",
" : IDomainEventHandler<${1:MyCreated}DomainEvent>",
"{",
" public async Task Handle(${1:MyCreated}DomainEvent notification, ",
" CancellationToken cancellationToken)",
" {",
" // 实现业务逻辑",
" throw new NotImplementedException();",
" }",
"}"
],
"description": "创建领域事件处理器"
},
"FastEndpoint - NCP Style": {
"prefix": "epp",
"body": [
"sealed class ${1:My}Endpoint(IMediator mediator) : Endpoint<${1:My}Request, ResponseData<${1:My}Response>>",
"{",
" public override void Configure()",
" {",
" ${2:Post}(\"${3:route-pattern}\");",
" AllowAnonymous();",
" }",
"",
" public override async Task HandleAsync(${1:My}Request r, CancellationToken c)",
" {",
" var cmd = new ${1:My}Command(r.Property1, r.Property2);",
" var result = await mediator.Send(cmd, c);",
" var res = new ${1:My}Response();",
" await SendOkAsync(res.AsResponseData(), c);",
" $0",
" }",
"}",
"",
"sealed record ${1:My}Request();",
"",
"sealed record ${1:My}Response();",
"",
"sealed class ${1:My}Validator : Validator<${1:My}Request>",
"{",
" public ${1:My}Validator()",
" {",
" // RuleFor(x => x.Property).NotEmpty();",
" }",
"}",
"",
"sealed class ${1:My}Summary : Summary<${1:My}Endpoint, ${1:My}Request>",
"{",
" public ${1:My}Summary()",
" {",
" Summary = \"${4:Summary text goes here...}\";",
" Description = \"${5:Description text goes here...}\";",
" }",
"}"
],
"description": "endpoint vertical slice - NCP"
},
"Pre-processor": {
"prefix": "preproc",
"body": [
"sealed class ${1:MyProcessor} : IPreProcessor<${2:My}Request>",
"{",
" public Task PreProcessAsync(${2:My}Request r, HttpContext ctx, List<ValidationFailure> fails, CancellationToken c)",
" {",
" $0",
" }",
"}"
],
"description": "pre-processor"
},
"NetCorePal Integration Event Converter": {
"prefix": "ncpiec",
"body": [
"public class ${1:MyCreated}IntegrationEventConverter",
" : IIntegrationEventConverter<${1:MyCreated}DomainEvent, ${1:MyCreated}IntegrationEvent>",
"{",
" public ${1:MyCreated}IntegrationEvent Convert(${1:MyCreated}DomainEvent domainEvent)",
" {",
" // return new ${1:MyCreated}IntegrationEvent(domainEvent.Id);",
" throw new NotImplementedException();",
" }",
"}"
],
"description": "创建集成事件转换器"
},
"Endpoint Mapper": {
"prefix": "epmap",
"body": [
"sealed class ${1:My}Mapper : Mapper<${1:My}Request, ${1:My}Response, ${2:YourEntity}>",
"{",
" public override ${2:YourEntity} ToEntity(${1:My}Request r) => new()",
" {",
" $0",
" };",
"",
" public override ${1:My}Response FromEntity(${2:YourEntity} e) => new()",
" {",
" ",
" };",
"}"
],
"description": "endpoint mapper"
},
"FastEndpoint Full Vertical Slice": {
"prefix": "epfull",
"body": [
"sealed class ${1:My}Endpoint : Endpoint<${1:My}Request, ${1:My}Response, ${1:My}Mapper>",
"{",
" public override void Configure()",
" {",
" ${2:Post}(\"${3:route-pattern}\");",
" AllowAnonymous();",
" }",
"",
" public override async Task HandleAsync(${1:My}Request r, CancellationToken c)",
" {",
" $0",
" }",
"}",
"",
"sealed class ${1:My}Request",
"{",
"",
"}",
"",
"sealed class ${1:My}Response",
"{",
"",
"}",
"",
"sealed class ${1:My}Validator : Validator<${1:My}Request>",
"{",
" public ${1:My}Validator()",
" {",
"",
" }",
"}",
"",
"sealed class ${1:My}Mapper: Mapper<${1:My}Request, ${1:My}Response, ${4:YourEntity}>",
"{",
" public override ${4:YourEntity} ToEntity(${1:My}Request r) => new()",
" {",
"",
" };",
"",
" public override ${1:My}Response FromEntity(${4:YourEntity} e) => new()",
" {",
"",
" };",
"}",
"",
"sealed class ${1:My}Summary : Summary<${1:My}Endpoint, ${1:My}Request>",
"{",
" public ${1:My}Summary()",
" {",
" Summary = \"${5:Summary text goes here...}\";",
" Description = \"${6:Description text goes here...}\";",
" }",
"}"
],
"description": "endpoint vertical slice"
},
"Global Post-processor": {
"prefix": "postproc_g",
"body": [
"sealed class ${1:MyProcessor} : IGlobalPostProcessor",
"{",
" public Task PostProcessAsync(object req, object? res, HttpContext ctx, IReadOnlyCollection<ValidationFailure> fails, CancellationToken c)",
" {",
" $0",
" }",
"}"
],
"description": "global post-processor"
},
"Test Method": {
"prefix": "tstmethod",
"body": [
" [Fact]",
" public async Task ${1:Name_Of_The_Test}()",
" {",
" $0",
" }"
],
"description": "test method"
},
"NetCorePal Domain Event": {
"prefix": "ncpde",
"body": [
"public record ${1:MyCreated}DomainEvent() : IDomainEvent;"
],
"description": "创建领域事件"
},
"Endpoint Summary": {
"prefix": "epsum",
"body": [
"sealed class ${1:My}Summary : Summary<${1:My}Endpoint, ${1:My}Request>",
"{",
" public ${1:My}Summary()",
" {",
" Summary = \"${2:Summary text goes here...}\";",
" Description = \"${3:Description text goes here...}\";",
" $0",
" }",
"}"
],
"description": "endpoint summary"
},
"Endpoint Without Request": {
"prefix": "epnoreq",
"body": [
"sealed class ${1:My}Endpoint : EndpointWithoutRequest",
"{",
" public override void Configure()",
" {",
" ${2:Get}(\"${3:route}\");",
" AllowAnonymous();",
" }",
"",
" public override async Task HandleAsync(CancellationToken c)",
" {",
" $0",
" }",
"}"
],
"description": "endpoint without request"
},
"Endpoint with Request & Response": {
"prefix": "epreqres",
"body": [
"sealed class ${1:My}Request",
"{",
"",
"}",
"",
"sealed class ${1:My}Response",
"{",
"",
"}",
"",
"sealed class ${1:My}Endpoint : Endpoint<${1:My}Request, ${1:My}Response>",
"{",
" public override void Configure()",
" {",
" ${2:Post}(\"${3:route-pattern}\");",
" AllowAnonymous();",
" }",
"",
" public override async Task HandleAsync(${1:My}Request r, CancellationToken c)",
" {",
" $0",
" }",
"}"
],
"description": "endpoint with request & response"
},
"NetCorePal Entity Configuration": {
"prefix": "ncpconfig",
"body": [
"public class ${1:Entity}Configuration : IEntityTypeConfiguration<${1:Entity}>",
"{",
" public void Configure(EntityTypeBuilder<${1:Entity}> builder)",
" {",
" builder.ToTable(\"${2:table}\");",
" builder.HasKey(t => t.Id);",
" builder.Property(t => t.Id)",
" /*.UseSnowFlakeValueGenerator()*/ // 如果使用 SnowFlake ID 生成器,请取消注释",
" /*.UseGuidVersion7ValueGenerator()*/ // 如果使用 Guid Version 7 ID 生成器,请取消注释",
" ;",
"",
" // Configure other properties if needed",
" $0",
" }",
"}"
],
"description": "创建实体配置类"
}
}

View File

@ -0,0 +1,34 @@
<Project>
<Import Project="$(MSBuildThisFileDirectory)\eng\versions.props"/>
<PropertyGroup>
<Authors>Fengling.Backend</Authors>
<Product>Fengling.Backend</Product>
<owners>Fengling.Backend</owners>
<PackagePrefix>Fengling.Backend</PackagePrefix>
<PackageIconUrl></PackageIconUrl>
<PackageProjectUrl></PackageProjectUrl>
<PackageLicenseUrl></PackageLicenseUrl>
<RepositoryType>git</RepositoryType>
<RepositoryUrl></RepositoryUrl>
<GenerateAssemblyConfigurationAttribute>True</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>True</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>True</GenerateAssemblyProductAttribute>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591;NU1507;S125;CS9107;</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="$(IsTestProject) != 'true'">
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<WarningsAsErrors>$(WarningsAsErrors);CS8625;CS8604;CS8602;CS8600;CS8618;CS8601;CS8603</WarningsAsErrors>
</PropertyGroup>
<ItemGroup Condition="$(IsTestProject) != 'true'">
<PackageReference Include="SonarAnalyzer.CSharp">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,3 @@
<Project>
<!-- Keep empty for now, version management moved to Directory.Packages.props -->
</Project>

View File

@ -0,0 +1,152 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<PropertyGroup>
<!-- Third-party package versions -->
<NetCorePalVersion>3.2.1</NetCorePalVersion>
<FastEndpointsVersion>7.1.1</FastEndpointsVersion>
<TestcontainersVersion>4.9.0</TestcontainersVersion>
<AspireVersion>13.1.0</AspireVersion>
<OpenTelemetryVersion>1.14.0</OpenTelemetryVersion>
<NetCorePalTestcontainerVersion>1.0.5</NetCorePalTestcontainerVersion>
<NetCorePalAspireVersion>1.1.2</NetCorePalAspireVersion>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0" />
<PackageVersion Include="AspNet.Security.OAuth.Feishu" Version="9.0.0" />
<PackageVersion Include="AspNet.Security.OAuth.Weixin" Version="9.0.0" />
<!-- Database providers - framework specific versions -->
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
<PackageVersion Include="DM.Microsoft.EntityFrameworkCore" Version="9.0.0.37033" />
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql.Json.Microsoft" Version="9.0.0" />
<PackageVersion Include="MySql.EntityFrameworkCore" Version="9.0.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />
<PackageVersion Include="DotNetCore.EntityFrameworkCore.GaussDB" Version="9.0.0" />
<PackageVersion Include="DotNetCore.EntityFrameworkCore.KingbaseES" Version="9.0.0" />
<PackageVersion Include="MongoDB.EntityFrameworkCore" Version="9.0.3" />
<!-- ASP.NET Core and Microsoft packages - framework specific versions -->
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="Microsoft.CodeAnalysis" Version="4.12.0" />
<!-- CAP packages for .NET 9.0+ -->
<PackageVersion Include="DotNetCore.CAP.Dashboard" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.RabbitMQ" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.Kafka" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.AzureServiceBus" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.AmazonSQS" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.NATS" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.RedisStreams" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.Pulsar" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.OpenTelemetry" Version="8.4.1" />
<!-- FastEndpoints -->
<PackageVersion Include="FastEndpoints" Version="$(FastEndpointsVersion)" />
<PackageVersion Include="FastEndpoints.Swagger" Version="$(FastEndpointsVersion)" />
<PackageVersion Include="FastEndpoints.Swagger.Swashbuckle" Version="2.3.0" />
<!-- Other packages -->
<PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageVersion Include="Hangfire.AspNetCore" Version="1.8.17" />
<PackageVersion Include="Hangfire.Redis.StackExchange" Version="1.9.4" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageVersion Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1" />
<PackageVersion Include="Refit.HttpClientFactory" Version="8.0.0" />
<PackageVersion Include="Refit.Newtonsoft.Json" Version="8.0.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageVersion Include="Serilog.Enrichers.ClientInfo" Version="2.1.2" />
<PackageVersion Include="Serilog.Sinks.OpenTelemetry" Version="4.1.0" />
<PackageVersion Include="StackExchange.Redis" Version="2.9.32" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.2.0" />
<!-- Aspire packages -->
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.Docker" Version="13.1.0-preview.1.25616.3" />
<PackageVersion Include="Aspire.Hosting.Testing" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.MySql" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.SqlServer" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.PostgreSQL" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.MongoDB" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.Redis" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.RabbitMQ" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.Kafka" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.NATS" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.StackExchange.Redis" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Pomelo.EntityFrameworkCore.MySql" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Microsoft.EntityFrameworkCore.SqlServer" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="$(AspireVersion)" />
<PackageVersion Include="CommunityToolkit.Aspire.Hosting.MongoDB.Extensions" Version="13.0.0" />
<PackageVersion Include="Aspire.RabbitMQ.Client" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Confluent.Kafka" Version="$(AspireVersion)" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.1.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="$(OpenTelemetryVersion)" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="$(OpenTelemetryVersion)" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="$(OpenTelemetryVersion)" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="$(OpenTelemetryVersion)" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="$(OpenTelemetryVersion)" />
<PackageVersion Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.13.0-beta.1" />
<PackageVersion Include="Npgsql.OpenTelemetry" Version="8.0.8" />
<!-- NetCorePal packages -->
<PackageVersion Include="NetCorePal.Context.AspNetCore" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Context.CAP" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Context.Shared" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.AspNetCore" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.CodeAnalysis" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.DistributedLocks.Redis" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.DistributedTransactions.CAP.MySql" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.DistributedTransactions.CAP.SqlServer" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.DistributedTransactions.CAP.PostgreSQL" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.DistributedTransactions.CAP.Sqlite" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.DistributedTransactions.CAP.DMDB" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.DistributedTransactions.CAP.MongoDB" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.DistributedTransactions.CAP.GaussDB" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.DistributedTransactions.CAP.KingbaseES" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.Domain.Abstractions" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.Jwt.StackExchangeRedis" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.MicrosoftServiceDiscovery" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.MultiEnv" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.NewtonsoftJson" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.Primitives" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.Repository.EntityFrameworkCore" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.Repository.EntityFrameworkCore.Snowflake" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.OpenTelemetry.Diagnostics" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Aspire.Hosting.DMDB" Version="$(NetCorePalAspireVersion)" />
<PackageVersion Include="NetCorePal.Aspire.Hosting.OpenGauss" Version="$(NetCorePalAspireVersion)" />
<PackageVersion Include="NetCorePal.Aspire.Hosting.MongoDB" Version="$(NetCorePalAspireVersion)" />
<!-- Testing packages -->
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="Testcontainers" Version="$(TestcontainersVersion)" />
<PackageVersion Include="Testcontainers.MySql" Version="$(TestcontainersVersion)" />
<PackageVersion Include="Testcontainers.PostgreSql" Version="$(TestcontainersVersion)" />
<PackageVersion Include="Testcontainers.MongoDb" Version="$(TestcontainersVersion)" />
<PackageVersion Include="Testcontainers.MsSql" Version="$(TestcontainersVersion)" />
<PackageVersion Include="Testcontainers.RabbitMq" Version="$(TestcontainersVersion)" />
<PackageVersion Include="Testcontainers.Kafka" Version="$(TestcontainersVersion)" />
<PackageVersion Include="Testcontainers.Nats" Version="$(TestcontainersVersion)" />
<PackageVersion Include="Testcontainers.Redis" Version="$(TestcontainersVersion)" />
<PackageVersion Include="NetCorePal.Testcontainers.DMDB" Version="$(NetCorePalTestcontainerVersion)" />
<PackageVersion Include="NetCorePal.Testcontainers.KingbaseES" Version="$(NetCorePalTestcontainerVersion)" />
<PackageVersion Include="NetCorePal.Testcontainers.OpenGauss" Version="$(NetCorePalTestcontainerVersion)" />
<PackageVersion Include="xunit.v3" Version="3.2.1" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="FastEndpoints.Testing" Version="$(FastEndpointsVersion)" />
<!-- Code analysis -->
<PackageVersion Include="SonarAnalyzer.CSharp" Version="10.3.0.106239" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,918 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/QuickList/=F0CA621CDF5AB24282D8CDC11C520997/Entry/=2CBD6971A7955044AD2624B84FB49E38/@KeyIndexDefined">False</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/QuickList/=F0CA621CDF5AB24282D8CDC11C520997/Entry/=40C163D436D8ED48A6D01A0AFEFC5556/@KeyIndexDefined">False</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/QuickList/=F0CA621CDF5AB24282D8CDC11C520997/Entry/=567DCF4B487C244A9F6BB46E4E9F3B84/@KeyIndexDefined">False</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/QuickList/=F0CA621CDF5AB24282D8CDC11C520997/Entry/=7F2A1BE8D0078241A9AE7802038BAD3C/@KeyIndexDefined">False</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/QuickList/=F0CA621CDF5AB24282D8CDC11C520997/Entry/=C4795E57DDEC1C4F97BBC8C7173EBBCA/@KeyIndexDefined">False</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Description/@EntryValue">post-processor</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Shortcut/@EntryValue">postproc</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Text/@EntryValue">sealed class $name$ : IPostProcessor&lt;$dto$Request, $dto$Response&gt;
{
public Task PostProcessAsync($dto$Request req, $dto$Response res, HttpContext ctx, IReadOnlyCollection&lt;ValidationFailure&gt; fails, CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Field/=dto/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Field/=dto/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Field/=dto/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Field/=name/Expression/@EntryValue">constant("MyProcessor")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Description/@EntryValue">test class</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Shortcut/@EntryValue">tstclass</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Text/@EntryValue">namespace Tests;
public class $name$Tests : TestClass&lt;$fixture$Fixture&gt;
{
public $name$Tests($fixture$Fixture f, ITestOutputHelper o) : base(f, o) { }
[Fact]
public async Task $test_name$()
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Field/=fixture/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Field/=fixture/Expression/@EntryValue">constant("App")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Field/=fixture/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Field/=name/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Field/=test_005Fname/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Field/=test_005Fname/Expression/@EntryValue">constant("Name_Of_The_Test")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Field/=test_005Fname/Order/@EntryValue">2</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Description/@EntryValue">endpoint with request only</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Shortcut/@EntryValue">epreq</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Text/@EntryValue">sealed class $epName$Request
{
}
sealed class $epName$Endpoint : Endpoint&lt;$epName$Request&gt;
{
public override void Configure()
{
$verb$("$route$");
AllowAnonymous();
}
public override async Task HandleAsync($epName$Request r, CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Field/=epName/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Field/=epName/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Field/=epName/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Field/=route/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Field/=route/Expression/@EntryValue">constant("route-pattern")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Field/=route/Order/@EntryValue">2</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Field/=verb/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Field/=verb/Expression/@EntryValue">constant("Post")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Field/=verb/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Description/@EntryValue">创建命令</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Field/=name/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Reformat/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Shortcut/@EntryValue">ncpcmd</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Text/@EntryValue">public record $name$Command() : ICommand;
public class $name$CommandValidator : AbstractValidator&lt;$name$Command&gt;
{
public $name$CommandValidator()
{
// 添加验证规则示例:
// RuleFor(x =&gt; x.Property).NotEmpty();
}
}
public class $name$CommandHandler : ICommandHandler&lt;$name$Command&gt;
{
public async Task Handle(
$name$Command request,
CancellationToken cancellationToken)
{
// 实现业务逻辑
throw new NotImplementedException();
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Description/@EntryValue">创建命令(含返回值)</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Field/=name/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Reformat/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Shortcut/@EntryValue">ncpcmdres</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Text/@EntryValue">public record $name$Command() : ICommand&lt;$name$CommandResponse&gt;;
public record $name$CommandResponse();
public class $name$CommandValidator : AbstractValidator&lt;$name$Command&gt;
{
public $name$CommandValidator()
{
// 添加验证规则示例:
// RuleFor(x =&gt; x.Property).NotEmpty();
}
}
public class $name$CommandHandler : ICommandHandler&lt;$name$Command, $name$CommandResponse&gt;
{
public async Task&lt;$name$CommandResponse&gt; Handle(
$name$Command request,
CancellationToken cancellationToken)
{
// 实现业务逻辑
throw new NotImplementedException();
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Description/@EntryValue">endpoint request &amp; response dtos</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Shortcut/@EntryValue">epdto</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Text/@EntryValue">sealed class $name$Request
{
$END$
}
sealed class $name$Response
{
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Field/=name/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Description/@EntryValue">创建聚合根</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Field/=name/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Reformat/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Shortcut/@EntryValue">ncpar</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Text/@EntryValue">public partial record $name$Id : IInt64StronglyTypedId;
public class $name$ : Entity&lt;$name$Id&gt;, IAggregateRoot
{
protected $name$() { }
}
</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Description/@EntryValue">test fixture</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Shortcut/@EntryValue">tstfixture</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Text/@EntryValue">namespace Tests;
public class $name$Fixture : TestFixture&lt;Program&gt;
{
public $name$Fixture(IMessageSink s) : base(s) { }
protected override Task SetupAsync()
{
$END$
}
protected override void ConfigureServices(IServiceCollection s)
{
}
protected override Task TearDownAsync()
{
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Field/=name/Expression/@EntryValue">constant("App")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Applicability/=File/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/CustomProperties/=Extension/@EntryIndexedValue">cs</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/CustomProperties/=FileName/@EntryIndexedValue">Endpoint</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/CustomProperties/=ValidateFileName/@EntryIndexedValue">False</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Description/@EntryValue">FastEndpoints Feature File Set</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Reformat/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Text/@EntryValue">namespace $name_space$;
sealed class Request
{
}
sealed class Validator : Validator&lt;Request&gt;
{
public Validator()
{
}
}
sealed class Response
{
public string Message =&gt; "This endpoint hasn't been implemented yet!";
}
sealed class Endpoint : Endpoint&lt;Request, Response, Mapper&gt;
{
public override void Configure()
{
Post("$route$");
}
public override async Task HandleAsync(Request r, CancellationToken c)
{
await SendAsync(new Response());$END$
}
}
sealed class Mapper : Mapper&lt;Request, Response, object&gt;
{
}
static class Data
{
}</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/UITag/@EntryValue">Class/Interface</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Field/=name_005Fspace/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Field/=name_005Fspace/Expression/@EntryValue">fileDefaultNamespace()</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Field/=name_005Fspace/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Field/=route/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Field/=route/Expression/@EntryValue">constant("route-pattern")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Field/=route/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Scope/=E8F0594528C33E45BBFEC6CFE851095D/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Scope/=E8F0594528C33E45BBFEC6CFE851095D/Type/@EntryValue">InCSharpProjectFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Description/@EntryValue">event handler</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Shortcut/@EntryValue">evnt</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Text/@EntryValue">sealed class $name$ : IEvent
{
}
sealed class $name$Handler : IEventHandler&lt;$name$&gt;
{
public Task HandleAsync($name$ e, CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Field/=name/Expression/@EntryValue">constant("MyEvent")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Description/@EntryValue">创建仓储</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Field/=name/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Reformat/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Shortcut/@EntryValue">ncprepo</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Text/@EntryValue">public interface I$name$Repository : IRepository&lt;$name$, $name$Id&gt;;
public class $name$Repository(ApplicationDbContext context)
: RepositoryBase&lt;$name$, $name$Id, ApplicationDbContext&gt;(context),
I$name$Repository
{
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Description/@EntryValue">endpoint data</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Shortcut/@EntryValue">epdat</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Text/@EntryValue">static class $name$Data
{
$END$
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Field/=name/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Description/@EntryValue">command handler with result</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Shortcut/@EntryValue">cmdres</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Text/@EntryValue">sealed class $name$ : ICommand&lt;$name$Result&gt;
{
}
sealed class $name$Result
{
}
sealed class $name$Handler : ICommandHandler&lt;$name$, $name$Result&gt;
{
public Task&lt;$name$Result&gt; ExecuteAsync($name$ cmd, CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Field/=name/Expression/@EntryValue">constant("MyCommand")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Description/@EntryValue">command handler</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Shortcut/@EntryValue">cmd</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Text/@EntryValue">sealed class $name$ : ICommand
{
}
sealed class $name$Handler : ICommandHandler&lt;$name$&gt;
{
public Task ExecuteAsync($name$ cmd, CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Field/=name/Expression/@EntryValue">constant("MyCommand")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Description/@EntryValue">endpoint validator</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Shortcut/@EntryValue">epval</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Text/@EntryValue">sealed class $name$Validator : Validator&lt;$name$Request&gt;
{
public $name$Validator()
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Field/=name/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Description/@EntryValue">global pre-processor</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Shortcut/@EntryValue">preproc_g</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Text/@EntryValue">sealed class $name$ : IGlobalPreProcessor
{
public Task PreProcessAsync(object r, HttpContext ctx, List&lt;ValidationFailure&gt; fails, CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Field/=name/Expression/@EntryValue">constant("MyProcessor")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Description/@EntryValue">endpoint with response only</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Shortcut/@EntryValue">epres</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Text/@EntryValue">sealed class $epName$Response
{
}
sealed class $epName$Endpoint : EndpointWithoutRequest&lt;$epName$Response&gt;
{
public override void Configure()
{
$verb$("$route$");
AllowAnonymous();
}
public override async Task HandleAsync(CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Field/=epName/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Field/=epName/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Field/=epName/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Field/=route/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Field/=route/Expression/@EntryValue">constant("route-pattern")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Field/=route/Order/@EntryValue">2</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Field/=verb/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Field/=verb/Expression/@EntryValue">constant("Get")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Field/=verb/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Description/@EntryValue">创建集成事件与事件处理器</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Field/=name/Expression/@EntryValue">constant("MyCreated")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Reformat/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Shortcut/@EntryValue">ncpie</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Text/@EntryValue">public record $name$IntegrationEvent();
public class $name$IntegrationEventHandler(IMediator mediator) : IIntegrationEventHandler&lt;$name$IntegrationEvent&gt;
{
public Task HandleAsync($name$IntegrationEvent eventData, CancellationToken cancellationToken = default)
{
// var cmd = new $name$Command(eventData.Id);
// return mediator.Send(cmd, cancellationToken);
throw new NotImplementedException();
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Description/@EntryValue">创建领域事件处理器</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Field/=name/Expression/@EntryValue">constant("MyCreated")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Reformat/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Shortcut/@EntryValue">ncpdeh</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Text/@EntryValue">public class $name$DomainEventHandler(IMediator mediator)
: IDomainEventHandler&lt;$name$DomainEvent&gt;
{
public async Task Handle($name$DomainEvent notification,
CancellationToken cancellationToken)
{
// 实现业务逻辑
throw new NotImplementedException();
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Description/@EntryValue">endpoint vertical slice - NCP</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=descriptionText/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=descriptionText/Expression/@EntryValue">constant("Description text goes here...")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=descriptionText/Order/@EntryValue">4</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=epName/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=epName/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=epName/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=route/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=route/Expression/@EntryValue">constant("route-pattern")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=route/Order/@EntryValue">2</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=summaryText/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=summaryText/Expression/@EntryValue">constant("Summary text goes here...")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=summaryText/Order/@EntryValue">3</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=verb/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=verb/Expression/@EntryValue">constant("Post")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=verb/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Reformat/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Shortcut/@EntryValue">epp</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Text/@EntryValue">sealed class $epName$Endpoint(IMediator mediator) : Endpoint&lt;$epName$Request, ResponseData&lt;$epName$Response&gt;&gt;
{
public override void Configure()
{
$verb$("$route$");
AllowAnonymous();
}
public override async Task HandleAsync($epName$Request r, CancellationToken c)
{
var cmd = new $epName$Command(r.Property1, r.Property2);
var result = await mediator.Send(cmd, c);
var res = new $epName$Response();
await SendOkAsync(res.AsResponseData(), c);
$END$
}
}
sealed record $epName$Request();
sealed record $epName$Response();
sealed class $epName$Validator : Validator&lt;$epName$Request&gt;
{
public $epName$Validator()
{
// RuleFor(x =&gt; x.Property).NotEmpty();
}
}
sealed class $epName$Summary : Summary&lt;$epName$Endpoint, $epName$Request&gt;
{
public $epName$Summary()
{
Summary = "$summaryText$";
Description = "$descriptionText$";
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Description/@EntryValue">pre-processor</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Shortcut/@EntryValue">preproc</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Text/@EntryValue">sealed class $name$ : IPreProcessor&lt;$req$Request&gt;
{
public Task PreProcessAsync($req$Request r, HttpContext ctx, List&lt;ValidationFailure&gt; fails, CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Field/=name/Expression/@EntryValue">constant("MyProcessor")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Field/=req/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Field/=req/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Field/=req/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Description/@EntryValue">创建集成事件转换器</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Field/=name/Expression/@EntryValue">constant("MyCreated")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Reformat/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Shortcut/@EntryValue">ncpiec</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Text/@EntryValue">public class $name$IntegrationEventConverter
: IIntegrationEventConverter&lt;$name$DomainEvent, $name$IntegrationEvent&gt;
{
public $name$IntegrationEvent Convert($name$DomainEvent domainEvent)
{
// return new $name$IntegrationEvent(domainEvent.Id);
throw new NotImplementedException();
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Description/@EntryValue">endpoint mapper</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Shortcut/@EntryValue">epmap</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Text/@EntryValue">sealed class $epName$Mapper : Mapper&lt;$epName$Request, $epName$Response, $entity$&gt;
{
public override $entity$ ToEntity($epName$Request r) =&gt; new()
{
$END$
};
public override $epName$Response FromEntity($entity$ e) =&gt; new()
{
};
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Field/=entity/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Field/=entity/Expression/@EntryValue">constant("YourEntity")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Field/=entity/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Field/=epName/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Field/=epName/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Field/=epName/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Description/@EntryValue">endpoint vertical slice</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=descriptionText/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=descriptionText/Expression/@EntryValue">constant("Description text goes here...")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=descriptionText/Order/@EntryValue">5</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=summaryText/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=summaryText/Expression/@EntryValue">constant("Summary text goes here...")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=summaryText/Order/@EntryValue">4</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Shortcut/@EntryValue">epfull</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Text/@EntryValue">sealed class $epName$Endpoint : Endpoint&lt;$epName$Request, $epName$Response, $epName$Mapper&gt;
{
public override void Configure()
{
$verb$("$route$");
AllowAnonymous();
}
public override async Task HandleAsync($epName$Request r, CancellationToken c)
{
$END$
}
}
sealed class $epName$Request
{
}
sealed class $epName$Response
{
}
sealed class $epName$Validator : Validator&lt;$epName$Request&gt;
{
public $epName$Validator()
{
}
}
sealed class $epName$Mapper: Mapper&lt;$epName$Request, $epName$Response, $entity$&gt;
{
public override $entity$ ToEntity($epName$Request r) =&gt; new()
{
};
public override $epName$Response FromEntity($entity$ e) =&gt; new()
{
};
}
sealed class $epName$Summary : Summary&lt;$epName$Endpoint, $epName$Request&gt;
{
public $epName$Summary()
{
Summary = "$summaryText$";
Description = "$descriptionText$";
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=entity/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=entity/Expression/@EntryValue">constant("YourEntity")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=entity/Order/@EntryValue">3</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=epName/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=epName/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=epName/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=route/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=route/Expression/@EntryValue">constant("route-pattern")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=route/Order/@EntryValue">2</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=verb/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=verb/Expression/@EntryValue">constant("Post")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=verb/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Description/@EntryValue">global post-processor</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Shortcut/@EntryValue">postproc_g</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Text/@EntryValue">sealed class $name$ : IGlobalPostProcessor
{
public Task PostProcessAsync(object req, object? res, HttpContext ctx, IReadOnlyCollection&lt;ValidationFailure&gt; fails, CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Field/=name/Expression/@EntryValue">constant("MyProcessor")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Description/@EntryValue">test method</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Shortcut/@EntryValue">tstmethod</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Text/@EntryValue"> [Fact]
public async Task $test_name$()
{
$END$
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Field/=test_005Fname/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Field/=test_005Fname/Expression/@EntryValue">constant("Name_Of_The_Test")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Field/=test_005Fname/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Description/@EntryValue">创建领域事件</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Field/=name/Expression/@EntryValue">constant("MyCreated")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Reformat/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Shortcut/@EntryValue">ncpde</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Text/@EntryValue">public record $name$DomainEvent() : IDomainEvent;</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Description/@EntryValue">endpoint summary</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Shortcut/@EntryValue">epsum</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Text/@EntryValue">sealed class $name$Summary : Summary&lt;$name$Endpoint, $name$Request&gt;
{
public $name$Summary()
{
Summary = "$summaryText$";
Description = "$descriptionText$";
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Field/=descriptionText/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Field/=descriptionText/Expression/@EntryValue">constant("Description text goes here...")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Field/=descriptionText/Order/@EntryValue">2</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Field/=name/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Field/=summaryText/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Field/=summaryText/Expression/@EntryValue">constant("Summary text goes here...")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Field/=summaryText/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Description/@EntryValue">endpoint without request</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Shortcut/@EntryValue">epnoreq</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Text/@EntryValue">sealed class $My$Endpoint : EndpointWithoutRequest
{
public override void Configure()
{
$Get$("$route$");
AllowAnonymous();
}
public override async Task HandleAsync(CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Field/=Get/@KeyIndexDefined">True</s:Boolean>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Field/=Get/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Field/=My/@KeyIndexDefined">True</s:Boolean>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Field/=My/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Field/=route/@KeyIndexDefined">True</s:Boolean>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Field/=route/Order/@EntryValue">2</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Description/@EntryValue">endpoint with request &amp; response</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Shortcut/@EntryValue">epreqres</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Text/@EntryValue">sealed class $epName$Request
{
}
sealed class $epName$Response
{
}
sealed class $epName$Endpoint : Endpoint&lt;$epName$Request, $epName$Response&gt;
{
public override void Configure()
{
$verb$("$route$");
AllowAnonymous();
}
public override async Task HandleAsync($epName$Request r, CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Field/=epName/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Field/=epName/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Field/=epName/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Field/=route/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Field/=route/Expression/@EntryValue">constant("route-pattern")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Field/=route/Order/@EntryValue">2</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Field/=verb/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Field/=verb/Expression/@EntryValue">constant("Post")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Field/=verb/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Description/@EntryValue">创建实体配置类</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Shortcut/@EntryValue">ncpconfig</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Text/@EntryValue">public class $Entity$Configuration : IEntityTypeConfiguration&lt;$Entity$&gt;
{
public void Configure(EntityTypeBuilder&lt;$Entity$&gt; builder)
{
builder.ToTable("$table$");
builder.HasKey(t =&gt; t.Id);
builder.Property(t =&gt; t.Id)
/*.UseSnowFlakeValueGenerator()*/ // 如果使用 SnowFlake ID 生成器,请取消注释
/*.UseGuidVersion7ValueGenerator()*/ // 如果使用 Guid Version 7 ID 生成器,请取消注释
;
// Configure other properties if needed
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Field/=Entity/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Field/=Entity/Expression/@EntryValue">constant("Entity")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Field/=Entity/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Field/=table/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Field/=table/Expression/@EntryValue">constant("table")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Field/=table/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String></wpf:ResourceDictionary>

View File

@ -0,0 +1,21 @@
<Solution>
<Folder Name="/Solution Items/">
<File Path="Directory.Build.props" />
<File Path="Directory.Build.targets" />
<File Path="Directory.Packages.props" />
<File Path="eng/versions.props" />
<File Path="global.json" />
<File Path="NuGet.config" />
<File Path="README.md" />
</Folder>
<Folder Name="/src/">
<Project Path="src/Fengling.Backend.Domain/Fengling.Backend.Domain.csproj" />
<Project Path="src/Fengling.Backend.Infrastructure/Fengling.Backend.Infrastructure.csproj" />
<Project Path="src/Fengling.Backend.Web/Fengling.Backend.Web.csproj" />
</Folder>
<Folder Name="/test/">
<Project Path="test/Fengling.Backend.Domain.Tests/Fengling.Backend.Domain.Tests.csproj" />
<Project Path="test/Fengling.Backend.Infrastructure.Tests/Fengling.Backend.Infrastructure.Tests.csproj" />
<Project Path="test/Fengling.Backend.Web.Tests/Fengling.Backend.Web.Tests.csproj" />
</Folder>
</Solution>

7
Backend/NuGet.config Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<!-- <add key="netcorepal-preview" value="https://www.myget.org/F/netcorepal/api/v3/index.json" /> -->
</packageSources>
</configuration>

230
Backend/README.md Normal file
View File

@ -0,0 +1,230 @@
# Fengling.Backend
## 环境准备
### 使用 Aspire推荐
如果您的项目启用了 Aspire 支持(使用 `--UseAspire` 参数创建),只需要 Docker 环境即可,无需手动配置各种基础设施服务。
```bash
# 仅需确保 Docker 环境运行
docker version
# 直接运行 AppHost 项目Aspire 会自动管理所有依赖服务
cd src/Fengling.Backend.AppHost
dotnet run
```
Aspire 会自动为您:
- 启动和管理数据库容器MySQL、SQL Server、PostgreSQL、MongoDB 等)
- 启动和管理消息队列容器RabbitMQ、Kafka、NATS 等)
- 启动和管理 Redis 容器
- 提供统一的 Aspire Dashboard 界面查看所有服务状态
- 自动配置服务间的连接字符串和依赖关系
访问 Aspire Dashboard通常在 http://localhost:15888可以查看和管理所有服务。
### 推荐方式:使用初始化脚本(不使用 Aspire 时)
如果您没有启用 Aspire项目提供了完整的基础设施初始化脚本支持快速搭建开发环境
#### 使用 Docker Compose推荐
```bash
# 进入脚本目录
cd scripts
# 启动默认基础设施 (MySQL + Redis + RabbitMQ)
docker-compose up -d
# 使用 SQL Server 替代 MySQL
docker-compose --profile sqlserver up -d
# 使用 PostgreSQL 替代 MySQL
docker-compose --profile postgres up -d
# 使用 Kafka 替代 RabbitMQ
docker-compose --profile kafka up -d
# 停止所有服务
docker-compose down
# 停止并删除数据卷(完全清理)
docker-compose down -v
```
#### 使用初始化脚本
```bash
# Linux/macOS
cd scripts
./init-infrastructure.sh
# Windows PowerShell
cd scripts
.\init-infrastructure.ps1
# 清理环境
./clean-infrastructure.sh # Linux/macOS
.\clean-infrastructure.ps1 # Windows
```
### 手动方式:单独运行 Docker 容器
如果需要手动控制每个容器,可以使用以下命令:
```bash
# Redis
docker run --restart unless-stopped --name netcorepal-redis -p 6379:6379 -v netcorepal_redis_data:/data -d redis:7.2-alpine redis-server --appendonly yes --databases 1024
# MySQL
docker run --restart unless-stopped --name netcorepal-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_CHARACTER_SET_SERVER=utf8mb4 -e MYSQL_COLLATION_SERVER=utf8mb4_unicode_ci -e TZ=Asia/Shanghai -v netcorepal_mysql_data:/var/lib/mysql -d mysql:8.0
# RabbitMQ
docker run --restart unless-stopped --name netcorepal-rabbitmq -p 5672:5672 -p 15672:15672 -e RABBITMQ_DEFAULT_USER=guest -e RABBITMQ_DEFAULT_PASS=guest -v netcorepal_rabbitmq_data:/var/lib/rabbitmq -d rabbitmq:3.12-management-alpine
```
### 服务访问信息
启动后,可以通过以下地址访问各个服务:
- **Redis**: `localhost:6379`
- **MySQL**: `localhost:3306` (root/123456)
- **RabbitMQ AMQP**: `localhost:5672` (guest/guest)
- **RabbitMQ 管理界面**: http://localhost:15672 (guest/guest)
- **SQL Server**: `localhost:1433` (sa/Test123456!)
- **PostgreSQL**: `localhost:5432` (postgres/123456)
- **Kafka**: `localhost:9092`
- **Kafka UI**: http://localhost:8080
## IDE 代码片段配置
本模板提供了丰富的代码片段,帮助您快速生成常用的代码结构。
### Visual Studio 配置
运行以下 PowerShell 命令自动安装代码片段:
```powershell
cd vs-snippets
.\Install-VSSnippets.ps1
```
或者手动安装:
1. 打开 Visual Studio
2. 转到 `工具` > `代码片段管理器`
3. 导入 `vs-snippets/NetCorePalTemplates.snippet` 文件
### VS Code 配置
VS Code 的代码片段已预配置在 `.vscode/csharp.code-snippets` 文件中,打开项目时自动生效。
### JetBrains Rider 配置
Rider 用户可以直接使用 `Fengling.Backend.sln.DotSettings` 文件中的 Live Templates 配置。
### 可用的代码片段
#### NetCorePal (ncp) 快捷键
| 快捷键 | 描述 | 生成内容 |
|--------|------|----------|
| `ncpcmd` | NetCorePal 命令 | ICommand 实现(含验证器和处理器) |
| `ncpcmdres` | 命令(含返回值) | ICommand&lt;Response&gt; 实现 |
| `ncpar` | 聚合根 | Entity&lt;Id&gt; 和 IAggregateRoot |
| `ncprepo` | NetCorePal 仓储 | IRepository 接口和实现 |
| `ncpie` | 集成事件 | IntegrationEvent 和处理器 |
| `ncpdeh` | 域事件处理器 | IDomainEventHandler 实现 |
| `ncpiec` | 集成事件转换器 | IIntegrationEventConverter |
| `ncpde` | 域事件 | IDomainEvent 记录 |
#### Endpoint (ep) 快捷键
| 快捷键 | 描述 | 生成内容 |
|--------|------|----------|
| `epp` | FastEndpoint(NCP风格) | 完整的垂直切片实现 |
| `epreq` | 仅请求端点 | Endpoint&lt;Request&gt; |
| `epres` | 仅响应端点 | EndpointWithoutRequest&lt;Response&gt; |
| `epdto` | 端点 DTOs | Request 和 Response 类 |
| `epval` | 端点验证器 | Validator&lt;Request&gt; |
| `epmap` | 端点映射器 | Mapper&lt;Request, Response, Entity&gt; |
| `epfull` | 完整端点切片 | 带映射器的完整实现 |
| `epsum` | 端点摘要 | Summary&lt;Endpoint, Request&gt; |
| `epnoreq` | 无请求端点 | EndpointWithoutRequest |
| `epreqres` | 请求响应端点 | Endpoint&lt;Request, Response&gt; |
| `epdat` | 端点数据 | 静态数据类 |
更多详细配置请参考:[vs-snippets/README.md](vs-snippets/README.md)
## 依赖对框架与组件
+ [NetCorePal Cloud Framework](https://github.com/netcorepal/netcorepal-cloud-framework)
+ [ASP.NET Core](https://github.com/dotnet/aspnetcore)
+ [EFCore](https://github.com/dotnet/efcore)
+ [CAP](https://github.com/dotnetcore/CAP)
+ [MediatR](https://github.com/jbogard/MediatR)
+ [FluentValidation](https://docs.fluentvalidation.net/en/latest)
+ [Swashbuckle.AspNetCore.Swagger](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)
## 数据库迁移
```shell
# 安装工具 SEE https://learn.microsoft.com/zh-cn/ef/core/cli/dotnet#installing-the-tools
dotnet tool install --global dotnet-ef --version 9.0.0
# 强制更新数据库
dotnet ef database update -p src/Fengling.Backend.Infrastructure
# 创建迁移 SEEhttps://learn.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli
dotnet ef migrations add InitialCreate -p src/Fengling.Backend.Infrastructure
```
## 代码分析可视化
框架提供了强大的代码流分析和可视化功能帮助开发者直观地理解DDD架构中的组件关系和数据流向。
### 🎯 核心特性
+ **自动代码分析**:通过源生成器自动分析代码结构,识别控制器、命令、聚合根、事件等组件
+ **多种图表类型**:支持架构流程图、命令链路图、事件流程图、类图等多种可视化图表
+ **交互式HTML可视化**生成完整的交互式HTML页面内置导航和图表预览功能
+ **一键在线编辑**:集成"View in Mermaid Live"按钮,支持一键跳转到在线编辑器
### 🚀 快速开始
安装命令行工具来生成独立的HTML文件
```bash
# 安装全局工具
dotnet tool install -g NetCorePal.Extensions.CodeAnalysis.Tools
# 进入项目目录并生成可视化文件
cd src/Fengling.Backend.Web
netcorepal-codeanalysis generate --output architecture.html
```
### ✨ 主要功能
+ **交互式HTML页面**
+ 左侧树形导航,支持不同图表类型切换
+ 内置Mermaid.js实时渲染
+ 响应式设计,适配不同设备
+ 专业的现代化界面
+ **一键在线编辑**
+ 每个图表右上角的"View in Mermaid Live"按钮
+ 智能压缩算法优化URL长度
+ 自动跳转到[Mermaid Live Editor](https://mermaid.live/)
+ 支持在线编辑、导出图片、生成分享链接
### 📖 详细文档
完整的使用说明和示例请参考:
+ [代码流分析文档](https://netcorepal.github.io/netcorepal-cloud-framework/zh/code-analysis/code-flow-analysis/)
+ [代码分析工具文档](https://netcorepal.github.io/netcorepal-cloud-framework/zh/code-analysis/code-analysis-tools/)
## 关于监控
这里使用了`prometheus-net`作为与基础设施prometheus集成的监控方案默认通过地址 `/metrics` 输出监控指标。
更多信息请参见:[https://github.com/prometheus-net/prometheus-net](https://github.com/prometheus-net/prometheus-net)

View File

@ -0,0 +1,6 @@
<Project>
<PropertyGroup>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix></VersionSuffix>
</PropertyGroup>
</Project>

7
Backend/global.json Normal file
View File

@ -0,0 +1,7 @@
{
"sdk": {
"version": "10.0.100",
"allowPrerelease": true,
"rollForward": "latestMinor"
}
}

151
Backend/scripts/EXAMPLES.md Normal file
View File

@ -0,0 +1,151 @@
# Usage Examples
This document provides practical examples for using the infrastructure initialization scripts.
## Quick Start Examples
### Default Setup (MySQL + Redis + RabbitMQ)
```bash
# Using Docker Compose (Recommended)
docker compose up -d
# Using shell script (Linux/macOS)
./init-infrastructure.sh
# Using PowerShell (Windows)
.\init-infrastructure.ps1
```
### Different Database Options
```bash
# Use PostgreSQL instead of MySQL
docker compose --profile postgres up -d
# Use SQL Server instead of MySQL
docker compose --profile sqlserver up -d
# With PowerShell
.\init-infrastructure.ps1 -Postgres
.\init-infrastructure.ps1 -SqlServer
```
### Different Message Queue Options
```bash
# Use Kafka instead of RabbitMQ
docker compose --profile kafka up -d
# With PowerShell
.\init-infrastructure.ps1 -Kafka
```
### Cleanup Examples
```bash
# Stop services, keep data
docker compose down
./clean-infrastructure.sh
.\clean-infrastructure.ps1
# Stop services and remove all data
docker compose down -v
./clean-infrastructure.sh --volumes
.\clean-infrastructure.ps1 -Volumes
```
## Development Workflow
### Typical Development Session
```bash
# 1. Start infrastructure
cd scripts
docker compose up -d
# 2. Develop your application
cd ../src/Fengling.Backend.Web
dotnet run
# 3. Run tests
cd ../../
dotnet test
# 4. Stop infrastructure (keep data)
cd scripts
docker compose down
```
### Clean Development Environment
```bash
# Clean slate - remove everything including data
cd scripts
docker compose down -v
# Start fresh
docker compose up -d
```
## Troubleshooting
### Check Service Status
```bash
# List running containers
docker ps
# Check specific service logs
docker logs netcorepal-mysql
docker logs netcorepal-redis
docker logs netcorepal-rabbitmq
# Check service health
docker compose ps
```
### Common Issues
#### Port Already in Use
```bash
# Find what's using the port
netstat -tulpn | grep :3306 # Linux
netstat -ano | findstr :3306 # Windows
# Stop conflicting services
sudo systemctl stop mysql # Linux
net stop mysql80 # Windows
```
#### Container Won't Start
```bash
# Remove problematic container and restart
docker rm -f netcorepal-mysql
docker compose up -d mysql
```
#### Data Corruption
```bash
# Remove data volumes and start fresh
docker compose down -v
docker compose up -d
```
## Connection Strings for Development
Update your `appsettings.Development.json` with these connection strings:
```json
{
"ConnectionStrings": {
"Redis": "localhost:6379,defaultDatabase=0",
"MySql": "Server=localhost;Port=3306;Database=abctemplate;Uid=root;Pwd=123456;",
"SqlServer": "Server=localhost,1433;Database=abctemplate;User Id=sa;Password=Test123456!;TrustServerCertificate=true;",
"PostgreSQL": "Host=localhost;Port=5432;Database=abctemplate;Username=postgres;Password=123456;"
},
"RabbitMQ": {
"HostName": "localhost",
"Port": 5672,
"UserName": "guest",
"Password": "guest",
"VirtualHost": "/"
},
"Kafka": {
"BootstrapServers": "localhost:9092"
}
}
```

56
Backend/scripts/README.md Normal file
View File

@ -0,0 +1,56 @@
# Infrastructure Initialization Scripts
This directory contains scripts to help developers quickly set up the infrastructure needed for development and debugging.
## Available Scripts
- `docker-compose.yml` - Complete infrastructure setup using Docker Compose
- `init-infrastructure.sh` - Shell script for Linux/macOS
- `init-infrastructure.ps1` - PowerShell script for Windows
- `clean-infrastructure.sh` - Cleanup script for Linux/macOS
- `clean-infrastructure.ps1` - Cleanup script for Windows
## Quick Start
### Using Docker Compose (Recommended)
```bash
# Start all infrastructure services
docker-compose up -d
# Stop all services
docker-compose down
# Stop and remove volumes (clean start)
docker-compose down -v
```
### Using Individual Scripts
```bash
# Linux/macOS
./init-infrastructure.sh
# Windows PowerShell
.\init-infrastructure.ps1
```
## Infrastructure Components
The scripts will set up the following services:
### Database Options
- **MySQL** (default): Port 3306, root password: 123456
- **SQL Server**: Port 1433, SA password: Test123456!
- **PostgreSQL**: Port 5432, postgres password: 123456
### Cache & Message Queue
- **Redis**: Port 6379, no password
- **RabbitMQ**: Ports 5672 (AMQP), 15672 (Management UI), guest/guest
- **Kafka**: Port 9092 (when using Kafka option)
### Management Interfaces
- RabbitMQ Management: http://localhost:15672 (guest/guest)
- Kafka UI (if included): http://localhost:8080
## Configuration
The default configuration matches the test containers setup used in the project's integration tests.

View File

@ -0,0 +1,195 @@
# NetCorePal Template - Infrastructure Cleanup Script (PowerShell)
# This script stops and removes all infrastructure containers
param(
[switch]$Volumes,
[switch]$Help
)
$ErrorActionPreference = "Stop"
# Color functions for output
function Write-Info {
param([string]$Message)
Write-Host "[INFO] $Message" -ForegroundColor Blue
}
function Write-Success {
param([string]$Message)
Write-Host "[SUCCESS] $Message" -ForegroundColor Green
}
function Write-Warning {
param([string]$Message)
Write-Host "[WARNING] $Message" -ForegroundColor Yellow
}
function Write-Error {
param([string]$Message)
Write-Host "[ERROR] $Message" -ForegroundColor Red
}
function Show-Help {
Write-Host "NetCorePal Template - Infrastructure Cleanup" -ForegroundColor Green
Write-Host "===========================================" -ForegroundColor Green
Write-Host ""
Write-Host "Usage: .\clean-infrastructure.ps1 [OPTIONS]"
Write-Host ""
Write-Host "Clean up NetCorePal Template infrastructure containers"
Write-Host ""
Write-Host "Options:"
Write-Host " -Help Show this help message"
Write-Host " -Volumes Also remove data volumes (WARNING: This will delete all data!)"
Write-Host ""
Write-Host "Examples:"
Write-Host " .\clean-infrastructure.ps1 # Stop and remove containers, keep data"
Write-Host " .\clean-infrastructure.ps1 -Volumes # Stop and remove containers and all data volumes"
Write-Host ""
}
function Remove-Container {
param([string]$ContainerName)
try {
$exists = docker ps -a --format "table {{.Names}}" | Select-String "^$ContainerName$"
if ($exists) {
Write-Info "Stopping and removing $ContainerName..."
# Stop the container
try {
docker stop $ContainerName 2>$null | Out-Null
Write-Info "$ContainerName stopped"
}
catch {
Write-Warning "Could not stop $ContainerName (may already be stopped)"
}
# Remove the container
try {
docker rm $ContainerName 2>$null | Out-Null
Write-Success "$ContainerName removed"
}
catch {
Write-Warning "Could not remove $ContainerName"
}
}
else {
Write-Info "$ContainerName not found, skipping..."
}
}
catch {
Write-Warning "Error processing $ContainerName : $_"
}
}
function Remove-Volumes {
param([bool]$RemoveVolumes)
if ($RemoveVolumes) {
Write-Info "Removing data volumes..."
$volumes = @(
"netcorepal_redis_data",
"netcorepal_mysql_data",
"netcorepal_sqlserver_data",
"netcorepal_postgres_data",
"netcorepal_rabbitmq_data",
"netcorepal_zookeeper_data",
"netcorepal_zookeeper_logs",
"netcorepal_kafka_data"
)
foreach ($volume in $volumes) {
try {
$exists = docker volume ls --format "table {{.Name}}" | Select-String "^$volume$"
if ($exists) {
docker volume rm $volume 2>$null | Out-Null
Write-Success "Volume $volume removed"
}
}
catch {
Write-Warning "Could not remove volume $volume"
}
}
}
else {
Write-Info "Preserving data volumes (use -Volumes to remove them)"
}
}
function Remove-Network {
try {
$exists = docker network ls --format "table {{.Name}}" | Select-String "^netcorepal-network$"
if ($exists) {
Write-Info "Removing network netcorepal-network..."
try {
docker network rm netcorepal-network 2>$null | Out-Null
Write-Success "Network removed"
}
catch {
Write-Warning "Could not remove network (may still be in use)"
}
}
}
catch {
Write-Warning "Error checking network: $_"
}
}
function Start-Cleanup {
Write-Host ""
Write-Info "Starting infrastructure cleanup..."
Write-Host ""
# List of containers to clean up
$containers = @(
"netcorepal-redis",
"netcorepal-mysql",
"netcorepal-sqlserver",
"netcorepal-postgres",
"netcorepal-rabbitmq",
"netcorepal-kafka",
"netcorepal-kafka-ui",
"netcorepal-zookeeper"
)
# Clean up containers
foreach ($container in $containers) {
Remove-Container -ContainerName $container
}
# Clean up volumes if requested
Remove-Volumes -RemoveVolumes $Volumes
# Clean up network
Remove-Network
Write-Host ""
Write-Success "🎉 Infrastructure cleanup completed!"
Write-Host ""
if ($Volumes) {
Write-Warning "⚠️ All data has been removed. You'll need to reinitialize your databases."
}
else {
Write-Info "💾 Data volumes preserved. Data will be available when you restart the infrastructure."
}
Write-Host ""
Write-Info "Use '.\init-infrastructure.ps1' to restart the infrastructure"
}
# Main execution
Write-Host "🧹 NetCorePal Template - Infrastructure Cleanup" -ForegroundColor Green
Write-Host "===============================================" -ForegroundColor Green
if ($Help) {
Show-Help
exit 0
}
try {
Start-Cleanup
}
catch {
Write-Error "An error occurred during cleanup: $_"
exit 1
}

View File

@ -0,0 +1,177 @@
#!/bin/bash
# NetCorePal Template - Infrastructure Cleanup Script
# This script stops and removes all infrastructure containers
set -e
echo "🧹 NetCorePal Template - Infrastructure Cleanup"
echo "==============================================="
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored output
print_status() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Function to stop and remove container
cleanup_container() {
local container_name=$1
if docker ps -a --format 'table {{.Names}}' | grep -q "^$container_name$"; then
print_status "Stopping and removing $container_name..."
# Stop the container
if docker stop $container_name > /dev/null 2>&1; then
print_status "$container_name stopped"
else
print_warning "Could not stop $container_name (may already be stopped)"
fi
# Remove the container
if docker rm $container_name > /dev/null 2>&1; then
print_success "$container_name removed"
else
print_warning "Could not remove $container_name"
fi
else
print_status "$container_name not found, skipping..."
fi
}
# Function to remove volumes
cleanup_volumes() {
local remove_volumes=$1
if [ "$remove_volumes" = "true" ]; then
print_status "Removing data volumes..."
local volumes=(
"netcorepal_redis_data"
"netcorepal_mysql_data"
"netcorepal_sqlserver_data"
"netcorepal_postgres_data"
"netcorepal_rabbitmq_data"
"netcorepal_zookeeper_data"
"netcorepal_zookeeper_logs"
"netcorepal_kafka_data"
)
for volume in "${volumes[@]}"; do
if docker volume ls --format 'table {{.Name}}' | grep -q "^$volume$"; then
if docker volume rm "$volume" > /dev/null 2>&1; then
print_success "Volume $volume removed"
else
print_warning "Could not remove volume $volume"
fi
fi
done
else
print_status "Preserving data volumes (use --volumes to remove them)"
fi
}
# Function to remove network
cleanup_network() {
if docker network ls --format 'table {{.Name}}' | grep -q "^netcorepal-network$"; then
print_status "Removing network netcorepal-network..."
if docker network rm netcorepal-network > /dev/null 2>&1; then
print_success "Network removed"
else
print_warning "Could not remove network (may still be in use)"
fi
fi
}
# Main cleanup function
main() {
local remove_volumes=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--volumes|-v)
remove_volumes=true
shift
;;
--help|-h)
echo "Usage: $0 [OPTIONS]"
echo
echo "Clean up NetCorePal Template infrastructure containers"
echo
echo "Options:"
echo " -h, --help Show this help message"
echo " -v, --volumes Also remove data volumes (WARNING: This will delete all data!)"
echo
echo "Examples:"
echo " $0 # Stop and remove containers, keep data"
echo " $0 --volumes # Stop and remove containers and all data volumes"
exit 0
;;
*)
print_error "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done
echo
print_status "Starting infrastructure cleanup..."
echo
# List of containers to clean up
local containers=(
"netcorepal-redis"
"netcorepal-mysql"
"netcorepal-sqlserver"
"netcorepal-postgres"
"netcorepal-rabbitmq"
"netcorepal-kafka"
"netcorepal-kafka-ui"
"netcorepal-zookeeper"
)
# Clean up containers
for container in "${containers[@]}"; do
cleanup_container "$container"
done
# Clean up volumes if requested
cleanup_volumes "$remove_volumes"
# Clean up network
cleanup_network
echo
print_success "🎉 Infrastructure cleanup completed!"
echo
if [ "$remove_volumes" = "true" ]; then
print_warning "⚠️ All data has been removed. You'll need to reinitialize your databases."
else
print_status "💾 Data volumes preserved. Data will be available when you restart the infrastructure."
fi
echo
print_status "Use './init-infrastructure.sh' to restart the infrastructure"
}
# Execute main function with all arguments
main "$@"

View File

@ -0,0 +1,167 @@
services:
# Redis - Always included for caching and sessions
redis:
image: redis:7.2-alpine
container_name: netcorepal-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes --databases 1024
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
# MySQL Database (default option)
mysql:
image: mysql:8.0
container_name: netcorepal-mysql
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_CHARACTER_SET_SERVER: utf8mb4
MYSQL_COLLATION_SERVER: utf8mb4_unicode_ci
TZ: Asia/Shanghai
volumes:
- mysql_data:/var/lib/mysql
- ./mysql-init:/docker-entrypoint-initdb.d:ro
restart: unless-stopped
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p123456"]
interval: 10s
timeout: 5s
retries: 5
# SQL Server (alternative database option)
sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
container_name: netcorepal-sqlserver
ports:
- "1433:1433"
environment:
ACCEPT_EULA: Y
MSSQL_SA_PASSWORD: Test123456!
TZ: Asia/Shanghai
volumes:
- sqlserver_data:/var/opt/mssql
restart: unless-stopped
profiles:
- sqlserver
healthcheck:
test: ["CMD-SHELL", "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Test123456! -Q 'SELECT 1'"]
interval: 10s
timeout: 5s
retries: 5
# PostgreSQL (alternative database option)
postgres:
image: postgres:15-alpine
container_name: netcorepal-postgres
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: 123456
POSTGRES_DB: postgres
TZ: Asia/Shanghai
volumes:
- postgres_data:/var/lib/postgresql/data
- ./postgres-init:/docker-entrypoint-initdb.d:ro
restart: unless-stopped
profiles:
- postgres
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 3
# RabbitMQ (default message queue option)
rabbitmq:
image: rabbitmq:3.12-management-alpine
container_name: netcorepal-rabbitmq
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
volumes:
- rabbitmq_data:/var/lib/rabbitmq
restart: unless-stopped
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
interval: 10s
timeout: 5s
retries: 3
# Kafka (alternative message queue option)
zookeeper:
image: confluentinc/cp-zookeeper:7.4.0
container_name: netcorepal-zookeeper
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
volumes:
- zookeeper_data:/var/lib/zookeeper/data
- zookeeper_logs:/var/lib/zookeeper/log
restart: unless-stopped
profiles:
- kafka
kafka:
image: confluentinc/cp-kafka:7.4.0
container_name: netcorepal-kafka
depends_on:
- zookeeper
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'
volumes:
- kafka_data:/var/lib/kafka/data
restart: unless-stopped
profiles:
- kafka
healthcheck:
test: ["CMD-SHELL", "kafka-broker-api-versions --bootstrap-server localhost:9092"]
interval: 10s
timeout: 5s
retries: 5
# Kafka UI (optional management interface)
kafka-ui:
image: provectuslabs/kafka-ui:latest
container_name: netcorepal-kafka-ui
depends_on:
- kafka
ports:
- "8080:8080"
environment:
KAFKA_CLUSTERS_0_NAME: local
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092
restart: unless-stopped
profiles:
- kafka
volumes:
redis_data:
mysql_data:
sqlserver_data:
postgres_data:
rabbitmq_data:
zookeeper_data:
zookeeper_logs:
kafka_data:
networks:
default:
name: netcorepal-network

View File

@ -0,0 +1,258 @@
# NetCorePal Template - Infrastructure Initialization Script (PowerShell)
# This script initializes the required infrastructure for development
param(
[switch]$SqlServer,
[switch]$Postgres,
[switch]$Kafka,
[switch]$Help
)
$ErrorActionPreference = "Stop"
# Color functions for output
function Write-Info {
param([string]$Message)
Write-Host "[INFO] $Message" -ForegroundColor Blue
}
function Write-Success {
param([string]$Message)
Write-Host "[SUCCESS] $Message" -ForegroundColor Green
}
function Write-Warning {
param([string]$Message)
Write-Host "[WARNING] $Message" -ForegroundColor Yellow
}
function Write-Error {
param([string]$Message)
Write-Host "[ERROR] $Message" -ForegroundColor Red
}
function Show-Help {
Write-Host "NetCorePal Template - Infrastructure Initialization" -ForegroundColor Green
Write-Host "=================================================" -ForegroundColor Green
Write-Host ""
Write-Host "Usage: .\init-infrastructure.ps1 [OPTIONS]"
Write-Host ""
Write-Host "Initialize infrastructure containers for NetCorePal Template development"
Write-Host ""
Write-Host "Options:"
Write-Host " -Help Show this help message"
Write-Host " -SqlServer Use SQL Server database instead of MySQL"
Write-Host " -Postgres Use PostgreSQL database instead of MySQL"
Write-Host " -Kafka Use Kafka instead of RabbitMQ"
Write-Host ""
Write-Host "Examples:"
Write-Host " .\init-infrastructure.ps1 # Start with MySQL and RabbitMQ (default)"
Write-Host " .\init-infrastructure.ps1 -Postgres # Start with PostgreSQL and RabbitMQ"
Write-Host " .\init-infrastructure.ps1 -Kafka # Start with MySQL and Kafka"
Write-Host ""
}
function Test-Docker {
Write-Info "Checking Docker installation..."
try {
$null = Get-Command docker -ErrorAction Stop
}
catch {
Write-Error "Docker is not installed. Please install Docker Desktop first."
Write-Host "Download from: https://www.docker.com/products/docker-desktop/" -ForegroundColor Cyan
exit 1
}
try {
$null = docker info 2>$null
}
catch {
Write-Error "Docker is not running. Please start Docker Desktop first."
exit 1
}
Write-Success "Docker is installed and running"
}
function Start-Container {
param(
[string]$Name,
[string]$Image,
[string]$Ports,
[string]$Environment,
[string]$Volumes,
[string]$AdditionalArgs
)
Write-Info "Starting $Name container..."
# Stop and remove existing container if it exists
$existingContainer = docker ps -a --format "table {{.Names}}" | Select-String "^$Name$"
if ($existingContainer) {
Write-Warning "Stopping existing $Name container..."
docker stop $Name 2>$null | Out-Null
docker rm $Name 2>$null | Out-Null
}
# Build the docker run command
$cmd = "docker run --restart unless-stopped --name $Name"
if ($Ports) { $cmd += " $Ports" }
if ($Environment) { $cmd += " $Environment" }
if ($Volumes) { $cmd += " $Volumes" }
if ($AdditionalArgs) { $cmd += " $AdditionalArgs" }
$cmd += " -d $Image"
try {
Invoke-Expression $cmd | Out-Null
Write-Success "$Name container started successfully"
return $true
}
catch {
Write-Error "Failed to start $Name container: $_"
return $false
}
}
function Wait-ForContainer {
param(
[string]$ContainerName,
[int]$MaxAttempts = 30
)
Write-Info "Waiting for $ContainerName to be healthy..."
for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
$running = docker ps --filter "name=$ContainerName" --filter "status=running" | Select-String $ContainerName
if ($running) {
Write-Success "$ContainerName is running"
return $true
}
Write-Host "." -NoNewline
Start-Sleep -Seconds 2
}
Write-Host "" # New line after dots
Write-Error "$ContainerName failed to start properly"
return $false
}
function Start-Infrastructure {
Write-Host ""
Write-Info "Starting infrastructure setup..."
Write-Host ""
# Check prerequisites
Test-Docker
# Start Redis
$success = Start-Container -Name "netcorepal-redis" -Image "redis:7.2-alpine" `
-Ports "-p 6379:6379" `
-Volumes "-v netcorepal_redis_data:/data" `
-AdditionalArgs "redis-server --appendonly yes --databases 1024"
if ($success) {
Wait-ForContainer -ContainerName "netcorepal-redis" -MaxAttempts 15
}
# Start Database
if ($Postgres) {
Write-Info "Setting up PostgreSQL database..."
$success = Start-Container -Name "netcorepal-postgres" -Image "postgres:15-alpine" `
-Ports "-p 5432:5432" `
-Environment "-e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=123456 -e POSTGRES_DB=postgres -e TZ=Asia/Shanghai" `
-Volumes "-v netcorepal_postgres_data:/var/lib/postgresql/data"
if ($success) {
Wait-ForContainer -ContainerName "netcorepal-postgres" -MaxAttempts 30
}
}
elseif ($SqlServer) {
Write-Info "Setting up SQL Server database..."
$success = Start-Container -Name "netcorepal-sqlserver" -Image "mcr.microsoft.com/mssql/server:2022-latest" `
-Ports "-p 1433:1433" `
-Environment "-e ACCEPT_EULA=Y -e MSSQL_SA_PASSWORD=Test123456! -e TZ=Asia/Shanghai" `
-Volumes "-v netcorepal_sqlserver_data:/var/opt/mssql"
if ($success) {
Wait-ForContainer -ContainerName "netcorepal-sqlserver" -MaxAttempts 30
}
}
else {
Write-Info "Setting up MySQL database..."
$success = Start-Container -Name "netcorepal-mysql" -Image "mysql:8.0" `
-Ports "-p 3306:3306" `
-Environment "-e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_CHARACTER_SET_SERVER=utf8mb4 -e MYSQL_COLLATION_SERVER=utf8mb4_unicode_ci -e TZ=Asia/Shanghai" `
-Volumes "-v netcorepal_mysql_data:/var/lib/mysql"
if ($success) {
Wait-ForContainer -ContainerName "netcorepal-mysql" -MaxAttempts 30
}
}
# Start Message Queue
if ($Kafka) {
Write-Info "Setting up Kafka message queue..."
Write-Warning "Kafka setup requires Zookeeper. For full Kafka setup, please use Docker Compose:"
Write-Host "docker-compose --profile kafka up -d" -ForegroundColor Cyan
}
else {
Write-Info "Setting up RabbitMQ message queue..."
$success = Start-Container -Name "netcorepal-rabbitmq" -Image "rabbitmq:3.12-management-alpine" `
-Ports "-p 5672:5672 -p 15672:15672" `
-Environment "-e RABBITMQ_DEFAULT_USER=guest -e RABBITMQ_DEFAULT_PASS=guest" `
-Volumes "-v netcorepal_rabbitmq_data:/var/lib/rabbitmq"
if ($success) {
Wait-ForContainer -ContainerName "netcorepal-rabbitmq" -MaxAttempts 20
}
}
Write-Host ""
Write-Success "🎉 Infrastructure setup completed successfully!"
Write-Host ""
Write-Host "📋 Service Summary:" -ForegroundColor Cyan
Write-Host "==================="
Write-Host "✅ Redis: localhost:6379"
if ($Postgres) {
Write-Host "✅ PostgreSQL: localhost:5432 (postgres/123456)"
}
elseif ($SqlServer) {
Write-Host "✅ SQL Server: localhost:1433 (sa/Test123456!)"
}
else {
Write-Host "✅ MySQL: localhost:3306 (root/123456)"
}
if (-not $Kafka) {
Write-Host "✅ RabbitMQ: localhost:5672 (guest/guest)"
Write-Host "📊 RabbitMQ Management UI: http://localhost:15672"
}
Write-Host ""
Write-Host "💡 Tips:" -ForegroundColor Yellow
Write-Host "• Use 'docker ps' to see running containers"
Write-Host "• Use 'docker logs <container_name>' to check logs"
Write-Host "• Use '.\clean-infrastructure.ps1' to stop and remove all containers"
Write-Host ""
Write-Info "Ready for development! 🚀"
}
# Main execution
Write-Host "🚀 NetCorePal Template - Infrastructure Setup" -ForegroundColor Green
Write-Host "==============================================" -ForegroundColor Green
if ($Help) {
Show-Help
exit 0
}
try {
Start-Infrastructure
}
catch {
Write-Error "An error occurred during setup: $_"
exit 1
}

View File

@ -0,0 +1,200 @@
#!/bin/bash
# NetCorePal Template - Infrastructure Initialization Script
# This script initializes the required infrastructure for development
set -e
echo "🚀 NetCorePal Template - Infrastructure Setup"
echo "=============================================="
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored output
print_status() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if Docker is installed and running
check_docker() {
print_status "Checking Docker installation..."
if ! command -v docker &> /dev/null; then
print_error "Docker is not installed. Please install Docker first."
echo "Download from: https://www.docker.com/products/docker-desktop/"
exit 1
fi
if ! docker info &> /dev/null; then
print_error "Docker is not running. Please start Docker first."
exit 1
fi
print_success "Docker is installed and running"
}
# Function to run a Docker container with retry logic
run_container() {
local name=$1
local image=$2
local ports=$3
local environment=$4
local volumes=$5
local additional_args=$6
print_status "Starting $name container..."
# Stop and remove existing container if it exists
if docker ps -a --format 'table {{.Names}}' | grep -q "^$name$"; then
print_warning "Stopping existing $name container..."
docker stop $name > /dev/null 2>&1 || true
docker rm $name > /dev/null 2>&1 || true
fi
# Run the container
local cmd="docker run --restart unless-stopped --name $name $ports $environment $volumes $additional_args -d $image"
if eval $cmd > /dev/null; then
print_success "$name container started successfully"
return 0
else
print_error "Failed to start $name container"
return 1
fi
}
# Function to wait for container to be healthy
wait_for_container() {
local container_name=$1
local max_attempts=${2:-30}
local attempt=1
print_status "Waiting for $container_name to be healthy..."
while [ $attempt -le $max_attempts ]; do
if docker ps --filter "name=$container_name" --filter "status=running" | grep -q $container_name; then
print_success "$container_name is running"
return 0
fi
echo -n "."
sleep 2
((attempt++))
done
print_error "$container_name failed to start properly"
return 1
}
# Main execution
main() {
echo
print_status "Starting infrastructure setup..."
echo
# Check prerequisites
check_docker
# Start Redis
run_container "netcorepal-redis" "redis:7.2-alpine" \
"-p 6379:6379" \
"" \
"-v netcorepal_redis_data:/data" \
"redis-server --appendonly yes --databases 1024"
wait_for_container "netcorepal-redis" 15
# Start MySQL (default database)
print_status "Setting up MySQL database..."
run_container "netcorepal-mysql" "mysql:8.0" \
"-p 3306:3306" \
"-e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_CHARACTER_SET_SERVER=utf8mb4 -e MYSQL_COLLATION_SERVER=utf8mb4_unicode_ci -e TZ=Asia/Shanghai" \
"-v netcorepal_mysql_data:/var/lib/mysql" \
""
wait_for_container "netcorepal-mysql" 30
# Start RabbitMQ (default message queue)
print_status "Setting up RabbitMQ message queue..."
run_container "netcorepal-rabbitmq" "rabbitmq:3.12-management-alpine" \
"-p 5672:5672 -p 15672:15672" \
"-e RABBITMQ_DEFAULT_USER=guest -e RABBITMQ_DEFAULT_PASS=guest" \
"-v netcorepal_rabbitmq_data:/var/lib/rabbitmq" \
""
wait_for_container "netcorepal-rabbitmq" 20
echo
print_success "🎉 Infrastructure setup completed successfully!"
echo
echo "📋 Service Summary:"
echo "==================="
echo "✅ Redis: localhost:6379"
echo "✅ MySQL: localhost:3306 (root/123456)"
echo "✅ RabbitMQ: localhost:5672 (guest/guest)"
echo "📊 RabbitMQ Management UI: http://localhost:15672"
echo
echo "💡 Tips:"
echo "• Use 'docker ps' to see running containers"
echo "• Use 'docker logs <container_name>' to check logs"
echo "• Use './clean-infrastructure.sh' to stop and remove all containers"
echo
print_status "Ready for development! 🚀"
}
# Parse command line arguments
case "${1:-}" in
--help|-h)
echo "Usage: $0 [OPTIONS]"
echo
echo "Initialize infrastructure containers for NetCorePal Template development"
echo
echo "Options:"
echo " -h, --help Show this help message"
echo " --mysql Use MySQL database (default)"
echo " --sqlserver Use SQL Server database"
echo " --postgres Use PostgreSQL database"
echo " --kafka Use Kafka instead of RabbitMQ"
echo
echo "Examples:"
echo " $0 # Start with MySQL and RabbitMQ (default)"
echo " $0 --postgres # Start with PostgreSQL and RabbitMQ"
echo " $0 --kafka # Start with MySQL and Kafka"
exit 0
;;
--sqlserver)
print_status "SQL Server option will be implemented in Docker Compose version"
print_status "For now, use: docker-compose --profile sqlserver up -d"
exit 0
;;
--postgres)
print_status "PostgreSQL option will be implemented in Docker Compose version"
print_status "For now, use: docker-compose --profile postgres up -d"
exit 0
;;
--kafka)
print_status "Kafka option will be implemented in Docker Compose version"
print_status "For now, use: docker-compose --profile kafka up -d"
exit 0
;;
*)
main
;;
esac

View File

@ -0,0 +1,18 @@
-- MySQL Initialization Script for NetCorePal Template
-- This script creates the necessary database and user for development
-- Create development database if it doesn't exist
CREATE DATABASE IF NOT EXISTS `abctemplate` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Create a development user (optional - you can use root for development)
-- CREATE USER IF NOT EXISTS 'devuser'@'%' IDENTIFIED BY 'devpass123';
-- GRANT ALL PRIVILEGES ON `abctemplate`.* TO 'devuser'@'%';
-- Ensure root can connect from any host (for development only)
-- ALTER USER 'root'@'%' IDENTIFIED BY '123456';
-- GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;
-- Display completion message
SELECT 'MySQL initialization completed successfully' AS message;

View File

@ -0,0 +1,24 @@
-- PostgreSQL Initialization Script for NetCorePal Template
-- This script creates the necessary database and user for development
-- Create development database if it doesn't exist
SELECT 'CREATE DATABASE abctemplate'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'abctemplate')\gexec
-- Create a development user (optional - you can use postgres for development)
-- DO
-- $do$
-- BEGIN
-- IF NOT EXISTS (
-- SELECT FROM pg_catalog.pg_roles
-- WHERE rolname = 'devuser') THEN
-- CREATE ROLE devuser LOGIN PASSWORD 'devpass123';
-- END IF;
-- END
-- $do$;
-- Grant privileges to development user
-- GRANT ALL PRIVILEGES ON DATABASE abctemplate TO devuser;
-- Display completion message
SELECT 'PostgreSQL initialization completed successfully' AS message;

View File

@ -0,0 +1,239 @@
using Fengling.Backend.Domain.DomainEvents;
namespace Fengling.Backend.Domain.AggregatesModel.GiftAggregate;
/// <summary>
/// 礼品ID
/// </summary>
public partial record GiftId : IGuidStronglyTypedId;
/// <summary>
/// 礼品聚合根
/// </summary>
public class Gift : Entity<GiftId>, IAggregateRoot
{
protected Gift() { }
public Gift(
string name,
GiftType type,
string description,
string imageUrl,
int requiredPoints,
int totalStock,
int? limitPerMember = null)
{
Name = name;
Type = type;
Description = description;
ImageUrl = imageUrl;
RequiredPoints = requiredPoints;
TotalStock = totalStock;
AvailableStock = totalStock;
LimitPerMember = limitPerMember;
IsOnShelf = false;
SortOrder = 0;
CreatedAt = DateTime.UtcNow;
this.AddDomainEvent(new GiftCreatedDomainEvent(this));
}
/// <summary>
/// 礼品名称
/// </summary>
public string Name { get; private set; } = string.Empty;
/// <summary>
/// 礼品类型
/// </summary>
public GiftType Type { get; private set; }
/// <summary>
/// 描述
/// </summary>
public string Description { get; private set; } = string.Empty;
/// <summary>
/// 图片URL
/// </summary>
public string ImageUrl { get; private set; } = string.Empty;
/// <summary>
/// 所需积分
/// </summary>
public int RequiredPoints { get; private set; }
/// <summary>
/// 总库存
/// </summary>
public int TotalStock { get; private set; }
/// <summary>
/// 可用库存
/// </summary>
public int AvailableStock { get; private set; }
/// <summary>
/// 每人限兑数量
/// </summary>
public int? LimitPerMember { get; private set; }
/// <summary>
/// 是否上架
/// </summary>
public bool IsOnShelf { get; private set; }
/// <summary>
/// 排序
/// </summary>
public int SortOrder { get; private set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; private set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime UpdatedAt { get; private set; }
public Deleted Deleted { get; private set; } = new();
public RowVersion RowVersion { get; private set; } = new(0);
/// <summary>
/// 更新礼品信息
/// </summary>
public void Update(
string? name = null,
string? description = null,
string? imageUrl = null,
int? requiredPoints = null,
int? limitPerMember = null)
{
if (!string.IsNullOrWhiteSpace(name))
Name = name;
if (!string.IsNullOrWhiteSpace(description))
Description = description;
if (!string.IsNullOrWhiteSpace(imageUrl))
ImageUrl = imageUrl;
if (requiredPoints.HasValue && requiredPoints.Value > 0)
RequiredPoints = requiredPoints.Value;
if (limitPerMember.HasValue)
LimitPerMember = limitPerMember.Value;
UpdatedAt = DateTime.UtcNow;
this.AddDomainEvent(new GiftUpdatedDomainEvent(this));
}
/// <summary>
/// 增加库存
/// </summary>
public void AddStock(int quantity)
{
if (quantity <= 0)
throw new KnownException("库存数量必须大于0");
TotalStock += quantity;
AvailableStock += quantity;
UpdatedAt = DateTime.UtcNow;
}
/// <summary>
/// 预留库存(下单时)
/// </summary>
public void ReserveStock(int quantity)
{
if (quantity <= 0)
throw new KnownException("数量必须大于0");
if (AvailableStock < quantity)
throw new KnownException($"库存不足,当前可用:{AvailableStock},需要:{quantity}");
AvailableStock -= quantity;
if (AvailableStock <= 10)
{
this.AddDomainEvent(new GiftStockLowDomainEvent(this, AvailableStock));
}
this.AddDomainEvent(new GiftStockReservedDomainEvent(this, quantity));
}
/// <summary>
/// 扣减库存(确认发货后)
/// </summary>
public void DeductStock(int quantity)
{
if (quantity <= 0)
throw new KnownException("数量必须大于0");
this.AddDomainEvent(new GiftStockDeductedDomainEvent(this, quantity));
}
/// <summary>
/// 释放库存(订单取消时)
/// </summary>
public void ReleaseStock(int quantity)
{
if (quantity <= 0)
throw new KnownException("数量必须大于0");
AvailableStock += quantity;
this.AddDomainEvent(new GiftStockReleasedDomainEvent(this, quantity));
}
/// <summary>
/// 上架
/// </summary>
public void PutOnShelf()
{
if (AvailableStock <= 0)
throw new KnownException("库存为0,无法上架");
if (!IsOnShelf)
{
IsOnShelf = true;
UpdatedAt = DateTime.UtcNow;
this.AddDomainEvent(new GiftPutOnShelfDomainEvent(this));
}
}
/// <summary>
/// 下架
/// </summary>
public void PutOffShelf()
{
if (IsOnShelf)
{
IsOnShelf = false;
UpdatedAt = DateTime.UtcNow;
this.AddDomainEvent(new GiftPutOffShelfDomainEvent(this));
}
}
}
/// <summary>
/// 礼品类型
/// </summary>
public enum GiftType
{
/// <summary>
/// 实物
/// </summary>
Physical = 1,
/// <summary>
/// 虚拟(卡券等)
/// </summary>
Virtual = 2,
/// <summary>
/// 自有产品
/// </summary>
Product = 3
}

View File

@ -0,0 +1,102 @@
using Fengling.Backend.Domain.DomainEvents;
namespace Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate;
/// <summary>
/// 营销码ID
/// </summary>
public partial record MarketingCodeId : IGuidStronglyTypedId;
/// <summary>
/// 营销码聚合根
/// </summary>
public class MarketingCode : Entity<MarketingCodeId>, IAggregateRoot
{
protected MarketingCode() { }
public MarketingCode(
string code,
Guid productId,
string productName,
string batchNo,
DateTime? expiryDate = null)
{
Code = code;
ProductInfo = new ProductInfo(productId, productName);
BatchNo = batchNo;
IsUsed = false;
ExpiryDate = expiryDate;
CreatedAt = DateTime.UtcNow;
this.AddDomainEvent(new MarketingCodeGeneratedDomainEvent(this));
}
/// <summary>
/// 营销码(唯一)
/// </summary>
public string Code { get; private set; } = string.Empty;
/// <summary>
/// 产品信息
/// </summary>
public ProductInfo ProductInfo { get; private set; } = ProductInfo.Empty();
/// <summary>
/// 批次号
/// </summary>
public string BatchNo { get; private set; } = string.Empty;
/// <summary>
/// 是否已使用
/// </summary>
public bool IsUsed { get; private set; }
/// <summary>
/// 使用者会员ID
/// </summary>
public Guid? UsedByMemberId { get; private set; }
/// <summary>
/// 使用时间
/// </summary>
public DateTime? UsedAt { get; private set; }
/// <summary>
/// 过期时间
/// </summary>
public DateTime? ExpiryDate { get; private set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; private set; }
public Deleted Deleted { get; private set; } = new();
public RowVersion RowVersion { get; private set; } = new(0);
/// <summary>
/// 标记为已使用
/// </summary>
public void MarkAsUsed(Guid memberId)
{
if (IsUsed)
throw new KnownException("该营销码已被使用");
if (ExpiryDate.HasValue && ExpiryDate.Value < DateTime.UtcNow)
throw new KnownException("该营销码已过期");
IsUsed = true;
UsedByMemberId = memberId;
UsedAt = DateTime.UtcNow;
this.AddDomainEvent(new MarketingCodeUsedDomainEvent(this, memberId));
}
/// <summary>
/// 检查是否有效
/// </summary>
public bool IsValid()
{
return !IsUsed && (!ExpiryDate.HasValue || ExpiryDate.Value >= DateTime.UtcNow);
}
}

View File

@ -0,0 +1,39 @@
namespace Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate;
/// <summary>
/// 产品信息值对象
/// </summary>
public record ProductInfo
{
/// <summary>
/// 产品ID
/// </summary>
public Guid ProductId { get; init; }
/// <summary>
/// 产品名称
/// </summary>
public string ProductName { get; init; } = string.Empty;
/// <summary>
/// 品类ID(可选)
/// </summary>
public Guid? CategoryId { get; init; }
/// <summary>
/// 品类名称(可选)
/// </summary>
public string? CategoryName { get; init; }
private ProductInfo() { }
public ProductInfo(Guid productId, string productName, Guid? categoryId = null, string? categoryName = null)
{
ProductId = productId;
ProductName = productName;
CategoryId = categoryId;
CategoryName = categoryName;
}
public static ProductInfo Empty() => new(Guid.Empty, string.Empty);
}

View File

@ -0,0 +1,204 @@
using Fengling.Backend.Domain.DomainEvents;
namespace Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
/// <summary>
/// 会员ID强类型标识
/// </summary>
public partial record MemberId : IGuidStronglyTypedId;
/// <summary>
/// 会员聚合根
/// </summary>
public class Member : Entity<MemberId>, IAggregateRoot
{
protected Member() { }
public Member(string phone, string password, string? nickname = null)
{
Phone = phone;
Password = password;
Nickname = nickname ?? phone;
Level = MemberLevel.Default();
TotalPoints = 0;
AvailablePoints = 0;
Status = MemberStatus.Active;
RegisteredAt = DateTime.UtcNow;
this.AddDomainEvent(new MemberRegisteredDomainEvent(this));
}
/// <summary>
/// 手机号(唯一)
/// </summary>
public string Phone { get; private set; } = string.Empty;
/// <summary>
/// 密码(已加密)
/// </summary>
public string Password { get; private set; } = string.Empty;
/// <summary>
/// 昵称
/// </summary>
public string Nickname { get; private set; } = string.Empty;
/// <summary>
/// 会员等级
/// </summary>
public MemberLevel Level { get; private set; } = MemberLevel.Default();
/// <summary>
/// 累计总积分(用于等级判定)
/// </summary>
public int TotalPoints { get; private set; }
/// <summary>
/// 可用积分余额
/// </summary>
public int AvailablePoints { get; private set; }
/// <summary>
/// 会员状态
/// </summary>
public MemberStatus Status { get; private set; }
/// <summary>
/// 注册时间
/// </summary>
public DateTime RegisteredAt { get; private set; }
public Deleted Deleted { get; private set; } = new();
public RowVersion RowVersion { get; private set; } = new(0);
/// <summary>
/// 修改密码
/// </summary>
public void ChangePassword(string newPassword)
{
Password = newPassword;
this.AddDomainEvent(new MemberPasswordChangedDomainEvent(this));
}
/// <summary>
/// 更新资料
/// </summary>
public void UpdateProfile(string? nickname)
{
if (!string.IsNullOrWhiteSpace(nickname))
{
Nickname = nickname;
this.AddDomainEvent(new MemberProfileUpdatedDomainEvent(this));
}
}
/// <summary>
/// 升级会员等级
/// </summary>
public void UpgradeLevel(MemberLevel newLevel)
{
if (newLevel.RequiredPoints > Level.RequiredPoints)
{
Level = newLevel;
this.AddDomainEvent(new MemberLevelUpgradedDomainEvent(this, newLevel));
}
}
/// <summary>
/// 禁用会员
/// </summary>
public void Disable(string reason)
{
if (Status == MemberStatus.Active)
{
Status = MemberStatus.Disabled;
this.AddDomainEvent(new MemberDisabledDomainEvent(this, reason));
}
}
/// <summary>
/// 启用会员
/// </summary>
public void Enable()
{
if (Status == MemberStatus.Disabled)
{
Status = MemberStatus.Active;
this.AddDomainEvent(new MemberEnabledDomainEvent(this));
}
}
/// <summary>
/// 增加积分
/// </summary>
public void AddPoints(int amount, string source, Guid relatedId, DateTime expiryDate)
{
if (amount <= 0)
throw new KnownException("积分数量必须大于0");
TotalPoints += amount;
AvailablePoints += amount;
this.AddDomainEvent(new PointsAddedDomainEvent(Id, amount, source, relatedId, expiryDate));
}
/// <summary>
/// 消费积分
/// </summary>
public void ConsumePoints(int amount, string reason, Guid orderId)
{
if (amount <= 0)
throw new KnownException("积分数量必须大于0");
if (AvailablePoints < amount)
throw new KnownException($"积分余额不足,当前可用:{AvailablePoints},需要:{amount}");
AvailablePoints -= amount;
this.AddDomainEvent(new PointsConsumedDomainEvent(Id, amount, reason, orderId));
}
/// <summary>
/// 退还积分(订单取消)
/// </summary>
public void RefundPoints(int amount, string reason, Guid orderId)
{
if (amount <= 0)
throw new KnownException("积分数量必须大于0");
AvailablePoints += amount;
this.AddDomainEvent(new PointsRefundedDomainEvent(Id, amount, reason, orderId));
}
/// <summary>
/// 过期积分
/// </summary>
public void ExpirePoints(int amount)
{
if (amount <= 0) return;
if (AvailablePoints < amount)
amount = AvailablePoints;
AvailablePoints -= amount;
this.AddDomainEvent(new PointsExpiredDomainEvent(Id, amount));
}
}
/// <summary>
/// 会员状态枚举
/// </summary>
public enum MemberStatus
{
/// <summary>
/// 正常
/// </summary>
Active = 1,
/// <summary>
/// 禁用
/// </summary>
Disabled = 2
}

View File

@ -0,0 +1,42 @@
namespace Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
/// <summary>
/// 会员等级值对象
/// </summary>
public record MemberLevel
{
/// <summary>
/// 等级编码
/// </summary>
public string LevelCode { get; init; } = string.Empty;
/// <summary>
/// 等级名称
/// </summary>
public string LevelName { get; init; } = string.Empty;
/// <summary>
/// 所需积分
/// </summary>
public int RequiredPoints { get; init; }
/// <summary>
/// 积分奖励倍率
/// </summary>
public decimal BonusRate { get; init; }
private MemberLevel() { }
public MemberLevel(string levelCode, string levelName, int requiredPoints, decimal bonusRate = 1.0m)
{
LevelCode = levelCode;
LevelName = levelName;
RequiredPoints = requiredPoints;
BonusRate = bonusRate;
}
/// <summary>
/// 默认等级
/// </summary>
public static MemberLevel Default() => new("NORMAL", "普通会员", 0, 1.0m);
}

View File

@ -0,0 +1,118 @@
using Fengling.Backend.Domain.DomainEvents;
namespace Fengling.Backend.Domain.AggregatesModel.MemberLevelRuleAggregate;
/// <summary>
/// 会员等级规则ID
/// </summary>
public partial record MemberLevelRuleId : IGuidStronglyTypedId;
/// <summary>
/// 会员等级规则聚合根
/// </summary>
public class MemberLevelRule : Entity<MemberLevelRuleId>, IAggregateRoot
{
protected MemberLevelRule() { }
public MemberLevelRule(
string levelCode,
string levelName,
int requiredPoints,
decimal bonusPointsRate = 1.0m,
int sortOrder = 0)
{
LevelCode = levelCode;
LevelName = levelName;
RequiredPoints = requiredPoints;
BonusPointsRate = bonusPointsRate;
SortOrder = sortOrder;
IsActive = true;
CreatedAt = DateTime.UtcNow;
this.AddDomainEvent(new MemberLevelRuleCreatedDomainEvent(this));
}
/// <summary>
/// 等级编码(唯一)
/// </summary>
public string LevelCode { get; private set; } = string.Empty;
/// <summary>
/// 等级名称
/// </summary>
public string LevelName { get; private set; } = string.Empty;
/// <summary>
/// 所需积分
/// </summary>
public int RequiredPoints { get; private set; }
/// <summary>
/// 积分奖励倍率
/// </summary>
public decimal BonusPointsRate { get; private set; }
/// <summary>
/// 排序(越小越高)
/// </summary>
public int SortOrder { get; private set; }
/// <summary>
/// 是否激活
/// </summary>
public bool IsActive { get; private set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; private set; }
public Deleted Deleted { get; private set; } = new();
public RowVersion RowVersion { get; private set; } = new(0);
/// <summary>
/// 更新规则
/// </summary>
public void Update(
string? levelName = null,
int? requiredPoints = null,
decimal? bonusPointsRate = null,
int? sortOrder = null)
{
if (!string.IsNullOrWhiteSpace(levelName))
LevelName = levelName;
if (requiredPoints.HasValue && requiredPoints.Value >= 0)
RequiredPoints = requiredPoints.Value;
if (bonusPointsRate.HasValue && bonusPointsRate.Value > 0)
BonusPointsRate = bonusPointsRate.Value;
if (sortOrder.HasValue)
SortOrder = sortOrder.Value;
this.AddDomainEvent(new MemberLevelRuleUpdatedDomainEvent(this));
}
/// <summary>
/// 激活
/// </summary>
public void Activate()
{
if (!IsActive)
{
IsActive = true;
}
}
/// <summary>
/// 停用
/// </summary>
public void Deactivate()
{
if (IsActive)
{
IsActive = false;
}
}
}

View File

@ -0,0 +1,201 @@
using Fengling.Backend.Domain.DomainEvents;
namespace Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
/// <summary>
/// 积分规则ID
/// </summary>
public partial record PointsRuleId : IGuidStronglyTypedId;
/// <summary>
/// 积分规则聚合根
/// </summary>
public class PointsRule : Entity<PointsRuleId>, IAggregateRoot
{
protected PointsRule() { }
public PointsRule(
string ruleName,
PointsRuleType ruleType,
int pointsValue,
DateTime startDate,
DateTime? endDate = null,
Guid? productId = null,
Guid? categoryId = null,
string? memberLevelCode = null,
decimal bonusMultiplier = 1.0m)
{
RuleName = ruleName;
RuleType = ruleType;
PointsValue = pointsValue;
BonusMultiplier = bonusMultiplier;
StartDate = startDate;
EndDate = endDate;
ProductId = productId;
CategoryId = categoryId;
MemberLevelCode = memberLevelCode;
IsActive = true;
CreatedAt = DateTime.UtcNow;
this.AddDomainEvent(new PointsRuleCreatedDomainEvent(this));
}
/// <summary>
/// 规则名称
/// </summary>
public string RuleName { get; private set; } = string.Empty;
/// <summary>
/// 规则类型
/// </summary>
public PointsRuleType RuleType { get; private set; }
/// <summary>
/// 积分值
/// </summary>
public int PointsValue { get; private set; }
/// <summary>
/// 奖励倍数
/// </summary>
public decimal BonusMultiplier { get; private set; }
/// <summary>
/// 生效开始时间
/// </summary>
public DateTime StartDate { get; private set; }
/// <summary>
/// 生效结束时间(可选)
/// </summary>
public DateTime? EndDate { get; private set; }
/// <summary>
/// 产品ID(产品维度规则)
/// </summary>
public Guid? ProductId { get; private set; }
/// <summary>
/// 品类ID(品类维度规则)
/// </summary>
public Guid? CategoryId { get; private set; }
/// <summary>
/// 会员等级编码(会员维度规则)
/// </summary>
public string? MemberLevelCode { get; private set; }
/// <summary>
/// 是否激活
/// </summary>
public bool IsActive { get; private set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; private set; }
public Deleted Deleted { get; private set; } = new();
public RowVersion RowVersion { get; private set; } = new(0);
/// <summary>
/// 更新规则
/// </summary>
public void Update(
string? ruleName = null,
int? pointsValue = null,
decimal? bonusMultiplier = null,
DateTime? startDate = null,
DateTime? endDate = null)
{
if (!string.IsNullOrWhiteSpace(ruleName))
RuleName = ruleName;
if (pointsValue.HasValue && pointsValue.Value > 0)
PointsValue = pointsValue.Value;
if (bonusMultiplier.HasValue && bonusMultiplier.Value > 0)
BonusMultiplier = bonusMultiplier.Value;
if (startDate.HasValue)
StartDate = startDate.Value;
if (endDate.HasValue)
EndDate = endDate.Value;
this.AddDomainEvent(new PointsRuleUpdatedDomainEvent(this));
}
/// <summary>
/// 激活规则
/// </summary>
public void Activate()
{
if (!IsActive)
{
IsActive = true;
this.AddDomainEvent(new PointsRuleActivatedDomainEvent(this));
}
}
/// <summary>
/// 停用规则
/// </summary>
public void Deactivate()
{
if (IsActive)
{
IsActive = false;
this.AddDomainEvent(new PointsRuleDeactivatedDomainEvent(this));
}
}
/// <summary>
/// 检查规则是否匹配
/// </summary>
public bool IsMatch(Guid productId, Guid? categoryId, string memberLevelCode, DateTime scanDate)
{
if (!IsActive) return false;
if (scanDate < StartDate || (EndDate.HasValue && scanDate > EndDate.Value)) return false;
// 产品维度
if (ProductId.HasValue && ProductId.Value != productId) return false;
// 品类维度
if (CategoryId.HasValue && categoryId.HasValue && CategoryId.Value != categoryId.Value) return false;
// 会员等级维度
if (!string.IsNullOrWhiteSpace(MemberLevelCode) && MemberLevelCode != memberLevelCode) return false;
return true;
}
/// <summary>
/// 计算最终积分
/// </summary>
public int CalculatePoints()
{
return (int)(PointsValue * BonusMultiplier);
}
}
/// <summary>
/// 积分规则类型
/// </summary>
public enum PointsRuleType
{
/// <summary>
/// 产品维度
/// </summary>
Product = 1,
/// <summary>
/// 时间维度
/// </summary>
Time = 2,
/// <summary>
/// 会员等级维度
/// </summary>
MemberLevel = 3
}

View File

@ -0,0 +1,183 @@
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
namespace Fengling.Backend.Domain.AggregatesModel.PointsTransactionAggregate;
/// <summary>
/// 积分交易ID
/// </summary>
public partial record PointsTransactionId : IGuidStronglyTypedId;
/// <summary>
/// 积分交易聚合根
/// </summary>
public class PointsTransaction : Entity<PointsTransactionId>, IAggregateRoot
{
protected PointsTransaction() { }
/// <summary>
/// 创建积分交易记录
/// </summary>
public static PointsTransaction CreateEarnTransaction(
MemberId memberId,
int amount,
string source,
string reason,
Guid relatedId,
DateTime expiryDate)
{
if (amount <= 0)
throw new KnownException("积分数量必须大于0");
return new PointsTransaction
{
MemberId = memberId,
Type = PointsTransactionType.Earn,
Amount = amount,
Source = source,
Reason = reason,
RelatedId = relatedId,
ExpiryDate = expiryDate,
CreatedAt = DateTime.UtcNow
};
}
/// <summary>
/// 创建积分消费记录
/// </summary>
public static PointsTransaction CreateConsumeTransaction(
MemberId memberId,
int amount,
string reason,
Guid orderId)
{
if (amount <= 0)
throw new KnownException("积分数量必须大于0");
return new PointsTransaction
{
MemberId = memberId,
Type = PointsTransactionType.Consume,
Amount = amount,
Source = "兑换消费",
Reason = reason,
RelatedId = orderId,
CreatedAt = DateTime.UtcNow
};
}
/// <summary>
/// 创建积分退还记录
/// </summary>
public static PointsTransaction CreateRefundTransaction(
MemberId memberId,
int amount,
string reason,
Guid orderId)
{
if (amount <= 0)
throw new KnownException("积分数量必须大于0");
return new PointsTransaction
{
MemberId = memberId,
Type = PointsTransactionType.Refund,
Amount = amount,
Source = "订单退还",
Reason = reason,
RelatedId = orderId,
CreatedAt = DateTime.UtcNow
};
}
/// <summary>
/// 创建积分过期记录
/// </summary>
public static PointsTransaction CreateExpireTransaction(
MemberId memberId,
int amount,
Guid batchId)
{
if (amount <= 0)
throw new KnownException("积分数量必须大于0");
return new PointsTransaction
{
MemberId = memberId,
Type = PointsTransactionType.Expire,
Amount = amount,
Source = "积分过期",
Reason = "积分已过期自动失效",
RelatedId = batchId,
CreatedAt = DateTime.UtcNow
};
}
/// <summary>
/// 会员ID
/// </summary>
public MemberId MemberId { get; private set; } = default!;
/// <summary>
/// 交易类型
/// </summary>
public PointsTransactionType Type { get; private set; }
/// <summary>
/// 积分数量
/// </summary>
public int Amount { get; private set; }
/// <summary>
/// 来源
/// </summary>
public string Source { get; private set; } = string.Empty;
/// <summary>
/// 原因描述
/// </summary>
public string Reason { get; private set; } = string.Empty;
/// <summary>
/// 关联ID(营销码ID/订单ID等)
/// </summary>
public Guid RelatedId { get; private set; }
/// <summary>
/// 过期时间(仅获取类型有效)
/// </summary>
public DateTime? ExpiryDate { get; private set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; private set; }
public Deleted Deleted { get; private set; } = new();
public RowVersion RowVersion { get; private set; } = new(0);
}
/// <summary>
/// 积分交易类型
/// </summary>
public enum PointsTransactionType
{
/// <summary>
/// 获得
/// </summary>
Earn = 1,
/// <summary>
/// 消费
/// </summary>
Consume = 2,
/// <summary>
/// 过期
/// </summary>
Expire = 3,
/// <summary>
/// 退还
/// </summary>
Refund = 4
}

View File

@ -0,0 +1,60 @@
namespace Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate;
/// <summary>
/// 收货地址值对象
/// </summary>
public record Address
{
/// <summary>
/// 收货人姓名
/// </summary>
public string ReceiverName { get; init; } = string.Empty;
/// <summary>
/// 联系电话
/// </summary>
public string Phone { get; init; } = string.Empty;
/// <summary>
/// 省
/// </summary>
public string Province { get; init; } = string.Empty;
/// <summary>
/// 市
/// </summary>
public string City { get; init; } = string.Empty;
/// <summary>
/// 区/县
/// </summary>
public string District { get; init; } = string.Empty;
/// <summary>
/// 详细地址
/// </summary>
public string DetailAddress { get; init; } = string.Empty;
private Address() { }
public Address(
string receiverName,
string phone,
string province,
string city,
string district,
string detailAddress)
{
ReceiverName = receiverName;
Phone = phone;
Province = province;
City = city;
District = district;
DetailAddress = detailAddress;
}
/// <summary>
/// 获取完整地址
/// </summary>
public string GetFullAddress() => $"{Province}{City}{District}{DetailAddress}";
}

View File

@ -0,0 +1,200 @@
using Fengling.Backend.Domain.DomainEvents;
namespace Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate;
/// <summary>
/// 兑换订单ID
/// </summary>
public partial record RedemptionOrderId : IGuidStronglyTypedId;
/// <summary>
/// 兑换订单聚合根
/// </summary>
public class RedemptionOrder : Entity<RedemptionOrderId>, IAggregateRoot
{
protected RedemptionOrder() { }
public RedemptionOrder(
string orderNo,
Guid memberId,
Guid giftId,
string giftName,
int giftType,
int quantity,
int consumedPoints,
Address? shippingAddress = null)
{
OrderNo = orderNo;
MemberId = memberId;
GiftId = giftId;
GiftName = giftName;
GiftType = giftType;
Quantity = quantity;
ConsumedPoints = consumedPoints;
ShippingAddress = shippingAddress;
Status = RedemptionOrderStatus.Pending;
CreatedAt = DateTime.UtcNow;
this.AddDomainEvent(new RedemptionOrderCreatedDomainEvent(this));
}
/// <summary>
/// 订单号(唯一)
/// </summary>
public string OrderNo { get; private set; } = string.Empty;
/// <summary>
/// 会员ID
/// </summary>
public Guid MemberId { get; private set; }
/// <summary>
/// 礼品ID
/// </summary>
public Guid GiftId { get; private set; }
/// <summary>
/// 礼品名称
/// </summary>
public string GiftName { get; private set; } = string.Empty;
/// <summary>
/// 礼品类型
/// </summary>
public int GiftType { get; private set; }
/// <summary>
/// 数量
/// </summary>
public int Quantity { get; private set; }
/// <summary>
/// 消耗积分
/// </summary>
public int ConsumedPoints { get; private set; }
/// <summary>
/// 收货地址(实物礼品)
/// </summary>
public Address? ShippingAddress { get; private set; }
/// <summary>
/// 物流单号
/// </summary>
public string? TrackingNo { get; private set; }
/// <summary>
/// 订单状态
/// </summary>
public RedemptionOrderStatus Status { get; private set; }
/// <summary>
/// 取消原因
/// </summary>
public string? CancelReason { get; private set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; private set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime UpdatedAt { get; private set; }
public Deleted Deleted { get; private set; } = new();
public RowVersion RowVersion { get; private set; } = new(0);
/// <summary>
/// 标记为已发货
/// </summary>
public void MarkAsDispatched(string? trackingNo = null)
{
if (Status != RedemptionOrderStatus.Pending)
throw new KnownException($"订单状态错误,当前状态:{Status}");
Status = RedemptionOrderStatus.Dispatched;
TrackingNo = trackingNo;
UpdatedAt = DateTime.UtcNow;
this.AddDomainEvent(new RedemptionOrderDispatchedDomainEvent(this));
}
/// <summary>
/// 标记为已送达
/// </summary>
public void MarkAsDelivered()
{
if (Status != RedemptionOrderStatus.Dispatched)
throw new KnownException($"订单状态错误,当前状态:{Status}");
Status = RedemptionOrderStatus.Delivered;
UpdatedAt = DateTime.UtcNow;
this.AddDomainEvent(new RedemptionOrderDeliveredDomainEvent(this));
}
/// <summary>
/// 完成订单
/// </summary>
public void Complete()
{
if (Status == RedemptionOrderStatus.Completed || Status == RedemptionOrderStatus.Cancelled)
throw new KnownException($"订单已{Status},无法完成");
Status = RedemptionOrderStatus.Completed;
UpdatedAt = DateTime.UtcNow;
this.AddDomainEvent(new RedemptionOrderCompletedDomainEvent(this));
}
/// <summary>
/// 取消订单
/// </summary>
public void Cancel(string reason)
{
if (Status == RedemptionOrderStatus.Completed)
throw new KnownException("订单已完成,无法取消");
if (Status == RedemptionOrderStatus.Cancelled)
throw new KnownException("订单已取消");
Status = RedemptionOrderStatus.Cancelled;
CancelReason = reason;
UpdatedAt = DateTime.UtcNow;
this.AddDomainEvent(new RedemptionOrderCancelledDomainEvent(this));
}
}
/// <summary>
/// 订单状态
/// </summary>
public enum RedemptionOrderStatus
{
/// <summary>
/// 待处理
/// </summary>
Pending = 1,
/// <summary>
/// 已发货
/// </summary>
Dispatched = 2,
/// <summary>
/// 已送达
/// </summary>
Delivered = 3,
/// <summary>
/// 已完成
/// </summary>
Completed = 4,
/// <summary>
/// 已取消
/// </summary>
Cancelled = 5
}

View File

@ -0,0 +1,43 @@
using Fengling.Backend.Domain.AggregatesModel.GiftAggregate;
namespace Fengling.Backend.Domain.DomainEvents;
/// <summary>
/// 礼品创建领域事件
/// </summary>
public record GiftCreatedDomainEvent(Gift Gift) : IDomainEvent;
/// <summary>
/// 礼品更新领域事件
/// </summary>
public record GiftUpdatedDomainEvent(Gift Gift) : IDomainEvent;
/// <summary>
/// 礼品库存预留领域事件
/// </summary>
public record GiftStockReservedDomainEvent(Gift Gift, int Quantity) : IDomainEvent;
/// <summary>
/// 礼品库存扣减领域事件
/// </summary>
public record GiftStockDeductedDomainEvent(Gift Gift, int Quantity) : IDomainEvent;
/// <summary>
/// 礼品库存释放领域事件
/// </summary>
public record GiftStockReleasedDomainEvent(Gift Gift, int Quantity) : IDomainEvent;
/// <summary>
/// 礼品库存预警领域事件
/// </summary>
public record GiftStockLowDomainEvent(Gift Gift, int CurrentStock) : IDomainEvent;
/// <summary>
/// 礼品上架领域事件
/// </summary>
public record GiftPutOnShelfDomainEvent(Gift Gift) : IDomainEvent;
/// <summary>
/// 礼品下架领域事件
/// </summary>
public record GiftPutOffShelfDomainEvent(Gift Gift) : IDomainEvent;

View File

@ -0,0 +1,13 @@
using Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate;
namespace Fengling.Backend.Domain.DomainEvents;
/// <summary>
/// 营销码生成领域事件
/// </summary>
public record MarketingCodeGeneratedDomainEvent(MarketingCode MarketingCode) : IDomainEvent;
/// <summary>
/// 营销码使用领域事件(核心事件:触发积分发放)
/// </summary>
public record MarketingCodeUsedDomainEvent(MarketingCode MarketingCode, Guid MemberId) : IDomainEvent;

View File

@ -0,0 +1,68 @@
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
namespace Fengling.Backend.Domain.DomainEvents;
/// <summary>
/// 会员注册领域事件
/// </summary>
public record MemberRegisteredDomainEvent(Member Member) : IDomainEvent;
/// <summary>
/// 会员密码修改领域事件
/// </summary>
public record MemberPasswordChangedDomainEvent(Member Member) : IDomainEvent;
/// <summary>
/// 会员资料更新领域事件
/// </summary>
public record MemberProfileUpdatedDomainEvent(Member Member) : IDomainEvent;
/// <summary>
/// 会员等级升级领域事件
/// </summary>
public record MemberLevelUpgradedDomainEvent(Member Member, MemberLevel NewLevel) : IDomainEvent;
/// <summary>
/// 会员禁用领域事件
/// </summary>
public record MemberDisabledDomainEvent(Member Member, string Reason) : IDomainEvent;
/// <summary>
/// 会员启用领域事件
/// </summary>
public record MemberEnabledDomainEvent(Member Member) : IDomainEvent;
/// <summary>
/// 积分增加领域事件
/// </summary>
public record PointsAddedDomainEvent(
MemberId MemberId,
int Amount,
string Source,
Guid RelatedId,
DateTime ExpiryDate) : IDomainEvent;
/// <summary>
/// 积分消费领域事件
/// </summary>
public record PointsConsumedDomainEvent(
MemberId MemberId,
int Amount,
string Reason,
Guid OrderId) : IDomainEvent;
/// <summary>
/// 积分退还领域事件
/// </summary>
public record PointsRefundedDomainEvent(
MemberId MemberId,
int Amount,
string Reason,
Guid OrderId) : IDomainEvent;
/// <summary>
/// 积分过期领域事件
/// </summary>
public record PointsExpiredDomainEvent(
MemberId MemberId,
int Amount) : IDomainEvent;

View File

@ -0,0 +1,13 @@
using Fengling.Backend.Domain.AggregatesModel.MemberLevelRuleAggregate;
namespace Fengling.Backend.Domain.DomainEvents;
/// <summary>
/// 会员等级规则创建领域事件
/// </summary>
public record MemberLevelRuleCreatedDomainEvent(MemberLevelRule Rule) : IDomainEvent;
/// <summary>
/// 会员等级规则更新领域事件
/// </summary>
public record MemberLevelRuleUpdatedDomainEvent(MemberLevelRule Rule) : IDomainEvent;

View File

@ -0,0 +1,23 @@
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
namespace Fengling.Backend.Domain.DomainEvents;
/// <summary>
/// 积分规则创建领域事件
/// </summary>
public record PointsRuleCreatedDomainEvent(PointsRule PointsRule) : IDomainEvent;
/// <summary>
/// 积分规则更新领域事件
/// </summary>
public record PointsRuleUpdatedDomainEvent(PointsRule PointsRule) : IDomainEvent;
/// <summary>
/// 积分规则激活领域事件
/// </summary>
public record PointsRuleActivatedDomainEvent(PointsRule PointsRule) : IDomainEvent;
/// <summary>
/// 积分规则停用领域事件
/// </summary>
public record PointsRuleDeactivatedDomainEvent(PointsRule PointsRule) : IDomainEvent;

View File

@ -0,0 +1,28 @@
using Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate;
namespace Fengling.Backend.Domain.DomainEvents;
/// <summary>
/// 兑换订单创建领域事件(核心事件:触发库存预留和积分扣减)
/// </summary>
public record RedemptionOrderCreatedDomainEvent(RedemptionOrder Order) : IDomainEvent;
/// <summary>
/// 兑换订单发货领域事件
/// </summary>
public record RedemptionOrderDispatchedDomainEvent(RedemptionOrder Order) : IDomainEvent;
/// <summary>
/// 兑换订单送达领域事件
/// </summary>
public record RedemptionOrderDeliveredDomainEvent(RedemptionOrder Order) : IDomainEvent;
/// <summary>
/// 兑换订单完成领域事件
/// </summary>
public record RedemptionOrderCompletedDomainEvent(RedemptionOrder Order) : IDomainEvent;
/// <summary>
/// 兑换订单取消领域事件(触发库存释放和积分退还)
/// </summary>
public record RedemptionOrderCancelledDomainEvent(RedemptionOrder Order) : IDomainEvent;

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NetCorePal.Extensions.CodeAnalysis" />
<PackageReference Include="NetCorePal.Extensions.Domain.Abstractions" />
<PackageReference Include="NetCorePal.Extensions.Primitives" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="SonarAnalyzer.CSharp">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,2 @@
global using NetCorePal.Extensions.Domain;
global using NetCorePal.Extensions.Primitives;

View File

@ -0,0 +1,38 @@
namespace Fengling.Backend.Domain.IntegrationEvents;
/// <summary>
/// 积分获得集成事件
/// </summary>
public record PointsEarnedIntegrationEvent(
Guid MemberId,
int Amount,
string Source,
string Reason,
Guid RelatedId,
DateTime ExpiryDate) : IIntegrationEvent;
/// <summary>
/// 积分消费集成事件
/// </summary>
public record PointsConsumedIntegrationEvent(
Guid MemberId,
int Amount,
string Reason,
Guid OrderId) : IIntegrationEvent;
/// <summary>
/// 积分退还集成事件
/// </summary>
public record PointsRefundedIntegrationEvent(
Guid MemberId,
int Amount,
string Reason,
Guid OrderId) : IIntegrationEvent;
/// <summary>
/// 积分过期集成事件
/// </summary>
public record PointsExpiredIntegrationEvent(
Guid MemberId,
int Amount,
Guid BatchId) : IIntegrationEvent;

View File

@ -0,0 +1,52 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using NetCorePal.Extensions.DistributedTransactions.CAP.Persistence;
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
using Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate;
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
using Fengling.Backend.Domain.AggregatesModel.PointsTransactionAggregate;
using Fengling.Backend.Domain.AggregatesModel.GiftAggregate;
using Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate;
namespace Fengling.Backend.Infrastructure;
public partial class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IMediator mediator)
: AppDbContextBase(options, mediator)
, ISqliteCapDataStorage
{
// 会员聚合
public DbSet<Member> Members => Set<Member>();
public DbSet<PointsTransaction> PointsTransactions => Set<PointsTransaction>();
// 营销码聚合
public DbSet<MarketingCode> MarketingCodes => Set<MarketingCode>();
// 积分规则聚合
public DbSet<PointsRule> PointsRules => Set<PointsRule>();
// 礼品聚合
public DbSet<Gift> Gifts => Set<Gift>();
// 兑换订单聚合
public DbSet<RedemptionOrder> RedemptionOrders => Set<RedemptionOrder>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
if (modelBuilder is null)
{
throw new ArgumentNullException(nameof(modelBuilder));
}
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
base.OnModelCreating(modelBuilder);
}
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
ConfigureStronglyTypedIdValueConverter(configurationBuilder);
base.ConfigureConventions(configurationBuilder);
}
}

View File

@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.DependencyInjection;
namespace Fengling.Backend.Infrastructure;
public class DesignTimeApplicationDbContextFactory: IDesignTimeDbContextFactory<ApplicationDbContext>
{
public ApplicationDbContext CreateDbContext(string[] args)
{
IServiceCollection services = new ServiceCollection();
services.AddMediatR(c =>
c.RegisterServicesFromAssemblies(typeof(DesignTimeApplicationDbContextFactory).Assembly));
services.AddDbContext<ApplicationDbContext>(options =>
{
// change connectionstring if you want to run command "dotnet ef database update"
options.UseSqlite("Data Source=fengling.db",
b =>
{
b.MigrationsAssembly(typeof(DesignTimeApplicationDbContextFactory).Assembly.FullName);
});
});
var provider = services.BuildServiceProvider();
var dbContext = provider.CreateScope().ServiceProvider.GetRequiredService<ApplicationDbContext>();
return dbContext;
}
}

View File

@ -0,0 +1,71 @@
using Fengling.Backend.Domain.AggregatesModel.GiftAggregate;
namespace Fengling.Backend.Infrastructure.EntityConfigurations;
public class GiftEntityTypeConfiguration : IEntityTypeConfiguration<Gift>
{
public void Configure(EntityTypeBuilder<Gift> builder)
{
builder.ToTable("Gifts");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.UseGuidVersion7ValueGenerator()
.HasComment("礼品ID");
builder.Property(x => x.Name)
.IsRequired()
.HasMaxLength(100)
.HasComment("礼品名称");
builder.Property(x => x.Type)
.IsRequired()
.HasComment("礼品类型(1:实物,2:虚拟,3:自有产品)");
builder.Property(x => x.Description)
.IsRequired()
.HasMaxLength(500)
.HasComment("描述");
builder.Property(x => x.ImageUrl)
.IsRequired()
.HasMaxLength(500)
.HasComment("图片URL");
builder.Property(x => x.RequiredPoints)
.IsRequired()
.HasComment("所需积分");
builder.Property(x => x.TotalStock)
.IsRequired()
.HasComment("总库存");
builder.Property(x => x.AvailableStock)
.IsRequired()
.HasComment("可用库存");
builder.Property(x => x.LimitPerMember)
.HasComment("每人限兑数量");
builder.Property(x => x.IsOnShelf)
.IsRequired()
.HasComment("是否上架");
builder.Property(x => x.SortOrder)
.IsRequired()
.HasComment("排序");
builder.Property(x => x.CreatedAt)
.IsRequired()
.HasComment("创建时间");
builder.Property(x => x.UpdatedAt)
.IsRequired()
.HasComment("更新时间");
// 索引
builder.HasIndex(x => x.IsOnShelf).HasDatabaseName("IX_Gifts_IsOnShelf");
builder.HasIndex(x => x.Type).HasDatabaseName("IX_Gifts_Type");
builder.HasIndex(x => x.SortOrder).HasDatabaseName("IX_Gifts_SortOrder");
}
}

View File

@ -0,0 +1,72 @@
using Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate;
namespace Fengling.Backend.Infrastructure.EntityConfigurations;
public class MarketingCodeEntityTypeConfiguration : IEntityTypeConfiguration<MarketingCode>
{
public void Configure(EntityTypeBuilder<MarketingCode> builder)
{
builder.ToTable("MarketingCodes");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.UseGuidVersion7ValueGenerator()
.HasComment("营销码ID");
builder.Property(x => x.Code)
.IsRequired()
.HasMaxLength(50)
.HasComment("营销码");
builder.Property(x => x.BatchNo)
.IsRequired()
.HasMaxLength(50)
.HasComment("批次号");
builder.Property(x => x.IsUsed)
.IsRequired()
.HasComment("是否已使用");
builder.Property(x => x.UsedByMemberId)
.HasComment("使用者会员ID");
builder.Property(x => x.UsedAt)
.HasComment("使用时间");
builder.Property(x => x.ExpiryDate)
.HasComment("过期时间");
builder.Property(x => x.CreatedAt)
.IsRequired()
.HasComment("创建时间");
// 产品信息值对象
builder.OwnsOne(x => x.ProductInfo, product =>
{
product.Property(p => p.ProductId)
.IsRequired()
.HasColumnName("ProductId")
.HasComment("产品ID");
product.Property(p => p.ProductName)
.IsRequired()
.HasMaxLength(100)
.HasColumnName("ProductName")
.HasComment("产品名称");
product.Property(p => p.CategoryId)
.HasColumnName("CategoryId")
.HasComment("品类ID");
product.Property(p => p.CategoryName)
.HasMaxLength(100)
.HasColumnName("CategoryName")
.HasComment("品类名称");
});
// 索引
builder.HasIndex(x => x.Code).IsUnique().HasDatabaseName("IX_MarketingCodes_Code");
builder.HasIndex(x => x.BatchNo).HasDatabaseName("IX_MarketingCodes_BatchNo");
builder.HasIndex(x => x.IsUsed).HasDatabaseName("IX_MarketingCodes_IsUsed");
}
}

View File

@ -0,0 +1,77 @@
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
namespace Fengling.Backend.Infrastructure.EntityConfigurations;
public class MemberEntityTypeConfiguration : IEntityTypeConfiguration<Member>
{
public void Configure(EntityTypeBuilder<Member> builder)
{
builder.ToTable("Members");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.UseGuidVersion7ValueGenerator()
.HasComment("会员ID");
builder.Property(x => x.Phone)
.IsRequired()
.HasMaxLength(20)
.HasComment("手机号");
builder.Property(x => x.Password)
.IsRequired()
.HasMaxLength(100)
.HasComment("密码(已加密)");
builder.Property(x => x.Nickname)
.IsRequired()
.HasMaxLength(50)
.HasComment("昵称");
builder.Property(x => x.TotalPoints)
.IsRequired()
.HasComment("累计总积分");
builder.Property(x => x.AvailablePoints)
.IsRequired()
.HasComment("可用积分");
builder.Property(x => x.Status)
.IsRequired()
.HasComment("状态(1:正常,2:禁用)");
builder.Property(x => x.RegisteredAt)
.IsRequired()
.HasComment("注册时间");
// 会员等级值对象
builder.OwnsOne(x => x.Level, level =>
{
level.Property(l => l.LevelCode)
.IsRequired()
.HasMaxLength(20)
.HasColumnName("LevelCode")
.HasComment("等级编码");
level.Property(l => l.LevelName)
.IsRequired()
.HasMaxLength(50)
.HasColumnName("LevelName")
.HasComment("等级名称");
level.Property(l => l.RequiredPoints)
.IsRequired()
.HasColumnName("RequiredPoints")
.HasComment("所需积分");
level.Property(l => l.BonusRate)
.IsRequired()
.HasColumnName("BonusRate")
.HasComment("积分奖励倍率");
});
// 索引
builder.HasIndex(x => x.Phone).IsUnique().HasDatabaseName("IX_Members_Phone");
builder.HasIndex(x => x.Status).HasDatabaseName("IX_Members_Status");
}
}

View File

@ -0,0 +1,64 @@
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
namespace Fengling.Backend.Infrastructure.EntityConfigurations;
public class PointsRuleEntityTypeConfiguration : IEntityTypeConfiguration<PointsRule>
{
public void Configure(EntityTypeBuilder<PointsRule> builder)
{
builder.ToTable("PointsRules");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.UseGuidVersion7ValueGenerator()
.HasComment("积分规则ID");
builder.Property(x => x.RuleName)
.IsRequired()
.HasMaxLength(100)
.HasComment("规则名称");
builder.Property(x => x.RuleType)
.IsRequired()
.HasComment("规则类型(1:产品,2:时间,3:会员等级)");
builder.Property(x => x.PointsValue)
.IsRequired()
.HasComment("积分值");
builder.Property(x => x.BonusMultiplier)
.IsRequired()
.HasComment("奖励倍数");
builder.Property(x => x.StartDate)
.IsRequired()
.HasComment("生效开始时间");
builder.Property(x => x.EndDate)
.HasComment("生效结束时间");
builder.Property(x => x.ProductId)
.HasComment("产品ID");
builder.Property(x => x.CategoryId)
.HasComment("品类ID");
builder.Property(x => x.MemberLevelCode)
.HasMaxLength(20)
.HasComment("会员等级编码");
builder.Property(x => x.IsActive)
.IsRequired()
.HasComment("是否激活");
builder.Property(x => x.CreatedAt)
.IsRequired()
.HasComment("创建时间");
// 索引
builder.HasIndex(x => x.IsActive).HasDatabaseName("IX_PointsRules_IsActive");
builder.HasIndex(x => x.ProductId).HasDatabaseName("IX_PointsRules_ProductId");
builder.HasIndex(x => x.MemberLevelCode).HasDatabaseName("IX_PointsRules_MemberLevelCode");
builder.HasIndex(x => new { x.StartDate, x.EndDate }).HasDatabaseName("IX_PointsRules_DateRange");
}
}

View File

@ -0,0 +1,55 @@
using Fengling.Backend.Domain.AggregatesModel.PointsTransactionAggregate;
namespace Fengling.Backend.Infrastructure.EntityConfigurations;
public class PointsTransactionEntityTypeConfiguration : IEntityTypeConfiguration<PointsTransaction>
{
public void Configure(EntityTypeBuilder<PointsTransaction> builder)
{
builder.ToTable("PointsTransactions");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.UseGuidVersion7ValueGenerator()
.HasComment("积分流水ID");
builder.Property(x => x.MemberId)
.IsRequired()
.HasComment("会员ID");
builder.Property(x => x.Type)
.IsRequired()
.HasComment("交易类型(1:获得,2:消费,3:过期,4:退还)");
builder.Property(x => x.Amount)
.IsRequired()
.HasComment("积分数量");
builder.Property(x => x.Source)
.IsRequired()
.HasMaxLength(100)
.HasComment("来源");
builder.Property(x => x.Reason)
.IsRequired()
.HasMaxLength(200)
.HasComment("原因描述");
builder.Property(x => x.RelatedId)
.IsRequired()
.HasComment("关联ID");
builder.Property(x => x.ExpiryDate)
.HasComment("过期时间");
builder.Property(x => x.CreatedAt)
.IsRequired()
.HasComment("创建时间");
// 索引
builder.HasIndex(x => x.MemberId).HasDatabaseName("IX_PointsTransactions_MemberId");
builder.HasIndex(x => x.RelatedId).IsUnique().HasDatabaseName("IX_PointsTransactions_RelatedId");
builder.HasIndex(x => x.Type).HasDatabaseName("IX_PointsTransactions_Type");
builder.HasIndex(x => x.CreatedAt).HasDatabaseName("IX_PointsTransactions_CreatedAt");
}
}

View File

@ -0,0 +1,106 @@
using Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate;
namespace Fengling.Backend.Infrastructure.EntityConfigurations;
public class RedemptionOrderEntityTypeConfiguration : IEntityTypeConfiguration<RedemptionOrder>
{
public void Configure(EntityTypeBuilder<RedemptionOrder> builder)
{
builder.ToTable("RedemptionOrders");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.UseGuidVersion7ValueGenerator()
.HasComment("兑换订单ID");
builder.Property(x => x.OrderNo)
.IsRequired()
.HasMaxLength(50)
.HasComment("订单号");
builder.Property(x => x.MemberId)
.IsRequired()
.HasComment("会员ID");
builder.Property(x => x.GiftId)
.IsRequired()
.HasComment("礼品ID");
builder.Property(x => x.GiftName)
.IsRequired()
.HasMaxLength(100)
.HasComment("礼品名称");
builder.Property(x => x.GiftType)
.IsRequired()
.HasComment("礼品类型");
builder.Property(x => x.Quantity)
.IsRequired()
.HasComment("数量");
builder.Property(x => x.ConsumedPoints)
.IsRequired()
.HasComment("消耗积分");
builder.Property(x => x.TrackingNo)
.HasMaxLength(100)
.HasComment("物流单号");
builder.Property(x => x.Status)
.IsRequired()
.HasComment("订单状态(1:待处理,2:已发货,3:已送达,4:已完成,5:已取消)");
builder.Property(x => x.CancelReason)
.HasMaxLength(500)
.HasComment("取消原因");
builder.Property(x => x.CreatedAt)
.IsRequired()
.HasComment("创建时间");
builder.Property(x => x.UpdatedAt)
.IsRequired()
.HasComment("更新时间");
// 收货地址值对象
builder.OwnsOne(x => x.ShippingAddress, address =>
{
address.Property(a => a.ReceiverName)
.HasMaxLength(50)
.HasColumnName("ReceiverName")
.HasComment("收货人姓名");
address.Property(a => a.Phone)
.HasMaxLength(20)
.HasColumnName("ReceiverPhone")
.HasComment("联系电话");
address.Property(a => a.Province)
.HasMaxLength(50)
.HasColumnName("Province")
.HasComment("省");
address.Property(a => a.City)
.HasMaxLength(50)
.HasColumnName("City")
.HasComment("市");
address.Property(a => a.District)
.HasMaxLength(50)
.HasColumnName("District")
.HasComment("区/县");
address.Property(a => a.DetailAddress)
.HasMaxLength(200)
.HasColumnName("DetailAddress")
.HasComment("详细地址");
});
// 索引
builder.HasIndex(x => x.OrderNo).IsUnique().HasDatabaseName("IX_RedemptionOrders_OrderNo");
builder.HasIndex(x => x.MemberId).HasDatabaseName("IX_RedemptionOrders_MemberId");
builder.HasIndex(x => x.Status).HasDatabaseName("IX_RedemptionOrders_Status");
builder.HasIndex(x => x.CreatedAt).HasDatabaseName("IX_RedemptionOrders_CreatedAt");
}
}

View File

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite"/>
<PackageReference Include="NetCorePal.Extensions.DistributedTransactions.CAP.Sqlite"/>
<PackageReference Include="NetCorePal.Extensions.Repository.EntityFrameworkCore"/>
<PackageReference Include="NetCorePal.Extensions.Repository.EntityFrameworkCore.Snowflake"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Fengling.Backend.Domain\Fengling.Backend.Domain.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Update="SonarAnalyzer.CSharp">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations\"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,6 @@
global using NetCorePal.Extensions.Domain;
global using NetCorePal.Extensions.Primitives;
global using NetCorePal.Extensions.Repository;
global using NetCorePal.Extensions.Repository.EntityFrameworkCore;
global using Microsoft.EntityFrameworkCore;
global using Microsoft.EntityFrameworkCore.Metadata.Builders;

View File

@ -0,0 +1,599 @@
// <auto-generated />
using System;
using Fengling.Backend.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Fengling.Backend.Infrastructure.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260211044819_Init")]
partial class Init
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.GiftAggregate.Gift", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("礼品ID");
b.Property<int>("AvailableStock")
.HasColumnType("INTEGER")
.HasComment("可用库存");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT")
.HasComment("描述");
b.Property<string>("ImageUrl")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT")
.HasComment("图片URL");
b.Property<bool>("IsOnShelf")
.HasColumnType("INTEGER")
.HasComment("是否上架");
b.Property<int?>("LimitPerMember")
.HasColumnType("INTEGER")
.HasComment("每人限兑数量");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("礼品名称");
b.Property<int>("RequiredPoints")
.HasColumnType("INTEGER")
.HasComment("所需积分");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER")
.HasComment("排序");
b.Property<int>("TotalStock")
.HasColumnType("INTEGER")
.HasComment("总库存");
b.Property<int>("Type")
.HasColumnType("INTEGER")
.HasComment("礼品类型(1:实物,2:虚拟,3:自有产品)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
.HasComment("更新时间");
b.HasKey("Id");
b.HasIndex("IsOnShelf")
.HasDatabaseName("IX_Gifts_IsOnShelf");
b.HasIndex("SortOrder")
.HasDatabaseName("IX_Gifts_SortOrder");
b.HasIndex("Type")
.HasDatabaseName("IX_Gifts_Type");
b.ToTable("Gifts", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("营销码ID");
b.Property<string>("BatchNo")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasComment("批次号");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasComment("营销码");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<DateTime?>("ExpiryDate")
.HasColumnType("TEXT")
.HasComment("过期时间");
b.Property<bool>("IsUsed")
.HasColumnType("INTEGER")
.HasComment("是否已使用");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<DateTime?>("UsedAt")
.HasColumnType("TEXT")
.HasComment("使用时间");
b.Property<Guid?>("UsedByMemberId")
.HasColumnType("TEXT")
.HasComment("使用者会员ID");
b.HasKey("Id");
b.HasIndex("BatchNo")
.HasDatabaseName("IX_MarketingCodes_BatchNo");
b.HasIndex("Code")
.IsUnique()
.HasDatabaseName("IX_MarketingCodes_Code");
b.HasIndex("IsUsed")
.HasDatabaseName("IX_MarketingCodes_IsUsed");
b.ToTable("MarketingCodes", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MemberAggregate.Member", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("会员ID");
b.Property<int>("AvailablePoints")
.HasColumnType("INTEGER")
.HasComment("可用积分");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<string>("Nickname")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasComment("昵称");
b.Property<string>("Password")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("密码(已加密)");
b.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT")
.HasComment("手机号");
b.Property<DateTime>("RegisteredAt")
.HasColumnType("TEXT")
.HasComment("注册时间");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER")
.HasComment("状态(1:正常,2:禁用)");
b.Property<int>("TotalPoints")
.HasColumnType("INTEGER")
.HasComment("累计总积分");
b.HasKey("Id");
b.HasIndex("Phone")
.IsUnique()
.HasDatabaseName("IX_Members_Phone");
b.HasIndex("Status")
.HasDatabaseName("IX_Members_Status");
b.ToTable("Members", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate.PointsRule", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("积分规则ID");
b.Property<decimal>("BonusMultiplier")
.HasColumnType("TEXT")
.HasComment("奖励倍数");
b.Property<Guid?>("CategoryId")
.HasColumnType("TEXT")
.HasComment("品类ID");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<DateTime?>("EndDate")
.HasColumnType("TEXT")
.HasComment("生效结束时间");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER")
.HasComment("是否激活");
b.Property<string>("MemberLevelCode")
.HasMaxLength(20)
.HasColumnType("TEXT")
.HasComment("会员等级编码");
b.Property<int>("PointsValue")
.HasColumnType("INTEGER")
.HasComment("积分值");
b.Property<Guid?>("ProductId")
.HasColumnType("TEXT")
.HasComment("产品ID");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("RuleName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("规则名称");
b.Property<int>("RuleType")
.HasColumnType("INTEGER")
.HasComment("规则类型(1:产品,2:时间,3:会员等级)");
b.Property<DateTime>("StartDate")
.HasColumnType("TEXT")
.HasComment("生效开始时间");
b.HasKey("Id");
b.HasIndex("IsActive")
.HasDatabaseName("IX_PointsRules_IsActive");
b.HasIndex("MemberLevelCode")
.HasDatabaseName("IX_PointsRules_MemberLevelCode");
b.HasIndex("ProductId")
.HasDatabaseName("IX_PointsRules_ProductId");
b.HasIndex("StartDate", "EndDate")
.HasDatabaseName("IX_PointsRules_DateRange");
b.ToTable("PointsRules", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.PointsTransactionAggregate.PointsTransaction", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("积分流水ID");
b.Property<int>("Amount")
.HasColumnType("INTEGER")
.HasComment("积分数量");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<DateTime?>("ExpiryDate")
.HasColumnType("TEXT")
.HasComment("过期时间");
b.Property<Guid>("MemberId")
.HasColumnType("TEXT")
.HasComment("会员ID");
b.Property<string>("Reason")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasComment("原因描述");
b.Property<Guid>("RelatedId")
.HasColumnType("TEXT")
.HasComment("关联ID");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("Source")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("来源");
b.Property<int>("Type")
.HasColumnType("INTEGER")
.HasComment("交易类型(1:获得,2:消费,3:过期,4:退还)");
b.HasKey("Id");
b.HasIndex("CreatedAt")
.HasDatabaseName("IX_PointsTransactions_CreatedAt");
b.HasIndex("MemberId")
.HasDatabaseName("IX_PointsTransactions_MemberId");
b.HasIndex("RelatedId")
.IsUnique()
.HasDatabaseName("IX_PointsTransactions_RelatedId");
b.HasIndex("Type")
.HasDatabaseName("IX_PointsTransactions_Type");
b.ToTable("PointsTransactions", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.RedemptionOrder", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("兑换订单ID");
b.Property<string>("CancelReason")
.HasMaxLength(500)
.HasColumnType("TEXT")
.HasComment("取消原因");
b.Property<int>("ConsumedPoints")
.HasColumnType("INTEGER")
.HasComment("消耗积分");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<Guid>("GiftId")
.HasColumnType("TEXT")
.HasComment("礼品ID");
b.Property<string>("GiftName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("礼品名称");
b.Property<int>("GiftType")
.HasColumnType("INTEGER")
.HasComment("礼品类型");
b.Property<Guid>("MemberId")
.HasColumnType("TEXT")
.HasComment("会员ID");
b.Property<string>("OrderNo")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasComment("订单号");
b.Property<int>("Quantity")
.HasColumnType("INTEGER")
.HasComment("数量");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER")
.HasComment("订单状态(1:待处理,2:已发货,3:已送达,4:已完成,5:已取消)");
b.Property<string>("TrackingNo")
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("物流单号");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
.HasComment("更新时间");
b.HasKey("Id");
b.HasIndex("CreatedAt")
.HasDatabaseName("IX_RedemptionOrders_CreatedAt");
b.HasIndex("MemberId")
.HasDatabaseName("IX_RedemptionOrders_MemberId");
b.HasIndex("OrderNo")
.IsUnique()
.HasDatabaseName("IX_RedemptionOrders_OrderNo");
b.HasIndex("Status")
.HasDatabaseName("IX_RedemptionOrders_Status");
b.ToTable("RedemptionOrders", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b =>
{
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.ProductInfo", "ProductInfo", b1 =>
{
b1.Property<Guid>("MarketingCodeId")
.HasColumnType("TEXT");
b1.Property<Guid?>("CategoryId")
.HasColumnType("TEXT")
.HasColumnName("CategoryId")
.HasComment("品类ID");
b1.Property<string>("CategoryName")
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("CategoryName")
.HasComment("品类名称");
b1.Property<Guid>("ProductId")
.HasColumnType("TEXT")
.HasColumnName("ProductId")
.HasComment("产品ID");
b1.Property<string>("ProductName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("ProductName")
.HasComment("产品名称");
b1.HasKey("MarketingCodeId");
b1.ToTable("MarketingCodes");
b1.WithOwner()
.HasForeignKey("MarketingCodeId");
});
b.Navigation("ProductInfo")
.IsRequired();
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MemberAggregate.Member", b =>
{
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MemberAggregate.MemberLevel", "Level", b1 =>
{
b1.Property<Guid>("MemberId")
.HasColumnType("TEXT");
b1.Property<decimal>("BonusRate")
.HasColumnType("TEXT")
.HasColumnName("BonusRate")
.HasComment("积分奖励倍率");
b1.Property<string>("LevelCode")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT")
.HasColumnName("LevelCode")
.HasComment("等级编码");
b1.Property<string>("LevelName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("LevelName")
.HasComment("等级名称");
b1.Property<int>("RequiredPoints")
.HasColumnType("INTEGER")
.HasColumnName("RequiredPoints")
.HasComment("所需积分");
b1.HasKey("MemberId");
b1.ToTable("Members");
b1.WithOwner()
.HasForeignKey("MemberId");
});
b.Navigation("Level")
.IsRequired();
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.RedemptionOrder", b =>
{
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.Address", "ShippingAddress", b1 =>
{
b1.Property<Guid>("RedemptionOrderId")
.HasColumnType("TEXT");
b1.Property<string>("City")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("City")
.HasComment("市");
b1.Property<string>("DetailAddress")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("DetailAddress")
.HasComment("详细地址");
b1.Property<string>("District")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("District")
.HasComment("区/县");
b1.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT")
.HasColumnName("ReceiverPhone")
.HasComment("联系电话");
b1.Property<string>("Province")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("Province")
.HasComment("省");
b1.Property<string>("ReceiverName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("ReceiverName")
.HasComment("收货人姓名");
b1.HasKey("RedemptionOrderId");
b1.ToTable("RedemptionOrders");
b1.WithOwner()
.HasForeignKey("RedemptionOrderId");
});
b.Navigation("ShippingAddress");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,290 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Fengling.Backend.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class Init : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Gifts",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false, comment: "礼品ID"),
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false, comment: "礼品名称"),
Type = table.Column<int>(type: "INTEGER", nullable: false, comment: "礼品类型(1:实物,2:虚拟,3:自有产品)"),
Description = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false, comment: "描述"),
ImageUrl = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false, comment: "图片URL"),
RequiredPoints = table.Column<int>(type: "INTEGER", nullable: false, comment: "所需积分"),
TotalStock = table.Column<int>(type: "INTEGER", nullable: false, comment: "总库存"),
AvailableStock = table.Column<int>(type: "INTEGER", nullable: false, comment: "可用库存"),
LimitPerMember = table.Column<int>(type: "INTEGER", nullable: true, comment: "每人限兑数量"),
IsOnShelf = table.Column<bool>(type: "INTEGER", nullable: false, comment: "是否上架"),
SortOrder = table.Column<int>(type: "INTEGER", nullable: false, comment: "排序"),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "创建时间"),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "更新时间"),
Deleted = table.Column<bool>(type: "INTEGER", nullable: false),
RowVersion = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Gifts", x => x.Id);
});
migrationBuilder.CreateTable(
name: "MarketingCodes",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false, comment: "营销码ID"),
Code = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false, comment: "营销码"),
ProductId = table.Column<Guid>(type: "TEXT", nullable: false, comment: "产品ID"),
ProductName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false, comment: "产品名称"),
CategoryId = table.Column<Guid>(type: "TEXT", nullable: true, comment: "品类ID"),
CategoryName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true, comment: "品类名称"),
BatchNo = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false, comment: "批次号"),
IsUsed = table.Column<bool>(type: "INTEGER", nullable: false, comment: "是否已使用"),
UsedByMemberId = table.Column<Guid>(type: "TEXT", nullable: true, comment: "使用者会员ID"),
UsedAt = table.Column<DateTime>(type: "TEXT", nullable: true, comment: "使用时间"),
ExpiryDate = table.Column<DateTime>(type: "TEXT", nullable: true, comment: "过期时间"),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "创建时间"),
Deleted = table.Column<bool>(type: "INTEGER", nullable: false),
RowVersion = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MarketingCodes", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Members",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false, comment: "会员ID"),
Phone = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false, comment: "手机号"),
Password = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false, comment: "密码(已加密)"),
Nickname = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false, comment: "昵称"),
LevelCode = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false, comment: "等级编码"),
LevelName = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false, comment: "等级名称"),
RequiredPoints = table.Column<int>(type: "INTEGER", nullable: false, comment: "所需积分"),
BonusRate = table.Column<decimal>(type: "TEXT", nullable: false, comment: "积分奖励倍率"),
TotalPoints = table.Column<int>(type: "INTEGER", nullable: false, comment: "累计总积分"),
AvailablePoints = table.Column<int>(type: "INTEGER", nullable: false, comment: "可用积分"),
Status = table.Column<int>(type: "INTEGER", nullable: false, comment: "状态(1:正常,2:禁用)"),
RegisteredAt = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "注册时间"),
Deleted = table.Column<bool>(type: "INTEGER", nullable: false),
RowVersion = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Members", x => x.Id);
});
migrationBuilder.CreateTable(
name: "PointsRules",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false, comment: "积分规则ID"),
RuleName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false, comment: "规则名称"),
RuleType = table.Column<int>(type: "INTEGER", nullable: false, comment: "规则类型(1:产品,2:时间,3:会员等级)"),
PointsValue = table.Column<int>(type: "INTEGER", nullable: false, comment: "积分值"),
BonusMultiplier = table.Column<decimal>(type: "TEXT", nullable: false, comment: "奖励倍数"),
StartDate = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "生效开始时间"),
EndDate = table.Column<DateTime>(type: "TEXT", nullable: true, comment: "生效结束时间"),
ProductId = table.Column<Guid>(type: "TEXT", nullable: true, comment: "产品ID"),
CategoryId = table.Column<Guid>(type: "TEXT", nullable: true, comment: "品类ID"),
MemberLevelCode = table.Column<string>(type: "TEXT", maxLength: 20, nullable: true, comment: "会员等级编码"),
IsActive = table.Column<bool>(type: "INTEGER", nullable: false, comment: "是否激活"),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "创建时间"),
Deleted = table.Column<bool>(type: "INTEGER", nullable: false),
RowVersion = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PointsRules", x => x.Id);
});
migrationBuilder.CreateTable(
name: "PointsTransactions",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false, comment: "积分流水ID"),
MemberId = table.Column<Guid>(type: "TEXT", nullable: false, comment: "会员ID"),
Type = table.Column<int>(type: "INTEGER", nullable: false, comment: "交易类型(1:获得,2:消费,3:过期,4:退还)"),
Amount = table.Column<int>(type: "INTEGER", nullable: false, comment: "积分数量"),
Source = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false, comment: "来源"),
Reason = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false, comment: "原因描述"),
RelatedId = table.Column<Guid>(type: "TEXT", nullable: false, comment: "关联ID"),
ExpiryDate = table.Column<DateTime>(type: "TEXT", nullable: true, comment: "过期时间"),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "创建时间"),
Deleted = table.Column<bool>(type: "INTEGER", nullable: false),
RowVersion = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PointsTransactions", x => x.Id);
});
migrationBuilder.CreateTable(
name: "RedemptionOrders",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false, comment: "兑换订单ID"),
OrderNo = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false, comment: "订单号"),
MemberId = table.Column<Guid>(type: "TEXT", nullable: false, comment: "会员ID"),
GiftId = table.Column<Guid>(type: "TEXT", nullable: false, comment: "礼品ID"),
GiftName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false, comment: "礼品名称"),
GiftType = table.Column<int>(type: "INTEGER", nullable: false, comment: "礼品类型"),
Quantity = table.Column<int>(type: "INTEGER", nullable: false, comment: "数量"),
ConsumedPoints = table.Column<int>(type: "INTEGER", nullable: false, comment: "消耗积分"),
ReceiverName = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true, comment: "收货人姓名"),
ReceiverPhone = table.Column<string>(type: "TEXT", maxLength: 20, nullable: true, comment: "联系电话"),
Province = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true, comment: "省"),
City = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true, comment: "市"),
District = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true, comment: "区/县"),
DetailAddress = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true, comment: "详细地址"),
TrackingNo = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true, comment: "物流单号"),
Status = table.Column<int>(type: "INTEGER", nullable: false, comment: "订单状态(1:待处理,2:已发货,3:已送达,4:已完成,5:已取消)"),
CancelReason = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true, comment: "取消原因"),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "创建时间"),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "更新时间"),
Deleted = table.Column<bool>(type: "INTEGER", nullable: false),
RowVersion = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_RedemptionOrders", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_Gifts_IsOnShelf",
table: "Gifts",
column: "IsOnShelf");
migrationBuilder.CreateIndex(
name: "IX_Gifts_SortOrder",
table: "Gifts",
column: "SortOrder");
migrationBuilder.CreateIndex(
name: "IX_Gifts_Type",
table: "Gifts",
column: "Type");
migrationBuilder.CreateIndex(
name: "IX_MarketingCodes_BatchNo",
table: "MarketingCodes",
column: "BatchNo");
migrationBuilder.CreateIndex(
name: "IX_MarketingCodes_Code",
table: "MarketingCodes",
column: "Code",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_MarketingCodes_IsUsed",
table: "MarketingCodes",
column: "IsUsed");
migrationBuilder.CreateIndex(
name: "IX_Members_Phone",
table: "Members",
column: "Phone",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Members_Status",
table: "Members",
column: "Status");
migrationBuilder.CreateIndex(
name: "IX_PointsRules_DateRange",
table: "PointsRules",
columns: new[] { "StartDate", "EndDate" });
migrationBuilder.CreateIndex(
name: "IX_PointsRules_IsActive",
table: "PointsRules",
column: "IsActive");
migrationBuilder.CreateIndex(
name: "IX_PointsRules_MemberLevelCode",
table: "PointsRules",
column: "MemberLevelCode");
migrationBuilder.CreateIndex(
name: "IX_PointsRules_ProductId",
table: "PointsRules",
column: "ProductId");
migrationBuilder.CreateIndex(
name: "IX_PointsTransactions_CreatedAt",
table: "PointsTransactions",
column: "CreatedAt");
migrationBuilder.CreateIndex(
name: "IX_PointsTransactions_MemberId",
table: "PointsTransactions",
column: "MemberId");
migrationBuilder.CreateIndex(
name: "IX_PointsTransactions_RelatedId",
table: "PointsTransactions",
column: "RelatedId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_PointsTransactions_Type",
table: "PointsTransactions",
column: "Type");
migrationBuilder.CreateIndex(
name: "IX_RedemptionOrders_CreatedAt",
table: "RedemptionOrders",
column: "CreatedAt");
migrationBuilder.CreateIndex(
name: "IX_RedemptionOrders_MemberId",
table: "RedemptionOrders",
column: "MemberId");
migrationBuilder.CreateIndex(
name: "IX_RedemptionOrders_OrderNo",
table: "RedemptionOrders",
column: "OrderNo",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_RedemptionOrders_Status",
table: "RedemptionOrders",
column: "Status");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Gifts");
migrationBuilder.DropTable(
name: "MarketingCodes");
migrationBuilder.DropTable(
name: "Members");
migrationBuilder.DropTable(
name: "PointsRules");
migrationBuilder.DropTable(
name: "PointsTransactions");
migrationBuilder.DropTable(
name: "RedemptionOrders");
}
}
}

View File

@ -0,0 +1,596 @@
// <auto-generated />
using System;
using Fengling.Backend.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Fengling.Backend.Infrastructure.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.GiftAggregate.Gift", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("礼品ID");
b.Property<int>("AvailableStock")
.HasColumnType("INTEGER")
.HasComment("可用库存");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT")
.HasComment("描述");
b.Property<string>("ImageUrl")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT")
.HasComment("图片URL");
b.Property<bool>("IsOnShelf")
.HasColumnType("INTEGER")
.HasComment("是否上架");
b.Property<int?>("LimitPerMember")
.HasColumnType("INTEGER")
.HasComment("每人限兑数量");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("礼品名称");
b.Property<int>("RequiredPoints")
.HasColumnType("INTEGER")
.HasComment("所需积分");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER")
.HasComment("排序");
b.Property<int>("TotalStock")
.HasColumnType("INTEGER")
.HasComment("总库存");
b.Property<int>("Type")
.HasColumnType("INTEGER")
.HasComment("礼品类型(1:实物,2:虚拟,3:自有产品)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
.HasComment("更新时间");
b.HasKey("Id");
b.HasIndex("IsOnShelf")
.HasDatabaseName("IX_Gifts_IsOnShelf");
b.HasIndex("SortOrder")
.HasDatabaseName("IX_Gifts_SortOrder");
b.HasIndex("Type")
.HasDatabaseName("IX_Gifts_Type");
b.ToTable("Gifts", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("营销码ID");
b.Property<string>("BatchNo")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasComment("批次号");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasComment("营销码");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<DateTime?>("ExpiryDate")
.HasColumnType("TEXT")
.HasComment("过期时间");
b.Property<bool>("IsUsed")
.HasColumnType("INTEGER")
.HasComment("是否已使用");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<DateTime?>("UsedAt")
.HasColumnType("TEXT")
.HasComment("使用时间");
b.Property<Guid?>("UsedByMemberId")
.HasColumnType("TEXT")
.HasComment("使用者会员ID");
b.HasKey("Id");
b.HasIndex("BatchNo")
.HasDatabaseName("IX_MarketingCodes_BatchNo");
b.HasIndex("Code")
.IsUnique()
.HasDatabaseName("IX_MarketingCodes_Code");
b.HasIndex("IsUsed")
.HasDatabaseName("IX_MarketingCodes_IsUsed");
b.ToTable("MarketingCodes", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MemberAggregate.Member", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("会员ID");
b.Property<int>("AvailablePoints")
.HasColumnType("INTEGER")
.HasComment("可用积分");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<string>("Nickname")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasComment("昵称");
b.Property<string>("Password")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("密码(已加密)");
b.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT")
.HasComment("手机号");
b.Property<DateTime>("RegisteredAt")
.HasColumnType("TEXT")
.HasComment("注册时间");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER")
.HasComment("状态(1:正常,2:禁用)");
b.Property<int>("TotalPoints")
.HasColumnType("INTEGER")
.HasComment("累计总积分");
b.HasKey("Id");
b.HasIndex("Phone")
.IsUnique()
.HasDatabaseName("IX_Members_Phone");
b.HasIndex("Status")
.HasDatabaseName("IX_Members_Status");
b.ToTable("Members", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate.PointsRule", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("积分规则ID");
b.Property<decimal>("BonusMultiplier")
.HasColumnType("TEXT")
.HasComment("奖励倍数");
b.Property<Guid?>("CategoryId")
.HasColumnType("TEXT")
.HasComment("品类ID");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<DateTime?>("EndDate")
.HasColumnType("TEXT")
.HasComment("生效结束时间");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER")
.HasComment("是否激活");
b.Property<string>("MemberLevelCode")
.HasMaxLength(20)
.HasColumnType("TEXT")
.HasComment("会员等级编码");
b.Property<int>("PointsValue")
.HasColumnType("INTEGER")
.HasComment("积分值");
b.Property<Guid?>("ProductId")
.HasColumnType("TEXT")
.HasComment("产品ID");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("RuleName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("规则名称");
b.Property<int>("RuleType")
.HasColumnType("INTEGER")
.HasComment("规则类型(1:产品,2:时间,3:会员等级)");
b.Property<DateTime>("StartDate")
.HasColumnType("TEXT")
.HasComment("生效开始时间");
b.HasKey("Id");
b.HasIndex("IsActive")
.HasDatabaseName("IX_PointsRules_IsActive");
b.HasIndex("MemberLevelCode")
.HasDatabaseName("IX_PointsRules_MemberLevelCode");
b.HasIndex("ProductId")
.HasDatabaseName("IX_PointsRules_ProductId");
b.HasIndex("StartDate", "EndDate")
.HasDatabaseName("IX_PointsRules_DateRange");
b.ToTable("PointsRules", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.PointsTransactionAggregate.PointsTransaction", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("积分流水ID");
b.Property<int>("Amount")
.HasColumnType("INTEGER")
.HasComment("积分数量");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<DateTime?>("ExpiryDate")
.HasColumnType("TEXT")
.HasComment("过期时间");
b.Property<Guid>("MemberId")
.HasColumnType("TEXT")
.HasComment("会员ID");
b.Property<string>("Reason")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasComment("原因描述");
b.Property<Guid>("RelatedId")
.HasColumnType("TEXT")
.HasComment("关联ID");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("Source")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("来源");
b.Property<int>("Type")
.HasColumnType("INTEGER")
.HasComment("交易类型(1:获得,2:消费,3:过期,4:退还)");
b.HasKey("Id");
b.HasIndex("CreatedAt")
.HasDatabaseName("IX_PointsTransactions_CreatedAt");
b.HasIndex("MemberId")
.HasDatabaseName("IX_PointsTransactions_MemberId");
b.HasIndex("RelatedId")
.IsUnique()
.HasDatabaseName("IX_PointsTransactions_RelatedId");
b.HasIndex("Type")
.HasDatabaseName("IX_PointsTransactions_Type");
b.ToTable("PointsTransactions", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.RedemptionOrder", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("兑换订单ID");
b.Property<string>("CancelReason")
.HasMaxLength(500)
.HasColumnType("TEXT")
.HasComment("取消原因");
b.Property<int>("ConsumedPoints")
.HasColumnType("INTEGER")
.HasComment("消耗积分");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<Guid>("GiftId")
.HasColumnType("TEXT")
.HasComment("礼品ID");
b.Property<string>("GiftName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("礼品名称");
b.Property<int>("GiftType")
.HasColumnType("INTEGER")
.HasComment("礼品类型");
b.Property<Guid>("MemberId")
.HasColumnType("TEXT")
.HasComment("会员ID");
b.Property<string>("OrderNo")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasComment("订单号");
b.Property<int>("Quantity")
.HasColumnType("INTEGER")
.HasComment("数量");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER")
.HasComment("订单状态(1:待处理,2:已发货,3:已送达,4:已完成,5:已取消)");
b.Property<string>("TrackingNo")
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("物流单号");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
.HasComment("更新时间");
b.HasKey("Id");
b.HasIndex("CreatedAt")
.HasDatabaseName("IX_RedemptionOrders_CreatedAt");
b.HasIndex("MemberId")
.HasDatabaseName("IX_RedemptionOrders_MemberId");
b.HasIndex("OrderNo")
.IsUnique()
.HasDatabaseName("IX_RedemptionOrders_OrderNo");
b.HasIndex("Status")
.HasDatabaseName("IX_RedemptionOrders_Status");
b.ToTable("RedemptionOrders", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b =>
{
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.ProductInfo", "ProductInfo", b1 =>
{
b1.Property<Guid>("MarketingCodeId")
.HasColumnType("TEXT");
b1.Property<Guid?>("CategoryId")
.HasColumnType("TEXT")
.HasColumnName("CategoryId")
.HasComment("品类ID");
b1.Property<string>("CategoryName")
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("CategoryName")
.HasComment("品类名称");
b1.Property<Guid>("ProductId")
.HasColumnType("TEXT")
.HasColumnName("ProductId")
.HasComment("产品ID");
b1.Property<string>("ProductName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("ProductName")
.HasComment("产品名称");
b1.HasKey("MarketingCodeId");
b1.ToTable("MarketingCodes");
b1.WithOwner()
.HasForeignKey("MarketingCodeId");
});
b.Navigation("ProductInfo")
.IsRequired();
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MemberAggregate.Member", b =>
{
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MemberAggregate.MemberLevel", "Level", b1 =>
{
b1.Property<Guid>("MemberId")
.HasColumnType("TEXT");
b1.Property<decimal>("BonusRate")
.HasColumnType("TEXT")
.HasColumnName("BonusRate")
.HasComment("积分奖励倍率");
b1.Property<string>("LevelCode")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT")
.HasColumnName("LevelCode")
.HasComment("等级编码");
b1.Property<string>("LevelName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("LevelName")
.HasComment("等级名称");
b1.Property<int>("RequiredPoints")
.HasColumnType("INTEGER")
.HasColumnName("RequiredPoints")
.HasComment("所需积分");
b1.HasKey("MemberId");
b1.ToTable("Members");
b1.WithOwner()
.HasForeignKey("MemberId");
});
b.Navigation("Level")
.IsRequired();
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.RedemptionOrder", b =>
{
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.Address", "ShippingAddress", b1 =>
{
b1.Property<Guid>("RedemptionOrderId")
.HasColumnType("TEXT");
b1.Property<string>("City")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("City")
.HasComment("市");
b1.Property<string>("DetailAddress")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("DetailAddress")
.HasComment("详细地址");
b1.Property<string>("District")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("District")
.HasComment("区/县");
b1.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT")
.HasColumnName("ReceiverPhone")
.HasComment("联系电话");
b1.Property<string>("Province")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("Province")
.HasComment("省");
b1.Property<string>("ReceiverName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("ReceiverName")
.HasComment("收货人姓名");
b1.HasKey("RedemptionOrderId");
b1.ToTable("RedemptionOrders");
b1.WithOwner()
.HasForeignKey("RedemptionOrderId");
});
b.Navigation("ShippingAddress");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,38 @@
using Fengling.Backend.Domain.AggregatesModel.GiftAggregate;
namespace Fengling.Backend.Infrastructure.Repositories;
public interface IGiftRepository : IRepository<Gift, GiftId>
{
Task<Gift?> GetByIdAsync(GiftId giftId, CancellationToken cancellationToken = default);
Task<List<Gift>> GetOnShelfGiftsAsync(CancellationToken cancellationToken = default);
Task<List<Gift>> GetByTypeAsync(GiftType type, CancellationToken cancellationToken = default);
}
public class GiftRepository(ApplicationDbContext context)
: RepositoryBase<Gift, GiftId, ApplicationDbContext>(context), IGiftRepository
{
public async Task<Gift?> GetByIdAsync(GiftId giftId, CancellationToken cancellationToken = default)
{
return await DbContext.Gifts
.FirstOrDefaultAsync(x => x.Id == giftId, cancellationToken);
}
public async Task<List<Gift>> GetOnShelfGiftsAsync(CancellationToken cancellationToken = default)
{
return await DbContext.Gifts
.Where(x => x.IsOnShelf)
.OrderBy(x => x.SortOrder)
.ThenByDescending(x => x.CreatedAt)
.ToListAsync(cancellationToken);
}
public async Task<List<Gift>> GetByTypeAsync(GiftType type, CancellationToken cancellationToken = default)
{
return await DbContext.Gifts
.Where(x => x.Type == type && x.IsOnShelf)
.OrderBy(x => x.SortOrder)
.ThenByDescending(x => x.CreatedAt)
.ToListAsync(cancellationToken);
}
}

View File

@ -0,0 +1,38 @@
using Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate;
namespace Fengling.Backend.Infrastructure.Repositories;
/// <summary>
/// 营销码仓储接口
/// </summary>
public interface IMarketingCodeRepository : IRepository<MarketingCode, MarketingCodeId>
{
/// <summary>
/// 根据营销码查询
/// </summary>
Task<MarketingCode?> GetByCodeAsync(string code, CancellationToken cancellationToken = default);
/// <summary>
/// 检查营销码是否存在
/// </summary>
Task<bool> CodeExistsAsync(string code, CancellationToken cancellationToken = default);
}
/// <summary>
/// 营销码仓储实现
/// </summary>
public class MarketingCodeRepository(ApplicationDbContext context)
: RepositoryBase<MarketingCode, MarketingCodeId, ApplicationDbContext>(context), IMarketingCodeRepository
{
public async Task<MarketingCode?> GetByCodeAsync(string code, CancellationToken cancellationToken = default)
{
return await DbContext.MarketingCodes
.FirstOrDefaultAsync(x => x.Code == code, cancellationToken);
}
public async Task<bool> CodeExistsAsync(string code, CancellationToken cancellationToken = default)
{
return await DbContext.MarketingCodes
.AnyAsync(x => x.Code == code, cancellationToken);
}
}

View File

@ -0,0 +1,38 @@
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
namespace Fengling.Backend.Infrastructure.Repositories;
/// <summary>
/// 会员仓储接口
/// </summary>
public interface IMemberRepository : IRepository<Member, MemberId>
{
/// <summary>
/// 根据手机号查询会员
/// </summary>
Task<Member?> GetByPhoneAsync(string phone, CancellationToken cancellationToken = default);
/// <summary>
/// 检查手机号是否存在
/// </summary>
Task<bool> PhoneExistsAsync(string phone, CancellationToken cancellationToken = default);
}
/// <summary>
/// 会员仓储实现
/// </summary>
public class MemberRepository(ApplicationDbContext context)
: RepositoryBase<Member, MemberId, ApplicationDbContext>(context), IMemberRepository
{
public async Task<Member?> GetByPhoneAsync(string phone, CancellationToken cancellationToken = default)
{
return await DbContext.Members
.FirstOrDefaultAsync(x => x.Phone == phone, cancellationToken);
}
public async Task<bool> PhoneExistsAsync(string phone, CancellationToken cancellationToken = default)
{
return await DbContext.Members
.AnyAsync(x => x.Phone == phone, cancellationToken);
}
}

View File

@ -0,0 +1,87 @@
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
namespace Fengling.Backend.Infrastructure.Repositories;
/// <summary>
/// 积分规则仓储接口
/// </summary>
public interface IPointsRuleRepository : IRepository<PointsRule, PointsRuleId>
{
/// <summary>
/// 获取有效的积分规则(匹配产品、会员等级、时间)
/// </summary>
Task<List<PointsRule>> GetEffectiveRulesAsync(
Guid productId,
Guid? categoryId,
string memberLevelCode,
DateTime scanDate,
CancellationToken cancellationToken = default);
/// <summary>
/// 检查是否存在冲突的规则(同维度)
/// </summary>
Task<bool> HasConflictingRuleAsync(
Guid? productId,
Guid? categoryId,
string? memberLevelCode,
DateTime startDate,
DateTime? endDate,
CancellationToken cancellationToken = default);
}
/// <summary>
/// 积分规则仓储实现
/// </summary>
public class PointsRuleRepository(ApplicationDbContext context)
: RepositoryBase<PointsRule, PointsRuleId, ApplicationDbContext>(context), IPointsRuleRepository
{
public async Task<List<PointsRule>> GetEffectiveRulesAsync(
Guid productId,
Guid? categoryId,
string memberLevelCode,
DateTime scanDate,
CancellationToken cancellationToken = default)
{
return await DbContext.PointsRules
.Where(x => x.IsActive)
.Where(x => x.StartDate <= scanDate && (x.EndDate == null || x.EndDate >= scanDate))
.Where(x => x.ProductId == null || x.ProductId == productId)
.Where(x => x.CategoryId == null || x.CategoryId == categoryId)
.Where(x => x.MemberLevelCode == null || x.MemberLevelCode == memberLevelCode)
.ToListAsync(cancellationToken);
}
public async Task<bool> HasConflictingRuleAsync(
Guid? productId,
Guid? categoryId,
string? memberLevelCode,
DateTime startDate,
DateTime? endDate,
CancellationToken cancellationToken = default)
{
var query = DbContext.PointsRules.AsQueryable();
// 检查维度是否完全一致
if (productId.HasValue)
query = query.Where(x => x.ProductId == productId);
else
query = query.Where(x => x.ProductId == null);
if (categoryId.HasValue)
query = query.Where(x => x.CategoryId == categoryId);
else
query = query.Where(x => x.CategoryId == null);
if (!string.IsNullOrWhiteSpace(memberLevelCode))
query = query.Where(x => x.MemberLevelCode == memberLevelCode);
else
query = query.Where(x => x.MemberLevelCode == null);
// 检查时间重叠
query = query.Where(x =>
x.StartDate <= (endDate ?? DateTime.MaxValue) &&
(x.EndDate == null || x.EndDate >= startDate));
return await query.AnyAsync(cancellationToken);
}
}

View File

@ -0,0 +1,41 @@
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
using Fengling.Backend.Domain.AggregatesModel.PointsTransactionAggregate;
namespace Fengling.Backend.Infrastructure.Repositories;
/// <summary>
/// 积分交易仓储接口
/// </summary>
public interface IPointsTransactionRepository : IRepository<PointsTransaction, PointsTransactionId>
{
/// <summary>
/// 根据会员ID查询积分交易记录
/// </summary>
Task<List<PointsTransaction>> GetByMemberIdAsync(MemberId memberId, CancellationToken cancellationToken = default);
/// <summary>
/// 检查关联ID是否已存在交易记录(用于幂等性)
/// </summary>
Task<bool> ExistsByRelatedIdAsync(Guid relatedId, CancellationToken cancellationToken = default);
}
/// <summary>
/// 积分交易仓储实现
/// </summary>
public class PointsTransactionRepository(ApplicationDbContext context)
: RepositoryBase<PointsTransaction, PointsTransactionId, ApplicationDbContext>(context), IPointsTransactionRepository
{
public async Task<List<PointsTransaction>> GetByMemberIdAsync(MemberId memberId, CancellationToken cancellationToken = default)
{
return await DbContext.PointsTransactions
.Where(x => x.MemberId == memberId)
.OrderByDescending(x => x.CreatedAt)
.ToListAsync(cancellationToken);
}
public async Task<bool> ExistsByRelatedIdAsync(Guid relatedId, CancellationToken cancellationToken = default)
{
return await DbContext.PointsTransactions
.AnyAsync(x => x.RelatedId == relatedId, cancellationToken);
}
}

View File

@ -0,0 +1,44 @@
using Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate;
namespace Fengling.Backend.Infrastructure.Repositories;
public interface IRedemptionOrderRepository : IRepository<RedemptionOrder, RedemptionOrderId>
{
Task<RedemptionOrder?> GetByIdAsync(RedemptionOrderId orderId, CancellationToken cancellationToken = default);
Task<RedemptionOrder?> GetByOrderNoAsync(string orderNo, CancellationToken cancellationToken = default);
Task<List<RedemptionOrder>> GetByMemberIdAsync(Guid memberId, CancellationToken cancellationToken = default);
Task<int> GetMemberRedemptionCountAsync(Guid memberId, Guid giftId, CancellationToken cancellationToken = default);
}
public class RedemptionOrderRepository(ApplicationDbContext context)
: RepositoryBase<RedemptionOrder, RedemptionOrderId, ApplicationDbContext>(context), IRedemptionOrderRepository
{
public async Task<RedemptionOrder?> GetByIdAsync(RedemptionOrderId orderId, CancellationToken cancellationToken = default)
{
return await DbContext.RedemptionOrders
.FirstOrDefaultAsync(x => x.Id == orderId, cancellationToken);
}
public async Task<RedemptionOrder?> GetByOrderNoAsync(string orderNo, CancellationToken cancellationToken = default)
{
return await DbContext.RedemptionOrders
.FirstOrDefaultAsync(x => x.OrderNo == orderNo, cancellationToken);
}
public async Task<List<RedemptionOrder>> GetByMemberIdAsync(Guid memberId, CancellationToken cancellationToken = default)
{
return await DbContext.RedemptionOrders
.Where(x => x.MemberId == memberId)
.OrderByDescending(x => x.CreatedAt)
.ToListAsync(cancellationToken);
}
public async Task<int> GetMemberRedemptionCountAsync(Guid memberId, Guid giftId, CancellationToken cancellationToken = default)
{
return await DbContext.RedemptionOrders
.Where(x => x.MemberId == memberId
&& x.GiftId == giftId
&& x.Status != RedemptionOrderStatus.Cancelled)
.SumAsync(x => x.Quantity, cancellationToken);
}
}

View File

@ -0,0 +1,176 @@
using Fengling.Backend.Domain.AggregatesModel.GiftAggregate;
using Fengling.Backend.Infrastructure.Repositories;
namespace Fengling.Backend.Web.Application.Commands.Gifts;
/// <summary>
/// 创建礼品命令
/// </summary>
public record CreateGiftCommand(
string Name,
int Type,
string Description,
string ImageUrl,
int RequiredPoints,
int TotalStock,
int? LimitPerMember = null) : ICommand<GiftId>;
public class CreateGiftCommandValidator : AbstractValidator<CreateGiftCommand>
{
public CreateGiftCommandValidator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
RuleFor(x => x.Type).IsInEnum();
RuleFor(x => x.Description).NotEmpty().MaximumLength(500);
RuleFor(x => x.ImageUrl).NotEmpty().MaximumLength(500);
RuleFor(x => x.RequiredPoints).GreaterThan(0);
RuleFor(x => x.TotalStock).GreaterThanOrEqualTo(0);
}
}
public class CreateGiftCommandHandler(IGiftRepository giftRepository)
: ICommandHandler<CreateGiftCommand, GiftId>
{
public async Task<GiftId> Handle(CreateGiftCommand request, CancellationToken cancellationToken)
{
var giftType = (GiftType)request.Type;
var gift = new Gift(
request.Name,
giftType,
request.Description,
request.ImageUrl,
request.RequiredPoints,
request.TotalStock,
request.LimitPerMember);
await giftRepository.AddAsync(gift, cancellationToken);
return gift.Id;
}
}
/// <summary>
/// 更新礼品命令
/// </summary>
public record UpdateGiftCommand(
Guid GiftId,
string? Name = null,
string? Description = null,
string? ImageUrl = null,
int? RequiredPoints = null,
int? LimitPerMember = null) : ICommand<ResponseData>;
public class UpdateGiftCommandValidator : AbstractValidator<UpdateGiftCommand>
{
public UpdateGiftCommandValidator()
{
RuleFor(x => x.GiftId).NotEmpty();
RuleFor(x => x.RequiredPoints).GreaterThan(0).When(x => x.RequiredPoints.HasValue);
RuleFor(x => x.Name).MaximumLength(100);
RuleFor(x => x.Description).MaximumLength(500);
RuleFor(x => x.ImageUrl).MaximumLength(500);
}
}
public class UpdateGiftCommandHandler(IGiftRepository giftRepository)
: ICommandHandler<UpdateGiftCommand, ResponseData>
{
public async Task<ResponseData> Handle(UpdateGiftCommand request, CancellationToken cancellationToken)
{
var giftId = new GiftId(request.GiftId);
var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken);
if (gift == null)
throw new KnownException("礼品不存在");
gift.Update(
request.Name,
request.Description,
request.ImageUrl,
request.RequiredPoints,
request.LimitPerMember);
await giftRepository.UpdateAsync(gift, cancellationToken);
return new ResponseData();
}
}
/// <summary>
/// 上架礼品命令
/// </summary>
public record PutOnShelfCommand(Guid GiftId) : ICommand<ResponseData>;
public class PutOnShelfCommandHandler(IGiftRepository giftRepository)
: ICommandHandler<PutOnShelfCommand, ResponseData>
{
public async Task<ResponseData> Handle(PutOnShelfCommand request, CancellationToken cancellationToken)
{
var giftId = new GiftId(request.GiftId);
var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken);
if (gift == null)
throw new KnownException("礼品不存在");
gift.PutOnShelf();
await giftRepository.UpdateAsync(gift, cancellationToken);
return new ResponseData();
}
}
/// <summary>
/// 下架礼品命令
/// </summary>
public record PutOffShelfCommand(Guid GiftId) : ICommand<ResponseData>;
public class PutOffShelfCommandHandler(IGiftRepository giftRepository)
: ICommandHandler<PutOffShelfCommand, ResponseData>
{
public async Task<ResponseData> Handle(PutOffShelfCommand request, CancellationToken cancellationToken)
{
var giftId = new GiftId(request.GiftId);
var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken);
if (gift == null)
throw new KnownException("礼品不存在");
gift.PutOffShelf();
await giftRepository.UpdateAsync(gift, cancellationToken);
return new ResponseData();
}
}
/// <summary>
/// 增加库存命令
/// </summary>
public record AddGiftStockCommand(Guid GiftId, int Quantity) : ICommand<ResponseData>;
public class AddGiftStockCommandValidator : AbstractValidator<AddGiftStockCommand>
{
public AddGiftStockCommandValidator()
{
RuleFor(x => x.GiftId).NotEmpty();
RuleFor(x => x.Quantity).GreaterThan(0);
}
}
public class AddGiftStockCommandHandler(IGiftRepository giftRepository)
: ICommandHandler<AddGiftStockCommand, ResponseData>
{
public async Task<ResponseData> Handle(AddGiftStockCommand request, CancellationToken cancellationToken)
{
var giftId = new GiftId(request.GiftId);
var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken);
if (gift == null)
throw new KnownException("礼品不存在");
gift.AddStock(request.Quantity);
await giftRepository.UpdateAsync(gift, cancellationToken);
return new ResponseData();
}
}

View File

@ -0,0 +1,107 @@
using Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate;
using Fengling.Backend.Infrastructure.Repositories;
namespace Fengling.Backend.Web.Application.Commands.MarketingCodes;
/// <summary>
/// 生成营销码命令
/// </summary>
public record GenerateMarketingCodesCommand(
string BatchNo,
Guid ProductId,
string ProductName,
int Quantity,
DateTime? ExpiryDate = null) : ICommand<GenerateMarketingCodesResponse>;
/// <summary>
/// 生成营销码响应
/// </summary>
public record GenerateMarketingCodesResponse(string BatchNo, int Count, List<string> Codes);
/// <summary>
/// 生成营销码命令验证器
/// </summary>
public class GenerateMarketingCodesCommandValidator : AbstractValidator<GenerateMarketingCodesCommand>
{
public GenerateMarketingCodesCommandValidator()
{
RuleFor(x => x.BatchNo)
.NotEmpty().WithMessage("批次号不能为空")
.MaximumLength(50).WithMessage("批次号最多50个字符");
RuleFor(x => x.ProductId)
.NotEmpty().WithMessage("产品ID不能为空");
RuleFor(x => x.ProductName)
.NotEmpty().WithMessage("产品名称不能为空")
.MaximumLength(100).WithMessage("产品名称最多100个字符");
RuleFor(x => x.Quantity)
.GreaterThan(0).WithMessage("数量必须大于0")
.LessThanOrEqualTo(10000).WithMessage("单次生成数量不能超过10000");
}
}
/// <summary>
/// 生成营销码命令处理器
/// </summary>
public class GenerateMarketingCodesCommandHandler(
IMarketingCodeRepository marketingCodeRepository,
ILogger<GenerateMarketingCodesCommandHandler> logger)
: ICommandHandler<GenerateMarketingCodesCommand, GenerateMarketingCodesResponse>
{
public async Task<GenerateMarketingCodesResponse> Handle(
GenerateMarketingCodesCommand command,
CancellationToken cancellationToken)
{
logger.LogInformation("开始生成营销码,批次:{BatchNo},数量:{Quantity}",
command.BatchNo, command.Quantity);
var codes = new List<string>();
var marketingCodes = new List<MarketingCode>();
// 生成营销码
for (int i = 0; i < command.Quantity; i++)
{
var code = GenerateUniqueCode(command.BatchNo, i);
// 检查码是否存在(虽然理论上不会重复)
if (await marketingCodeRepository.CodeExistsAsync(code, cancellationToken))
{
logger.LogWarning("营销码已存在,跳过:{Code}", code);
continue;
}
var marketingCode = new MarketingCode(
code,
command.ProductId,
command.ProductName,
command.BatchNo,
command.ExpiryDate);
marketingCodes.Add(marketingCode);
codes.Add(code);
}
// 批量保存
foreach (var marketingCode in marketingCodes)
{
await marketingCodeRepository.AddAsync(marketingCode, cancellationToken);
}
logger.LogInformation("营销码生成成功,批次:{BatchNo},实际生成数量:{Count}",
command.BatchNo, codes.Count);
return new GenerateMarketingCodesResponse(command.BatchNo, codes.Count, codes);
}
/// <summary>
/// 生成唯一码(批次号+序号+时间戳)
/// </summary>
private static string GenerateUniqueCode(string batchNo, int sequence)
{
var timestamp = DateTime.UtcNow.ToString("yyyyMMddHHmmss");
var random = Random.Shared.Next(1000, 9999);
return $"{batchNo}-{sequence + 1:D6}-{timestamp}-{random}";
}
}

View File

@ -0,0 +1,74 @@
using Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate;
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
using Fengling.Backend.Infrastructure.Repositories;
namespace Fengling.Backend.Web.Application.Commands.MarketingCodes;
/// <summary>
/// 使用营销码命令(扫码)
/// </summary>
public record UseMarketingCodeCommand(string Code, MemberId MemberId) : ICommand<UseMarketingCodeResponse>;
/// <summary>
/// 使用营销码响应
/// </summary>
public record UseMarketingCodeResponse(
MarketingCodeId MarketingCodeId,
string ProductName,
int EarnedPoints,
string Message);
/// <summary>
/// 使用营销码命令验证器
/// </summary>
public class UseMarketingCodeCommandValidator : AbstractValidator<UseMarketingCodeCommand>
{
public UseMarketingCodeCommandValidator()
{
RuleFor(x => x.Code)
.NotEmpty().WithMessage("营销码不能为空")
.MaximumLength(50).WithMessage("营销码格式不正确");
RuleFor(x => x.MemberId)
.NotEmpty().WithMessage("会员ID不能为空");
}
}
/// <summary>
/// 使用营销码命令处理器
/// </summary>
public class UseMarketingCodeCommandHandler(
IMarketingCodeRepository marketingCodeRepository,
IMemberRepository memberRepository)
: ICommandHandler<UseMarketingCodeCommand, UseMarketingCodeResponse>
{
public async Task<UseMarketingCodeResponse> Handle(
UseMarketingCodeCommand command,
CancellationToken cancellationToken)
{
// 1. 查询营销码
var marketingCode = await marketingCodeRepository.GetByCodeAsync(command.Code, cancellationToken)
?? throw new KnownException("营销码不存在");
// 2. 查询会员
var member = await memberRepository.GetAsync(command.MemberId, cancellationToken)
?? throw new KnownException("会员不存在");
// 3. 检查会员状态
if (member.Status == MemberStatus.Disabled)
throw new KnownException("该账号已被禁用,无法获取积分");
// 4. 标记营销码为已使用(会触发MarketingCodeUsedDomainEvent)
marketingCode.MarkAsUsed(command.MemberId.Id);
// 5. 更新营销码
await marketingCodeRepository.UpdateAsync(marketingCode, cancellationToken);
// 注意:积分发放由领域事件处理器异步处理(MarketingCodeUsedDomainEventHandler)
return new UseMarketingCodeResponse(
marketingCode.Id,
marketingCode.ProductInfo.ProductName,
0, // 实际积分由事件处理器计算
"扫码成功,积分正在发放中...");
}
}

View File

@ -0,0 +1,69 @@
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
using Fengling.Backend.Infrastructure.Repositories;
namespace Fengling.Backend.Web.Application.Commands.Members;
/// <summary>
/// 会员登录命令
/// </summary>
public record LoginMemberCommand(string Phone, string Password) : ICommand<LoginMemberResponse>;
/// <summary>
/// 登录响应
/// </summary>
public record LoginMemberResponse(MemberId MemberId, string Token);
/// <summary>
/// 会员登录命令验证器
/// </summary>
public class LoginMemberCommandValidator : AbstractValidator<LoginMemberCommand>
{
public LoginMemberCommandValidator()
{
RuleFor(x => x.Phone)
.NotEmpty().WithMessage("手机号不能为空");
RuleFor(x => x.Password)
.NotEmpty().WithMessage("密码不能为空");
}
}
/// <summary>
/// 会员登录命令处理器
/// </summary>
public class LoginMemberCommandHandler(
IMemberRepository memberRepository)
: ICommandHandler<LoginMemberCommand, LoginMemberResponse>
{
public async Task<LoginMemberResponse> Handle(LoginMemberCommand command, CancellationToken cancellationToken)
{
// 查询会员
var member = await memberRepository.GetByPhoneAsync(command.Phone, cancellationToken)
?? throw new KnownException("手机号或密码错误");
// 验证密码
var hashedPassword = HashPassword(command.Password);
if (member.Password != hashedPassword)
throw new KnownException("手机号或密码错误");
// 检查状态
if (member.Status == MemberStatus.Disabled)
throw new KnownException("该账号已被禁用");
// 生成Token(这里简化处理)
var token = GenerateToken(member.Id);
return new LoginMemberResponse(member.Id, token);
}
private static string HashPassword(string password)
{
return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(password));
}
private static string GenerateToken(MemberId memberId)
{
// TODO: 实际项目中应使用JWT
return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes($"Member:{memberId}:{DateTime.UtcNow:O}"));
}
}

View File

@ -0,0 +1,62 @@
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
using Fengling.Backend.Infrastructure.Repositories;
namespace Fengling.Backend.Web.Application.Commands.Members;
/// <summary>
/// 注册会员命令
/// </summary>
public record RegisterMemberCommand(string Phone, string Password, string? Nickname = null) : ICommand<MemberId>;
/// <summary>
/// 注册会员命令验证器
/// </summary>
public class RegisterMemberCommandValidator : AbstractValidator<RegisterMemberCommand>
{
public RegisterMemberCommandValidator()
{
RuleFor(x => x.Phone)
.NotEmpty().WithMessage("手机号不能为空")
.Matches(@"^1[3-9]\d{9}$").WithMessage("手机号格式不正确");
RuleFor(x => x.Password)
.NotEmpty().WithMessage("密码不能为空")
.MinimumLength(6).WithMessage("密码至少6位")
.MaximumLength(20).WithMessage("密码最多20位");
RuleFor(x => x.Nickname)
.MaximumLength(50).WithMessage("昵称最多50个字符");
}
}
/// <summary>
/// 注册会员命令处理器
/// </summary>
public class RegisterMemberCommandHandler(
IMemberRepository memberRepository)
: ICommandHandler<RegisterMemberCommand, MemberId>
{
public async Task<MemberId> Handle(RegisterMemberCommand command, CancellationToken cancellationToken)
{
// 检查手机号是否已存在
if (await memberRepository.PhoneExistsAsync(command.Phone, cancellationToken))
throw new KnownException("该手机号已注册");
// 密码加密(这里简化处理,实际应使用BCrypt等)
var hashedPassword = HashPassword(command.Password);
// 创建会员
var member = new Member(command.Phone, hashedPassword, command.Nickname);
// 保存
await memberRepository.AddAsync(member, cancellationToken);
return member.Id;
}
private static string HashPassword(string password)
{
// TODO: 实际项目中应使用BCrypt.Net或AspNetCore.Identity的PasswordHasher
return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(password));
}
}

View File

@ -0,0 +1,79 @@
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
using Fengling.Backend.Infrastructure.Repositories;
namespace Fengling.Backend.Web.Application.Commands.PointsRules;
/// <summary>
/// 创建积分规则命令
/// </summary>
public record CreatePointsRuleCommand(
string RuleName,
PointsRuleType RuleType,
int PointsValue,
DateTime StartDate,
DateTime? EndDate = null,
Guid? ProductId = null,
Guid? CategoryId = null,
string? MemberLevelCode = null,
decimal BonusMultiplier = 1.0m) : ICommand<PointsRuleId>;
/// <summary>
/// 创建积分规则命令验证器
/// </summary>
public class CreatePointsRuleCommandValidator : AbstractValidator<CreatePointsRuleCommand>
{
public CreatePointsRuleCommandValidator()
{
RuleFor(x => x.RuleName)
.NotEmpty().WithMessage("规则名称不能为空")
.MaximumLength(100).WithMessage("规则名称最多100个字符");
RuleFor(x => x.PointsValue)
.GreaterThan(0).WithMessage("积分值必须大于0");
RuleFor(x => x.BonusMultiplier)
.GreaterThan(0).WithMessage("奖励倍数必须大于0");
RuleFor(x => x.StartDate)
.NotEmpty().WithMessage("生效开始时间不能为空");
}
}
/// <summary>
/// 创建积分规则命令处理器
/// </summary>
public class CreatePointsRuleCommandHandler(
IPointsRuleRepository pointsRuleRepository)
: ICommandHandler<CreatePointsRuleCommand, PointsRuleId>
{
public async Task<PointsRuleId> Handle(CreatePointsRuleCommand command, CancellationToken cancellationToken)
{
// 检查是否存在冲突的规则(同维度)
if (await pointsRuleRepository.HasConflictingRuleAsync(
command.ProductId,
command.CategoryId,
command.MemberLevelCode,
command.StartDate,
command.EndDate,
cancellationToken))
{
throw new KnownException("存在冲突的积分规则,同一维度和时间范围内不允许重复规则");
}
// 创建规则
var rule = new PointsRule(
command.RuleName,
command.RuleType,
command.PointsValue,
command.StartDate,
command.EndDate,
command.ProductId,
command.CategoryId,
command.MemberLevelCode,
command.BonusMultiplier);
await pointsRuleRepository.AddAsync(rule, cancellationToken);
return rule.Id;
}
}

View File

@ -0,0 +1,173 @@
using Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate;
using Fengling.Backend.Domain.AggregatesModel.GiftAggregate;
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
using Fengling.Backend.Infrastructure.Repositories;
namespace Fengling.Backend.Web.Application.Commands.RedemptionOrders;
/// <summary>
/// 创建兑换订单命令
/// </summary>
public record CreateRedemptionOrderCommand(
Guid MemberId,
Guid GiftId,
int Quantity,
AddressDto? ShippingAddress = null) : ICommand<RedemptionOrderId>;
public record AddressDto(
string ReceiverName,
string Phone,
string Province,
string City,
string District,
string DetailAddress);
public class CreateRedemptionOrderCommandHandler(
IRedemptionOrderRepository redemptionOrderRepository,
IGiftRepository giftRepository,
IMemberRepository memberRepository) : ICommandHandler<CreateRedemptionOrderCommand, RedemptionOrderId>
{
public async Task<RedemptionOrderId> Handle(CreateRedemptionOrderCommand request, CancellationToken cancellationToken)
{
// 1. 获取礼品信息
var giftId = new GiftId(request.GiftId);
var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken);
if (gift == null)
throw new KnownException("礼品不存在");
if (!gift.IsOnShelf)
throw new KnownException("礼品已下架");
// 2. 检查库存
if (gift.AvailableStock < request.Quantity)
throw new KnownException($"库存不足,当前可用:{gift.AvailableStock}");
// 3. 检查限兑数量
if (gift.LimitPerMember.HasValue)
{
var redeemedCount = await redemptionOrderRepository.GetMemberRedemptionCountAsync(
request.MemberId, request.GiftId, cancellationToken);
if (redeemedCount + request.Quantity > gift.LimitPerMember.Value)
throw new KnownException($"超出限兑数量,每人限兑{gift.LimitPerMember.Value}个,已兑换{redeemedCount}个");
}
// 4. 获取会员信息并检查积分
var memberId = new MemberId(request.MemberId);
var member = await memberRepository.GetAsync(memberId, cancellationToken);
if (member == null)
throw new KnownException("会员不存在");
var consumedPoints = gift.RequiredPoints * request.Quantity;
if (member.AvailablePoints < consumedPoints)
throw new KnownException($"积分不足,需要{consumedPoints}分,当前{member.AvailablePoints}分");
// 5. 扣减积分
member.ConsumePoints(consumedPoints, "兑换礼品", Guid.NewGuid());
await memberRepository.UpdateAsync(member, cancellationToken);
// 6. 构造收货地址
Address? shippingAddress = null;
if (request.ShippingAddress != null)
{
shippingAddress = new Address(
request.ShippingAddress.ReceiverName,
request.ShippingAddress.Phone,
request.ShippingAddress.Province,
request.ShippingAddress.City,
request.ShippingAddress.District,
request.ShippingAddress.DetailAddress);
}
else if (gift.Type == GiftType.Physical)
{
throw new KnownException("实物礼品必须提供收货地址");
}
// 7. 生成订单号
var orderNo = $"RO{DateTime.UtcNow:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}";
// 8. 创建订单
var order = new RedemptionOrder(
orderNo,
request.MemberId,
request.GiftId,
gift.Name,
(int)gift.Type,
request.Quantity,
consumedPoints,
shippingAddress);
await redemptionOrderRepository.AddAsync(order, cancellationToken);
return order.Id;
}
}
/// <summary>
/// 标记订单为已发货命令
/// </summary>
public record MarkOrderAsDispatchedCommand(Guid OrderId, string? TrackingNo = null) : ICommand<ResponseData>;
public class MarkOrderAsDispatchedCommandHandler(
IRedemptionOrderRepository redemptionOrderRepository) : ICommandHandler<MarkOrderAsDispatchedCommand, ResponseData>
{
public async Task<ResponseData> Handle(MarkOrderAsDispatchedCommand request, CancellationToken cancellationToken)
{
var orderId = new RedemptionOrderId(request.OrderId);
var order = await redemptionOrderRepository.GetByIdAsync(orderId, cancellationToken);
if (order == null)
throw new KnownException("订单不存在");
order.MarkAsDispatched(request.TrackingNo);
await redemptionOrderRepository.UpdateAsync(order, cancellationToken);
return new ResponseData();
}
}
/// <summary>
/// 完成订单命令
/// </summary>
public record CompleteOrderCommand(Guid OrderId) : ICommand<ResponseData>;
public class CompleteOrderCommandHandler(
IRedemptionOrderRepository redemptionOrderRepository) : ICommandHandler<CompleteOrderCommand, ResponseData>
{
public async Task<ResponseData> Handle(CompleteOrderCommand request, CancellationToken cancellationToken)
{
var orderId = new RedemptionOrderId(request.OrderId);
var order = await redemptionOrderRepository.GetByIdAsync(orderId, cancellationToken);
if (order == null)
throw new KnownException("订单不存在");
order.Complete();
await redemptionOrderRepository.UpdateAsync(order, cancellationToken);
return new ResponseData();
}
}
/// <summary>
/// 取消订单命令
/// </summary>
public record CancelOrderCommand(Guid OrderId, string Reason) : ICommand<ResponseData>;
public class CancelOrderCommandHandler(
IRedemptionOrderRepository redemptionOrderRepository) : ICommandHandler<CancelOrderCommand, ResponseData>
{
public async Task<ResponseData> Handle(CancelOrderCommand request, CancellationToken cancellationToken)
{
var orderId = new RedemptionOrderId(request.OrderId);
var order = await redemptionOrderRepository.GetByIdAsync(orderId, cancellationToken);
if (order == null)
throw new KnownException("订单不存在");
order.Cancel(request.Reason);
await redemptionOrderRepository.UpdateAsync(order, cancellationToken);
return new ResponseData();
}
}

View File

@ -0,0 +1,77 @@
using Fengling.Backend.Domain.DomainEvents;
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
using Fengling.Backend.Infrastructure.Repositories;
namespace Fengling.Backend.Web.Application.DomainEventHandlers;
/// <summary>
/// 营销码使用领域事件处理器-发放积分
/// 核心业务流程:扫码 → 匹配规则 → 计算积分 → 发放积分
/// 注意:积分交易记录由集成事件处理器异步创建,保证解耦和幂等性
/// </summary>
public class MarketingCodeUsedDomainEventHandlerForEarnPoints(
IMemberRepository memberRepository,
IPointsRuleRepository pointsRuleRepository,
ILogger<MarketingCodeUsedDomainEventHandlerForEarnPoints> logger)
: IDomainEventHandler<MarketingCodeUsedDomainEvent>
{
public async Task Handle(MarketingCodeUsedDomainEvent domainEvent, CancellationToken cancellationToken)
{
var marketingCode = domainEvent.MarketingCode;
var memberId = new MemberId(domainEvent.MemberId);
logger.LogInformation("开始处理营销码使用事件,营销码:{Code},会员:{MemberId}",
marketingCode.Code, memberId);
try
{
// 1. 查询会员
var member = await memberRepository.GetAsync(memberId, cancellationToken);
if (member == null)
{
logger.LogError("会员不存在,无法发放积分.会员ID:{MemberId}", memberId);
return;
}
// 2. 获取有效的积分规则
var rules = await pointsRuleRepository.GetEffectiveRulesAsync(
marketingCode.ProductInfo.ProductId,
marketingCode.ProductInfo.CategoryId,
member.Level.LevelCode,
DateTime.UtcNow,
cancellationToken);
if (rules.Count == 0)
{
logger.LogWarning("未找到匹配的积分规则,无法发放积分.产品:{ProductId},会员等级:{LevelCode}",
marketingCode.ProductInfo.ProductId, member.Level.LevelCode);
return;
}
// 3. 计算应得积分(取最高值或累加,这里简化为取最高值)
var totalPoints = rules.Max(r => r.CalculatePoints());
// 4. 计算积分过期时间(默认1年)
var expiryDate = DateTime.UtcNow.AddYears(1);
// 5. 发放积分(会触发PointsAddedDomainEvent → 转换为PointsEarnedIntegrationEvent → 创建积分交易记录)
member.AddPoints(
totalPoints,
$"扫码获得-{marketingCode.ProductInfo.ProductName}",
marketingCode.Id.Id,
expiryDate);
// 6. 更新会员
await memberRepository.UpdateAsync(member, cancellationToken);
logger.LogInformation("积分发放成功.会员:{MemberId},积分:{Points},营销码:{Code}",
memberId, totalPoints, marketingCode.Code);
}
catch (Exception ex)
{
logger.LogError(ex, "处理营销码使用事件失败.营销码:{Code},会员:{MemberId}",
marketingCode.Code, memberId);
throw;
}
}
}

View File

@ -0,0 +1,95 @@
using Fengling.Backend.Domain.DomainEvents;
using Fengling.Backend.Domain.AggregatesModel.GiftAggregate;
using Fengling.Backend.Infrastructure.Repositories;
namespace Fengling.Backend.Web.Application.DomainEventHandlers;
/// <summary>
/// 兑换订单创建领域事件处理器:预留库存
/// </summary>
public class RedemptionOrderCreatedDomainEventHandler(
IGiftRepository giftRepository,
ILogger<RedemptionOrderCreatedDomainEventHandler> logger)
: IDomainEventHandler<RedemptionOrderCreatedDomainEvent>
{
public async Task Handle(RedemptionOrderCreatedDomainEvent notification, CancellationToken cancellationToken)
{
logger.LogInformation("处理兑换订单创建事件:预留库存. 订单ID:{OrderId}, 礼品ID:{GiftId}, 数量:{Quantity}",
notification.Order.Id, notification.Order.GiftId, notification.Order.Quantity);
var giftId = new GiftId(notification.Order.GiftId);
var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken);
if (gift == null)
{
logger.LogError("礼品不存在,无法预留库存. 礼品ID:{GiftId}", notification.Order.GiftId);
throw new KnownException("礼品不存在");
}
gift.ReserveStock(notification.Order.Quantity);
await giftRepository.UpdateAsync(gift, cancellationToken);
logger.LogInformation("库存预留成功. 礼品ID:{GiftId}, 预留数量:{Quantity}, 剩余可用:{Available}",
notification.Order.GiftId, notification.Order.Quantity, gift.AvailableStock);
}
}
/// <summary>
/// 兑换订单取消领域事件处理器:释放库存和退还积分
/// </summary>
public class RedemptionOrderCancelledDomainEventHandler(
IGiftRepository giftRepository,
ILogger<RedemptionOrderCancelledDomainEventHandler> logger)
: IDomainEventHandler<RedemptionOrderCancelledDomainEvent>
{
public async Task Handle(RedemptionOrderCancelledDomainEvent notification, CancellationToken cancellationToken)
{
logger.LogInformation("处理兑换订单取消事件:释放库存和退还积分. 订单ID:{OrderId}, 礼品ID:{GiftId}",
notification.Order.Id, notification.Order.GiftId);
var giftId = new GiftId(notification.Order.GiftId);
var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken);
if (gift == null)
{
logger.LogError("礼品不存在,无法释放库存. 礼品ID:{GiftId}", notification.Order.GiftId);
return;
}
gift.ReleaseStock(notification.Order.Quantity);
await giftRepository.UpdateAsync(gift, cancellationToken);
logger.LogInformation("库存释放成功. 礼品ID:{GiftId}, 释放数量:{Quantity}, 当前可用:{Available}",
notification.Order.GiftId, notification.Order.Quantity, gift.AvailableStock);
}
}
/// <summary>
/// 兑换订单发货领域事件处理器:扣减总库存
/// </summary>
public class RedemptionOrderDispatchedDomainEventHandler(
IGiftRepository giftRepository,
ILogger<RedemptionOrderDispatchedDomainEventHandler> logger)
: IDomainEventHandler<RedemptionOrderDispatchedDomainEvent>
{
public async Task Handle(RedemptionOrderDispatchedDomainEvent notification, CancellationToken cancellationToken)
{
logger.LogInformation("处理兑换订单发货事件:扣减总库存. 订单ID:{OrderId}, 礼品ID:{GiftId}",
notification.Order.Id, notification.Order.GiftId);
var giftId = new GiftId(notification.Order.GiftId);
var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken);
if (gift == null)
{
logger.LogError("礼品不存在,无法扣减库存. 礼品ID:{GiftId}", notification.Order.GiftId);
return;
}
gift.DeductStock(notification.Order.Quantity);
await giftRepository.UpdateAsync(gift, cancellationToken);
logger.LogInformation("库存扣减成功. 礼品ID:{GiftId}, 扣减数量:{Quantity}",
notification.Order.GiftId, notification.Order.Quantity);
}
}

View File

@ -0,0 +1,14 @@
namespace Fengling.Backend.Web.Application.Hubs;
public interface IChatClient
{
Task ReceiveMessage(string user, string message);
}
public class ChatHub : Microsoft.AspNetCore.SignalR.Hub<IChatClient>
{
public async Task SendMessage(string user, string message)
{
await Clients.All.ReceiveMessage(user, message);
}
}

View File

@ -0,0 +1,69 @@
using Fengling.Backend.Domain.DomainEvents;
using Fengling.Backend.Domain.IntegrationEvents;
namespace Fengling.Backend.Web.Application.IntegrationEventConverters;
/// <summary>
/// 积分增加领域事件转集成事件转换器
/// </summary>
public class PointsAddedToPointsEarnedConverter
: IIntegrationEventConverter<PointsAddedDomainEvent, PointsEarnedIntegrationEvent>
{
public PointsEarnedIntegrationEvent Convert(PointsAddedDomainEvent domainEvent)
{
return new PointsEarnedIntegrationEvent(
domainEvent.MemberId.Id,
domainEvent.Amount,
domainEvent.Source,
domainEvent.Source, // Reason使用Source
domainEvent.RelatedId,
domainEvent.ExpiryDate);
}
}
/// <summary>
/// 积分消费领域事件转集成事件转换器
/// </summary>
public class PointsConsumedToIntegrationEventConverter
: IIntegrationEventConverter<PointsConsumedDomainEvent, PointsConsumedIntegrationEvent>
{
public PointsConsumedIntegrationEvent Convert(PointsConsumedDomainEvent domainEvent)
{
return new PointsConsumedIntegrationEvent(
domainEvent.MemberId.Id,
domainEvent.Amount,
domainEvent.Reason,
domainEvent.OrderId);
}
}
/// <summary>
/// 积分退还领域事件转集成事件转换器
/// </summary>
public class PointsRefundedToIntegrationEventConverter
: IIntegrationEventConverter<PointsRefundedDomainEvent, PointsRefundedIntegrationEvent>
{
public PointsRefundedIntegrationEvent Convert(PointsRefundedDomainEvent domainEvent)
{
return new PointsRefundedIntegrationEvent(
domainEvent.MemberId.Id,
domainEvent.Amount,
domainEvent.Reason,
domainEvent.OrderId);
}
}
/// <summary>
/// 积分过期领域事件转集成事件转换器
/// </summary>
public class PointsExpiredToIntegrationEventConverter
: IIntegrationEventConverter<PointsExpiredDomainEvent, PointsExpiredIntegrationEvent>
{
public PointsExpiredIntegrationEvent Convert(PointsExpiredDomainEvent domainEvent)
{
return new PointsExpiredIntegrationEvent(
domainEvent.MemberId.Id,
domainEvent.Amount,
Guid.NewGuid()); // BatchId生成新的
}
}

View File

@ -0,0 +1,202 @@
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
using Fengling.Backend.Domain.AggregatesModel.PointsTransactionAggregate;
using Fengling.Backend.Domain.IntegrationEvents;
using Fengling.Backend.Infrastructure;
namespace Fengling.Backend.Web.Application.IntegrationEventHandlers;
/// <summary>
/// 积分获得集成事件处理器
/// </summary>
public class PointsEarnedIntegrationEventHandler(
ApplicationDbContext dbContext,
ILogger<PointsEarnedIntegrationEventHandler> logger)
: IIntegrationEventHandler<PointsEarnedIntegrationEvent>
{
public async Task HandleAsync(PointsEarnedIntegrationEvent integrationEvent, CancellationToken cancellationToken)
{
logger.LogInformation("接收到积分获得集成事件. 会员:{MemberId}, 积分:{Amount}, 关联ID:{RelatedId}",
integrationEvent.MemberId, integrationEvent.Amount, integrationEvent.RelatedId);
try
{
// 幂等性检查:基于RelatedId判断是否已创建过交易记录
var exists = await dbContext.PointsTransactions
.AnyAsync(x => x.RelatedId == integrationEvent.RelatedId, cancellationToken);
if (exists)
{
logger.LogWarning("积分交易记录已存在,跳过处理. 关联ID:{RelatedId}", integrationEvent.RelatedId);
return;
}
// 创建积分交易记录
var memberId = new MemberId(integrationEvent.MemberId);
var transaction = PointsTransaction.CreateEarnTransaction(
memberId,
integrationEvent.Amount,
integrationEvent.Source,
integrationEvent.Reason,
integrationEvent.RelatedId,
integrationEvent.ExpiryDate);
await dbContext.PointsTransactions.AddAsync(transaction, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
logger.LogInformation("积分交易记录创建成功. 会员:{MemberId}, 积分:{Amount}, 交易ID:{TransactionId}",
integrationEvent.MemberId, integrationEvent.Amount, transaction.Id);
}
catch (Exception ex)
{
logger.LogError(ex, "处理积分获得集成事件失败. 会员:{MemberId}, 积分:{Amount}",
integrationEvent.MemberId, integrationEvent.Amount);
throw;
}
}
}
/// <summary>
/// 积分消费集成事件处理器
/// </summary>
public class PointsConsumedIntegrationEventHandler(
ApplicationDbContext dbContext,
ILogger<PointsConsumedIntegrationEventHandler> logger)
: IIntegrationEventHandler<PointsConsumedIntegrationEvent>
{
public async Task HandleAsync(PointsConsumedIntegrationEvent integrationEvent, CancellationToken cancellationToken)
{
logger.LogInformation("接收到积分消费集成事件. 会员:{MemberId}, 积分:{Amount}, 订单ID:{OrderId}",
integrationEvent.MemberId, integrationEvent.Amount, integrationEvent.OrderId);
try
{
// 幂等性检查:基于OrderId + Consume类型判断是否已创建过交易记录
var exists = await dbContext.PointsTransactions
.AnyAsync(x => x.RelatedId == integrationEvent.OrderId
&& x.Type == PointsTransactionType.Consume, cancellationToken);
if (exists)
{
logger.LogWarning("积分消费记录已存在,跳过处理. 订单ID:{OrderId}", integrationEvent.OrderId);
return;
}
// 创建积分交易记录
var memberId = new MemberId(integrationEvent.MemberId);
var transaction = PointsTransaction.CreateConsumeTransaction(
memberId,
integrationEvent.Amount,
integrationEvent.Reason,
integrationEvent.OrderId);
await dbContext.PointsTransactions.AddAsync(transaction, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
logger.LogInformation("积分消费记录创建成功. 会员:{MemberId}, 积分:{Amount}, 交易ID:{TransactionId}",
integrationEvent.MemberId, integrationEvent.Amount, transaction.Id);
}
catch (Exception ex)
{
logger.LogError(ex, "处理积分消费集成事件失败. 会员:{MemberId}, 积分:{Amount}",
integrationEvent.MemberId, integrationEvent.Amount);
throw;
}
}
}
/// <summary>
/// 积分退还集成事件处理器
/// </summary>
public class PointsRefundedIntegrationEventHandler(
ApplicationDbContext dbContext,
ILogger<PointsRefundedIntegrationEventHandler> logger)
: IIntegrationEventHandler<PointsRefundedIntegrationEvent>
{
public async Task HandleAsync(PointsRefundedIntegrationEvent integrationEvent, CancellationToken cancellationToken)
{
logger.LogInformation("接收到积分退还集成事件. 会员:{MemberId}, 积分:{Amount}, 订单ID:{OrderId}",
integrationEvent.MemberId, integrationEvent.Amount, integrationEvent.OrderId);
try
{
// 幂等性检查:基于OrderId + Refund类型判断是否已创建过交易记录
var exists = await dbContext.PointsTransactions
.AnyAsync(x => x.RelatedId == integrationEvent.OrderId
&& x.Type == PointsTransactionType.Refund, cancellationToken);
if (exists)
{
logger.LogWarning("积分退还记录已存在,跳过处理. 订单ID:{OrderId}", integrationEvent.OrderId);
return;
}
// 创建积分交易记录
var memberId = new MemberId(integrationEvent.MemberId);
var transaction = PointsTransaction.CreateRefundTransaction(
memberId,
integrationEvent.Amount,
integrationEvent.Reason,
integrationEvent.OrderId);
await dbContext.PointsTransactions.AddAsync(transaction, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
logger.LogInformation("积分退还记录创建成功. 会员:{MemberId}, 积分:{Amount}, 交易ID:{TransactionId}",
integrationEvent.MemberId, integrationEvent.Amount, transaction.Id);
}
catch (Exception ex)
{
logger.LogError(ex, "处理积分退还集成事件失败. 会员:{MemberId}, 积分:{Amount}",
integrationEvent.MemberId, integrationEvent.Amount);
throw;
}
}
}
/// <summary>
/// 积分过期集成事件处理器
/// </summary>
public class PointsExpiredIntegrationEventHandler(
ApplicationDbContext dbContext,
ILogger<PointsExpiredIntegrationEventHandler> logger)
: IIntegrationEventHandler<PointsExpiredIntegrationEvent>
{
public async Task HandleAsync(PointsExpiredIntegrationEvent integrationEvent, CancellationToken cancellationToken)
{
logger.LogInformation("接收到积分过期集成事件. 会员:{MemberId}, 积分:{Amount}, 批次ID:{BatchId}",
integrationEvent.MemberId, integrationEvent.Amount, integrationEvent.BatchId);
try
{
// 幂等性检查:基于BatchId判断是否已创建过交易记录
var exists = await dbContext.PointsTransactions
.AnyAsync(x => x.RelatedId == integrationEvent.BatchId
&& x.Type == PointsTransactionType.Expire, cancellationToken);
if (exists)
{
logger.LogWarning("积分过期记录已存在,跳过处理. 批次ID:{BatchId}", integrationEvent.BatchId);
return;
}
// 创建积分交易记录
var memberId = new MemberId(integrationEvent.MemberId);
var transaction = PointsTransaction.CreateExpireTransaction(
memberId,
integrationEvent.Amount,
integrationEvent.BatchId);
await dbContext.PointsTransactions.AddAsync(transaction, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
logger.LogInformation("积分过期记录创建成功. 会员:{MemberId}, 积分:{Amount}, 交易ID:{TransactionId}",
integrationEvent.MemberId, integrationEvent.Amount, transaction.Id);
}
catch (Exception ex)
{
logger.LogError(ex, "处理积分过期集成事件失败. 会员:{MemberId}, 积分:{Amount}",
integrationEvent.MemberId, integrationEvent.Amount);
throw;
}
}
}

View File

@ -0,0 +1,96 @@
using Fengling.Backend.Infrastructure;
namespace Fengling.Backend.Web.Application.Queries.Gifts;
/// <summary>
/// 礼品列表查询
/// </summary>
public record GetGiftsQuery(int? Type = null, bool? IsOnShelf = null) : IQuery<List<GiftDto>>;
public record GiftDto
{
public Guid Id { get; init; }
public string Name { get; init; } = string.Empty;
public int Type { get; init; }
public string Description { get; init; } = string.Empty;
public string ImageUrl { get; init; } = string.Empty;
public int RequiredPoints { get; init; }
public int TotalStock { get; init; }
public int AvailableStock { get; init; }
public int? LimitPerMember { get; init; }
public bool IsOnShelf { get; init; }
public int SortOrder { get; init; }
public DateTime CreatedAt { get; init; }
}
public class GetGiftsQueryHandler(ApplicationDbContext dbContext) : IQueryHandler<GetGiftsQuery, List<GiftDto>>
{
public async Task<List<GiftDto>> Handle(GetGiftsQuery request, CancellationToken cancellationToken)
{
var query = dbContext.Gifts.AsQueryable();
if (request.Type.HasValue)
{
query = query.Where(x => (int)x.Type == request.Type.Value);
}
if (request.IsOnShelf.HasValue)
{
query = query.Where(x => x.IsOnShelf == request.IsOnShelf.Value);
}
var gifts = await query
.OrderBy(x => x.SortOrder)
.ThenByDescending(x => x.CreatedAt)
.Select(x => new GiftDto
{
Id = x.Id.Id,
Name = x.Name,
Type = (int)x.Type,
Description = x.Description,
ImageUrl = x.ImageUrl,
RequiredPoints = x.RequiredPoints,
TotalStock = x.TotalStock,
AvailableStock = x.AvailableStock,
LimitPerMember = x.LimitPerMember,
IsOnShelf = x.IsOnShelf,
SortOrder = x.SortOrder,
CreatedAt = x.CreatedAt
})
.ToListAsync(cancellationToken);
return gifts;
}
}
/// <summary>
/// 礼品详情查询
/// </summary>
public record GetGiftByIdQuery(Guid GiftId) : IQuery<GiftDto?>;
public class GetGiftByIdQueryHandler(ApplicationDbContext dbContext) : IQueryHandler<GetGiftByIdQuery, GiftDto?>
{
public async Task<GiftDto?> Handle(GetGiftByIdQuery request, CancellationToken cancellationToken)
{
var gift = await dbContext.Gifts
.Where(x => x.Id.Id == request.GiftId)
.Select(x => new GiftDto
{
Id = x.Id.Id,
Name = x.Name,
Type = (int)x.Type,
Description = x.Description,
ImageUrl = x.ImageUrl,
RequiredPoints = x.RequiredPoints,
TotalStock = x.TotalStock,
AvailableStock = x.AvailableStock,
LimitPerMember = x.LimitPerMember,
IsOnShelf = x.IsOnShelf,
SortOrder = x.SortOrder,
CreatedAt = x.CreatedAt
})
.FirstOrDefaultAsync(cancellationToken);
return gift;
}
}

View File

@ -0,0 +1,59 @@
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
using Fengling.Backend.Infrastructure;
namespace Fengling.Backend.Web.Application.Queries.Members;
/// <summary>
/// 查询会员信息
/// </summary>
public record GetMemberQuery(MemberId MemberId) : IQuery<MemberDto>;
/// <summary>
/// 会员DTO
/// </summary>
public record MemberDto(
MemberId Id,
string Phone,
string Nickname,
string LevelCode,
string LevelName,
int TotalPoints,
int AvailablePoints,
string Status,
DateTime RegisteredAt);
/// <summary>
/// 查询会员验证器
/// </summary>
public class GetMemberQueryValidator : AbstractValidator<GetMemberQuery>
{
public GetMemberQueryValidator()
{
RuleFor(x => x.MemberId).NotEmpty().WithMessage("会员ID不能为空");
}
}
/// <summary>
/// 查询会员处理器
/// </summary>
public class GetMemberQueryHandler(ApplicationDbContext context)
: IQueryHandler<GetMemberQuery, MemberDto>
{
public async Task<MemberDto> Handle(GetMemberQuery request, CancellationToken cancellationToken)
{
return await context.Members
.Where(x => x.Id == request.MemberId)
.Select(x => new MemberDto(
x.Id,
x.Phone,
x.Nickname,
x.Level.LevelCode,
x.Level.LevelName,
x.TotalPoints,
x.AvailablePoints,
x.Status.ToString(),
x.RegisteredAt))
.FirstOrDefaultAsync(cancellationToken)
?? throw new KnownException($"未找到会员,ID:{request.MemberId}");
}
}

View File

@ -0,0 +1,129 @@
using Fengling.Backend.Infrastructure;
namespace Fengling.Backend.Web.Application.Queries.RedemptionOrders;
/// <summary>
/// 兑换订单列表查询
/// </summary>
public record GetRedemptionOrdersQuery(Guid? MemberId = null, int? Status = null) : IQuery<List<RedemptionOrderDto>>;
public record RedemptionOrderDto
{
public Guid Id { get; init; }
public string OrderNo { get; init; } = string.Empty;
public Guid MemberId { get; init; }
public Guid GiftId { get; init; }
public string GiftName { get; init; } = string.Empty;
public int GiftType { get; init; }
public int Quantity { get; init; }
public int ConsumedPoints { get; init; }
public AddressDto? ShippingAddress { get; init; }
public string? TrackingNo { get; init; }
public int Status { get; init; }
public string? CancelReason { get; init; }
public DateTime CreatedAt { get; init; }
public DateTime UpdatedAt { get; init; }
}
public record AddressDto
{
public string ReceiverName { get; init; } = string.Empty;
public string Phone { get; init; } = string.Empty;
public string Province { get; init; } = string.Empty;
public string City { get; init; } = string.Empty;
public string District { get; init; } = string.Empty;
public string DetailAddress { get; init; } = string.Empty;
}
public class GetRedemptionOrdersQueryHandler(ApplicationDbContext dbContext)
: IQueryHandler<GetRedemptionOrdersQuery, List<RedemptionOrderDto>>
{
public async Task<List<RedemptionOrderDto>> Handle(GetRedemptionOrdersQuery request, CancellationToken cancellationToken)
{
var query = dbContext.RedemptionOrders.AsQueryable();
if (request.MemberId.HasValue)
{
query = query.Where(x => x.MemberId == request.MemberId.Value);
}
if (request.Status.HasValue)
{
query = query.Where(x => (int)x.Status == request.Status.Value);
}
var orders = await query
.OrderByDescending(x => x.CreatedAt)
.Select(x => new RedemptionOrderDto
{
Id = x.Id.Id,
OrderNo = x.OrderNo,
MemberId = x.MemberId,
GiftId = x.GiftId,
GiftName = x.GiftName,
GiftType = x.GiftType,
Quantity = x.Quantity,
ConsumedPoints = x.ConsumedPoints,
ShippingAddress = x.ShippingAddress == null ? null : new AddressDto
{
ReceiverName = x.ShippingAddress.ReceiverName,
Phone = x.ShippingAddress.Phone,
Province = x.ShippingAddress.Province,
City = x.ShippingAddress.City,
District = x.ShippingAddress.District,
DetailAddress = x.ShippingAddress.DetailAddress
},
TrackingNo = x.TrackingNo,
Status = (int)x.Status,
CancelReason = x.CancelReason,
CreatedAt = x.CreatedAt,
UpdatedAt = x.UpdatedAt
})
.ToListAsync(cancellationToken);
return orders;
}
}
/// <summary>
/// 订单详情查询
/// </summary>
public record GetRedemptionOrderByIdQuery(Guid OrderId) : IQuery<RedemptionOrderDto?>;
public class GetRedemptionOrderByIdQueryHandler(ApplicationDbContext dbContext)
: IQueryHandler<GetRedemptionOrderByIdQuery, RedemptionOrderDto?>
{
public async Task<RedemptionOrderDto?> Handle(GetRedemptionOrderByIdQuery request, CancellationToken cancellationToken)
{
var order = await dbContext.RedemptionOrders
.Where(x => x.Id.Id == request.OrderId)
.Select(x => new RedemptionOrderDto
{
Id = x.Id.Id,
OrderNo = x.OrderNo,
MemberId = x.MemberId,
GiftId = x.GiftId,
GiftName = x.GiftName,
GiftType = x.GiftType,
Quantity = x.Quantity,
ConsumedPoints = x.ConsumedPoints,
ShippingAddress = x.ShippingAddress == null ? null : new AddressDto
{
ReceiverName = x.ShippingAddress.ReceiverName,
Phone = x.ShippingAddress.Phone,
Province = x.ShippingAddress.Province,
City = x.ShippingAddress.City,
District = x.ShippingAddress.District,
DetailAddress = x.ShippingAddress.DetailAddress
},
TrackingNo = x.TrackingNo,
Status = (int)x.Status,
CancelReason = x.CancelReason,
CreatedAt = x.CreatedAt,
UpdatedAt = x.UpdatedAt
})
.FirstOrDefaultAsync(cancellationToken);
return order;
}
}

View File

@ -0,0 +1,11 @@
using Refit;
namespace Fengling.Backend.Web.Clients;
public interface IUserServiceClient
{
[Get("/users/{userId}")]
Task<UserDto> GetUserAsync(long userId);
}
public record UserDto(string Name, string Email, string Phone);

View File

@ -0,0 +1,21 @@
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore "src/Fengling.Backend.Web/Fengling.Backend.Web.csproj"
WORKDIR "/src/src/Fengling.Backend.Web"
RUN dotnet build "Fengling.Backend.Web.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "Fengling.Backend.Web.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Fengling.Backend.Web.dll"]

View File

@ -0,0 +1,53 @@
using FastEndpoints;
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
using Fengling.Backend.Web.Application.Commands.PointsRules;
namespace Fengling.Backend.Web.Endpoints.Admin;
/// <summary>
/// 创建积分规则请求
/// </summary>
public record CreatePointsRuleRequest(
string RuleName,
int RuleType,
int PointsValue,
DateTime StartDate,
DateTime? EndDate = null,
Guid? ProductId = null,
Guid? CategoryId = null,
string? MemberLevelCode = null,
decimal BonusMultiplier = 1.0m);
/// <summary>
/// 创建积分规则响应
/// </summary>
public record CreatePointsRuleResponse(PointsRuleId RuleId);
/// <summary>
/// 创建积分规则端点
/// </summary>
[Tags("Admin-PointsRules")]
[HttpPost("/api/admin/points-rules")]
[AllowAnonymous]
public class CreatePointsRuleEndpoint(IMediator mediator)
: Endpoint<CreatePointsRuleRequest, ResponseData<CreatePointsRuleResponse>>
{
public override async Task HandleAsync(CreatePointsRuleRequest req, CancellationToken ct)
{
var command = new CreatePointsRuleCommand(
req.RuleName,
(PointsRuleType)req.RuleType,
req.PointsValue,
req.StartDate,
req.EndDate,
req.ProductId,
req.CategoryId,
req.MemberLevelCode,
req.BonusMultiplier);
var ruleId = await mediator.Send(command, ct);
var response = new CreatePointsRuleResponse(ruleId);
await Send.OkAsync(response.AsResponseData(), ct);
}
}

View File

@ -0,0 +1,38 @@
using FastEndpoints;
using Fengling.Backend.Web.Application.Commands.MarketingCodes;
namespace Fengling.Backend.Web.Endpoints.Admin;
/// <summary>
/// 生成营销码请求
/// </summary>
public record GenerateMarketingCodesRequest(
string BatchNo,
Guid ProductId,
string ProductName,
int Quantity,
DateTime? ExpiryDate = null);
/// <summary>
/// 生成营销码端点
/// </summary>
[Tags("Admin-MarketingCodes")]
[HttpPost("/api/admin/marketing-codes/generate")]
[AllowAnonymous]
public class GenerateMarketingCodesEndpoint(IMediator mediator)
: Endpoint<GenerateMarketingCodesRequest, ResponseData<GenerateMarketingCodesResponse>>
{
public override async Task HandleAsync(GenerateMarketingCodesRequest req, CancellationToken ct)
{
var command = new GenerateMarketingCodesCommand(
req.BatchNo,
req.ProductId,
req.ProductName,
req.Quantity,
req.ExpiryDate);
var result = await mediator.Send(command, ct);
await Send.OkAsync(result.AsResponseData(), ct);
}
}

View File

@ -0,0 +1,110 @@
using FastEndpoints;
using Fengling.Backend.Web.Application.Commands.Gifts;
using Fengling.Backend.Web.Application.Queries.Gifts;
namespace Fengling.Backend.Web.Endpoints.Admin.Gifts;
/// <summary>
/// 创建礼品端点
/// </summary>
[Tags("Admin/Gifts")]
[HttpPost("/api/admin/gifts")]
public class CreateGiftEndpoint(IMediator mediator)
: Endpoint<CreateGiftCommand, ResponseData<Guid>>
{
public override async Task HandleAsync(CreateGiftCommand req, CancellationToken ct)
{
var giftId = await mediator.Send(req, ct);
await Send.OkAsync(giftId.Id.AsResponseData(), ct);
}
}
/// <summary>
/// 更新礼品端点
/// </summary>
[Tags("Admin/Gifts")]
[HttpPut("/api/admin/gifts/{GiftId}")]
public class UpdateGiftEndpoint(IMediator mediator)
: Endpoint<UpdateGiftCommand, ResponseData>
{
public override async Task HandleAsync(UpdateGiftCommand req, CancellationToken ct)
{
var result = await mediator.Send(req, ct);
await Send.OkAsync(result, ct);
}
}
/// <summary>
/// 上架礼品端点
/// </summary>
[Tags("Admin/Gifts")]
[HttpPost("/api/admin/gifts/{GiftId}/putonshelf")]
public class PutOnShelfEndpoint(IMediator mediator)
: Endpoint<PutOnShelfCommand, ResponseData>
{
public override async Task HandleAsync(PutOnShelfCommand req, CancellationToken ct)
{
var result = await mediator.Send(req, ct);
await Send.OkAsync(result, ct);
}
}
/// <summary>
/// 下架礼品端点
/// </summary>
[Tags("Admin/Gifts")]
[HttpPost("/api/admin/gifts/{GiftId}/putoffshelf")]
public class PutOffShelfEndpoint(IMediator mediator)
: Endpoint<PutOffShelfCommand, ResponseData>
{
public override async Task HandleAsync(PutOffShelfCommand req, CancellationToken ct)
{
var result = await mediator.Send(req, ct);
await Send.OkAsync(result, ct);
}
}
/// <summary>
/// 增加库存端点
/// </summary>
[Tags("Admin/Gifts")]
[HttpPost("/api/admin/gifts/{GiftId}/addstock")]
public class AddGiftStockEndpoint(IMediator mediator)
: Endpoint<AddGiftStockCommand, ResponseData>
{
public override async Task HandleAsync(AddGiftStockCommand req, CancellationToken ct)
{
var result = await mediator.Send(req, ct);
await Send.OkAsync(result, ct);
}
}
/// <summary>
/// 获取礼品列表端点
/// </summary>
[Tags("Admin/Gifts")]
[HttpGet("/api/admin/gifts")]
public class GetGiftsEndpoint(IMediator mediator)
: Endpoint<GetGiftsQuery, ResponseData<List<GiftDto>>>
{
public override async Task HandleAsync(GetGiftsQuery req, CancellationToken ct)
{
var gifts = await mediator.Send(req, ct);
await Send.OkAsync(gifts.AsResponseData(), ct);
}
}
/// <summary>
/// 获取礼品详情端点
/// </summary>
[Tags("Admin/Gifts")]
[HttpGet("/api/admin/gifts/{GiftId}")]
public class GetGiftByIdEndpoint(IMediator mediator)
: Endpoint<GetGiftByIdQuery, ResponseData<GiftDto?>>
{
public override async Task HandleAsync(GetGiftByIdQuery req, CancellationToken ct)
{
var gift = await mediator.Send(req, ct);
await Send.OkAsync(gift.AsResponseData(), ct);
}
}

View File

@ -0,0 +1,80 @@
using FastEndpoints;
using Fengling.Backend.Web.Application.Commands.RedemptionOrders;
using Fengling.Backend.Web.Application.Queries.RedemptionOrders;
namespace Fengling.Backend.Web.Endpoints.Admin.RedemptionOrders;
/// <summary>
/// 获取兑换订单列表端点
/// </summary>
[Tags("Admin/RedemptionOrders")]
[HttpGet("/api/admin/redemption-orders")]
public class GetRedemptionOrdersEndpoint(IMediator mediator)
: Endpoint<GetRedemptionOrdersQuery, ResponseData<List<RedemptionOrderDto>>>
{
public override async Task HandleAsync(GetRedemptionOrdersQuery req, CancellationToken ct)
{
var orders = await mediator.Send(req, ct);
await Send.OkAsync(orders.AsResponseData(), ct);
}
}
/// <summary>
/// 获取兑换订单详情端点
/// </summary>
[Tags("Admin/RedemptionOrders")]
[HttpGet("/api/admin/redemption-orders/{OrderId}")]
public class GetRedemptionOrderByIdEndpoint(IMediator mediator)
: Endpoint<GetRedemptionOrderByIdQuery, ResponseData<RedemptionOrderDto?>>
{
public override async Task HandleAsync(GetRedemptionOrderByIdQuery req, CancellationToken ct)
{
var order = await mediator.Send(req, ct);
await Send.OkAsync(order.AsResponseData(), ct);
}
}
/// <summary>
/// 标记订单为已发货端点
/// </summary>
[Tags("Admin/RedemptionOrders")]
[HttpPost("/api/admin/redemption-orders/{OrderId}/dispatch")]
public class MarkOrderAsDispatchedEndpoint(IMediator mediator)
: Endpoint<MarkOrderAsDispatchedCommand, ResponseData>
{
public override async Task HandleAsync(MarkOrderAsDispatchedCommand req, CancellationToken ct)
{
var result = await mediator.Send(req, ct);
await Send.OkAsync(result, ct);
}
}
/// <summary>
/// 完成订单端点
/// </summary>
[Tags("Admin/RedemptionOrders")]
[HttpPost("/api/admin/redemption-orders/{OrderId}/complete")]
public class CompleteOrderEndpoint(IMediator mediator)
: Endpoint<CompleteOrderCommand, ResponseData>
{
public override async Task HandleAsync(CompleteOrderCommand req, CancellationToken ct)
{
var result = await mediator.Send(req, ct);
await Send.OkAsync(result, ct);
}
}
/// <summary>
/// 取消订单端点
/// </summary>
[Tags("Admin/RedemptionOrders")]
[HttpPost("/api/admin/redemption-orders/{OrderId}/cancel")]
public class CancelOrderEndpoint(IMediator mediator)
: Endpoint<CancelOrderCommand, ResponseData>
{
public override async Task HandleAsync(CancelOrderCommand req, CancellationToken ct)
{
var result = await mediator.Send(req, ct);
await Send.OkAsync(result, ct);
}
}

View File

@ -0,0 +1,53 @@
using FastEndpoints;
using Fengling.Backend.Web.Application.Commands.RedemptionOrders;
using Fengling.Backend.Web.Application.Queries.Gifts;
namespace Fengling.Backend.Web.Endpoints.Gifts;
/// <summary>
/// 获取上架礼品列表端点(会员端)
/// </summary>
[Tags("Gifts")]
[HttpGet("/api/gifts")]
[AllowAnonymous]
public class GetOnShelfGiftsEndpoint(IMediator mediator)
: Endpoint<EmptyRequest, ResponseData<List<GiftDto>>>
{
public override async Task HandleAsync(EmptyRequest req, CancellationToken ct)
{
var query = new GetGiftsQuery(IsOnShelf: true);
var gifts = await mediator.Send(query, ct);
await Send.OkAsync(gifts.AsResponseData(), ct);
}
}
/// <summary>
/// 获取礼品详情端点(会员端)
/// </summary>
[Tags("Gifts")]
[HttpGet("/api/gifts/{GiftId}")]
[AllowAnonymous]
public class GetGiftDetailEndpoint(IMediator mediator)
: Endpoint<GetGiftByIdQuery, ResponseData<GiftDto?>>
{
public override async Task HandleAsync(GetGiftByIdQuery req, CancellationToken ct)
{
var gift = await mediator.Send(req, ct);
await Send.OkAsync(gift.AsResponseData(), ct);
}
}
/// <summary>
/// 兑换礼品端点(会员端)
/// </summary>
[Tags("Gifts")]
[HttpPost("/api/gifts/redeem")]
public class RedeemGiftEndpoint(IMediator mediator)
: Endpoint<CreateRedemptionOrderCommand, ResponseData<Guid>>
{
public override async Task HandleAsync(CreateRedemptionOrderCommand req, CancellationToken ct)
{
var orderId = await mediator.Send(req, ct);
await Send.OkAsync(orderId.Id.AsResponseData(), ct);
}
}

View File

@ -0,0 +1,25 @@
using FastEndpoints;
using FastEndpoints.Swagger;
using Microsoft.AspNetCore.Authorization;
using NetCorePal.Extensions.Dto;
namespace Fengling.Backend.Web.Endpoints;
/// <summary>
/// Hello
/// </summary>
public class HelloEndpoint : EndpointWithoutRequest<ResponseData<string>>
{
public override void Configure()
{
Tags("Hello");
Description(b => b.AutoTagOverride("Hello"));
Get("/api/hello");
AllowAnonymous();
}
public override Task HandleAsync(CancellationToken ct)
{
return Send.OkAsync("hello".AsResponseData(), cancellation: ct);
}
}

View File

@ -0,0 +1,44 @@
using FastEndpoints;
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
using Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate;
using Fengling.Backend.Web.Application.Commands.MarketingCodes;
namespace Fengling.Backend.Web.Endpoints.MarketingCodes;
/// <summary>
/// 扫码请求
/// </summary>
public record UseMarketingCodeRequest(string Code, MemberId MemberId);
/// <summary>
/// 扫码响应
/// </summary>
public record UseMarketingCodeEndpointResponse(
MarketingCodeId MarketingCodeId,
string ProductName,
int EarnedPoints,
string Message);
/// <summary>
/// 使用营销码端点(扫码)
/// </summary>
[Tags("MarketingCodes")]
[HttpPost("/api/marketing-codes/scan")]
[AllowAnonymous]
public class UseMarketingCodeEndpoint(IMediator mediator)
: Endpoint<UseMarketingCodeRequest, ResponseData<UseMarketingCodeEndpointResponse>>
{
public override async Task HandleAsync(UseMarketingCodeRequest req, CancellationToken ct)
{
var command = new UseMarketingCodeCommand(req.Code, req.MemberId);
var result = await mediator.Send(command, ct);
var response = new UseMarketingCodeEndpointResponse(
result.MarketingCodeId,
result.ProductName,
result.EarnedPoints,
result.Message);
await Send.OkAsync(response.AsResponseData(), ct);
}
}

View File

@ -0,0 +1,31 @@
using FastEndpoints;
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
using Fengling.Backend.Web.Application.Queries.Members;
namespace Fengling.Backend.Web.Endpoints.Members;
/// <summary>
/// 获取会员信息端点
/// </summary>
[Tags("Members")]
[HttpGet("/api/members/{memberId}")]
[AllowAnonymous]
public class GetMemberEndpoint(IMediator mediator)
: Endpoint<GetMemberEndpointRequest, ResponseData<MemberDto>>
{
public override async Task HandleAsync(GetMemberEndpointRequest req, CancellationToken ct)
{
var query = new GetMemberQuery(req.MemberId);
var result = await mediator.Send(query, ct);
await Send.OkAsync(result.AsResponseData(), ct);
}
}
/// <summary>
/// 获取会员请求
/// </summary>
public record GetMemberEndpointRequest
{
public MemberId MemberId { get; init; } = default!;
}

View File

@ -0,0 +1,34 @@
using FastEndpoints;
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
using Fengling.Backend.Web.Application.Commands.Members;
namespace Fengling.Backend.Web.Endpoints.Members;
/// <summary>
/// 登录请求
/// </summary>
public record LoginMemberRequest(string Phone, string Password);
/// <summary>
/// 登录响应
/// </summary>
public record LoginMemberEndpointResponse(MemberId MemberId, string Token);
/// <summary>
/// 会员登录端点
/// </summary>
[Tags("Members")]
[HttpPost("/api/members/login")]
[AllowAnonymous]
public class LoginMemberEndpoint(IMediator mediator)
: Endpoint<LoginMemberRequest, ResponseData<LoginMemberEndpointResponse>>
{
public override async Task HandleAsync(LoginMemberRequest req, CancellationToken ct)
{
var command = new LoginMemberCommand(req.Phone, req.Password);
var result = await mediator.Send(command, ct);
var response = new LoginMemberEndpointResponse(result.MemberId, result.Token);
await Send.OkAsync(response.AsResponseData(), ct);
}
}

View File

@ -0,0 +1,34 @@
using FastEndpoints;
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
using Fengling.Backend.Web.Application.Commands.Members;
namespace Fengling.Backend.Web.Endpoints.Members;
/// <summary>
/// 注册会员请求
/// </summary>
public record RegisterMemberRequest(string Phone, string Password, string? Nickname = null);
/// <summary>
/// 注册会员响应
/// </summary>
public record RegisterMemberResponse(MemberId MemberId, string Phone);
/// <summary>
/// 注册会员端点
/// </summary>
[Tags("Members")]
[HttpPost("/api/members/register")]
[AllowAnonymous]
public class RegisterMemberEndpoint(IMediator mediator)
: Endpoint<RegisterMemberRequest, ResponseData<RegisterMemberResponse>>
{
public override async Task HandleAsync(RegisterMemberRequest req, CancellationToken ct)
{
var command = new RegisterMemberCommand(req.Phone, req.Password, req.Nickname);
var memberId = await mediator.Send(command, ct);
var response = new RegisterMemberResponse(memberId, req.Phone);
await Send.OkAsync(response.AsResponseData(), ct);
}
}

View File

@ -0,0 +1,39 @@
using FastEndpoints;
using Fengling.Backend.Web.Application.Queries.RedemptionOrders;
namespace Fengling.Backend.Web.Endpoints.RedemptionOrders;
/// <summary>
/// 获取我的兑换订单列表端点(会员端)
/// </summary>
[Tags("RedemptionOrders")]
[HttpGet("/api/redemption-orders/my")]
public class GetMyRedemptionOrdersEndpoint(IMediator mediator)
: Endpoint<GetMyRedemptionOrdersRequest, ResponseData<List<RedemptionOrderDto>>>
{
public override async Task HandleAsync(GetMyRedemptionOrdersRequest req, CancellationToken ct)
{
// TODO: 从JWT Token中获取当前登录会员ID
// 暂时使用请求中的MemberId
var query = new GetRedemptionOrdersQuery(MemberId: req.MemberId, Status: req.Status);
var orders = await mediator.Send(query, ct);
await Send.OkAsync(orders.AsResponseData(), ct);
}
}
public record GetMyRedemptionOrdersRequest(Guid MemberId, int? Status = null);
/// <summary>
/// 获取订单详情端点(会员端)
/// </summary>
[Tags("RedemptionOrders")]
[HttpGet("/api/redemption-orders/{OrderId}")]
public class GetMyRedemptionOrderDetailEndpoint(IMediator mediator)
: Endpoint<GetRedemptionOrderByIdQuery, ResponseData<RedemptionOrderDto?>>
{
public override async Task HandleAsync(GetRedemptionOrderByIdQuery req, CancellationToken ct)
{
var order = await mediator.Send(req, ct);
await Send.OkAsync(order.AsResponseData(), ct);
}
}

View File

@ -0,0 +1,38 @@
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.DataProtection.StackExchangeRedis;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
namespace Fengling.Backend.Web.Extensions;
/// <summary>
/// Extension methods for configuring StackExchange.Redis-based data protection.
/// </summary>
public static class StackExchangeRedisDataProtectionBuilderExtensions
{
/// <summary>
/// Configures data protection to persist keys to StackExchange.Redis.
/// This method resolves IConnectionMultiplexer from DI, making it work with both
/// Aspire (where AddRedisClient registers the multiplexer) and non-Aspire scenarios.
/// </summary>
/// <param name="builder">The data protection builder.</param>
/// <param name="key">The Redis key where data protection keys will be stored.</param>
/// <returns>The data protection builder for chaining.</returns>
public static IDataProtectionBuilder PersistKeysToStackExchangeRedis(
this IDataProtectionBuilder builder,
RedisKey key)
{
builder.Services.AddSingleton<IConfigureOptions<KeyManagementOptions>>(services =>
{
var connectionMultiplexer = services.GetRequiredService<IConnectionMultiplexer>();
return new ConfigureOptions<KeyManagementOptions>(options =>
{
options.XmlRepository = new RedisXmlRepository(() => connectionMultiplexer.GetDatabase(), key);
});
});
return builder;
}
}

View File

@ -0,0 +1,25 @@
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Fengling.Backend.Web.Extensions;
public static class SwaggerGenOptionsExtionsions
{
public static SwaggerGenOptions AddEntityIdSchemaMap(this SwaggerGenOptions swaggerGenOptions)
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()
.Where(p => p.FullName != null && p.FullName.Contains("Fengling.Backend")))
{
foreach (var type in assembly.GetTypes())
{
if (type.IsClass && Array.Exists(type.GetInterfaces(), p => p == typeof(IEntityId)))
{
swaggerGenOptions.MapType(type,
() => new OpenApiSchema { Type = typeof(string).Name.ToLower() });
}
}
}
return swaggerGenOptions;
}
}

View File

@ -0,0 +1,68 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..</DockerfileContext>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AspNet.Security.OAuth.Feishu" />
<PackageReference Include="AspNet.Security.OAuth.Weixin" />
<PackageReference Include="DotNetCore.CAP.Dashboard" />
<PackageReference Include="DotNetCore.CAP.RedisStreams" />
<PackageReference Include="FastEndpoints" />
<PackageReference Include="FastEndpoints.Swagger" />
<PackageReference Include="FastEndpoints.Swagger.Swashbuckle" />
<PackageReference Include="FluentValidation.AspNetCore" />
<PackageReference Include="Hangfire.AspNetCore" />
<PackageReference Include="Hangfire.Redis.StackExchange" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" />
<PackageReference Include="NetCorePal.Context.AspNetCore" />
<PackageReference Include="NetCorePal.Context.CAP" />
<PackageReference Include="NetCorePal.Context.Shared" />
<PackageReference Include="NetCorePal.Extensions.AspNetCore" />
<PackageReference Include="NetCorePal.Extensions.CodeAnalysis" />
<PackageReference Include="NetCorePal.Extensions.DistributedLocks.Redis" />
<PackageReference Include="NetCorePal.Extensions.MultiEnv" />
<PackageReference Include="NetCorePal.Extensions.MicrosoftServiceDiscovery" />
<PackageReference Include="NetCorePal.Extensions.Primitives" />
<PackageReference Include="NetCorePal.Extensions.Jwt.StackExchangeRedis" />
<PackageReference Include="prometheus-net.AspNetCore" />
<PackageReference Include="prometheus-net.AspNetCore.HealthChecks" />
<PackageReference Include="Refit.HttpClientFactory" />
<PackageReference Include="Refit.Newtonsoft.Json" />
<PackageReference Include="Serilog.AspNetCore" />
<PackageReference Include="Serilog.Enrichers.ClientInfo" />
<PackageReference Include="Serilog.Sinks.OpenTelemetry" />
<PackageReference Include="StackExchange.Redis" />
<PackageReference Include="Swashbuckle.AspNetCore" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Fengling.Backend.Domain\Fengling.Backend.Domain.csproj" />
<ProjectReference Include="..\Fengling.Backend.Infrastructure\Fengling.Backend.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="SonarAnalyzer.CSharp">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,12 @@
global using NetCorePal.Extensions.AspNetCore;
global using NetCorePal.Extensions.DependencyInjection;
global using Microsoft.Extensions.DependencyInjection;
global using Fengling.Backend.Infrastructure;
global using FluentValidation;
global using NetCorePal.Extensions.Primitives;
global using MediatR;
global using NetCorePal.Extensions.Domain;
global using NetCorePal.Extensions.Dto;
global using NetCorePal.Extensions.DistributedTransactions;
global using Microsoft.AspNetCore.Authorization;
global using Microsoft.EntityFrameworkCore;

View File

@ -0,0 +1,245 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Prometheus;
using System.Reflection;
using System.Text.Json;
using Microsoft.AspNetCore.DataProtection;
using StackExchange.Redis;
using FluentValidation.AspNetCore;
using Fengling.Backend.Web.Clients;
using Fengling.Backend.Web.Extensions;
using Fengling.Backend.Web.Utils;
using FastEndpoints;
using Serilog;
using Serilog.Formatting.Json;
using Hangfire;
using Hangfire.Redis.StackExchange;
using Microsoft.AspNetCore.Http.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Refit;
using NetCorePal.Extensions.CodeAnalysis;
Log.Logger = new LoggerConfiguration()
.Enrich.WithClientIp()
.WriteTo.Console(new JsonFormatter())
.CreateLogger();
try
{
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog();
#region SignalR
builder.Services.AddHealthChecks();
builder.Services.AddMvc()
.AddNewtonsoftJson(options => { options.SerializerSettings.AddNetCorePalJsonConverters(); });
builder.Services.AddSignalR();
#endregion
#region Prometheus监控
builder.Services.AddHealthChecks().ForwardToPrometheus();
builder.Services.AddHttpClient(Options.DefaultName)
.UseHttpClientMetrics();
#endregion
// Add services to the container.
#region
var redis = await ConnectionMultiplexer.ConnectAsync(builder.Configuration.GetConnectionString("Redis")!);
builder.Services.AddSingleton<IConnectionMultiplexer>(_ => redis);
// DataProtection - use custom extension that resolves IConnectionMultiplexer from DI
builder.Services.AddDataProtection()
.PersistKeysToStackExchangeRedis("DataProtection-Keys");
// 配置JWT认证
builder.Services.Configure<AppConfiguration>(builder.Configuration.GetSection("AppConfiguration"));
var appConfig = builder.Configuration.GetSection("AppConfiguration").Get<AppConfiguration>() ?? new AppConfiguration { JwtIssuer = "netcorepal", JwtAudience = "netcorepal" };
builder.Services.AddAuthentication().AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.TokenValidationParameters.ValidAudience = appConfig.JwtAudience;
options.TokenValidationParameters.ValidateAudience = true;
options.TokenValidationParameters.ValidIssuer = appConfig.JwtIssuer;
options.TokenValidationParameters.ValidateIssuer = true;
});
builder.Services.AddNetCorePalJwt().AddRedisStore();
#endregion
#region Controller
builder.Services.AddControllers().AddNetCorePalSystemTextJson();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c => c.AddEntityIdSchemaMap()); //强类型id swagger schema 映射
#endregion
#region FastEndpoints
builder.Services.AddFastEndpoints(o => o.IncludeAbstractValidators = true);
builder.Services.Configure<JsonOptions>(o =>
o.SerializerOptions.AddNetCorePalJsonConverters());
#endregion
#region
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
builder.Services.AddKnownExceptionErrorModelInterceptor();
#endregion
#region
builder.Services.AddRepositories(typeof(ApplicationDbContext).Assembly);
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlite(builder.Configuration.GetConnectionString("SQLite"));
// 仅在开发环境启用敏感数据日志,防止生产环境泄露敏感信息
if (builder.Environment.IsDevelopment())
{
options.EnableSensitiveDataLogging();
}
options.EnableDetailedErrors();
});
builder.Services.AddUnitOfWork<ApplicationDbContext>();
builder.Services.AddRedisLocks();
builder.Services.AddContext().AddEnvContext().AddCapContextProcessor();
builder.Services.AddNetCorePalServiceDiscoveryClient();
builder.Services.AddIntegrationEvents(typeof(Program))
.UseCap<ApplicationDbContext>(b =>
{
b.RegisterServicesFromAssemblies(typeof(Program));
b.AddContextIntegrationFilters();
});
builder.Services.AddCap(x =>
{
x.UseNetCorePalStorage<ApplicationDbContext>();
x.JsonSerializerOptions.AddNetCorePalJsonConverters();
x.ConsumerThreadCount = Environment.ProcessorCount;
x.UseRedis(builder.Configuration.GetConnectionString("Redis")!);
x.UseDashboard(); //CAP Dashboard path /cap
});
#endregion
builder.Services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssemblies(Assembly.GetExecutingAssembly())
.AddCommandLockBehavior()
.AddKnownExceptionValidationBehavior()
.AddUnitOfWorkBehaviors());
#region
builder.Services.AddMultiEnv(envOption => envOption.ServiceName = "Abc.Template")
.UseMicrosoftServiceDiscovery();
builder.Services.AddConfigurationServiceEndpointProvider();
#endregion
#region
var jsonSerializerSettings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};
jsonSerializerSettings.AddNetCorePalJsonConverters();
var ser = new NewtonsoftJsonContentSerializer(jsonSerializerSettings);
var settings = new RefitSettings(ser);
builder.Services.AddRefitClient<IUserServiceClient>(settings)
.ConfigureHttpClient(client =>
client.BaseAddress = new Uri(builder.Configuration.GetValue<string>("https+http://user:8080")!))
.AddMultiEnvMicrosoftServiceDiscovery() //多环境服务发现支持
.AddStandardResilienceHandler(); //添加标准的重试策略
#endregion
#region Jobs
builder.Services.AddHangfire(x => { x.UseRedisStorage(builder.Configuration.GetConnectionString("Redis")); });
builder.Services.AddHangfireServer(); //hangfire dashboard path /hangfire
#endregion
var app = builder.Build();
// 在非生产环境中执行数据库迁移包括开发、测试、Staging等环境
if (!app.Environment.IsProduction())
{
using var scope = app.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await dbContext.Database.MigrateAsync();
}
app.UseKnownExceptionHandler();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseStaticFiles();
//app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication(); // Authentication 必须在 Authorization 之前
app.UseAuthorization();
app.MapControllers();
app.UseFastEndpoints();
#region SignalR
app.MapHub<Fengling.Backend.Web.Application.Hubs.ChatHub>("/chat");
#endregion
app.UseHttpMetrics();
app.MapHealthChecks("/health");
app.MapMetrics(); // 通过 /metrics 访问指标
// Code analysis endpoint
app.MapGet("/code-analysis", () =>
{
var assemblies = new List<Assembly> { typeof(Program).Assembly, typeof(ApplicationDbContext).Assembly };
var html = VisualizationHtmlBuilder.GenerateVisualizationHtml(
CodeFlowAnalysisHelper.GetResultFromAssemblies(assemblies.ToArray())
);
return Results.Content(html, "text/html; charset=utf-8");
});
app.UseHangfireDashboard();
await app.RunAsync();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
await Log.CloseAndFlushAsync();
}
#pragma warning disable S1118
public partial class Program
#pragma warning restore S1118
{
}

View File

@ -0,0 +1,25 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5511",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7435;http://localhost:5511",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,18 @@
namespace Fengling.Backend.Web.Utils;
public class AppConfiguration
{
public string Secret { get; set; } = string.Empty;
public int TokenExpiryInMinutes { get; set; }
/// <summary>
/// JWT Issuer签发者
/// </summary>
public string JwtIssuer { get; set; } = "netcorepal";
/// <summary>
/// JWT Audience受众
/// </summary>
public string JwtAudience { get; set; } = "netcorepal";
}

View File

@ -0,0 +1,24 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"SQLite": "Data Source=fengling.db",
"Redis": "81.68.223.70:6379"
},
"Services": {
"user": {
"https": [
"https://user:8443"
]
},
"user-v2": {
"https": [
"https://user-v2:8443"
]
}
}
}

View File

@ -0,0 +1,28 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"SQLite": "Data Source=fengling.db",
"Redis": "81.68.223.70:6379"
},
"RedisStreams": {
"ConnectionString": "81.68.223.70:6379"
},
"Services": {
"user": {
"https": [
"https://user:8443"
]
},
"user-v2": {
"https": [
"https://user-v2:8443"
]
}
}
}

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Fengling.Backend.Domain\Fengling.Backend.Domain.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,2 @@
global using Xunit;
global using NetCorePal.Extensions.Primitives;

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Fengling.Backend.Infrastructure\Fengling.Backend.Infrastructure.csproj" />
</ItemGroup>
</Project>

Some files were not shown because too many files have changed in this diff Show More