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