chore(build): 添加基础构建配置和版本管理
- 新增 .dockerignore 文件,忽略多种临时及中间文件 - 新增 .gitattributes 文件,配置文本文件换行及合并行为 - 新增详细的 .gitignore 文件,排除多种开发及生成文件 - 新增 VS Code C# 代码片段,提升开发效率 - 添加 Directory.Build.props,统一 MSBuild 配置和代码分析规则 - 添加空的 Directory.Build.targets,预留构建任务扩展位置 - 添加 Directory.Packages.props,实现依赖包版本集中管理和声明
This commit is contained in:
commit
e24925e1ed
25
Backend/.dockerignore
Normal file
25
Backend/.dockerignore
Normal 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
63
Backend/.gitattributes
vendored
Normal 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
398
Backend/.gitignore
vendored
Normal 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
625
Backend/.vscode/csharp.code-snippets
vendored
Normal 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": "创建实体配置类"
|
||||
}
|
||||
}
|
||||
34
Backend/Directory.Build.props
Normal file
34
Backend/Directory.Build.props
Normal 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>
|
||||
3
Backend/Directory.Build.targets
Normal file
3
Backend/Directory.Build.targets
Normal file
@ -0,0 +1,3 @@
|
||||
<Project>
|
||||
<!-- Keep empty for now, version management moved to Directory.Packages.props -->
|
||||
</Project>
|
||||
152
Backend/Directory.Packages.props
Normal file
152
Backend/Directory.Packages.props
Normal 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>
|
||||
918
Backend/Fengling.Backend.sln.DotSettings
Normal file
918
Backend/Fengling.Backend.sln.DotSettings
Normal 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<$dto$Request, $dto$Response>
|
||||
{
|
||||
public Task PostProcessAsync($dto$Request req, $dto$Response res, HttpContext ctx, IReadOnlyCollection<ValidationFailure> 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<$fixture$Fixture>
|
||||
{
|
||||
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<$epName$Request>
|
||||
{
|
||||
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<$name$Command>
|
||||
{
|
||||
public $name$CommandValidator()
|
||||
{
|
||||
// 添加验证规则示例:
|
||||
// RuleFor(x => x.Property).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class $name$CommandHandler : ICommandHandler<$name$Command>
|
||||
{
|
||||
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<$name$CommandResponse>;
|
||||
|
||||
public record $name$CommandResponse();
|
||||
|
||||
public class $name$CommandValidator : AbstractValidator<$name$Command>
|
||||
{
|
||||
public $name$CommandValidator()
|
||||
{
|
||||
// 添加验证规则示例:
|
||||
// RuleFor(x => x.Property).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class $name$CommandHandler : ICommandHandler<$name$Command, $name$CommandResponse>
|
||||
{
|
||||
public async Task<$name$CommandResponse> 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 & 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<$name$Id>, 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<Program>
|
||||
{
|
||||
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<Request>
|
||||
{
|
||||
public Validator()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Response
|
||||
{
|
||||
public string Message => "This endpoint hasn't been implemented yet!";
|
||||
}
|
||||
|
||||
sealed class Endpoint : Endpoint<Request, Response, Mapper>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("$route$");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(Request r, CancellationToken c)
|
||||
{
|
||||
await SendAsync(new Response());$END$
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Mapper : Mapper<Request, Response, object>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
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<$name$>
|
||||
{
|
||||
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<$name$, $name$Id>;
|
||||
|
||||
public class $name$Repository(ApplicationDbContext context)
|
||||
: RepositoryBase<$name$, $name$Id, ApplicationDbContext>(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<$name$Result>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
sealed class $name$Result
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
sealed class $name$Handler : ICommandHandler<$name$, $name$Result>
|
||||
{
|
||||
public Task<$name$Result> 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<$name$>
|
||||
{
|
||||
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<$name$Request>
|
||||
{
|
||||
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<ValidationFailure> 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<$epName$Response>
|
||||
{
|
||||
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<$name$IntegrationEvent>
|
||||
{
|
||||
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<$name$DomainEvent>
|
||||
{
|
||||
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<$epName$Request, ResponseData<$epName$Response>>
|
||||
{
|
||||
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<$epName$Request>
|
||||
{
|
||||
public $epName$Validator()
|
||||
{
|
||||
// RuleFor(x => x.Property).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
sealed class $epName$Summary : Summary<$epName$Endpoint, $epName$Request>
|
||||
{
|
||||
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<$req$Request>
|
||||
{
|
||||
public Task PreProcessAsync($req$Request r, HttpContext ctx, List<ValidationFailure> 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<$name$DomainEvent, $name$IntegrationEvent>
|
||||
{
|
||||
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<$epName$Request, $epName$Response, $entity$>
|
||||
{
|
||||
public override $entity$ ToEntity($epName$Request r) => new()
|
||||
{
|
||||
$END$
|
||||
};
|
||||
|
||||
public override $epName$Response FromEntity($entity$ e) => 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<$epName$Request, $epName$Response, $epName$Mapper>
|
||||
{
|
||||
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<$epName$Request>
|
||||
{
|
||||
public $epName$Validator()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
sealed class $epName$Mapper: Mapper<$epName$Request, $epName$Response, $entity$>
|
||||
{
|
||||
public override $entity$ ToEntity($epName$Request r) => new()
|
||||
{
|
||||
|
||||
};
|
||||
|
||||
public override $epName$Response FromEntity($entity$ e) => new()
|
||||
{
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
sealed class $epName$Summary : Summary<$epName$Endpoint, $epName$Request>
|
||||
{
|
||||
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<ValidationFailure> 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<$name$Endpoint, $name$Request>
|
||||
{
|
||||
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 & 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<$epName$Request, $epName$Response>
|
||||
{
|
||||
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<$Entity$>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<$Entity$> builder)
|
||||
{
|
||||
builder.ToTable("$table$");
|
||||
builder.HasKey(t => t.Id);
|
||||
builder.Property(t => 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>
|
||||
21
Backend/Fengling.Backend.slnx
Normal file
21
Backend/Fengling.Backend.slnx
Normal 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
7
Backend/NuGet.config
Normal 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
230
Backend/README.md
Normal 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<Response> 实现 |
|
||||
| `ncpar` | 聚合根 | Entity<Id> 和 IAggregateRoot |
|
||||
| `ncprepo` | NetCorePal 仓储 | IRepository 接口和实现 |
|
||||
| `ncpie` | 集成事件 | IntegrationEvent 和处理器 |
|
||||
| `ncpdeh` | 域事件处理器 | IDomainEventHandler 实现 |
|
||||
| `ncpiec` | 集成事件转换器 | IIntegrationEventConverter |
|
||||
| `ncpde` | 域事件 | IDomainEvent 记录 |
|
||||
|
||||
#### Endpoint (ep) 快捷键
|
||||
| 快捷键 | 描述 | 生成内容 |
|
||||
|--------|------|----------|
|
||||
| `epp` | FastEndpoint(NCP风格) | 完整的垂直切片实现 |
|
||||
| `epreq` | 仅请求端点 | Endpoint<Request> |
|
||||
| `epres` | 仅响应端点 | EndpointWithoutRequest<Response> |
|
||||
| `epdto` | 端点 DTOs | Request 和 Response 类 |
|
||||
| `epval` | 端点验证器 | Validator<Request> |
|
||||
| `epmap` | 端点映射器 | Mapper<Request, Response, Entity> |
|
||||
| `epfull` | 完整端点切片 | 带映射器的完整实现 |
|
||||
| `epsum` | 端点摘要 | Summary<Endpoint, Request> |
|
||||
| `epnoreq` | 无请求端点 | EndpointWithoutRequest |
|
||||
| `epreqres` | 请求响应端点 | Endpoint<Request, Response> |
|
||||
| `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
|
||||
|
||||
# 创建迁移 SEE:https://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)
|
||||
|
||||
|
||||
6
Backend/eng/versions.props
Normal file
6
Backend/eng/versions.props
Normal file
@ -0,0 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<VersionPrefix>1.0.0</VersionPrefix>
|
||||
<VersionSuffix></VersionSuffix>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
7
Backend/global.json
Normal file
7
Backend/global.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "10.0.100",
|
||||
"allowPrerelease": true,
|
||||
"rollForward": "latestMinor"
|
||||
}
|
||||
}
|
||||
151
Backend/scripts/EXAMPLES.md
Normal file
151
Backend/scripts/EXAMPLES.md
Normal 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
56
Backend/scripts/README.md
Normal 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.
|
||||
195
Backend/scripts/clean-infrastructure.ps1
Normal file
195
Backend/scripts/clean-infrastructure.ps1
Normal 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
|
||||
}
|
||||
177
Backend/scripts/clean-infrastructure.sh
Normal file
177
Backend/scripts/clean-infrastructure.sh
Normal 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 "$@"
|
||||
167
Backend/scripts/docker-compose.yml
Normal file
167
Backend/scripts/docker-compose.yml
Normal 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
|
||||
258
Backend/scripts/init-infrastructure.ps1
Normal file
258
Backend/scripts/init-infrastructure.ps1
Normal 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
|
||||
}
|
||||
200
Backend/scripts/init-infrastructure.sh
Normal file
200
Backend/scripts/init-infrastructure.sh
Normal 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
|
||||
18
Backend/scripts/mysql-init/01-init.sql
Normal file
18
Backend/scripts/mysql-init/01-init.sql
Normal 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;
|
||||
24
Backend/scripts/postgres-init/01-init.sql
Normal file
24
Backend/scripts/postgres-init/01-init.sql
Normal 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;
|
||||
@ -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
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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}";
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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>
|
||||
2
Backend/src/Fengling.Backend.Domain/GlobalUsings.cs
Normal file
2
Backend/src/Fengling.Backend.Domain/GlobalUsings.cs
Normal file
@ -0,0 +1,2 @@
|
||||
global using NetCorePal.Extensions.Domain;
|
||||
global using NetCorePal.Extensions.Primitives;
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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;
|
||||
599
Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211044819_Init.Designer.cs
generated
Normal file
599
Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211044819_Init.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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}";
|
||||
}
|
||||
}
|
||||
@ -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, // 实际积分由事件处理器计算
|
||||
"扫码成功,积分正在发放中...");
|
||||
}
|
||||
}
|
||||
@ -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}"));
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
14
Backend/src/Fengling.Backend.Web/Application/Hubs/ChatHub.cs
Normal file
14
Backend/src/Fengling.Backend.Web/Application/Hubs/ChatHub.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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生成新的
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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}");
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
21
Backend/src/Fengling.Backend.Web/Dockerfile
Normal file
21
Backend/src/Fengling.Backend.Web/Dockerfile
Normal 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"]
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
25
Backend/src/Fengling.Backend.Web/Endpoints/HelloEndpoint.cs
Normal file
25
Backend/src/Fengling.Backend.Web/Endpoints/HelloEndpoint.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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!;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
68
Backend/src/Fengling.Backend.Web/Fengling.Backend.Web.csproj
Normal file
68
Backend/src/Fengling.Backend.Web/Fengling.Backend.Web.csproj
Normal 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>
|
||||
12
Backend/src/Fengling.Backend.Web/GlobalUsings.cs
Normal file
12
Backend/src/Fengling.Backend.Web/GlobalUsings.cs
Normal 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;
|
||||
245
Backend/src/Fengling.Backend.Web/Program.cs
Normal file
245
Backend/src/Fengling.Backend.Web/Program.cs
Normal 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
|
||||
{
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Backend/src/Fengling.Backend.Web/Utils/AppConfiguration.cs
Normal file
18
Backend/src/Fengling.Backend.Web/Utils/AppConfiguration.cs
Normal 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";
|
||||
}
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Backend/src/Fengling.Backend.Web/appsettings.json
Normal file
28
Backend/src/Fengling.Backend.Web/appsettings.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -0,0 +1,2 @@
|
||||
global using Xunit;
|
||||
global using NetCorePal.Extensions.Primitives;
|
||||
@ -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
Loading…
Reference in New Issue
Block a user