refactor: major project restructuring and cleanup

Changes:

- Remove deprecated Fengling.Activity and YarpGateway.Admin projects

- Add points processing services with distributed lock support

- Update Vben frontend with gateway management pages

- Add gateway config controller and database listener

- Update routing to use header-mixed-nav layout

- Add comprehensive test suites for Member services

- Add YarpGateway integration tests

- Update package versions in Directory.Packages.props

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
sam 2026-02-15 10:34:07 +08:00
commit ab8d12527e
98 changed files with 8034 additions and 0 deletions

25
.dockerignore Normal file
View File

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

63
.gitattributes vendored Normal file
View File

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

398
.gitignore vendored Normal file
View File

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

34
Directory.Build.props Normal file
View File

@ -0,0 +1,34 @@
<Project>
<Import Project="$(MSBuildThisFileDirectory)\eng\versions.props"/>
<PropertyGroup>
<Authors>Fengling.Activity</Authors>
<Product>Fengling.Activity</Product>
<owners>Fengling.Activity</owners>
<PackagePrefix>Fengling.Activity</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
Directory.Build.targets Normal file
View File

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

152
Directory.Packages.props Normal file
View File

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

View File

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

7
NuGet.config Normal file
View File

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

230
README.md Normal file
View File

@ -0,0 +1,230 @@
# Fengling.Activity
## 环境准备
### 使用 Aspire推荐
如果您的项目启用了 Aspire 支持(使用 `--UseAspire` 参数创建),只需要 Docker 环境即可,无需手动配置各种基础设施服务。
```bash
# 仅需确保 Docker 环境运行
docker version
# 直接运行 AppHost 项目Aspire 会自动管理所有依赖服务
cd src/Fengling.Activity.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.Activity.sln.DotSettings` 文件中的 Live Templates 配置。
### 可用的代码片段
#### NetCorePal (ncp) 快捷键
| 快捷键 | 描述 | 生成内容 |
|--------|------|----------|
| `ncpcmd` | NetCorePal 命令 | ICommand 实现(含验证器和处理器) |
| `ncpcmdres` | 命令(含返回值) | ICommand&lt;Response&gt; 实现 |
| `ncpar` | 聚合根 | Entity&lt;Id&gt; 和 IAggregateRoot |
| `ncprepo` | NetCorePal 仓储 | IRepository 接口和实现 |
| `ncpie` | 集成事件 | IntegrationEvent 和处理器 |
| `ncpdeh` | 域事件处理器 | IDomainEventHandler 实现 |
| `ncpiec` | 集成事件转换器 | IIntegrationEventConverter |
| `ncpde` | 域事件 | IDomainEvent 记录 |
#### Endpoint (ep) 快捷键
| 快捷键 | 描述 | 生成内容 |
|--------|------|----------|
| `epp` | FastEndpoint(NCP风格) | 完整的垂直切片实现 |
| `epreq` | 仅请求端点 | Endpoint&lt;Request&gt; |
| `epres` | 仅响应端点 | EndpointWithoutRequest&lt;Response&gt; |
| `epdto` | 端点 DTOs | Request 和 Response 类 |
| `epval` | 端点验证器 | Validator&lt;Request&gt; |
| `epmap` | 端点映射器 | Mapper&lt;Request, Response, Entity&gt; |
| `epfull` | 完整端点切片 | 带映射器的完整实现 |
| `epsum` | 端点摘要 | Summary&lt;Endpoint, Request&gt; |
| `epnoreq` | 无请求端点 | EndpointWithoutRequest |
| `epreqres` | 请求响应端点 | Endpoint&lt;Request, Response&gt; |
| `epdat` | 端点数据 | 静态数据类 |
更多详细配置请参考:[vs-snippets/README.md](vs-snippets/README.md)
## 依赖对框架与组件
+ [NetCorePal Cloud Framework](https://github.com/netcorepal/netcorepal-cloud-framework)
+ [ASP.NET Core](https://github.com/dotnet/aspnetcore)
+ [EFCore](https://github.com/dotnet/efcore)
+ [CAP](https://github.com/dotnetcore/CAP)
+ [MediatR](https://github.com/jbogard/MediatR)
+ [FluentValidation](https://docs.fluentvalidation.net/en/latest)
+ [Swashbuckle.AspNetCore.Swagger](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)
## 数据库迁移
```shell
# 安装工具 SEE https://learn.microsoft.com/zh-cn/ef/core/cli/dotnet#installing-the-tools
dotnet tool install --global dotnet-ef --version 9.0.0
# 强制更新数据库
dotnet ef database update -p src/Fengling.Activity.Infrastructure
# 创建迁移 SEEhttps://learn.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli
dotnet ef migrations add InitialCreate -p src/Fengling.Activity.Infrastructure
```
## 代码分析可视化
框架提供了强大的代码流分析和可视化功能帮助开发者直观地理解DDD架构中的组件关系和数据流向。
### 🎯 核心特性
+ **自动代码分析**:通过源生成器自动分析代码结构,识别控制器、命令、聚合根、事件等组件
+ **多种图表类型**:支持架构流程图、命令链路图、事件流程图、类图等多种可视化图表
+ **交互式HTML可视化**生成完整的交互式HTML页面内置导航和图表预览功能
+ **一键在线编辑**:集成"View in Mermaid Live"按钮,支持一键跳转到在线编辑器
### 🚀 快速开始
安装命令行工具来生成独立的HTML文件
```bash
# 安装全局工具
dotnet tool install -g NetCorePal.Extensions.CodeAnalysis.Tools
# 进入项目目录并生成可视化文件
cd src/Fengling.Activity.Web
netcorepal-codeanalysis generate --output architecture.html
```
### ✨ 主要功能
+ **交互式HTML页面**
+ 左侧树形导航,支持不同图表类型切换
+ 内置Mermaid.js实时渲染
+ 响应式设计,适配不同设备
+ 专业的现代化界面
+ **一键在线编辑**
+ 每个图表右上角的"View in Mermaid Live"按钮
+ 智能压缩算法优化URL长度
+ 自动跳转到[Mermaid Live Editor](https://mermaid.live/)
+ 支持在线编辑、导出图片、生成分享链接
### 📖 详细文档
完整的使用说明和示例请参考:
+ [代码流分析文档](https://netcorepal.github.io/netcorepal-cloud-framework/zh/code-analysis/code-flow-analysis/)
+ [代码分析工具文档](https://netcorepal.github.io/netcorepal-cloud-framework/zh/code-analysis/code-analysis-tools/)
## 关于监控
这里使用了`prometheus-net`作为与基础设施prometheus集成的监控方案默认通过地址 `/metrics` 输出监控指标。
更多信息请参见:[https://github.com/prometheus-net/prometheus-net](https://github.com/prometheus-net/prometheus-net)

View File

@ -0,0 +1,58 @@
version: '3.8'
services:
# Activity Service
activity-service:
build:
context: .
dockerfile: Dockerfile
ports:
- "5001:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__PostgreSQL=Host=postgres;Port=15432;Database=fengling_activity;Username=postgres;Password=postgres
- ConnectionStrings__Redis=Host=redis;Port=6379
depends_on:
- postgres
- redis
networks:
- fengling-network
# YARP Gateway
gateway:
image: mcr.microsoft.com/dotnet/aspnet:10.0
ports:
- "5000:8080"
volumes:
- ./gateway-config.json:/app/gateway-config.json:ro
environment:
- ASPNETCORE_ENVIRONMENT=Development
networks:
- fengling-network
# PostgreSQL (shared instance)
postgres:
image: postgres:14
environment:
- POSTGRES_PASSWORD=postgres
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- fengling-network
# Redis (shared instance)
redis:
image: redis:7-alpine
ports:
- "6379:6379"
networks:
- fengling-network
networks:
fengling-network:
driver: bridge
volumes:
postgres-data:

273
docs/GATEWAY_INTEGRATION.md Normal file
View File

@ -0,0 +1,273 @@
# Activity Service Gateway Integration Guide
## Overview
This document describes how to integrate the Activity Engine microservice with the YARP reverse proxy gateway.
## Architecture
```
Client Request
┌─────────────────────────────────┐
│ YARP Gateway │
│ │
│ Route: /api/activity/** │
│ Cluster: activity-service │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ Activity Service │
│ http://localhost:5001 │
│ │
│ GET /api/activity/campaigns │
│ POST /api/activity/campaigns │
│ GET /api/activity/campaigns/.. │
└─────────────────────────────────┘
```
## Configuration Steps
### Option 1: Using Command Line Script
#### Bash (Linux/macOS)
```bash
# Make script executable
chmod +x scripts/gateway/register-activity-service.sh
# Register with global route (all tenants)
./scripts/gateway/register-activity-service.sh \
--gateway-url "http://localhost:5000" \
--service-name "activity" \
--cluster-id "activity-service" \
--address "http://localhost:5001"
# Register with tenant-specific route
./scripts/gateway/register-activity-service.sh \
--gateway-url "http://localhost:5000" \
--tenant-code "tenant001" \
--service-name "activity" \
--address "http://localhost:5001"
```
#### PowerShell (Windows)
```powershell
# Register with global route
pwsh scripts/gateway/Register-ActivityService.ps1 `
-GatewayUrl "http://localhost:5000" `
-ServiceName "activity" `
-ClusterId "activity-service" `
-InstanceAddress "http://localhost:5001" `
-IsGlobal
# Register with tenant-specific route
pwsh scripts/gateway/Register-ActivityService.ps1 `
-GatewayUrl "http://localhost:5000" `
-TenantCode "tenant001" `
-ServiceName "activity" `
-InstanceAddress "http://localhost:5001"
```
### Option 2: Using Gateway Management API
#### 1. Add Service Instance
```bash
curl -X POST "http://localhost:5000/api/gateway/clusters/activity-service/instances" \
-H "Content-Type: application/json" \
-d '{
"destinationId": "activity-1",
"address": "http://localhost:5001",
"weight": 1
}'
```
#### 2. Add Global Route
```bash
curl -X POST "http://localhost:5000/api/gateway/routes/global" \
-H "Content-Type: application/json" \
-d '{
"serviceName": "activity",
"clusterId": "activity-service",
"pathPattern": "/api/activity/{**path}"
}'
```
#### 3. Reload Configuration
```bash
curl -X POST "http://localhost:5000/api/gateway/reload"
```
### Option 3: Database Direct Insert
#### Insert Service Instance
```sql
INSERT INTO "ServiceInstances" (
"Id",
"ClusterId",
"DestinationId",
"Address",
"Health",
"Weight",
"Status",
"CreatedTime"
)
VALUES (
EXTRACT(EPOCH FROM NOW())::bigint * 1000,
'activity-service',
'activity-1',
'http://localhost:5001',
1,
1,
1,
NOW()
);
```
#### Insert Global Route
```sql
INSERT INTO "TenantRoutes" (
"Id",
"TenantCode",
"ServiceName",
"ClusterId",
"PathPattern",
"Priority",
"Status",
"IsGlobal",
"CreatedTime"
)
VALUES (
EXTRACT(EPOCH FROM NOW())::bigint * 1000 + 1,
'',
'activity',
'activity-service',
'/api/activity/{**path}',
0,
1,
true,
NOW()
);
```
## API Endpoints
After configuration, the following endpoints will be available through the gateway:
| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/activity/api/campaigns` | Create a new campaign |
| GET | `/api/activity/api/campaigns` | List all campaigns |
| GET | `/api/activity/api/campaigns/{id}` | Get campaign by ID |
| POST | `/api/activity/api/campaigns/{id}/publish` | Publish a campaign |
## Health Check
The Activity service exposes a health check endpoint at `/health`:
```bash
# Direct access
curl http://localhost:5001/health
# Through gateway
curl http://localhost:5000/api/activity/health
```
## Multiple Instances
For high availability, add multiple service instances:
```bash
# Add second instance
curl -X POST "http://localhost:5000/api/gateway/clusters/activity-service/instances" \
-H "Content-Type: application/json" \
-d '{
"destinationId": "activity-2",
"address": "http://localhost:5002",
"weight": 1
}'
# Reload configuration
curl -X POST "http://localhost:5000/api/gateway/reload"
```
## Load Balancing
The gateway uses **Distributed Weighted Round Robin** load balancing by default. Adjust weights for traffic distribution:
```bash
# Increase weight for a high-performance instance
curl -X PUT "http://localhost:5000/api/gateway/instances/12345" \
-H "Content-Type: application/json" \
-d '{"weight": 2}'
```
## Monitoring
### View Routes
```bash
# Global routes
curl http://localhost:5000/api/gateway/routes/global
# Tenant routes
curl http://localhost:5000/api/gateway/tenants/{tenantCode}/routes
```
### View Instances
```bash
curl http://localhost:5000/api/gateway/clusters/activity-service/instances
```
## Troubleshooting
### Route Not Working
1. Check if route is configured: `GET /api/gateway/routes/global`
2. Verify instance health: `GET /api/gateway/clusters/{clusterId}/instances`
3. Reload configuration: `POST /api/gateway/reload`
### 404 Not Found
- Ensure the path pattern matches the request URL
- Check if the route is active (Status = 1)
### 502 Bad Gateway
- Verify the service instance is running
- Check the service address is accessible from the gateway
- Verify health check endpoint: `GET /health`
## Configuration Reference
### GwServiceInstance Fields
| Field | Type | Description |
|-------|------|-------------|
| ClusterId | string | Unique cluster identifier |
| DestinationId | string | Unique destination identifier |
| Address | string | Service address (http://host:port) |
| Weight | int | Load balancing weight (default: 1) |
| Health | int | Health status (1 = healthy) |
| Status | int | Active status (1 = active) |
### GwTenantRoute Fields
| Field | Type | Description |
|-------|------|-------------|
| TenantCode | string | Tenant identifier (empty for global) |
| ServiceName | string | Logical service name |
| ClusterId | string | Target cluster identifier |
| PathPattern | string | URL path pattern with placeholders |
| Priority | int | Route priority (lower = higher priority) |
| IsGlobal | bool | Whether route applies to all tenants |
## Path Pattern Syntax
YARP uses ASP.NET Core routing syntax for path patterns:
| Pattern | Description |
|---------|-------------|
| `/api/activity` | Exact match |
| `/api/activity/{**path}` | Catch-all segment |
| `/api/activity/{id}` | Parameter segment |
| `/api/activity/{id}/rewards` | Multiple parameters |
For more details, see [ASP.NET Core Routing](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing)

198
docs/NAMING_CONVENTION.md Normal file
View File

@ -0,0 +1,198 @@
# Service Naming Convention
## Overview
This document defines the naming convention for all microservices in the Fengling platform.
## URL Structure
```
{ServicePrefix}/{Version}/{Resource}/{Action}
```
### Components
| Component | Description | Examples |
|-----------|-------------|----------|
| ServicePrefix | Unique service identifier | `activity`, `member`, `order` |
| Version | API version | `v1`, `v2` |
| Resource | Domain entity or resource | `campaigns`, `users`, `orders` |
| Action | Optional action endpoint | `publish`, `cancel` |
## Service Registry
| Service | Prefix | Port | Health Endpoint |
|---------|--------|------|-----------------|
| Activity | `activity` | 5001 | `/health` |
| Member | `member` | 5002 | `/health` |
| Order | `order` | 5003 | `/health` |
| Payment | `payment` | 5004 | `/health` |
| RiskControl | `risk` | 5005 | `/health` |
## Cluster Naming Convention
```
{ServicePrefix}-service
```
Examples:
- `activity-service`
- `member-service`
- `order-service`
- `payment-service`
- `risk-service`
## Gateway Route Configuration
### Global Route Pattern
```
/{ServicePrefix}/{**path} -> {ServicePrefix}-service
```
### Examples
**Activity Service:**
```yaml
Route:
PathPattern: /activity/v1/{**path}
ClusterId: activity-service
Endpoints:
GET /activity/v1/campaigns # List campaigns
POST /activity/v1/campaigns # Create campaign
GET /activity/v1/campaigns/{id} # Get campaign
POST /activity/v1/campaigns/{id}/publish # Publish campaign
```
**Member Service:**
```yaml
Route:
PathPattern: /member/v1/{**path}
ClusterId: member-service
Endpoints:
GET /member/v1/users # List users
POST /member/v1/users # Create user
GET /member/v1/users/{id} # Get user
PUT /member/v1/users/{id} # Update user
```
**Order Service:**
```yaml
Route:
PathPattern: /order/v1/{**path}
ClusterId: order-service
Endpoints:
GET /order/v1/orders # List orders
POST /order/v1/orders # Create order
GET /order/v1/orders/{id} # Get order
PUT /order/v1/orders/{id}/cancel # Cancel order
```
## Gateway Registration
### Activity Service Registration Script
```bash
# Register Activity Service
./scripts/gateway/register-service.sh \
--service-prefix "activity" \
--gateway-url "http://localhost:5000" \
--address "http://localhost:5001"
# After registration, access endpoints at:
# GET http://localhost:5000/activity/v1/campaigns
# POST http://localhost:5000/activity/v1/campaigns
```
## Versioning Strategy
### Version Lifecycle
| Version | Status | Description |
|---------|--------|-------------|
| v1 | Active | Current stable API |
| v2 | Planning | Next major version |
| beta | Testing | Beta releases |
### Versioning Rules
1. **Major Version** (`v1`, `v2`): Breaking changes require version bump
2. **Minor Updates**: Backward-compatible additions don't require version change
3. **Deprecation**: Old versions should be supported for at least 6 months
## Multi-Tenancy Support
### Tenant-Specific Routes
For dedicated tenant instances:
```
/tenant/{tenantCode}/{ServicePrefix}/{Version}/{**path}
```
Example:
```
/tenant/acme/activity/v1/campaigns
```
## Health Check Endpoints
| Service | Health Path |
|---------|-------------|
| Activity | `/health` |
| Member | `/health` |
| Order | `/health` |
| Payment | `/health` |
| Risk | `/health` |
## Monitoring & Metrics
### Service Labels
| Label | Value |
|-------|-------|
| service | `activity` |
| version | `v1` |
| tenant | `{tenantCode}` |
## Best Practices
### 1. Consistent Naming
- Use lowercase for all components
- Use hyphens for multi-word names: `risk-control` NOT `riskControl`
- Avoid abbreviations: `campaigns` NOT `cmps`
### 2. Resource Naming
- Use plural nouns for collections: `campaigns` NOT `campaignList`
- Use singular for single resources: `campaign/{id}` NOT `campaigns/{id}`
### 3. Action Endpoints
- Use HTTP verbs for CRUD: `GET`, `POST`, `PUT`, `DELETE`
- Use specific verbs for actions: `publish`, `cancel`, `activate`
### 4. Path Parameters
- Use descriptive names: `/campaigns/{campaignId}` NOT `/campaigns/{id}`
- Consistent parameter naming across services
## Migration Guide
### Updating Existing Routes
**Before:**
```
/api/campaigns
/api/orders
```
**After:**
```
/activity/v1/campaigns
/order/v1/orders
```
**Migration Strategy:**
1. Register new routes with v1
2. Keep old routes active (alias)
3. Update clients to use new format
4. Remove old routes after transition period

6
eng/versions.props Normal file
View File

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

7
global.json Normal file
View File

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

151
scripts/EXAMPLES.md Normal file
View File

@ -0,0 +1,151 @@
# Usage Examples
This document provides practical examples for using the infrastructure initialization scripts.
## Quick Start Examples
### Default Setup (MySQL + Redis + RabbitMQ)
```bash
# Using Docker Compose (Recommended)
docker compose up -d
# Using shell script (Linux/macOS)
./init-infrastructure.sh
# Using PowerShell (Windows)
.\init-infrastructure.ps1
```
### Different Database Options
```bash
# Use PostgreSQL instead of MySQL
docker compose --profile postgres up -d
# Use SQL Server instead of MySQL
docker compose --profile sqlserver up -d
# With PowerShell
.\init-infrastructure.ps1 -Postgres
.\init-infrastructure.ps1 -SqlServer
```
### Different Message Queue Options
```bash
# Use Kafka instead of RabbitMQ
docker compose --profile kafka up -d
# With PowerShell
.\init-infrastructure.ps1 -Kafka
```
### Cleanup Examples
```bash
# Stop services, keep data
docker compose down
./clean-infrastructure.sh
.\clean-infrastructure.ps1
# Stop services and remove all data
docker compose down -v
./clean-infrastructure.sh --volumes
.\clean-infrastructure.ps1 -Volumes
```
## Development Workflow
### Typical Development Session
```bash
# 1. Start infrastructure
cd scripts
docker compose up -d
# 2. Develop your application
cd ../src/Fengling.Activity.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=15432;Database=abctemplate;Username=postgres;Password=123456;"
},
"RabbitMQ": {
"HostName": "localhost",
"Port": 5672,
"UserName": "guest",
"Password": "guest",
"VirtualHost": "/"
},
"Kafka": {
"BootstrapServers": "localhost:9092"
}
}
```

56
scripts/README.md Normal file
View File

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

View File

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

View File

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

167
scripts/docker-compose.yml Normal file
View File

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

View File

@ -0,0 +1,135 @@
# Activity服务接入YARP网关配置脚本
# PowerShell: pwsh Register-ActivityService.ps1 -GatewayUrl "http://localhost:5000"
param(
[string]$GatewayUrl = "http://localhost:5000",
[string]$ServiceName = "activity",
[string]$ClusterId = "activity-service",
[string]$PathPattern = "/api/activity/{**path}",
[string]$InstanceAddress = "http://localhost:5001",
[string]$DestinationId = "activity-1",
[int]$Weight = 1,
[switch]$IsGlobal = $true,
[string]$TenantCode = ""
)
$ErrorActionPreference = "Stop"
function Register-ActivityService {
param(
[string]$GatewayUrl,
[string]$ServiceName,
[string]$ClusterId,
[string]$PathPattern,
[string]$InstanceAddress,
[string]$DestinationId,
[int]$Weight,
[bool]$IsGlobal,
[string]$TenantCode
)
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Activity Service Gateway Configuration" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# Step 1: Add Service Instance (Cluster)
Write-Host "[Step 1] Adding service instance to cluster..." -ForegroundColor Yellow
$instanceBody = @{
destinationId = $DestinationId
address = $InstanceAddress
weight = $Weight
} | ConvertTo-Json
try {
$instanceResponse = Invoke-RestMethod -Uri "$GatewayUrl/api/gateway/clusters/$ClusterId/instances" `
-Method Post `
-ContentType "application/json" `
-Body $instanceBody
Write-Host " ✓ Instance added: $DestinationId -> $InstanceAddress" -ForegroundColor Green
}
catch {
if ($_.Exception.Response.StatusCode -eq "BadRequest") {
Write-Host " Instance already exists, skipping..." -ForegroundColor Gray
}
else {
throw $_
}
}
# Step 2: Add Route
Write-Host ""
Write-Host "[Step 2] Adding gateway route..." -ForegroundColor Yellow
if ($IsGlobal) {
$routeBody = @{
serviceName = $ServiceName
clusterId = $ClusterId
pathPattern = $PathPattern
} | ConvertTo-Json
$routeUrl = "$GatewayUrl/api/gateway/routes/global"
$routeDescription = "global route"
}
else {
$routeBody = @{
serviceName = $ServiceName
pathPattern = $PathPattern
} | ConvertTo-Json
$routeUrl = "$GatewayUrl/api/gateway/tenants/$TenantCode/routes"
$routeDescription = "tenant route for $TenantCode"
}
try {
$routeResponse = Invoke-RestMethod -Uri $routeUrl `
-Method Post `
-ContentType "application/json" `
-Body $routeBody
Write-Host " ✓ Route added: $PathPattern -> $ClusterId" -ForegroundColor Green
}
catch {
if ($_.Exception.Response.StatusCode -eq "BadRequest") {
Write-Host " Route already exists, skipping..." -ForegroundColor Gray
}
else {
throw $_
}
}
# Step 3: Reload Config
Write-Host ""
Write-Host "[Step 3] Reloading gateway configuration..." -ForegroundColor Yellow
$reloadResponse = Invoke-RestMethod -Uri "$GatewayUrl/api/gateway/reload" -Method Post
Write-Host " ✓ Configuration reloaded" -ForegroundColor Green
# Summary
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Configuration Complete!" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Service: $ServiceName" -ForegroundColor White
Write-Host "Cluster: $ClusterId" -ForegroundColor White
Write-Host "Route: $routeDescription" -ForegroundColor White
Write-Host "Pattern: $PathPattern" -ForegroundColor White
Write-Host "Target: $InstanceAddress" -ForegroundColor White
Write-Host ""
Write-Host "Test the service at:" -ForegroundColor Yellow
Write-Host " $GatewayUrl$($PathPattern.Replace('{**path}', 'campaigns'))" -ForegroundColor Cyan
Write-Host ""
}
# Execute
Register-ActivityService -GatewayUrl $GatewayUrl `
-ServiceName $ServiceName `
-ClusterId $ClusterId `
-PathPattern $PathPattern `
-InstanceAddress $InstanceAddress `
-DestinationId $DestinationId `
-Weight $Weight `
-IsGlobal $IsGlobal.IsPresent `
-TenantCode $TenantCode

View File

@ -0,0 +1,118 @@
# Service Gateway Registration Script
param(
[string]$Prefix = "activity",
[string]$Version = "v1",
[string]$GatewayUrl = "http://localhost:5000",
[string]$Address = "http://localhost:5001",
[string]$DestinationId = $null,
[int]$Weight = 1,
[switch]$Global = $true,
[string]$TenantCode = ""
)
$ErrorActionPreference = "Stop"
# Derived values
$ClusterId = "$Prefix-service"
if ([string]::IsNullOrEmpty($DestinationId)) {
$DestinationId = "$Prefix-1"
}
$PathPattern = "/$Prefix/$Version/{**path}"
Write-Host "╔════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
Write-Host "║ Service Gateway Registration ║" -ForegroundColor Cyan
Write-Host "╚════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
Write-Host ""
# Step 1: Add Service Instance
Write-Host "[1/3] Adding service instance..." -ForegroundColor Yellow
$instanceBody = @{
destinationId = $DestinationId
address = $Address
weight = $Weight
} | ConvertTo-Json
try {
$null = Invoke-RestMethod -Uri "$GatewayUrl/api/gateway/clusters/$ClusterId/instances" `
-Method Post `
-ContentType "application/json" `
-Body $instanceBody
Write-Host " ✓ Instance: $DestinationId -> $Address" -ForegroundColor Green
}
catch {
if ($_.Exception.Response.StatusCode -eq "BadRequest") {
Write-Host " Instance may already exist" -ForegroundColor Gray
}
else {
throw $_
}
}
# Step 2: Add Route
Write-Host ""
Write-Host "[2/3] Configuring gateway route..." -ForegroundColor Yellow
if ($Global) {
$routeBody = @{
serviceName = $Prefix
clusterId = $ClusterId
pathPattern = $PathPattern
} | ConvertTo-Json
$routeType = "Global"
}
else {
$routeBody = @{
serviceName = $Prefix
pathPattern = $PathPattern
} | ConvertTo-Json
$routeType = "Tenant [$TenantCode]"
}
try {
$routeUrl = if ($Global) {
"$GatewayUrl/api/gateway/routes/global"
}
else {
"$GatewayUrl/api/gateway/tenants/$TenantCode/routes"
}
$null = Invoke-RestMethod -Uri $routeUrl `
-Method Post `
-ContentType "application/json" `
-Body $routeBody
Write-Host " ✓ Route: $PathPattern -> $ClusterId" -ForegroundColor Green
}
catch {
if ($_.Exception.Response.StatusCode -eq "BadRequest") {
Write-Host " Route may already exist" -ForegroundColor Gray
}
else {
throw $_
}
}
# Step 3: Reload Config
Write-Host ""
Write-Host "[3/3] Reloading gateway configuration..." -ForegroundColor Yellow
$null = Invoke-RestMethod -Uri "$GatewayUrl/api/gateway/reload" -Method Post
Write-Host " ✓ Configuration reloaded" -ForegroundColor Green
# Summary
Write-Host ""
Write-Host "════════════════════════════════════════════════════════════" -ForegroundColor Cyan
Write-Host " Service Registered Successfully!" -ForegroundColor Cyan
Write-Host "════════════════════════════════════════════════════════════" -ForegroundColor Cyan
Write-Host ""
Write-Host " Service: $Prefix" -ForegroundColor White
Write-Host " Version: $Version" -ForegroundColor White
Write-Host " Cluster: $ClusterId" -ForegroundColor White
Write-Host " Type: $routeType" -ForegroundColor White
Write-Host " Pattern: $PathPattern" -ForegroundColor White
Write-Host " Address: $Address" -ForegroundColor White
Write-Host ""
Write-Host " Available Endpoints:" -ForegroundColor Yellow
Write-Host " GET $GatewayUrl/$Prefix/$Version/campaigns" -ForegroundColor Cyan
Write-Host " POST $GatewayUrl/$Prefix/$Version/campaigns" -ForegroundColor Cyan
Write-Host " GET $GatewayUrl/$Prefix/$Version/campaigns/{id}" -ForegroundColor Cyan
Write-Host " POST $GatewayUrl/$Prefix/$Version/campaigns/{id}/publish" -ForegroundColor Cyan
Write-Host ""

View File

@ -0,0 +1,185 @@
#!/bin/bash
# Service Gateway Registration Script
# Usage: ./register-service.sh [OPTIONS]
#
# Options:
# -p, --prefix Service prefix (default: activity)
# -v, --version API version (default: v1)
# -g, --gateway-url Gateway URL (default: http://localhost:5000)
# -a, --address Service address (default: http://localhost:5001)
# -d, --destination Destination ID (default: {prefix}-1)
# -w, --weight Instance weight (default: 1)
# -G, --global Create global route (default: true)
# -t, --tenant-code Tenant code for tenant-specific route
# -h, --help Show this help
set -e
# Default values
SERVICE_PREFIX="activity"
API_VERSION="v1"
GATEWAY_URL="http://localhost:5000"
SERVICE_ADDRESS="http://localhost:5001"
WEIGHT=1
IS_GLOBAL=true
TENANT_CODE=""
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-p|--prefix)
SERVICE_PREFIX="$2"
shift 2
;;
-v|--version)
API_VERSION="$2"
shift 2
;;
-g|--gateway-url)
GATEWAY_URL="$2"
shift 2
;;
-a|--address)
SERVICE_ADDRESS="$2"
shift 2
;;
-d|--destination)
DESTINATION_ID="$2"
shift 2
;;
-w|--weight)
WEIGHT="$2"
shift 2
;;
-G|--global)
IS_GLOBAL=true
shift
;;
-t|--tenant-code)
IS_GLOBAL=false
TENANT_CODE="$2"
shift 2
;;
-h|--help)
grep -A 50 '# Usage:' "$0" | tail -n +2 | head -n 20
exit 0
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
# Set derived values
CLUSTER_ID="${SERVICE_PREFIX}-service"
DESTINATION_ID="${DESTINATION_ID:-${SERVICE_PREFIX}-1}"
PATH_PATTERN="/${SERVICE_PREFIX}/${API_VERSION}/{**path}"
echo -e "${CYAN}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║ Service Gateway Registration ║${NC}"
echo -e "${CYAN}╚════════════════════════════════════════════════════════════╝${NC}"
echo ""
# Step 1: Add Service Instance
echo -e "${YELLOW}[1/3] Adding service instance...${NC}"
INSTANCE_BODY=$(cat <<EOF
{
"destinationId": "$DESTINATION_ID",
"address": "$SERVICE_ADDRESS",
"weight": $WEIGHT
}
EOF
)
HTTP_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
"$GATEWAY_URL/api/gateway/clusters/$CLUSTER_ID/instances" \
-H "Content-Type: application/json" \
-d "$INSTANCE_BODY")
HTTP_CODE=$(echo "$HTTP_RESPONSE" | tail -n 1)
RESPONSE_BODY=$(echo "$HTTP_RESPONSE" | sed '$d')
if [ "$HTTP_CODE" -eq 200 ]; then
echo -e " ${GREEN}✓ Instance: $DESTINATION_ID -> $SERVICE_ADDRESS${NC}"
else
echo -e " ${YELLOW} Instance may already exist${NC}"
fi
# Step 2: Add Route
echo ""
echo -e "${YELLOW}[2/3] Configuring gateway route...${NC}"
if [ "$IS_GLOBAL" = true ]; then
ROUTE_URL="$GATEWAY_URL/api/gateway/routes/global"
ROUTE_BODY=$(cat <<EOF
{
"serviceName": "$SERVICE_PREFIX",
"clusterId": "$CLUSTER_ID",
"pathPattern": "$PATH_PATTERN"
}
EOF
)
ROUTE_TYPE="Global"
else
ROUTE_URL="$GATEWAY_URL/api/gateway/tenants/$TENANT_CODE/routes"
ROUTE_TYPE="Tenant [$TENANT_CODE]"
ROUTE_BODY=$(cat <<EOF
{
"serviceName": "$SERVICE_PREFIX",
"pathPattern": "$PATH_PATTERN"
}
EOF
)
fi
HTTP_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
"$ROUTE_URL" \
-H "Content-Type: application/json" \
-d "$ROUTE_BODY")
HTTP_CODE=$(echo "$HTTP_RESPONSE" | tail -n 1)
RESPONSE_BODY=$(echo "$HTTP_RESPONSE" | sed '$d')
if [ "$HTTP_CODE" -eq 200 ]; then
echo -e " ${GREEN}✓ Route: $PATH_PATTERN -> $CLUSTER_ID${NC}"
else
echo -e " ${YELLOW} Route may already exist${NC}"
fi
# Step 3: Reload Config
echo ""
echo -e "${YELLOW}[3/3] Reloading gateway configuration...${NC}"
curl -s -X POST "$GATEWAY_URL/api/gateway/reload" > /dev/null
echo -e " ${GREEN}✓ Configuration reloaded${NC}"
# Summary
echo ""
echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}"
echo -e "${CYAN} Service Registered Successfully!${NC}"
echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}"
echo ""
echo -e " ${WHITE}Service:${NC} $SERVICE_PREFIX"
echo -e " ${WHITE}Version:${NC} $API_VERSION"
echo -e " ${WHITE}Cluster:${NC} $CLUSTER_ID"
echo -e " ${WHITE}Type:${NC} $ROUTE_TYPE"
echo -e " ${WHITE}Pattern:${NC} $PATH_PATTERN"
echo -e " ${WHITE}Address:${NC} $SERVICE_ADDRESS"
echo ""
echo -e " ${CYAN}Available Endpoints:${NC}"
echo -e " GET $GATEWAY_URL/${SERVICE_PREFIX}/${API_VERSION}/campaigns"
echo -e " POST $GATEWAY_URL/${SERVICE_PREFIX}/${API_VERSION}/campaigns"
echo -e " GET $GATEWAY_URL/${SERVICE_PREFIX}/${API_VERSION}/campaigns/\{id\}"
echo -e " POST $GATEWAY_URL/${SERVICE_PREFIX}/${API_VERSION}/campaigns/\{id\}/publish"
echo ""

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,139 @@
namespace Fengling.Activity.Domain.Aggregates.Campaigns;
using Fengling.Activity.Domain.Enums;
using Fengling.Activity.Domain.Events.Campaigns;
using Fengling.Activity.Domain.ValueObjects;
public class Campaign : Entity<CampaignId>, IAggregateRoot
{
public TenantId TenantId { get; private set; } = null!;
public CampaignName Name { get; private set; } = null!;
public CampaignType Type { get; private set; }
public CampaignStatus Status { get; private set; } = CampaignStatus.Draft;
public string? Description { get; private set; }
public TimeRange TimeRange { get; private set; } = null!;
public int? MaxParticipants { get; private set; }
public int? MaxRewardsPerMember { get; private set; }
private readonly List<ConditionConfig> _conditions = new();
public IReadOnlyCollection<ConditionConfig> Conditions => _conditions.AsReadOnly();
private readonly List<ConstraintConfig> _constraints = new();
public IReadOnlyCollection<ConstraintConfig> Constraints => _constraints.AsReadOnly();
private readonly List<ActionConfig> _actions = new();
public IReadOnlyCollection<ActionConfig> Actions => _actions.AsReadOnly();
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; private set; }
public int Version { get; private set; } = 1;
private Campaign()
{
}
public static Campaign Create(
TenantId tenantId,
CampaignName name,
CampaignType type,
TimeRange timeRange,
string? description = null)
{
var campaign = new Campaign
{
TenantId = tenantId,
Name = name,
Type = type,
TimeRange = timeRange,
Description = description,
Status = CampaignStatus.Draft,
CreatedAt = DateTime.UtcNow
};
campaign.AddDomainEvent(new CampaignCreatedEvent(campaign.Id, tenantId, name, type, timeRange, campaign.CreatedAt));
return campaign;
}
public void Publish()
{
if (Status != CampaignStatus.Draft && Status != CampaignStatus.Paused)
throw new InvalidOperationException("Only draft or paused campaigns can be published");
if (!TimeRange.IsActive())
throw new InvalidOperationException("Campaign time range must be active to publish");
Status = CampaignStatus.Published;
UpdatedAt = DateTime.UtcNow;
Version++;
AddDomainEvent(new CampaignPublishedEvent(Id, TenantId, DateTime.UtcNow));
}
public void Pause()
{
if (Status != CampaignStatus.Published)
throw new InvalidOperationException("Only published campaigns can be paused");
Status = CampaignStatus.Paused;
UpdatedAt = DateTime.UtcNow;
Version++;
AddDomainEvent(new CampaignPausedEvent(Id, TenantId, DateTime.UtcNow));
}
public void Complete()
{
if (Status != CampaignStatus.Published)
throw new InvalidOperationException("Only published campaigns can be completed");
Status = CampaignStatus.Completed;
UpdatedAt = DateTime.UtcNow;
Version++;
AddDomainEvent(new CampaignCompletedEvent(Id, TenantId, DateTime.UtcNow));
}
public void Cancel()
{
if (Status == CampaignStatus.Completed)
throw new InvalidOperationException("Completed campaigns cannot be cancelled");
Status = CampaignStatus.Cancelled;
UpdatedAt = DateTime.UtcNow;
Version++;
AddDomainEvent(new CampaignCancelledEvent(Id, TenantId, DateTime.UtcNow));
}
public void AddCondition(ConditionConfig condition)
{
_conditions.Add(condition);
UpdatedAt = DateTime.UtcNow;
}
public void AddConstraint(ConstraintConfig constraint)
{
_constraints.Add(constraint);
UpdatedAt = DateTime.UtcNow;
}
public void AddAction(ActionConfig action)
{
_actions.Add(action);
UpdatedAt = DateTime.UtcNow;
}
public void UpdateTimeRange(TimeRange timeRange)
{
TimeRange = timeRange;
UpdatedAt = DateTime.UtcNow;
}
public void UpdateMaxParticipants(int? maxParticipants)
{
MaxParticipants = maxParticipants;
UpdatedAt = DateTime.UtcNow;
}
public void UpdateMaxRewardsPerMember(int? maxRewards)
{
MaxRewardsPerMember = maxRewards;
UpdatedAt = DateTime.UtcNow;
}
}

View File

@ -0,0 +1,90 @@
namespace Fengling.Activity.Domain.Aggregates.ParticipationRecords;
using Fengling.Activity.Domain.Enums;
using Fengling.Activity.Domain.Events.ParticipationRecords;
using Fengling.Activity.Domain.ValueObjects;
public class ParticipationRecord : Entity<ParticipationRecordId>, IAggregateRoot
{
public CampaignId CampaignId { get; private set; } = null!;
public TenantId TenantId { get; private set; } = null!;
public Guid MemberId { get; private set; }
public ParticipationStatus Status { get; private set; } = ParticipationStatus.Participating;
public int CurrentProgress { get; private set; }
public int? TargetProgress { get; private set; }
public int RewardsGranted { get; private set; }
public DateTime ParticipatedAt { get; private set; } = DateTime.UtcNow;
public DateTime? CompletedAt { get; private set; }
public DateTime? ExpiredAt { get; private set; }
public DateTime? UpdatedAt { get; private set; }
private ParticipationRecord()
{
}
public static ParticipationRecord Create(
CampaignId campaignId,
TenantId tenantId,
Guid memberId,
int? targetProgress = null)
{
var record = new ParticipationRecord
{
CampaignId = campaignId,
TenantId = tenantId,
MemberId = memberId,
TargetProgress = targetProgress,
Status = ParticipationStatus.Participating,
ParticipatedAt = DateTime.UtcNow
};
record.AddDomainEvent(new TaskParticipatedEvent(record.Id, campaignId, tenantId, memberId, record.ParticipatedAt));
return record;
}
public void UpdateProgress(int progress)
{
CurrentProgress = progress;
if (TargetProgress.HasValue && CurrentProgress >= TargetProgress.Value)
{
Complete();
}
UpdatedAt = DateTime.UtcNow;
}
public void Complete()
{
if (Status == ParticipationStatus.Completed)
return;
Status = ParticipationStatus.Completed;
CompletedAt = DateTime.UtcNow;
AddDomainEvent(new TaskCompletedEvent(Id, CampaignId, TenantId, MemberId, CompletedAt.Value));
UpdatedAt = DateTime.UtcNow;
}
public void Cancel()
{
if (Status == ParticipationStatus.Cancelled)
return;
Status = ParticipationStatus.Cancelled;
UpdatedAt = DateTime.UtcNow;
}
public void Expire()
{
if (Status != ParticipationStatus.Participating)
return;
Status = ParticipationStatus.Expired;
ExpiredAt = DateTime.UtcNow;
UpdatedAt = DateTime.UtcNow;
}
public void GrantReward()
{
RewardsGranted++;
UpdatedAt = DateTime.UtcNow;
}
}

View File

@ -0,0 +1,18 @@
namespace Fengling.Activity.Domain.Aggregates.ParticipationRecords;
public class ParticipationRecordId : IEquatable<ParticipationRecordId>
{
public Guid Value { get; }
private ParticipationRecordId(Guid value) => Value = value;
public static ParticipationRecordId New() => new(Guid.NewGuid());
public static ParticipationRecordId FromGuid(Guid value) => new(value);
public static ParticipationRecordId Parse(string value) => new(Guid.Parse(value));
public bool Equals(ParticipationRecordId? other) => other?.Value == Value;
public override bool Equals(object? obj) => obj is ParticipationRecordId other && Equals(other);
public override int GetHashCode() => Value.GetHashCode();
public static implicit operator Guid(ParticipationRecordId id) => id.Value;
public override string ToString() => Value.ToString();
}

View File

@ -0,0 +1,46 @@
namespace Fengling.Activity.Domain.Aggregates.RewardGrantRecords;
using Fengling.Activity.Domain.Aggregates.ParticipationRecords;
using Fengling.Activity.Domain.Enums;
using Fengling.Activity.Domain.ValueObjects;
public class RewardGrantRecord : Entity<RewardGrantRecordId>, IAggregateRoot
{
public CampaignId CampaignId { get; private set; } = null!;
public TenantId TenantId { get; private set; } = null!;
public Guid MemberId { get; private set; }
public ParticipationRecordId? ParticipationRecordId { get; private set; }
public RewardType RewardType { get; private set; }
public int RewardAmount { get; private set; }
public string? RewardCode { get; private set; }
public string? Remark { get; private set; }
public DateTime GrantedAt { get; private set; } = DateTime.UtcNow;
private RewardGrantRecord()
{
}
public static RewardGrantRecord Create(
CampaignId campaignId,
TenantId tenantId,
Guid memberId,
ParticipationRecordId? participationRecordId,
RewardType rewardType,
int rewardAmount,
string? rewardCode = null,
string? remark = null)
{
return new RewardGrantRecord
{
CampaignId = campaignId,
TenantId = tenantId,
MemberId = memberId,
ParticipationRecordId = participationRecordId,
RewardType = rewardType,
RewardAmount = rewardAmount,
RewardCode = rewardCode,
Remark = remark,
GrantedAt = DateTime.UtcNow
};
}
}

View File

@ -0,0 +1,18 @@
namespace Fengling.Activity.Domain.Aggregates.RewardGrantRecords;
public class RewardGrantRecordId : IEquatable<RewardGrantRecordId>
{
public Guid Value { get; }
private RewardGrantRecordId(Guid value) => Value = value;
public static RewardGrantRecordId New() => new(Guid.NewGuid());
public static RewardGrantRecordId FromGuid(Guid value) => new(value);
public static RewardGrantRecordId Parse(string value) => new(Guid.Parse(value));
public bool Equals(RewardGrantRecordId? other) => other?.Value == Value;
public override bool Equals(object? obj) => obj is RewardGrantRecordId other && Equals(other);
public override int GetHashCode() => Value.GetHashCode();
public static implicit operator Guid(RewardGrantRecordId id) => id.Value;
public override string ToString() => Value.ToString();
}

View File

@ -0,0 +1,10 @@
namespace Fengling.Activity.Domain.Enums;
public enum CampaignStatus
{
Draft = 0,
Published = 1,
Paused = 2,
Completed = 3,
Cancelled = 4
}

View File

@ -0,0 +1,12 @@
namespace Fengling.Activity.Domain.Enums;
public enum CampaignType
{
Task = 1,
Recurring = 2,
Consumption = 3,
Social = 4,
Gamification = 5,
MemberExclusive = 6,
Festival = 7
}

View File

@ -0,0 +1,9 @@
namespace Fengling.Activity.Domain.Enums;
public enum ParticipationStatus
{
Participating = 1,
Completed = 2,
Expired = 3,
Cancelled = 4
}

View File

@ -0,0 +1,11 @@
namespace Fengling.Activity.Domain.Enums;
public enum RewardType
{
Points = 1,
Coupon = 2,
Physical = 3,
Virtual = 4,
Currency = 5,
Experience = 6
}

View File

@ -0,0 +1,9 @@
namespace Fengling.Activity.Domain.Events.Campaigns;
using Fengling.Activity.Domain.ValueObjects;
using NetCorePal.Extensions.Domain;
public record CampaignCancelledEvent(
CampaignId CampaignId,
TenantId TenantId,
DateTime CancelledAt) : IDomainEvent;

View File

@ -0,0 +1,9 @@
namespace Fengling.Activity.Domain.Events.Campaigns;
using Fengling.Activity.Domain.ValueObjects;
using NetCorePal.Extensions.Domain;
public record CampaignCompletedEvent(
CampaignId CampaignId,
TenantId TenantId,
DateTime CompletedAt) : IDomainEvent;

View File

@ -0,0 +1,13 @@
namespace Fengling.Activity.Domain.Events.Campaigns;
using Fengling.Activity.Domain.Enums;
using Fengling.Activity.Domain.ValueObjects;
using NetCorePal.Extensions.Domain;
public record CampaignCreatedEvent(
CampaignId CampaignId,
TenantId TenantId,
CampaignName Name,
CampaignType Type,
TimeRange TimeRange,
DateTime CreatedAt) : IDomainEvent;

View File

@ -0,0 +1,9 @@
namespace Fengling.Activity.Domain.Events.Campaigns;
using Fengling.Activity.Domain.ValueObjects;
using NetCorePal.Extensions.Domain;
public record CampaignPausedEvent(
CampaignId CampaignId,
TenantId TenantId,
DateTime PausedAt) : IDomainEvent;

View File

@ -0,0 +1,9 @@
namespace Fengling.Activity.Domain.Events.Campaigns;
using Fengling.Activity.Domain.ValueObjects;
using NetCorePal.Extensions.Domain;
public record CampaignPublishedEvent(
CampaignId CampaignId,
TenantId TenantId,
DateTime PublishedAt) : IDomainEvent;

View File

@ -0,0 +1,12 @@
namespace Fengling.Activity.Domain.Events.ParticipationRecords;
using Fengling.Activity.Domain.Aggregates.ParticipationRecords;
using Fengling.Activity.Domain.ValueObjects;
using NetCorePal.Extensions.Domain;
public record TaskCompletedEvent(
ParticipationRecordId RecordId,
CampaignId CampaignId,
TenantId TenantId,
Guid MemberId,
DateTime CompletedAt) : IDomainEvent;

View File

@ -0,0 +1,12 @@
namespace Fengling.Activity.Domain.Events.ParticipationRecords;
using Fengling.Activity.Domain.Aggregates.ParticipationRecords;
using Fengling.Activity.Domain.ValueObjects;
using NetCorePal.Extensions.Domain;
public record TaskParticipatedEvent(
ParticipationRecordId RecordId,
CampaignId CampaignId,
TenantId TenantId,
Guid MemberId,
DateTime ParticipatedAt) : IDomainEvent;

View File

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

View File

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

View File

@ -0,0 +1,17 @@
namespace Fengling.Activity.Domain.Repositories;
using Fengling.Activity.Domain.Aggregates.Campaigns;
using Fengling.Activity.Domain.Enums;
using Fengling.Activity.Domain.ValueObjects;
public interface ICampaignRepository
{
Task<Campaign?> GetByIdAsync(CampaignId id, CancellationToken cancellationToken = default);
Task<List<Campaign>> GetByTenantIdAsync(TenantId tenantId, CancellationToken cancellationToken = default);
Task<List<Campaign>> GetByStatusAsync(CampaignStatus status, CancellationToken cancellationToken = default);
Task<List<Campaign>> GetActiveCampaignsAsync(TenantId tenantId, CancellationToken cancellationToken = default);
Task AddAsync(Campaign campaign, CancellationToken cancellationToken = default);
Task UpdateAsync(Campaign campaign, CancellationToken cancellationToken = default);
Task DeleteAsync(Campaign campaign, CancellationToken cancellationToken = default);
Task<bool> ExistsAsync(CampaignId id, CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,19 @@
namespace Fengling.Activity.Domain.Repositories;
using Fengling.Activity.Domain.Aggregates.Campaigns;
using Fengling.Activity.Domain.Aggregates.ParticipationRecords;
using Fengling.Activity.Domain.Enums;
using Fengling.Activity.Domain.ValueObjects;
public interface IParticipationRecordRepository
{
Task<ParticipationRecord?> GetByIdAsync(ParticipationRecordId id, CancellationToken cancellationToken = default);
Task<List<ParticipationRecord>> GetByCampaignIdAsync(CampaignId campaignId, CancellationToken cancellationToken = default);
Task<List<ParticipationRecord>> GetByMemberIdAsync(TenantId tenantId, Guid memberId, CancellationToken cancellationToken = default);
Task<ParticipationRecord?> GetByCampaignAndMemberAsync(CampaignId campaignId, Guid memberId, CancellationToken cancellationToken = default);
Task<List<ParticipationRecord>> GetByStatusAsync(CampaignId campaignId, ParticipationStatus status, CancellationToken cancellationToken = default);
Task<int> GetParticipationCountAsync(CampaignId campaignId, Guid memberId, CancellationToken cancellationToken = default);
Task AddAsync(ParticipationRecord record, CancellationToken cancellationToken = default);
Task UpdateAsync(ParticipationRecord record, CancellationToken cancellationToken = default);
Task DeleteAsync(ParticipationRecord record, CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,18 @@
namespace Fengling.Activity.Domain.Repositories;
using Fengling.Activity.Domain.Aggregates.Campaigns;
using Fengling.Activity.Domain.Aggregates.RewardGrantRecords;
using Fengling.Activity.Domain.Enums;
using Fengling.Activity.Domain.ValueObjects;
public interface IRewardGrantRecordRepository
{
Task<RewardGrantRecord?> GetByIdAsync(RewardGrantRecordId id, CancellationToken cancellationToken = default);
Task<List<RewardGrantRecord>> GetByCampaignIdAsync(CampaignId campaignId, CancellationToken cancellationToken = default);
Task<List<RewardGrantRecord>> GetByMemberIdAsync(TenantId tenantId, Guid memberId, CancellationToken cancellationToken = default);
Task<int> GetRewardCountAsync(CampaignId campaignId, Guid memberId, CancellationToken cancellationToken = default);
Task AddAsync(RewardGrantRecord record, CancellationToken cancellationToken = default);
Task UpdateAsync(RewardGrantRecord record, CancellationToken cancellationToken = default);
Task DeleteAsync(RewardGrantRecord record, CancellationToken cancellationToken = default);
Task<List<RewardGrantRecord>> GetRewardsByDateRangeAsync(TenantId tenantId, DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,158 @@
namespace Fengling.Activity.Domain.Services;
using Fengling.Activity.Domain.Aggregates.Campaigns;
using Fengling.Activity.Domain.Aggregates.ParticipationRecords;
using Fengling.Activity.Domain.Aggregates.RewardGrantRecords;
using Fengling.Activity.Domain.Enums;
using Fengling.Activity.Domain.Repositories;
using Fengling.Activity.Domain.Strategies;
using Fengling.Activity.Domain.ValueObjects;
public class CampaignExecutionService
{
private readonly ICampaignRepository _campaignRepository;
private readonly IParticipationRecordRepository _participationRepository;
private readonly IRewardGrantRecordRepository _rewardRepository;
private readonly IStrategyFactory _strategyFactory;
public CampaignExecutionService(
ICampaignRepository campaignRepository,
IParticipationRecordRepository participationRepository,
IRewardGrantRecordRepository rewardRepository,
IStrategyFactory strategyFactory)
{
_campaignRepository = campaignRepository;
_participationRepository = participationRepository;
_rewardRepository = rewardRepository;
_strategyFactory = strategyFactory;
}
public async Task<CampaignExecutionResult> ExecuteCampaignAsync(
CampaignId campaignId,
TenantId tenantId,
Guid memberId,
string? memberLevel = null,
CancellationToken cancellationToken = default)
{
var campaign = await _campaignRepository.GetByIdAsync(campaignId, cancellationToken);
if (campaign == null)
{
return CampaignExecutionResult.Fail("Campaign not found");
}
if (campaign.TenantId != tenantId)
{
return CampaignExecutionResult.Fail("Campaign does not belong to this tenant");
}
if (campaign.Status != CampaignStatus.Published)
{
return CampaignExecutionResult.Fail("Campaign is not active");
}
var now = DateTime.UtcNow;
if (!campaign.TimeRange.Contains(now))
{
return CampaignExecutionResult.Fail("Campaign is not within valid time range");
}
var existingRecord = await _participationRepository.GetByCampaignAndMemberAsync(campaignId, memberId, cancellationToken);
if (existingRecord != null && existingRecord.Status != ParticipationStatus.Cancelled)
{
return CampaignExecutionResult.Fail("Member has already participated in this campaign");
}
var context = new CampaignContext
{
CampaignId = campaignId.Value,
TenantId = tenantId.Value,
MemberId = memberId,
MemberLevel = memberLevel,
CurrentTime = now
};
foreach (var condition in campaign.Conditions)
{
var strategy = _strategyFactory.GetConditionStrategy(condition.StrategyType);
if (strategy == null)
{
return CampaignExecutionResult.Fail($"Condition strategy not found: {condition.StrategyType}");
}
var result = await strategy.ExecuteAsync(context, condition, cancellationToken);
if (!result.IsSatisfied)
{
return CampaignExecutionResult.Fail(result.Message ?? "Condition not met");
}
}
foreach (var constraint in campaign.Constraints)
{
var strategy = _strategyFactory.GetConstraintStrategy(constraint.StrategyType);
if (strategy == null)
{
return CampaignExecutionResult.Fail($"Constraint strategy not found: {constraint.StrategyType}");
}
var result = await strategy.ExecuteAsync(context, constraint, cancellationToken);
if (!result.IsValid)
{
return CampaignExecutionResult.Fail(result.Message ?? "Constraint not satisfied");
}
}
var participation = ParticipationRecord.Create(campaignId, tenantId, memberId);
var rewards = new List<RewardGrantRecord>();
foreach (var action in campaign.Actions)
{
var strategy = _strategyFactory.GetActionStrategy(action.StrategyType);
if (strategy == null)
{
continue;
}
var result = await strategy.ExecuteAsync(context, action, cancellationToken);
if (result.IsSuccess && result.Rewards != null)
{
var rewardType = Enum.Parse<RewardType>(result.Rewards.GetValueOrDefault("RewardType", "Points")?.ToString() ?? "Points");
var amount = Convert.ToInt32(result.Rewards.GetValueOrDefault("Amount", 0));
if (amount > 0)
{
var reward = RewardGrantRecord.Create(
campaignId,
tenantId,
memberId,
participation.Id,
rewardType,
amount,
result.Rewards.GetValueOrDefault("RewardCode")?.ToString(),
result.Message);
rewards.Add(reward);
participation.GrantReward();
}
}
}
return CampaignExecutionResult.Success(participation, rewards);
}
}
public record CampaignExecutionResult(
bool IsSuccess,
string? ErrorMessage,
ParticipationRecord? Participation,
List<RewardGrantRecord>? Rewards)
{
public static CampaignExecutionResult Success(ParticipationRecord participation, List<RewardGrantRecord> rewards)
{
return new CampaignExecutionResult(true, null, participation, rewards);
}
public static CampaignExecutionResult Fail(string errorMessage)
{
return new CampaignExecutionResult(false, errorMessage, null, null);
}
}

View File

@ -0,0 +1,7 @@
namespace Fengling.Activity.Domain.Strategies;
public record ActionResult(bool IsSuccess, string? Message = null, Dictionary<string, object>? Rewards = null)
{
public static ActionResult Success(Dictionary<string, object>? rewards = null) => new(true, null, rewards);
public static ActionResult Fail(string message) => new(false, message);
}

View File

@ -0,0 +1,24 @@
namespace Fengling.Activity.Domain.Strategies.Actions;
using Fengling.Activity.Domain.ValueObjects;
public class CouponAction : IActionStrategy
{
public string StrategyType => "Coupon";
public Task<ActionResult> ExecuteAsync(CampaignContext context, ActionConfig config, CancellationToken cancellationToken = default)
{
var couponTemplateId = config.GetParameter<string>("CouponTemplateId");
var quantity = config.GetParameter<int?>("Quantity") ?? 1;
var rewards = new Dictionary<string, object?>
{
{ "CouponTemplateId", couponTemplateId },
{ "Quantity", quantity },
{ "CampaignId", context.CampaignId.ToString() },
{ "GrantedAt", DateTime.UtcNow }
};
return Task.FromResult(ActionResult.Success(rewards));
}
}

View File

@ -0,0 +1,24 @@
namespace Fengling.Activity.Domain.Strategies.Actions;
using Fengling.Activity.Domain.ValueObjects;
public class FixedRewardAction : IActionStrategy
{
public string StrategyType => "FixedReward";
public Task<ActionResult> ExecuteAsync(CampaignContext context, ActionConfig config, CancellationToken cancellationToken = default)
{
var rewardType = config.GetParameter<string>("RewardType") ?? "Points";
var amount = config.GetParameter<int>("Amount");
var rewards = new Dictionary<string, object?>
{
{ "RewardType", rewardType },
{ "Amount", amount },
{ "CampaignId", context.CampaignId.ToString() },
{ "GrantedAt", DateTime.UtcNow }
};
return Task.FromResult(ActionResult.Success(rewards));
}
}

View File

@ -0,0 +1,58 @@
namespace Fengling.Activity.Domain.Strategies.Actions;
using Fengling.Activity.Domain.ValueObjects;
public class LotteryAction : IActionStrategy
{
public string StrategyType => "Lottery";
public Task<ActionResult> ExecuteAsync(CampaignContext context, ActionConfig config, CancellationToken cancellationToken = default)
{
var lotteryPoolId = config.GetParameter<string>("LotteryPoolId");
var random = new Random();
var prizes = context.AdditionalData.TryGetValue("LotteryPrizes", out var prizesObj)
? prizesObj is List<Dictionary<string, object>> list ? list : null
: null;
Dictionary<string, object>? selectedPrize = null;
if (prizes != null && prizes.Count > 0)
{
var totalWeight = prizes.Sum(p => p.TryGetValue("Weight", out var w) && w is int weight ? weight : 1);
var randomValue = random.Next(totalWeight);
var currentWeight = 0;
foreach (var prize in prizes)
{
var weight = prize.TryGetValue("Weight", out var w) && w is int i ? i : 1;
currentWeight += weight;
if (randomValue < currentWeight)
{
selectedPrize = prize;
break;
}
}
}
var rewards = new Dictionary<string, object?>
{
{ "LotteryPoolId", lotteryPoolId ?? "" },
{ "CampaignId", context.CampaignId.ToString() },
{ "GrantedAt", DateTime.UtcNow }
};
if (selectedPrize != null)
{
rewards["Prize"] = selectedPrize;
var name = selectedPrize.TryGetValue("Name", out var n) ? n?.ToString() : null;
rewards["Message"] = $"Congratulations! You won: {name ?? "a prize"}";
}
else
{
rewards["Message"] = "Better luck next time!";
}
return Task.FromResult(ActionResult.Success(rewards));
}
}

View File

@ -0,0 +1,24 @@
namespace Fengling.Activity.Domain.Strategies.Actions;
using Fengling.Activity.Domain.ValueObjects;
public class PointsAction : IActionStrategy
{
public string StrategyType => "Points";
public Task<ActionResult> ExecuteAsync(CampaignContext context, ActionConfig config, CancellationToken cancellationToken = default)
{
var points = config.GetParameter<int>("Points");
var source = config.GetParameter<string>("Source") ?? "Campaign";
var rewards = new Dictionary<string, object?>
{
{ "Points", points },
{ "Source", source },
{ "CampaignId", context.CampaignId.ToString() },
{ "GrantedAt", DateTime.UtcNow }
};
return Task.FromResult(ActionResult.Success(rewards));
}
}

View File

@ -0,0 +1,28 @@
namespace Fengling.Activity.Domain.Strategies.Actions;
using Fengling.Activity.Domain.ValueObjects;
public class RandomRewardAction : IActionStrategy
{
public string StrategyType => "RandomReward";
public Task<ActionResult> ExecuteAsync(CampaignContext context, ActionConfig config, CancellationToken cancellationToken = default)
{
var minAmount = config.GetParameter<int>("MinAmount");
var maxAmount = config.GetParameter<int>("MaxAmount");
var rewardType = config.GetParameter<string>("RewardType") ?? "Points";
var random = new Random();
var amount = random.Next(minAmount, maxAmount + 1);
var rewards = new Dictionary<string, object?>
{
{ "RewardType", rewardType },
{ "Amount", amount },
{ "CampaignId", context.CampaignId.ToString() },
{ "GrantedAt", DateTime.UtcNow }
};
return Task.FromResult(ActionResult.Success(rewards));
}
}

View File

@ -0,0 +1,11 @@
namespace Fengling.Activity.Domain.Strategies;
public class CampaignContext
{
public Guid CampaignId { get; set; }
public Guid TenantId { get; set; }
public Guid MemberId { get; set; }
public string? MemberLevel { get; set; }
public DateTime CurrentTime { get; set; } = DateTime.UtcNow;
public Dictionary<string, object> AdditionalData { get; } = new();
}

View File

@ -0,0 +1,7 @@
namespace Fengling.Activity.Domain.Strategies;
public record ConditionResult(bool IsSatisfied, string? Message = null)
{
public static ConditionResult Success() => new(true);
public static ConditionResult Fail(string message) => new(false, message);
}

View File

@ -0,0 +1,27 @@
namespace Fengling.Activity.Domain.Strategies.Conditions;
using Fengling.Activity.Domain.ValueObjects;
public class AccumulatedParticipationCondition : IConditionStrategy
{
public string StrategyType => "AccumulatedParticipation";
public Task<ConditionResult> ExecuteAsync(CampaignContext context, ConditionConfig config, CancellationToken cancellationToken = default)
{
var requiredCount = config.GetParameter<int>("RequiredCount");
var campaignId = config.GetParameter<string>("CampaignId");
if (context.AdditionalData.TryGetValue("AccumulatedParticipationCount", out var countObj) &&
countObj is int currentCount)
{
if (currentCount >= requiredCount)
{
return Task.FromResult(ConditionResult.Success());
}
return Task.FromResult(ConditionResult.Fail($"Accumulated participation {currentCount} does not meet requirement {requiredCount}"));
}
return Task.FromResult(ConditionResult.Success());
}
}

View File

@ -0,0 +1,26 @@
namespace Fengling.Activity.Domain.Strategies.Conditions;
using Fengling.Activity.Domain.ValueObjects;
public class FirstTimeCondition : IConditionStrategy
{
public string StrategyType => "FirstTime";
public Task<ConditionResult> ExecuteAsync(CampaignContext context, ConditionConfig config, CancellationToken cancellationToken = default)
{
var checkFirstTimeOnly = config.GetParameter<bool>("CheckFirstTimeOnly");
if (!checkFirstTimeOnly)
{
return Task.FromResult(ConditionResult.Success());
}
if (context.AdditionalData.TryGetValue("HasParticipatedBefore", out var hasParticipatedObj) &&
hasParticipatedObj is bool hasParticipated && hasParticipated)
{
return Task.FromResult(ConditionResult.Fail("Member has already participated before"));
}
return Task.FromResult(ConditionResult.Success());
}
}

View File

@ -0,0 +1,44 @@
namespace Fengling.Activity.Domain.Strategies.Conditions;
using Fengling.Activity.Domain.ValueObjects;
public class MemberLevelCondition : IConditionStrategy
{
public string StrategyType => "MemberLevel";
public Task<ConditionResult> ExecuteAsync(CampaignContext context, ConditionConfig config, CancellationToken cancellationToken = default)
{
var requiredLevel = config.GetParameter<string>("RequiredLevel");
var currentLevel = context.MemberLevel;
if (string.IsNullOrEmpty(requiredLevel))
{
return Task.FromResult(ConditionResult.Fail("Required level not configured"));
}
if (string.IsNullOrEmpty(currentLevel))
{
return Task.FromResult(ConditionResult.Fail("Member level not available"));
}
var isLevelMet = CompareLevels(currentLevel, requiredLevel);
return Task.FromResult(isLevelMet
? ConditionResult.Success()
: ConditionResult.Fail($"Member level {currentLevel} does not meet requirement {requiredLevel}"));
}
private static bool CompareLevels(string current, string required)
{
var levelOrder = new[] { "Bronze", "Silver", "Gold", "Platinum", "Diamond", "VIP" };
var currentIndex = Array.IndexOf(levelOrder, current);
var requiredIndex = Array.IndexOf(levelOrder, required);
if (currentIndex == -1 || requiredIndex == -1)
{
return current.Equals(required, StringComparison.OrdinalIgnoreCase);
}
return currentIndex >= requiredIndex;
}
}

View File

@ -0,0 +1,22 @@
namespace Fengling.Activity.Domain.Strategies.Conditions;
using Fengling.Activity.Domain.ValueObjects;
public class TimeRangeCondition : IConditionStrategy
{
public string StrategyType => "TimeRange";
public Task<ConditionResult> ExecuteAsync(CampaignContext context, ConditionConfig config, CancellationToken cancellationToken = default)
{
var startHour = config.GetParameter<int>("StartHour");
var endHour = config.GetParameter<int>("EndHour");
var currentHour = context.CurrentTime.Hour;
if (currentHour >= startHour && currentHour < endHour)
{
return Task.FromResult(ConditionResult.Success());
}
return Task.FromResult(ConditionResult.Fail($"Current time {currentHour}:00 is not within allowed range {startHour}:00-{endHour}:00"));
}
}

View File

@ -0,0 +1,8 @@
namespace Fengling.Activity.Domain.Strategies;
public record ConstraintResult(bool IsValid, string? Message = null, int? RemainingCount = null)
{
public static ConstraintResult Pass() => new(true);
public static ConstraintResult Fail(string message) => new(false, message);
public static ConstraintResult LimitReached(string message, int remaining) => new(false, message, remaining);
}

View File

@ -0,0 +1,27 @@
namespace Fengling.Activity.Domain.Strategies.Constraints;
using Fengling.Activity.Domain.ValueObjects;
public class DailyLimitConstraint : IConstraintStrategy
{
public string StrategyType => "DailyLimit";
public Task<ConstraintResult> ExecuteAsync(CampaignContext context, ConstraintConfig config, CancellationToken cancellationToken = default)
{
var maxCount = config.GetParameter<int>("MaxCount");
var currentDate = context.CurrentTime.Date.ToString("yyyy-MM-dd");
if (context.AdditionalData.TryGetValue($"DailyCount_{currentDate}", out var countObj) &&
countObj is int currentCount)
{
if (currentCount >= maxCount)
{
return Task.FromResult(ConstraintResult.LimitReached($"Daily limit of {maxCount} reached", 0));
}
return Task.FromResult(ConstraintResult.Pass());
}
return Task.FromResult(ConstraintResult.Pass());
}
}

View File

@ -0,0 +1,28 @@
namespace Fengling.Activity.Domain.Strategies.Constraints;
using Fengling.Activity.Domain.ValueObjects;
public class FrequencyLimitConstraint : IConstraintStrategy
{
public string StrategyType => "FrequencyLimit";
public Task<ConstraintResult> ExecuteAsync(CampaignContext context, ConstraintConfig config, CancellationToken cancellationToken = default)
{
var intervalMinutes = config.GetParameter<int>("IntervalMinutes");
var lastParticipation = context.AdditionalData.TryGetValue("LastParticipationTime", out var timeObj)
? timeObj is DateTime dt ? dt : (DateTime?)null
: null;
if (lastParticipation.HasValue)
{
var timeSinceLast = (context.CurrentTime - lastParticipation.Value).TotalMinutes;
if (timeSinceLast < intervalMinutes)
{
var waitMinutes = intervalMinutes - (int)timeSinceLast;
return Task.FromResult(ConstraintResult.LimitReached($"Please wait {waitMinutes} minutes", waitMinutes));
}
}
return Task.FromResult(ConstraintResult.Pass());
}
}

View File

@ -0,0 +1,26 @@
namespace Fengling.Activity.Domain.Strategies.Constraints;
using Fengling.Activity.Domain.ValueObjects;
public class TotalLimitConstraint : IConstraintStrategy
{
public string StrategyType => "TotalLimit";
public Task<ConstraintResult> ExecuteAsync(CampaignContext context, ConstraintConfig config, CancellationToken cancellationToken = default)
{
var maxCount = config.GetParameter<int>("MaxCount");
if (context.AdditionalData.TryGetValue("TotalCount", out var countObj) &&
countObj is int currentCount)
{
if (currentCount >= maxCount)
{
return Task.FromResult(ConstraintResult.LimitReached($"Total limit of {maxCount} reached", 0));
}
return Task.FromResult(ConstraintResult.Pass());
}
return Task.FromResult(ConstraintResult.Pass());
}
}

View File

@ -0,0 +1,9 @@
namespace Fengling.Activity.Domain.Strategies;
using Fengling.Activity.Domain.ValueObjects;
public interface IActionStrategy
{
string StrategyType { get; }
Task<ActionResult> ExecuteAsync(CampaignContext context, ActionConfig config, CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,9 @@
namespace Fengling.Activity.Domain.Strategies;
using Fengling.Activity.Domain.ValueObjects;
public interface IConditionStrategy
{
string StrategyType { get; }
Task<ConditionResult> ExecuteAsync(CampaignContext context, ConditionConfig config, CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,9 @@
namespace Fengling.Activity.Domain.Strategies;
using Fengling.Activity.Domain.ValueObjects;
public interface IConstraintStrategy
{
string StrategyType { get; }
Task<ConstraintResult> ExecuteAsync(CampaignContext context, ConstraintConfig config, CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,8 @@
namespace Fengling.Activity.Domain.Strategies;
public interface IStrategyFactory
{
IConditionStrategy? GetConditionStrategy(string strategyType);
IConstraintStrategy? GetConstraintStrategy(string strategyType);
IActionStrategy? GetActionStrategy(string strategyType);
}

View File

@ -0,0 +1,41 @@
namespace Fengling.Activity.Domain.ValueObjects;
public class ActionConfig : IEquatable<ActionConfig>
{
public string StrategyType { get; }
public IReadOnlyDictionary<string, object> Parameters { get; }
private ActionConfig(string strategyType, Dictionary<string, object> parameters)
{
StrategyType = strategyType;
Parameters = parameters;
}
public static ActionConfig Create(string strategyType, Dictionary<string, object> parameters)
{
if (string.IsNullOrWhiteSpace(strategyType))
throw new ArgumentException("Strategy type cannot be empty");
return new ActionConfig(strategyType, parameters);
}
public T? GetParameter<T>(string key)
{
if (Parameters.TryGetValue(key, out var value) && value is T typedValue)
return typedValue;
return default;
}
public bool Equals(ActionConfig? other)
{
if (other is null) return false;
if (StrategyType != other.StrategyType) return false;
return Parameters.Count == other.Parameters.Count &&
Parameters.All(p => other.Parameters.TryGetValue(p.Key, out var ov) &&
(p.Value?.Equals(ov) ?? ov is null));
}
public override bool Equals(object? obj) => Equals(obj as ActionConfig);
public override int GetHashCode() => HashCode.Combine(StrategyType, Parameters);
public static bool operator ==(ActionConfig a, ActionConfig b) => a.Equals(b);
public static bool operator !=(ActionConfig a, ActionConfig b) => !a.Equals(b);
}

View File

@ -0,0 +1,18 @@
namespace Fengling.Activity.Domain.ValueObjects;
public class CampaignId : IEquatable<CampaignId>
{
public Guid Value { get; }
private CampaignId(Guid value) => Value = value;
public static CampaignId New() => new(Guid.NewGuid());
public static CampaignId FromGuid(Guid value) => new(value);
public static CampaignId Parse(string value) => new(Guid.Parse(value));
public bool Equals(CampaignId? other) => other?.Value == Value;
public override bool Equals(object? obj) => obj is CampaignId other && Equals(other);
public override int GetHashCode() => Value.GetHashCode();
public static implicit operator Guid(CampaignId id) => id.Value;
public override string ToString() => Value.ToString();
}

View File

@ -0,0 +1,29 @@
namespace Fengling.Activity.Domain.ValueObjects;
public class CampaignName : IEquatable<CampaignName>
{
public string Value { get; }
private CampaignName(string value) => Value = value;
public static CampaignName Create(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Campaign name cannot be empty");
if (name.Length > 200)
throw new ArgumentException("Campaign name cannot exceed 200 characters");
return new CampaignName(name.Trim());
}
public bool Equals(CampaignName? other)
{
if (other is null) return false;
return Value == other.Value;
}
public override bool Equals(object? obj) => Equals(obj as CampaignName);
public override int GetHashCode() => Value.GetHashCode();
public static bool operator ==(CampaignName a, CampaignName b) => a.Equals(b);
public static bool operator !=(CampaignName a, CampaignName b) => !a.Equals(b);
public override string ToString() => Value;
}

View File

@ -0,0 +1,41 @@
namespace Fengling.Activity.Domain.ValueObjects;
public class ConditionConfig : IEquatable<ConditionConfig>
{
public string StrategyType { get; }
public IReadOnlyDictionary<string, object> Parameters { get; }
private ConditionConfig(string strategyType, Dictionary<string, object> parameters)
{
StrategyType = strategyType;
Parameters = parameters;
}
public static ConditionConfig Create(string strategyType, Dictionary<string, object> parameters)
{
if (string.IsNullOrWhiteSpace(strategyType))
throw new ArgumentException("Strategy type cannot be empty");
return new ConditionConfig(strategyType, parameters);
}
public T? GetParameter<T>(string key)
{
if (Parameters.TryGetValue(key, out var value) && value is T typedValue)
return typedValue;
return default;
}
public bool Equals(ConditionConfig? other)
{
if (other is null) return false;
if (StrategyType != other.StrategyType) return false;
return Parameters.Count == other.Parameters.Count &&
Parameters.All(p => other.Parameters.TryGetValue(p.Key, out var ov) &&
(p.Value?.Equals(ov) ?? ov is null));
}
public override bool Equals(object? obj) => Equals(obj as ConditionConfig);
public override int GetHashCode() => HashCode.Combine(StrategyType, Parameters);
public static bool operator ==(ConditionConfig a, ConditionConfig b) => a.Equals(b);
public static bool operator !=(ConditionConfig a, ConditionConfig b) => !a.Equals(b);
}

View File

@ -0,0 +1,41 @@
namespace Fengling.Activity.Domain.ValueObjects;
public class ConstraintConfig : IEquatable<ConstraintConfig>
{
public string StrategyType { get; }
public IReadOnlyDictionary<string, object> Parameters { get; }
private ConstraintConfig(string strategyType, Dictionary<string, object> parameters)
{
StrategyType = strategyType;
Parameters = parameters;
}
public static ConstraintConfig Create(string strategyType, Dictionary<string, object> parameters)
{
if (string.IsNullOrWhiteSpace(strategyType))
throw new ArgumentException("Strategy type cannot be empty");
return new ConstraintConfig(strategyType, parameters);
}
public T? GetParameter<T>(string key)
{
if (Parameters.TryGetValue(key, out var value) && value is T typedValue)
return typedValue;
return default;
}
public bool Equals(ConstraintConfig? other)
{
if (other is null) return false;
if (StrategyType != other.StrategyType) return false;
return Parameters.Count == other.Parameters.Count &&
Parameters.All(p => other.Parameters.TryGetValue(p.Key, out var ov) &&
(p.Value?.Equals(ov) ?? ov is null));
}
public override bool Equals(object? obj) => Equals(obj as ConstraintConfig);
public override int GetHashCode() => HashCode.Combine(StrategyType, Parameters);
public static bool operator ==(ConstraintConfig a, ConstraintConfig b) => a.Equals(b);
public static bool operator !=(ConstraintConfig a, ConstraintConfig b) => !a.Equals(b);
}

View File

@ -0,0 +1,18 @@
namespace Fengling.Activity.Domain.ValueObjects;
public class TenantId : IEquatable<TenantId>
{
public Guid Value { get; }
private TenantId(Guid value) => Value = value;
public static TenantId New() => new(Guid.NewGuid());
public static TenantId FromGuid(Guid value) => new(value);
public static TenantId Parse(string value) => new(Guid.Parse(value));
public bool Equals(TenantId? other) => other?.Value == Value;
public override bool Equals(object? obj) => obj is TenantId other && Equals(other);
public override int GetHashCode() => Value.GetHashCode();
public static implicit operator Guid(TenantId id) => id.Value;
public override string ToString() => Value.ToString();
}

View File

@ -0,0 +1,35 @@
namespace Fengling.Activity.Domain.ValueObjects;
public class TimeRange : IEquatable<TimeRange>
{
public DateTime StartTime { get; }
public DateTime EndTime { get; }
private TimeRange(DateTime startTime, DateTime endTime)
{
StartTime = startTime;
EndTime = endTime;
}
public static TimeRange Create(DateTime startTime, DateTime endTime)
{
if (startTime >= endTime)
throw new ArgumentException("Start time must be before end time");
return new TimeRange(startTime, endTime);
}
public bool Contains(DateTime time) => time >= StartTime && time <= EndTime;
public bool IsActive() => Contains(DateTime.UtcNow);
public TimeSpan Duration() => EndTime - StartTime;
public bool Equals(TimeRange? other)
{
if (other is null) return false;
return StartTime == other.StartTime && EndTime == other.EndTime;
}
public override bool Equals(object? obj) => Equals(obj as TimeRange);
public override int GetHashCode() => HashCode.Combine(StartTime, EndTime);
public static bool operator ==(TimeRange a, TimeRange b) => a.Equals(b);
public static bool operator !=(TimeRange a, TimeRange b) => !a.Equals(b);
}

View File

@ -0,0 +1,30 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using NetCorePal.Extensions.DistributedTransactions.CAP.Persistence;
namespace Fengling.Activity.Infrastructure;
public partial class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IMediator mediator)
: AppDbContextBase(options, mediator)
, IPostgreSqlCapDataStorage
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
if (modelBuilder is null)
{
throw new ArgumentNullException(nameof(modelBuilder));
}
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
base.OnModelCreating(modelBuilder);
}
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
ConfigureStronglyTypedIdValueConverter(configurationBuilder);
base.ConfigureConventions(configurationBuilder);
}
}

View File

@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.DependencyInjection;
namespace Fengling.Activity.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.UseNpgsql("Host=any;Database=any;Username=any;Password=any",
b =>
{
b.MigrationsAssembly(typeof(DesignTimeApplicationDbContextFactory).Assembly.FullName);
});
});
var provider = services.BuildServiceProvider();
var dbContext = provider.CreateScope().ServiceProvider.GetRequiredService<ApplicationDbContext>();
return dbContext;
}
}

View File

@ -0,0 +1,67 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Fengling.Activity.Domain.Aggregates.Campaigns;
using Fengling.Activity.Domain.Aggregates.ParticipationRecords;
using Fengling.Activity.Domain.Aggregates.RewardGrantRecords;
using Fengling.Activity.Domain.Enums;
using Fengling.Activity.Domain.ValueObjects;
namespace Fengling.Activity.Infrastructure.EntityConfigurations;
public class CampaignEntityConfiguration : IEntityTypeConfiguration<Campaign>
{
public void Configure(EntityTypeBuilder<Campaign> builder)
{
builder.ToTable("act_campaigns");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.HasColumnName("id")
.ValueGeneratedNever();
builder.Property(x => x.TenantId)
.HasColumnName("tenant_id")
.IsRequired();
builder.Property(x => x.Name)
.HasColumnName("name")
.HasMaxLength(200)
.IsRequired();
builder.Property(x => x.Type)
.HasColumnName("type")
.HasConversion<int>()
.IsRequired();
builder.Property(x => x.Status)
.HasColumnName("status")
.HasConversion<int>()
.IsRequired();
builder.Property(x => x.Description)
.HasColumnName("description")
.HasMaxLength(1000);
builder.Property(x => x.TimeRange)
.HasColumnName("time_range")
.HasColumnType("jsonb");
builder.Property(x => x.MaxParticipants)
.HasColumnName("max_participants");
builder.Property(x => x.MaxRewardsPerMember)
.HasColumnName("max_rewards_per_member");
builder.Property(x => x.CreatedAt)
.HasColumnName("created_at")
.IsRequired();
builder.Property(x => x.UpdatedAt)
.HasColumnName("updated_at");
builder.Property(x => x.Version)
.HasColumnName("version")
.IsRequired();
}
}

View File

@ -0,0 +1,64 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Fengling.Activity.Domain.Aggregates.ParticipationRecords;
using Fengling.Activity.Domain.Enums;
namespace Fengling.Activity.Infrastructure.EntityConfigurations;
public class ParticipationRecordEntityConfiguration : IEntityTypeConfiguration<ParticipationRecord>
{
public void Configure(EntityTypeBuilder<ParticipationRecord> builder)
{
builder.ToTable("act_participation_records");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.HasColumnName("id")
.ValueGeneratedNever();
builder.Property(x => x.CampaignId)
.HasColumnName("campaign_id")
.IsRequired();
builder.Property(x => x.TenantId)
.HasColumnName("tenant_id")
.IsRequired();
builder.Property(x => x.MemberId)
.HasColumnName("member_id")
.IsRequired();
builder.Property(x => x.Status)
.HasColumnName("status")
.HasConversion<int>()
.IsRequired();
builder.Property(x => x.CurrentProgress)
.HasColumnName("current_progress")
.IsRequired();
builder.Property(x => x.TargetProgress)
.HasColumnName("target_progress");
builder.Property(x => x.RewardsGranted)
.HasColumnName("rewards_granted")
.IsRequired();
builder.Property(x => x.ParticipatedAt)
.HasColumnName("participated_at")
.IsRequired();
builder.Property(x => x.CompletedAt)
.HasColumnName("completed_at");
builder.Property(x => x.ExpiredAt)
.HasColumnName("expired_at");
builder.Property(x => x.UpdatedAt)
.HasColumnName("updated_at");
builder.HasIndex(x => new { x.CampaignId, x.MemberId })
.HasDatabaseName("ix_participation_campaign_member");
}
}

View File

@ -0,0 +1,62 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Fengling.Activity.Domain.Aggregates.RewardGrantRecords;
using Fengling.Activity.Domain.Enums;
namespace Fengling.Activity.Infrastructure.EntityConfigurations;
public class RewardGrantRecordEntityConfiguration : IEntityTypeConfiguration<RewardGrantRecord>
{
public void Configure(EntityTypeBuilder<RewardGrantRecord> builder)
{
builder.ToTable("act_reward_grant_records");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.HasColumnName("id")
.ValueGeneratedNever();
builder.Property(x => x.CampaignId)
.HasColumnName("campaign_id")
.IsRequired();
builder.Property(x => x.TenantId)
.HasColumnName("tenant_id")
.IsRequired();
builder.Property(x => x.MemberId)
.HasColumnName("member_id")
.IsRequired();
builder.Property(x => x.ParticipationRecordId)
.HasColumnName("participation_record_id");
builder.Property(x => x.RewardType)
.HasColumnName("reward_type")
.HasConversion<int>()
.IsRequired();
builder.Property(x => x.RewardAmount)
.HasColumnName("reward_amount")
.IsRequired();
builder.Property(x => x.RewardCode)
.HasColumnName("reward_code")
.HasMaxLength(100);
builder.Property(x => x.Remark)
.HasColumnName("remark")
.HasMaxLength(500);
builder.Property(x => x.GrantedAt)
.HasColumnName("granted_at")
.IsRequired();
builder.HasIndex(x => new { x.CampaignId, x.MemberId })
.HasDatabaseName("ix_reward_campaign_member");
builder.HasIndex(x => x.GrantedAt)
.HasDatabaseName("ix_reward_granted_at");
}
}

View File

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

View File

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

View File

@ -0,0 +1,140 @@
// <auto-generated />
using System;
using Fengling.Activity.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Fengling.Activity.Infrastructure.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260122054728_Init")]
partial class Init
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.CapLock", b =>
{
b.Property<string>("Key")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("Instance")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("LastLockTime")
.HasColumnType("TIMESTAMP");
b.HasKey("Key");
b.ToTable("CAPLock", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("Added")
.HasColumnType("TIMESTAMP");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TIMESTAMP");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<int?>("Retries")
.HasColumnType("integer");
b.Property<string>("StatusName")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<string>("Version")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.HasKey("Id");
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName");
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName");
b.ToTable("CAPPublishedMessage", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.ReceivedMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("Added")
.HasColumnType("TIMESTAMP");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TIMESTAMP");
b.Property<string>("Group")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(400)
.HasColumnType("character varying(400)");
b.Property<int?>("Retries")
.HasColumnType("integer");
b.Property<string>("StatusName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Version")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.HasKey("Id");
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName")
.HasDatabaseName("IX_ExpiresAt_StatusName1");
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName")
.HasDatabaseName("IX_Version_ExpiresAt_StatusName1");
b.ToTable("CAPReceivedMessage", (string)null);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,101 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Fengling.Activity.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class Init : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "CAPLock",
columns: table => new
{
Key = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
Instance = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
LastLockTime = table.Column<DateTime>(type: "TIMESTAMP", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_CAPLock", x => x.Key);
});
migrationBuilder.CreateTable(
name: "CAPPublishedMessage",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Version = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true),
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
Content = table.Column<string>(type: "TEXT", nullable: true),
Retries = table.Column<int>(type: "integer", nullable: true),
Added = table.Column<DateTime>(type: "TIMESTAMP", nullable: false),
ExpiresAt = table.Column<DateTime>(type: "TIMESTAMP", nullable: true),
StatusName = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CAPPublishedMessage", x => x.Id);
});
migrationBuilder.CreateTable(
name: "CAPReceivedMessage",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Version = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true),
Name = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
Group = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
Content = table.Column<string>(type: "TEXT", nullable: true),
Retries = table.Column<int>(type: "integer", nullable: true),
Added = table.Column<DateTime>(type: "TIMESTAMP", nullable: false),
ExpiresAt = table.Column<DateTime>(type: "TIMESTAMP", nullable: true),
StatusName = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CAPReceivedMessage", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_ExpiresAt_StatusName",
table: "CAPPublishedMessage",
columns: new[] { "ExpiresAt", "StatusName" });
migrationBuilder.CreateIndex(
name: "IX_Version_ExpiresAt_StatusName",
table: "CAPPublishedMessage",
columns: new[] { "Version", "ExpiresAt", "StatusName" });
migrationBuilder.CreateIndex(
name: "IX_ExpiresAt_StatusName1",
table: "CAPReceivedMessage",
columns: new[] { "ExpiresAt", "StatusName" });
migrationBuilder.CreateIndex(
name: "IX_Version_ExpiresAt_StatusName1",
table: "CAPReceivedMessage",
columns: new[] { "Version", "ExpiresAt", "StatusName" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CAPLock");
migrationBuilder.DropTable(
name: "CAPPublishedMessage");
migrationBuilder.DropTable(
name: "CAPReceivedMessage");
}
}
}

View File

@ -0,0 +1,137 @@
// <auto-generated />
using System;
using Fengling.Activity.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Fengling.Activity.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")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.CapLock", b =>
{
b.Property<string>("Key")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("Instance")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("LastLockTime")
.HasColumnType("TIMESTAMP");
b.HasKey("Key");
b.ToTable("CAPLock", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("Added")
.HasColumnType("TIMESTAMP");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TIMESTAMP");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<int?>("Retries")
.HasColumnType("integer");
b.Property<string>("StatusName")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<string>("Version")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.HasKey("Id");
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName");
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName");
b.ToTable("CAPPublishedMessage", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.ReceivedMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("Added")
.HasColumnType("TIMESTAMP");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TIMESTAMP");
b.Property<string>("Group")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(400)
.HasColumnType("character varying(400)");
b.Property<int?>("Retries")
.HasColumnType("integer");
b.Property<string>("StatusName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Version")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.HasKey("Id");
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName")
.HasDatabaseName("IX_ExpiresAt_StatusName1");
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName")
.HasDatabaseName("IX_Version_ExpiresAt_StatusName1");
b.ToTable("CAPReceivedMessage", (string)null);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,69 @@
using Microsoft.EntityFrameworkCore;
using Fengling.Activity.Domain.Aggregates.Campaigns;
using Fengling.Activity.Domain.Enums;
using Fengling.Activity.Domain.Repositories;
using Fengling.Activity.Domain.ValueObjects;
using Fengling.Activity.Infrastructure;
namespace Fengling.Activity.Infrastructure.Repositories;
public class CampaignRepository : ICampaignRepository
{
private readonly ApplicationDbContext _context;
public CampaignRepository(ApplicationDbContext context) => _context = context;
public async Task<Campaign?> GetByIdAsync(CampaignId id, CancellationToken cancellationToken = default)
{
return await _context.Set<Campaign>()
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
}
public async Task<List<Campaign>> GetByTenantIdAsync(TenantId tenantId, CancellationToken cancellationToken = default)
{
return await _context.Set<Campaign>()
.Where(x => x.TenantId == tenantId)
.ToListAsync(cancellationToken);
}
public async Task<List<Campaign>> GetByStatusAsync(CampaignStatus status, CancellationToken cancellationToken = default)
{
return await _context.Set<Campaign>()
.Where(x => x.Status == status)
.ToListAsync(cancellationToken);
}
public async Task<List<Campaign>> GetActiveCampaignsAsync(TenantId tenantId, CancellationToken cancellationToken = default)
{
var now = DateTime.UtcNow;
return await _context.Set<Campaign>()
.Where(x => x.TenantId == tenantId &&
x.Status == CampaignStatus.Published &&
x.TimeRange.StartTime <= now &&
x.TimeRange.EndTime >= now)
.ToListAsync(cancellationToken);
}
public async Task AddAsync(Campaign campaign, CancellationToken cancellationToken = default)
{
await _context.Set<Campaign>().AddAsync(campaign, cancellationToken);
}
public async Task UpdateAsync(Campaign campaign, CancellationToken cancellationToken = default)
{
_context.Set<Campaign>().Update(campaign);
await Task.CompletedTask;
}
public async Task DeleteAsync(Campaign campaign, CancellationToken cancellationToken = default)
{
_context.Set<Campaign>().Remove(campaign);
await Task.CompletedTask;
}
public async Task<bool> ExistsAsync(CampaignId id, CancellationToken cancellationToken = default)
{
return await _context.Set<Campaign>()
.AnyAsync(x => x.Id == id, cancellationToken);
}
}

View File

@ -0,0 +1,72 @@
using Microsoft.EntityFrameworkCore;
using Fengling.Activity.Domain.Aggregates.Campaigns;
using Fengling.Activity.Domain.Aggregates.ParticipationRecords;
using Fengling.Activity.Domain.Enums;
using Fengling.Activity.Domain.Repositories;
using Fengling.Activity.Domain.ValueObjects;
using Fengling.Activity.Infrastructure;
namespace Fengling.Activity.Infrastructure.Repositories;
public class ParticipationRecordRepository : IParticipationRecordRepository
{
private readonly ApplicationDbContext _context;
public ParticipationRecordRepository(ApplicationDbContext context) => _context = context;
public async Task<ParticipationRecord?> GetByIdAsync(ParticipationRecordId id, CancellationToken cancellationToken = default)
{
return await _context.Set<ParticipationRecord>()
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
}
public async Task<List<ParticipationRecord>> GetByCampaignIdAsync(CampaignId campaignId, CancellationToken cancellationToken = default)
{
return await _context.Set<ParticipationRecord>()
.Where(x => x.CampaignId == campaignId)
.ToListAsync(cancellationToken);
}
public async Task<List<ParticipationRecord>> GetByMemberIdAsync(TenantId tenantId, Guid memberId, CancellationToken cancellationToken = default)
{
return await _context.Set<ParticipationRecord>()
.Where(x => x.TenantId == tenantId && x.MemberId == memberId)
.ToListAsync(cancellationToken);
}
public async Task<ParticipationRecord?> GetByCampaignAndMemberAsync(CampaignId campaignId, Guid memberId, CancellationToken cancellationToken = default)
{
return await _context.Set<ParticipationRecord>()
.FirstOrDefaultAsync(x => x.CampaignId == campaignId && x.MemberId == memberId, cancellationToken);
}
public async Task<List<ParticipationRecord>> GetByStatusAsync(CampaignId campaignId, ParticipationStatus status, CancellationToken cancellationToken = default)
{
return await _context.Set<ParticipationRecord>()
.Where(x => x.CampaignId == campaignId && x.Status == status)
.ToListAsync(cancellationToken);
}
public async Task<int> GetParticipationCountAsync(CampaignId campaignId, Guid memberId, CancellationToken cancellationToken = default)
{
return await _context.Set<ParticipationRecord>()
.CountAsync(x => x.CampaignId == campaignId && x.MemberId == memberId, cancellationToken);
}
public async Task AddAsync(ParticipationRecord record, CancellationToken cancellationToken = default)
{
await _context.Set<ParticipationRecord>().AddAsync(record, cancellationToken);
}
public async Task UpdateAsync(ParticipationRecord record, CancellationToken cancellationToken = default)
{
_context.Set<ParticipationRecord>().Update(record);
await Task.CompletedTask;
}
public async Task DeleteAsync(ParticipationRecord record, CancellationToken cancellationToken = default)
{
_context.Set<ParticipationRecord>().Remove(record);
await Task.CompletedTask;
}
}

View File

@ -0,0 +1,68 @@
using Microsoft.EntityFrameworkCore;
using Fengling.Activity.Domain.Aggregates.Campaigns;
using Fengling.Activity.Domain.Aggregates.RewardGrantRecords;
using Fengling.Activity.Domain.Enums;
using Fengling.Activity.Domain.Repositories;
using Fengling.Activity.Domain.ValueObjects;
using Fengling.Activity.Infrastructure;
namespace Fengling.Activity.Infrastructure.Repositories;
public class RewardGrantRecordRepository : IRewardGrantRecordRepository
{
private readonly ApplicationDbContext _context;
public RewardGrantRecordRepository(ApplicationDbContext context) => _context = context;
public async Task<RewardGrantRecord?> GetByIdAsync(RewardGrantRecordId id, CancellationToken cancellationToken = default)
{
return await _context.Set<RewardGrantRecord>()
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
}
public async Task<List<RewardGrantRecord>> GetByCampaignIdAsync(CampaignId campaignId, CancellationToken cancellationToken = default)
{
return await _context.Set<RewardGrantRecord>()
.Where(x => x.CampaignId == campaignId)
.ToListAsync(cancellationToken);
}
public async Task<List<RewardGrantRecord>> GetByMemberIdAsync(TenantId tenantId, Guid memberId, CancellationToken cancellationToken = default)
{
return await _context.Set<RewardGrantRecord>()
.Where(x => x.TenantId == tenantId && x.MemberId == memberId)
.ToListAsync(cancellationToken);
}
public async Task<int> GetRewardCountAsync(CampaignId campaignId, Guid memberId, CancellationToken cancellationToken = default)
{
return await _context.Set<RewardGrantRecord>()
.CountAsync(x => x.CampaignId == campaignId && x.MemberId == memberId, cancellationToken);
}
public async Task AddAsync(RewardGrantRecord record, CancellationToken cancellationToken = default)
{
await _context.Set<RewardGrantRecord>().AddAsync(record, cancellationToken);
}
public async Task UpdateAsync(RewardGrantRecord record, CancellationToken cancellationToken = default)
{
_context.Set<RewardGrantRecord>().Update(record);
await Task.CompletedTask;
}
public async Task DeleteAsync(RewardGrantRecord record, CancellationToken cancellationToken = default)
{
_context.Set<RewardGrantRecord>().Remove(record);
await Task.CompletedTask;
}
public async Task<List<RewardGrantRecord>> GetRewardsByDateRangeAsync(TenantId tenantId, DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default)
{
return await _context.Set<RewardGrantRecord>()
.Where(x => x.TenantId == tenantId &&
x.GrantedAt >= startDate &&
x.GrantedAt <= endDate)
.ToListAsync(cancellationToken);
}
}

View File

@ -0,0 +1,148 @@
using Microsoft.AspNetCore.Mvc;
using Fengling.Activity.Domain.Aggregates.Campaigns;
using Fengling.Activity.Domain.Enums;
using Fengling.Activity.Domain.Repositories;
using Fengling.Activity.Domain.ValueObjects;
namespace Fengling.Activity.Web.Controllers;
[ApiController]
[Route("api/[controller]")]
public class CampaignsController : ControllerBase
{
private readonly ICampaignRepository _campaignRepository;
public CampaignsController(ICampaignRepository campaignRepository)
{
_campaignRepository = campaignRepository;
}
[HttpPost]
public async Task<ActionResult<CreateCampaignResponse>> CreateCampaign(
[FromBody] CreateCampaignRequest request,
CancellationToken cancellationToken)
{
var campaign = Campaign.Create(
TenantId.FromGuid(request.TenantId),
CampaignName.Create(request.Name),
request.Type,
TimeRange.Create(request.StartTime, request.EndTime),
request.Description);
if (request.MaxParticipants.HasValue)
{
campaign.UpdateMaxParticipants(request.MaxParticipants);
}
if (request.MaxRewardsPerMember.HasValue)
{
campaign.UpdateMaxRewardsPerMember(request.MaxRewardsPerMember);
}
await _campaignRepository.AddAsync(campaign, cancellationToken);
return CreatedAtAction(nameof(GetCampaign), new { campaignId = campaign.Id.Value }, new CreateCampaignResponse
{
CampaignId = campaign.Id.Value,
Name = campaign.Name.Value,
Status = campaign.Status.ToString(),
CreatedAt = campaign.CreatedAt
});
}
[HttpGet("{campaignId:guid}")]
public async Task<ActionResult<GetCampaignResponse>> GetCampaign(Guid campaignId, CancellationToken cancellationToken)
{
var campaign = await _campaignRepository.GetByIdAsync(CampaignId.FromGuid(campaignId), cancellationToken);
if (campaign == null)
{
return NotFound(new { error = "Campaign not found" });
}
return Ok(new GetCampaignResponse
{
CampaignId = campaign.Id.Value,
TenantId = campaign.TenantId.Value,
Name = campaign.Name.Value,
Type = campaign.Type.ToString(),
Status = campaign.Status.ToString(),
Description = campaign.Description,
StartTime = campaign.TimeRange.StartTime,
EndTime = campaign.TimeRange.EndTime,
MaxParticipants = campaign.MaxParticipants,
MaxRewardsPerMember = campaign.MaxRewardsPerMember,
CreatedAt = campaign.CreatedAt,
UpdatedAt = campaign.UpdatedAt
});
}
[HttpPost("{campaignId:guid}/publish")]
public async Task<ActionResult<PublishCampaignResponse>> PublishCampaign(Guid campaignId, CancellationToken cancellationToken)
{
var campaign = await _campaignRepository.GetByIdAsync(CampaignId.FromGuid(campaignId), cancellationToken);
if (campaign == null)
{
return NotFound(new { error = "Campaign not found" });
}
try
{
campaign.Publish();
await _campaignRepository.UpdateAsync(campaign, cancellationToken);
return Ok(new PublishCampaignResponse
{
CampaignId = campaign.Id.Value,
Status = campaign.Status.ToString(),
UpdatedAt = campaign.UpdatedAt
});
}
catch (InvalidOperationException ex)
{
return BadRequest(new { error = ex.Message });
}
}
}
public class CreateCampaignRequest
{
public Guid TenantId { get; set; }
public string Name { get; set; } = "";
public CampaignType Type { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public string? Description { get; set; }
public int? MaxParticipants { get; set; }
public int? MaxRewardsPerMember { get; set; }
}
public class CreateCampaignResponse
{
public Guid CampaignId { get; set; }
public string Name { get; set; } = "";
public string Status { get; set; } = "";
public DateTime CreatedAt { get; set; }
}
public class GetCampaignResponse
{
public Guid CampaignId { get; set; }
public Guid TenantId { get; set; }
public string Name { get; set; } = "";
public string Type { get; set; } = "";
public string Status { get; set; } = "";
public string? Description { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public int? MaxParticipants { get; set; }
public int? MaxRewardsPerMember { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
}
public class PublishCampaignResponse
{
public Guid CampaignId { get; set; }
public string Status { get; set; } = "";
public DateTime? UpdatedAt { get; set; }
}

View File

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

View File

@ -0,0 +1,68 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..</DockerfileContext>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AspNet.Security.OAuth.Feishu" />
<PackageReference Include="AspNet.Security.OAuth.Weixin" />
<PackageReference Include="DotNetCore.CAP.Dashboard" />
<PackageReference Include="DotNetCore.CAP.RabbitMQ" />
<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.Activity.Domain\Fengling.Activity.Domain.csproj" />
<ProjectReference Include="..\Fengling.Activity.Infrastructure\Fengling.Activity.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="SonarAnalyzer.CSharp">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,10 @@
global using NetCorePal.Extensions.AspNetCore;
global using NetCorePal.Extensions.DependencyInjection;
global using Microsoft.Extensions.DependencyInjection;
global using Fengling.Activity.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;

View File

@ -0,0 +1,105 @@
using Microsoft.EntityFrameworkCore;
using Prometheus;
using System.Reflection;
using FastEndpoints;
using Serilog;
using Serilog.Formatting.Json;
using FluentValidation.AspNetCore;
using NetCorePal.Extensions.CodeAnalysis;
Log.Logger = new LoggerConfiguration()
.Enrich.WithClientIp()
.WriteTo.Console(new JsonFormatter())
.CreateLogger();
try
{
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog();
builder.Services.AddHealthChecks();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddFastEndpoints(o => o.IncludeAbstractValidators = true);
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
builder.Services.AddKnownExceptionErrorModelInterceptor();
builder.Services.AddRepositories(typeof(ApplicationDbContext).Assembly);
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseNpgsql(builder.Configuration.GetConnectionString("PostgreSQL"));
if (builder.Environment.IsDevelopment())
{
options.EnableSensitiveDataLogging();
}
options.EnableDetailedErrors();
});
builder.Services.AddUnitOfWork<ApplicationDbContext>();
builder.Services.AddContext().AddEnvContext().AddCapContextProcessor();
builder.Services.AddIntegrationEvents(typeof(Program))
.UseCap<ApplicationDbContext>(b =>
{
b.RegisterServicesFromAssemblies(typeof(Program));
b.AddContextIntegrationFilters();
});
builder.Services.AddCap(x =>
{
x.UseNetCorePalStorage<ApplicationDbContext>();
x.ConsumerThreadCount = Environment.ProcessorCount;
x.UseRabbitMQ(p => builder.Configuration.GetSection("RabbitMQ").Bind(p));
x.UseDashboard();
});
builder.Services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssemblies(Assembly.GetExecutingAssembly())
.AddCommandLockBehavior()
.AddKnownExceptionValidationBehavior()
.AddUnitOfWorkBehaviors());
var app = builder.Build();
if (!app.Environment.IsProduction())
{
using var scope = app.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await dbContext.Database.MigrateAsync();
}
app.UseKnownExceptionHandler();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
app.UseFastEndpoints();
app.UseHttpMetrics();
app.MapHealthChecks("/health");
app.MapMetrics();
await app.RunAsync();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
await Log.CloseAndFlushAsync();
}
#pragma warning disable S1118
public partial class Program
#pragma warning restore S1118
{
}

View File

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

View File

@ -0,0 +1,31 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"PostgreSQL": "Host=localhost;Database=dev;Username=postgres;Password=123456",
"Redis": "81.68.223.70:16379,password=sl52788542"
},
"RabbitMQ": {
"HostName": "localhost",
"UserName": "guest",
"Password": "guest",
"VirtualHost": "/",
"Port": 5672
},
"Services": {
"user": {
"https": [
"https://user:8443"
]
},
"user-v2": {
"https": [
"https://user-v2:8443"
]
}
}
}

View File

@ -0,0 +1,32 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"PostgreSQL": "Host=localhost;Database=dev;Username=postgres;Password=123456",
"Redis": "81.68.223.70:16379,password=sl52788542"
},
"RabbitMQ": {
"HostName": "localhost",
"UserName": "guest",
"Password": "guest",
"VirtualHost": "/",
"Port": 5672
},
"Services": {
"user": {
"https": [
"https://user:8443"
]
},
"user-v2": {
"https": [
"https://user-v2:8443"
]
}
}
}

View File

@ -0,0 +1,83 @@
# NetCorePal Template - Visual Studio Code Snippets Installer
# Auto install Visual Studio code snippets
param(
[string]$VisualStudioVersion = "2022",
[switch]$ShowPathOnly
)
$ErrorActionPreference = "Stop"
Write-Host "NetCorePal Template - Visual Studio Code Snippets Installer" -ForegroundColor Green
Write-Host "=================================================" -ForegroundColor Green
# Get current script directory
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$SnippetFile = Join-Path $ScriptDir "NetCorePalTemplates.snippet"
# Check if snippet file exists
if (-not (Test-Path $SnippetFile)) {
Write-Error "Snippet file not found: $SnippetFile"
exit 1
}
# Build Visual Studio snippets directory path
$VSSnippetsPath = "$env:USERPROFILE\Documents\Visual Studio $VisualStudioVersion\Code Snippets\Visual C#\My Code Snippets"
Write-Host "Target directory: $VSSnippetsPath" -ForegroundColor Yellow
# If only showing path, don't execute installation
if ($ShowPathOnly) {
Write-Host ""
Write-Host "Manual installation steps:" -ForegroundColor Cyan
Write-Host "1. Ensure target directory exists: $VSSnippetsPath" -ForegroundColor White
Write-Host "2. Copy file: $SnippetFile" -ForegroundColor White
Write-Host "3. To target directory: $VSSnippetsPath" -ForegroundColor White
Write-Host "4. Restart Visual Studio" -ForegroundColor White
Write-Host ""
Write-Host "Or use Tools > Code Snippets Manager > Import in Visual Studio" -ForegroundColor Yellow
return
}
# Create directory if it doesn't exist
if (-not (Test-Path $VSSnippetsPath)) {
Write-Host "Creating snippets directory..." -ForegroundColor Yellow
New-Item -ItemType Directory -Path $VSSnippetsPath -Force | Out-Null
}
# Copy snippet file
$DestinationFile = Join-Path $VSSnippetsPath "NetCorePalTemplates.snippet"
try {
Copy-Item -Path $SnippetFile -Destination $DestinationFile -Force
Write-Host "Code snippets installed successfully!" -ForegroundColor Green
Write-Host " Source file: $SnippetFile" -ForegroundColor Gray
Write-Host " Target file: $DestinationFile" -ForegroundColor Gray
Write-Host ""
Write-Host "Available snippet shortcuts:" -ForegroundColor Cyan
Write-Host " postproc - PostProcessor class" -ForegroundColor White
Write-Host " tstclass - Test class" -ForegroundColor White
Write-Host " ncpcmd - NetCorePal command" -ForegroundColor White
Write-Host " ncpcmdres - Command response" -ForegroundColor White
Write-Host " evnt - Domain event" -ForegroundColor White
Write-Host " ncprepo - Repository interface" -ForegroundColor White
Write-Host " epp - FastEndpoint" -ForegroundColor White
Write-Host ""
Write-Host "Usage:" -ForegroundColor Cyan
Write-Host "1. Open C# file in Visual Studio" -ForegroundColor White
Write-Host "2. Type shortcut (like 'postproc')" -ForegroundColor White
Write-Host "3. Press Tab key twice" -ForegroundColor White
Write-Host "4. Fill parameters and press Tab to switch to next parameter" -ForegroundColor White
Write-Host ""
Write-Host "Note: If Visual Studio is running, restart it to load new snippets." -ForegroundColor Yellow
}
catch {
Write-Error "Installation failed: $($_.Exception.Message)"
exit 1
}
Write-Host ""
Write-Host "Installation completed!" -ForegroundColor Green

File diff suppressed because it is too large Load Diff