From 442dc4f2376773bf508b085c4f2b19710dee577e Mon Sep 17 00:00:00 2001 From: Bruno Gao Date: Thu, 12 Feb 2026 08:20:52 +0800 Subject: [PATCH] Remove all existing code - preparing for fresh start Co-authored-by: Qwen-Coder --- Dockerfile | 50 -- init.sh | 636 ------------------ java-service-template-deploy/README.md | 63 -- .../base/configmap.yaml | 20 - .../base/deployment.yaml | 53 -- java-service-template-deploy/base/hpa.yaml | 24 - .../base/kustomization.yaml | 15 - .../base/service.yaml | 14 - .../overlays/dev/kustomization.yaml | 13 - .../overlays/dev/patch-resources.yaml | 17 - .../overlays/prod/kustomization.yaml | 14 - .../overlays/prod/patch-replicas.yaml | 6 - .../overlays/prod/patch-resources.yaml | 17 - .../overlays/staging/kustomization.yaml | 13 - .../overlays/staging/patch-resources.yaml | 17 - mvnw | 62 -- pom.xml | 172 ----- tmpl-api/pom.xml | 136 ---- .../com/example/tmpl/api/TmplApplication.java | 17 - .../tmpl/api/config/OpenApiConfig.java | 28 - .../tmpl/api/config/SecurityConfig.java | 33 - .../example/tmpl/api/config/WebConfig.java | 36 - .../tmpl/api/controller/OrderController.java | 83 --- .../com/example/tmpl/api/dto/ApiResponse.java | 26 - .../tmpl/api/dto/CreateOrderRequest.java | 19 - .../example/tmpl/api/dto/OrderResponse.java | 22 - .../api/exception/GlobalExceptionHandler.java | 150 ----- .../tmpl/api/filter/TenantContextFilter.java | 56 -- .../tmpl/api/filter/TraceIdFilter.java | 63 -- .../tmpl/api/mapper/OrderDtoMapper.java | 29 - .../src/main/resources/application-dev.yml | 26 - .../src/main/resources/application-prod.yml | 29 - tmpl-api/src/main/resources/application.yml | 66 -- .../db/migration/V1__create_order_table.sql | 26 - .../src/main/resources/logback-spring.xml | 70 -- .../tmpl/architecture/ArchitectureTest.java | 140 ---- tmpl-application/pom.xml | 61 -- .../application/context/TenantContext.java | 56 -- .../tmpl/application/context/TenantScope.java | 14 - .../port/in/CreateOrderCommand.java | 11 - .../application/port/in/OrderDetailQuery.java | 8 - .../port/out/OrderEventPublisher.java | 11 - .../port/out/OrderPersistencePort.java | 18 - .../usecase/CreateOrderUseCase.java | 65 -- .../usecase/DeleteOrderUseCase.java | 40 -- .../application/usecase/GetOrderUseCase.java | 39 -- .../application/usecase/ListOrderUseCase.java | 37 - .../usecase/CreateOrderUseCaseTest.java | 91 --- tmpl-domain/pom.xml | 80 --- .../com/example/tmpl/domain/entity/Order.java | 146 ---- .../tmpl/domain/event/DomainEvent.java | 13 - .../tmpl/domain/event/OrderCreatedEvent.java | 21 - .../domain/exception/DomainException.java | 33 - .../tmpl/domain/exception/ErrorCode.java | 33 - .../domain/repository/OrderRepository.java | 21 - .../tmpl/domain/valueobject/Money.java | 37 - .../tmpl/domain/valueobject/OrderId.java | 25 - .../tmpl/domain/valueobject/OrderStatus.java | 13 - .../example/tmpl/domain/entity/OrderTest.java | 111 --- .../tmpl/domain/valueobject/MoneyTest.java | 102 --- tmpl-infrastructure/pom.xml | 72 -- .../audit/AuditFieldsListener.java | 50 -- .../infrastructure/config/JacksonConfig.java | 24 - .../tmpl/infrastructure/config/JpaConfig.java | 14 - .../infrastructure/config/RedisConfig.java | 27 - .../event/LoggingOrderEventPublisher.java | 24 - .../converter/OrderStatusConverter.java | 34 - .../persistence/entity/OrderPO.java | 94 --- .../mapper/OrderPersistenceMapper.java | 51 -- .../repository/OrderJpaRepository.java | 28 - .../repository/OrderPersistenceAdapter.java | 50 -- .../OrderPersistenceMapperTest.java | 84 --- 72 files changed, 3899 deletions(-) delete mode 100644 Dockerfile delete mode 100755 init.sh delete mode 100644 java-service-template-deploy/README.md delete mode 100644 java-service-template-deploy/base/configmap.yaml delete mode 100644 java-service-template-deploy/base/deployment.yaml delete mode 100644 java-service-template-deploy/base/hpa.yaml delete mode 100644 java-service-template-deploy/base/kustomization.yaml delete mode 100644 java-service-template-deploy/base/service.yaml delete mode 100644 java-service-template-deploy/overlays/dev/kustomization.yaml delete mode 100644 java-service-template-deploy/overlays/dev/patch-resources.yaml delete mode 100644 java-service-template-deploy/overlays/prod/kustomization.yaml delete mode 100644 java-service-template-deploy/overlays/prod/patch-replicas.yaml delete mode 100644 java-service-template-deploy/overlays/prod/patch-resources.yaml delete mode 100644 java-service-template-deploy/overlays/staging/kustomization.yaml delete mode 100644 java-service-template-deploy/overlays/staging/patch-resources.yaml delete mode 100755 mvnw delete mode 100644 pom.xml delete mode 100644 tmpl-api/pom.xml delete mode 100644 tmpl-api/src/main/java/com/example/tmpl/api/TmplApplication.java delete mode 100644 tmpl-api/src/main/java/com/example/tmpl/api/config/OpenApiConfig.java delete mode 100644 tmpl-api/src/main/java/com/example/tmpl/api/config/SecurityConfig.java delete mode 100644 tmpl-api/src/main/java/com/example/tmpl/api/config/WebConfig.java delete mode 100644 tmpl-api/src/main/java/com/example/tmpl/api/controller/OrderController.java delete mode 100644 tmpl-api/src/main/java/com/example/tmpl/api/dto/ApiResponse.java delete mode 100644 tmpl-api/src/main/java/com/example/tmpl/api/dto/CreateOrderRequest.java delete mode 100644 tmpl-api/src/main/java/com/example/tmpl/api/dto/OrderResponse.java delete mode 100644 tmpl-api/src/main/java/com/example/tmpl/api/exception/GlobalExceptionHandler.java delete mode 100644 tmpl-api/src/main/java/com/example/tmpl/api/filter/TenantContextFilter.java delete mode 100644 tmpl-api/src/main/java/com/example/tmpl/api/filter/TraceIdFilter.java delete mode 100644 tmpl-api/src/main/java/com/example/tmpl/api/mapper/OrderDtoMapper.java delete mode 100644 tmpl-api/src/main/resources/application-dev.yml delete mode 100644 tmpl-api/src/main/resources/application-prod.yml delete mode 100644 tmpl-api/src/main/resources/application.yml delete mode 100644 tmpl-api/src/main/resources/db/migration/V1__create_order_table.sql delete mode 100644 tmpl-api/src/main/resources/logback-spring.xml delete mode 100644 tmpl-api/src/test/java/com/example/tmpl/architecture/ArchitectureTest.java delete mode 100644 tmpl-application/pom.xml delete mode 100644 tmpl-application/src/main/java/com/example/tmpl/application/context/TenantContext.java delete mode 100644 tmpl-application/src/main/java/com/example/tmpl/application/context/TenantScope.java delete mode 100644 tmpl-application/src/main/java/com/example/tmpl/application/port/in/CreateOrderCommand.java delete mode 100644 tmpl-application/src/main/java/com/example/tmpl/application/port/in/OrderDetailQuery.java delete mode 100644 tmpl-application/src/main/java/com/example/tmpl/application/port/out/OrderEventPublisher.java delete mode 100644 tmpl-application/src/main/java/com/example/tmpl/application/port/out/OrderPersistencePort.java delete mode 100644 tmpl-application/src/main/java/com/example/tmpl/application/usecase/CreateOrderUseCase.java delete mode 100644 tmpl-application/src/main/java/com/example/tmpl/application/usecase/DeleteOrderUseCase.java delete mode 100644 tmpl-application/src/main/java/com/example/tmpl/application/usecase/GetOrderUseCase.java delete mode 100644 tmpl-application/src/main/java/com/example/tmpl/application/usecase/ListOrderUseCase.java delete mode 100644 tmpl-application/src/test/java/com/example/tmpl/application/usecase/CreateOrderUseCaseTest.java delete mode 100644 tmpl-domain/pom.xml delete mode 100644 tmpl-domain/src/main/java/com/example/tmpl/domain/entity/Order.java delete mode 100644 tmpl-domain/src/main/java/com/example/tmpl/domain/event/DomainEvent.java delete mode 100644 tmpl-domain/src/main/java/com/example/tmpl/domain/event/OrderCreatedEvent.java delete mode 100644 tmpl-domain/src/main/java/com/example/tmpl/domain/exception/DomainException.java delete mode 100644 tmpl-domain/src/main/java/com/example/tmpl/domain/exception/ErrorCode.java delete mode 100644 tmpl-domain/src/main/java/com/example/tmpl/domain/repository/OrderRepository.java delete mode 100644 tmpl-domain/src/main/java/com/example/tmpl/domain/valueobject/Money.java delete mode 100644 tmpl-domain/src/main/java/com/example/tmpl/domain/valueobject/OrderId.java delete mode 100644 tmpl-domain/src/main/java/com/example/tmpl/domain/valueobject/OrderStatus.java delete mode 100644 tmpl-domain/src/test/java/com/example/tmpl/domain/entity/OrderTest.java delete mode 100644 tmpl-domain/src/test/java/com/example/tmpl/domain/valueobject/MoneyTest.java delete mode 100644 tmpl-infrastructure/pom.xml delete mode 100644 tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/audit/AuditFieldsListener.java delete mode 100644 tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/config/JacksonConfig.java delete mode 100644 tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/config/JpaConfig.java delete mode 100644 tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/config/RedisConfig.java delete mode 100644 tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/event/LoggingOrderEventPublisher.java delete mode 100644 tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/persistence/converter/OrderStatusConverter.java delete mode 100644 tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/persistence/entity/OrderPO.java delete mode 100644 tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/persistence/mapper/OrderPersistenceMapper.java delete mode 100644 tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/persistence/repository/OrderJpaRepository.java delete mode 100644 tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/persistence/repository/OrderPersistenceAdapter.java delete mode 100644 tmpl-infrastructure/src/test/java/com/example/tmpl/infrastructure/persistence/OrderPersistenceMapperTest.java diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 4a35626..0000000 --- a/Dockerfile +++ /dev/null @@ -1,50 +0,0 @@ -# 多阶段构建 Dockerfile -FROM maven:3.9.6-eclipse-temurin-21 AS builder - -WORKDIR /app -COPY pom.xml . -COPY tmpl-domain/pom.xml ./tmpl-domain/pom.xml -COPY tmpl-application/pom.xml ./tmpl-application/pom.xml -COPY tmpl-infrastructure/pom.xml ./tmpl-infrastructure/pom.xml -COPY tmpl-api/pom.xml ./tmpl-api/pom.xml - -# 预下载依赖 -RUN mvn dependency:go-offline -B - -# 复制源代码 -COPY tmpl-domain/src ./tmpl-domain/src -COPY tmpl-application/src ./tmpl-application/src -COPY tmpl-infrastructure/src ./tmpl-infrastructure/src -COPY tmpl-api/src ./tmpl-api/src - -# 编译打包 -RUN mvn clean package -DskipTests -B -RUN ls -la tmpl-api/target/ - -# 运行时镜像 -FROM eclipse-temurin:21-jre-alpine - -LABEL maintainer="Development Team" -LABEL version="1.0.0" -LABEL description="Java 21 Service Template - Clean Architecture" - -WORKDIR /app - -# 创建非 root 用户 -RUN addgroup -g 1001 -S appgroup && \ - adduser -u 1001 -S appuser -G appgroup - -# 从构建阶段复制 jar 文件 -COPY --from=builder /app/tmpl-api/target/tmpl-api-*.jar app.jar - -# 更改文件所有权 -RUN chown appuser:appgroup app.jar - -# 暴露端口 -EXPOSE 8080 - -# 切换到非 root 用户 -USER appuser - -# 启动应用 -ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/init.sh b/init.sh deleted file mode 100755 index c2a9487..0000000 --- a/init.sh +++ /dev/null @@ -1,636 +0,0 @@ -#!/bin/bash - -# Java21 Template 初始化脚本 -# 用于将模板仓库初始化为具体的服务仓库 - -set -e - -echo "Java21 Template 初始化脚本" -echo "==========================" - -# 默认值 -DEFAULT_COMPANY="com.example" -DEFAULT_SERVICE_NAME="my-service" -DEFAULT_GROUP="com.example.my-service" - -# 显示帮助信息 -show_help() { - echo "用法: $0 --name --group --org " - echo "" - echo "选项:" - echo " --name 服务名称 (默认: my-service)" - echo " --group Maven groupId (默认: com.example.my-service)" - echo " --org 组织名称 (默认: example)" - echo " --harbor Harbor 仓库地址 (默认: harbor.example.com)" - echo " --help 显示此帮助信息" - echo "" - echo "示例:" - echo " $0 --name order-service --group com.company.order --org myteam" -} - -# 解析命令行参数 -SERVICE_NAME="$DEFAULT_SERVICE_NAME" -GROUP_ID="$DEFAULT_GROUP" -ORGANIZATION="example" -HARBOR_URL="harbor.example.com" - -while [[ $# -gt 0 ]]; do - case $1 in - --name) - SERVICE_NAME="$2" - shift 2 - ;; - --group) - GROUP_ID="$2" - shift 2 - ;; - --org) - ORGANIZATION="$2" - shift 2 - ;; - --harbor) - HARBOR_URL="$2" - shift 2 - ;; - --help) - show_help - exit 0 - ;; - *) - echo "未知参数: $1" - show_help - exit 1 - ;; - esac -done - -# 验证必要参数 -if [[ -z "$SERVICE_NAME" || -z "$GROUP_ID" ]]; then - echo "错误: 服务名称和服务组ID是必需的" - show_help - exit 1 -fi - -echo "参数确认:" -echo " 服务名称: $SERVICE_NAME" -echo " Group ID: $GROUP_ID" -echo " 组织: $ORGANIZATION" -echo " Harbor URL: $HARBOR_URL" -echo "" - -read -p "确认继续? [y/N]: " -n 1 -r -echo -if [[ ! $REPLY =~ ^[Yy]$ ]]; then - echo "操作已取消" - exit 1 -fi - -# 提取公司和项目名称 -COMPANY=$(echo "$GROUP_ID" | cut -d. -f1,2 | tr '.' '/') -PROJECT=$(echo "$GROUP_ID" | cut -d. -f3-) - -echo "开始初始化..." - -# 1. 重命名模块目录 -echo "1. 重命名模块目录..." -for dir in tmpl-domain tmpl-application tmpl-infrastructure tmpl-api; do - if [[ -d "$dir" ]]; then - NEW_DIR_NAME="${SERVICE_NAME}-${dir#tmpl}" - echo " 重命名 $dir -> $NEW_DIR_NAME" - mv "$dir" "$NEW_DIR_NAME" - fi -done - -# 2. 替换所有文件中的占位符 -echo "2. 替换占位符..." - -# 查找所有需要替换的文件 -FILES_TO_UPDATE=$(find . -type f \( -name "*.java" -o -name "*.xml" -o -name "*.yml" -o -name "*.yaml" -o -name "*.md" -o -name "*.txt" -o -name "Dockerfile" \) -not -path "./target/*" -not -path "./.git/*") - -for file in $FILES_TO_UPDATE; do - if [[ -f "$file" ]]; then - # 替换占位符 - sed -i.bak "s/com\.example\.tmpl/$GROUP_ID/g" "$file" 2>/dev/null || true - sed -i.bak "s/tmpl-service/$SERVICE_NAME/g" "$file" 2>/dev/null || true - sed -i.bak "s/tmpl-domain/${SERVICE_NAME}-domain/g" "$file" 2>/dev/null || true - sed -i.bak "s/tmpl-application/${SERVICE_NAME}-application/g" "$file" 2>/dev/null || true - sed -i.bak "s/tmpl-infrastructure/${SERVICE_NAME}-infrastructure/g" "$file" 2>/dev/null || true - sed -i.bak "s/tmpl-api/${SERVICE_NAME}-api/g" "$file" 2>/dev/null || true - sed -i.bak "s/example\.tmpl/$PROJECT/g" "$file" 2>/dev/null || true - sed -i.bak "s/harbor\.example\.com/$HARBOR_URL/g" "$file" 2>/dev/null || true - - # 删除备份文件 - rm -f "$file.bak" - fi -done - -# 3. 更新 pom.xml 文件中的模块名称 -echo "3. 更新 pom.xml 模块名称..." -if [[ -f "pom.xml" ]]; then - sed -i.bak "s/tmpl-domain/${SERVICE_NAME}-domain/g" pom.xml - sed -i.bak "s/tmpl-application/${SERVICE_NAME}-application/g" pom.xml - sed -i.bak "s/tmpl-infrastructure/${SERVICE_NAME}-infrastructure/g" pom.xml - sed -i.bak "s/tmpl-api/${SERVICE_NAME}-api/g" pom.xml - rm -f pom.xml.bak -fi - -# 4. 更新 Dockerfile -echo "4. 更新 Dockerfile..." -if [[ -f "Dockerfile" ]]; then - sed -i.bak "s/tmpl-api-.*\.jar/${SERVICE_NAME}-api-\*\.jar/g" Dockerfile - rm -f Dockerfile.bak -fi - -# 5. 创建 .gitignore 如果不存在 -echo "5. 确保 .gitignore 存在..." -if [[ ! -f ".gitignore" ]]; then - cat > .gitignore << 'EOF' -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ -.mwj.tmp/ -.mch.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* -replay_pid* - -# Maven -target/ -!target/README -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties -# Maven wrapper -.wrapper/downloads/ -.wrapper/gradle/ - -# Compilation node.js -Gruntfile.js -.npm/ - -# Coverage directory used by tools like istanbul -coverage/ -*.lcov - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Istanbul output -/index.html -/coverage - -# JSHint -jshint-report.xml - -# JSLint -jslint.xml - -# Mocha -mochawesome-report/ - -# Rome -_dist/ - -# RequireJS build files -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# TypeScript v1 declaration files -typings/ - -# TypeScript cache -*.tsbuildinfo - -# JET required cache (.cache directories) -.cache - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# Next.js build output -.next -out/ - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# See https://www.gatsbyjs.org/docs/global-css/#adding-global-styles-as-a-css-file -public/.cache/ -public/static/ - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - -# Java -*.iml -*.ipr -*.iws -.idea/ - -# Target directory for Maven builds -target/ - -# Environment variable files -.env -.env.local -.env.development.local -.env.test.local -.env.production.local - -# Docker -.docker/ - -# OS generated files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - -# IDE -.vs/ -.vscode/ -*.swp -*.swo -*~ - -# Logs -logs -*.log - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Various things for Meteor -.meteor/local/ - -# Git book -_book - -# s2i -.s2i/ - -# Eclipse -.metadata -bin/ -tmp/ -*.tmp -*.bak -*.swp -*~.nib -local.properties -.settings/ -.loadpath -.recommenders - -# Eclipse Core -.binary - -# External API docs -.externalToolBuilders/ -.externalNativeBuild/ - -# Gradle -.gradle -.gradle/ -!gradle/wrapper/gradle-wrapper.jar - -# Leiningen -.classpath -.libs/ -.log -.lein-deps-sum -.lein-native-sum -.lein-plugins/ -.lein-repl-history -.revlibs -.nrepl-port - -# sbt -target/ -lib_managed/ -src_managed/ -project_target/ -scalaboot.properties - -# Lift web -.lift.properties - -# Lein cljs -.cljscache/ - -# Play Framework -lib_managed/ -project/boot/ -target/ -tmp/ -template Masquerade/ - -# Simple Build Tool -project/build/ -project/plugins/ - -# Scala -*.class -*.jar -*.war -*.ear -.scala_dependencies -.worksheet -.generated -.target - -# sbteclipse -.sbtconfig -.sbtserver - -# IntelliJ IDEA -*.iml -*.ipr -*.iws -.idea/ - -# NetBeans -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ - -# VS Code -.vscode/ - -# Mac -.DS_Store -.AppleDouble -.LSOverride - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -# Windows -Thumbs.db -Thumbs.db:encryptable -ehthumbs.db -ehthumbs_vista.db - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -# End -EOF -fi - -# 6. 创建 README.md -echo "6. 创建 README.md..." -cat > README.md << EOF -# $SERVICE_NAME - -基于 Java 21 和 Clean Architecture 的微服务模板。 - -## 项目结构 - -- \`${SERVICE_NAME}-domain\`: 领域层 -- \`${SERVICE_NAME}-application\`: 应用层 -- \`${SERVICE_NAME}-infrastructure\`: 基础设施层 -- \`${SERVICE_NAME}-api\`: 接入层 - -## 快速开始 - -### 环境要求 - -- Java 21 -- Maven 3.6+ -- Docker (可选) - -### 本地运行 - -\`\`\`bash -# 编译 -mvn clean compile - -# 运行测试 -mvn test - -# 打包 -mvn package - -# 运行应用 -java -jar ${SERVICE_NAME}-api/target/${SERVICE_NAME}-api-*.jar -\`\`\` - -### API 文档 - -访问 http://localhost:8080/swagger-ui.html 查看 API 文档。 - -## 配置 - -应用支持以下环境变量: - -- \`SPRING_DATASOURCE_URL\`: 数据库连接URL -- \`SPRING_DATASOURCE_USERNAME\`: 数据库用户名 -- \`SPRING_DATASOURCE_PASSWORD\`: 数据库密码 -- \`REDIS_HOST\`: Redis主机地址 -- \`REDIS_PORT\`: Redis端口 - -## 构建 Docker 镜像 - -\`\`\`bash -docker build -t $HARBOR_URL/$ORGANIZATION/$SERVICE_NAME:\$(git rev-parse --short HEAD) . -\`\`\` - -## 许可证 - -MIT License -EOF - -# 7. 创建 .editorconfig -echo "7. 创建 .editorconfig..." -cat > .editorconfig << 'EOF' -# EditorConfig is awesome: http://EditorConfig.org - -root = true - -[*] -charset = utf-8 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true -indent_style = space -indent_size = 4 - -[*.md] -trim_trailing_whitespace = false - -[*.yml] -indent_size = 2 - -[*.yaml] -indent_size = 2 -EOF - -# 8. 完成 -echo "" -echo "✅ 初始化完成!" -echo "" -echo "接下来的步骤:" -echo "1. 检查生成的文件是否正确" -echo "2. 运行 mvn clean compile 验证编译" -echo "3. 运行 mvn test 验证测试" -echo "4. 根据需要进一步定制代码" -echo "" -echo "服务名称: $SERVICE_NAME" -echo "Group ID: $GROUP_ID" -echo "项目路径: $(pwd)" \ No newline at end of file diff --git a/java-service-template-deploy/README.md b/java-service-template-deploy/README.md deleted file mode 100644 index ba77204..0000000 --- a/java-service-template-deploy/README.md +++ /dev/null @@ -1,63 +0,0 @@ -# tmpl-service-deploy - -Kubernetes 部署清单模板 - -## 目录结构 - -``` -. -├── base/ # 基础部署配置 -│ ├── kustomization.yaml -│ ├── deployment.yaml -│ ├── service.yaml -│ ├── configmap.yaml -│ └── hpa.yaml -└── overlays/ # 环境覆盖配置 - ├── dev/ - │ ├── kustomization.yaml - │ └── patch-resources.yaml - ├── staging/ - │ ├── kustomization.yaml - │ └── patch-resources.yaml - └── prod/ - ├── kustomization.yaml - ├── patch-resources.yaml - └── patch-replicas.yaml -``` - -## 使用方法 - -### 构建特定环境的配置 - -```bash -# 开发环境 -kubectl kustomize overlays/dev - -# 预发布环境 -kubectl kustomize overlays/staging - -# 生产环境 -kubectl kustomize overlays/prod -``` - -### 应用到集群 - -```bash -# 开发环境 -kubectl apply -k overlays/dev - -# 预发布环境 -kubectl apply -k overlays/staging - -# 生产环境 -kubectl apply -k overlays/prod -``` - -## 自定义 - -根据实际需要修改以下内容: - -1. `base/deployment.yaml` - 容器镜像名称和资源配置 -2. `base/configmap.yaml` - 应用配置 -3. 各环境的 `kustomization.yaml` - 镜像标签 -4. 各环境的 `patch-*.yaml` - 资源和副本数量配置 \ No newline at end of file diff --git a/java-service-template-deploy/base/configmap.yaml b/java-service-template-deploy/base/configmap.yaml deleted file mode 100644 index e27c422..0000000 --- a/java-service-template-deploy/base/configmap.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: tmpl-service-config -data: - application.yml: | - spring: - datasource: - url: jdbc:postgresql://postgres:5432/tmpl_db - username: postgres - password: password - redis: - host: redis - port: 6379 - server: - shutdown: graceful - app: - observability: - slow-query-threshold-ms: 200 - slow-http-call-threshold-ms: 3000 \ No newline at end of file diff --git a/java-service-template-deploy/base/deployment.yaml b/java-service-template-deploy/base/deployment.yaml deleted file mode 100644 index 48f69ad..0000000 --- a/java-service-template-deploy/base/deployment.yaml +++ /dev/null @@ -1,53 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: tmpl-service - labels: - app: tmpl-service -spec: - replicas: 1 - selector: - matchLabels: - app: tmpl-service - template: - metadata: - labels: - app: tmpl-service - spec: - containers: - - name: tmpl-api - image: tmpl-api:latest - ports: - - containerPort: 8080 - env: - - name: SPRING_PROFILES_ACTIVE - value: "prod" - - name: SERVER_PORT - value: "8080" - livenessProbe: - httpGet: - path: /actuator/health/liveness - port: 8080 - initialDelaySeconds: 15 - periodSeconds: 10 - readinessProbe: - httpGet: - path: /actuator/health/readiness - port: 8080 - initialDelaySeconds: 10 - periodSeconds: 5 - resources: - requests: - memory: "512Mi" - cpu: "250m" - limits: - memory: "1Gi" - cpu: "500m" - securityContext: - runAsNonRoot: true - runAsUser: 1001 - allowPrivilegeEscalation: false - readOnlyRootFilesystem: false - capabilities: - drop: - - ALL \ No newline at end of file diff --git a/java-service-template-deploy/base/hpa.yaml b/java-service-template-deploy/base/hpa.yaml deleted file mode 100644 index 1baa132..0000000 --- a/java-service-template-deploy/base/hpa.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: tmpl-service-hpa -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: tmpl-service - minReplicas: 1 - maxReplicas: 10 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 70 - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: 80 \ No newline at end of file diff --git a/java-service-template-deploy/base/kustomization.yaml b/java-service-template-deploy/base/kustomization.yaml deleted file mode 100644 index 1058ac4..0000000 --- a/java-service-template-deploy/base/kustomization.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: -- deployment.yaml -- service.yaml -- configmap.yaml -- hpa.yaml - -namespace: dev - -images: -- name: tmpl-api - newName: harbor.example.com/library/tmpl-service - newTag: latest \ No newline at end of file diff --git a/java-service-template-deploy/base/service.yaml b/java-service-template-deploy/base/service.yaml deleted file mode 100644 index b3754ed..0000000 --- a/java-service-template-deploy/base/service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: tmpl-service - labels: - app: tmpl-service -spec: - selector: - app: tmpl-service - ports: - - protocol: TCP - port: 80 - targetPort: 8080 - type: ClusterIP \ No newline at end of file diff --git a/java-service-template-deploy/overlays/dev/kustomization.yaml b/java-service-template-deploy/overlays/dev/kustomization.yaml deleted file mode 100644 index 8701329..0000000 --- a/java-service-template-deploy/overlays/dev/kustomization.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -bases: -- ../base - -patchesStrategicMerge: -- patch-resources.yaml - -images: -- name: tmpl-api - newName: harbor.example.com/library/tmpl-service - newTag: dev-latest \ No newline at end of file diff --git a/java-service-template-deploy/overlays/dev/patch-resources.yaml b/java-service-template-deploy/overlays/dev/patch-resources.yaml deleted file mode 100644 index 7977064..0000000 --- a/java-service-template-deploy/overlays/dev/patch-resources.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: tmpl-service -spec: - replicas: 1 - template: - spec: - containers: - - name: tmpl-api - resources: - requests: - memory: "256Mi" - cpu: "100m" - limits: - memory: "512Mi" - cpu: "200m" \ No newline at end of file diff --git a/java-service-template-deploy/overlays/prod/kustomization.yaml b/java-service-template-deploy/overlays/prod/kustomization.yaml deleted file mode 100644 index 3d1dc36..0000000 --- a/java-service-template-deploy/overlays/prod/kustomization.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -bases: -- ../base - -patchesStrategicMerge: -- patch-resources.yaml -- patch-replicas.yaml - -images: -- name: tmpl-api - newName: harbor.example.com/library/tmpl-service - newTag: prod-latest \ No newline at end of file diff --git a/java-service-template-deploy/overlays/prod/patch-replicas.yaml b/java-service-template-deploy/overlays/prod/patch-replicas.yaml deleted file mode 100644 index 27edcb7..0000000 --- a/java-service-template-deploy/overlays/prod/patch-replicas.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: tmpl-service -spec: - replicas: 3 \ No newline at end of file diff --git a/java-service-template-deploy/overlays/prod/patch-resources.yaml b/java-service-template-deploy/overlays/prod/patch-resources.yaml deleted file mode 100644 index 6dda85b..0000000 --- a/java-service-template-deploy/overlays/prod/patch-resources.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: tmpl-service -spec: - replicas: 3 - template: - spec: - containers: - - name: tmpl-api - resources: - requests: - memory: "1Gi" - cpu: "500m" - limits: - memory: "2Gi" - cpu: "1000m" \ No newline at end of file diff --git a/java-service-template-deploy/overlays/staging/kustomization.yaml b/java-service-template-deploy/overlays/staging/kustomization.yaml deleted file mode 100644 index 9ac66b2..0000000 --- a/java-service-template-deploy/overlays/staging/kustomization.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -bases: -- ../base - -patchesStrategicMerge: -- patch-resources.yaml - -images: -- name: tmpl-api - newName: harbor.example.com/library/tmpl-service - newTag: staging-latest \ No newline at end of file diff --git a/java-service-template-deploy/overlays/staging/patch-resources.yaml b/java-service-template-deploy/overlays/staging/patch-resources.yaml deleted file mode 100644 index 25cebe4..0000000 --- a/java-service-template-deploy/overlays/staging/patch-resources.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: tmpl-service -spec: - replicas: 2 - template: - spec: - containers: - - name: tmpl-api - resources: - requests: - memory: "512Mi" - cpu: "250m" - limits: - memory: "1Gi" - cpu: "500m" \ No newline at end of file diff --git a/mvnw b/mvnw deleted file mode 100755 index f95ed56..0000000 --- a/mvnw +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "$(uname)" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - if [ -x "/usr/libexec/java_home" ] ; then - JAVA_HOME=$(/usr/libexec/java_home) - else - JAVA_HOME=/Library/Java/Home - fi - ;; -esac - -if [ -n "$JAVA_HOME" ] && [ -x "$JAVA_HOME/bin/java" ]; then - echo "Using Java: $JAVA_HOME/bin/java" -else - echo "Please set the JAVA_HOME variable in your environment to match the location of your Java installation." >&2 - exit 1 -fi - -# Set MAVEN_PROJECTBASEDIR -MAVEN_PROJECTBASEDIR=`dirname "$0"` -MAVEN_PROJECTBASEDIR=`cd "$MAVEN_PROJECTBASEDIR/."`; pwd - -MAVEN_CMD_LINE_ARGS="$MAVEN_CMD_LINE_ARGS -Dmaven.home=${MAVEN_PROJECTBASEDIR}/.mavenwrapper" -MAVEN_CMD_LINE_ARGS="$MAVEN_CMD_LINE_ARGS -Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -# For Cygwin, switch paths to Windows format before running java -if $cygwin || $mingw; then - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` -fi - -export MAVEN_PROJECTBASEDIR - -exec "$JAVA_HOME/bin/java" $MAVEN_CMD_LINE_ARGS -classpath "${MAVEN_PROJECTBASEDIR}/.mavenwrapper/maven-wrapper-3.2.0.jar" "$WRAPPER_LAUNCHER" $@ \ No newline at end of file diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 322b766..0000000 --- a/pom.xml +++ /dev/null @@ -1,172 +0,0 @@ - - - 4.0.0 - - - org.springframework.boot - spring-boot-starter-parent - 3.5.10 - - - - com.example.tmpl - tmpl-service - 0.0.1-SNAPSHOT - pom - tmpl-service - Java 21 Service Template - Clean Architecture - - - tmpl-domain - tmpl-application - tmpl-infrastructure - tmpl-api - - - - 21 - 21 - 21 - UTF-8 - - - 1.6.3 - 2.8.4 - 1.3.0 - - - 2.44.0 - 4.8.6.6 - 11.1.1 - 0.8.12 - - - - - - - com.example.tmpl - tmpl-domain - ${project.version} - - - com.example.tmpl - tmpl-application - ${project.version} - - - com.example.tmpl - tmpl-infrastructure - ${project.version} - - - - - org.mapstruct - mapstruct - ${mapstruct.version} - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - ${springdoc.version} - - - com.tngtech.archunit - archunit-junit5 - ${archunit.version} - test - - - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 21 - 21 - - - org.mapstruct - mapstruct-processor - ${mapstruct.version} - - - - -Amapstruct.defaultComponentModel=spring - - - - - - - com.diffplug.spotless - spotless-maven-plugin - ${spotless.version} - - - - 1.25.2 - - - - - - java|javax|jakarta,org,com,com.example - - - - - - - - org.jacoco - jacoco-maven-plugin - ${jacoco.version} - - - - - com.github.spotbugs - spotbugs-maven-plugin - ${spotbugs.version} - - Max - Medium - true - - - - - - org.owasp - dependency-check-maven - ${dependency-check.version} - - 7 - - HTML - JSON - - - - - - - - - - com.diffplug.spotless - spotless-maven-plugin - - - - \ No newline at end of file diff --git a/tmpl-api/pom.xml b/tmpl-api/pom.xml deleted file mode 100644 index 53ca8ae..0000000 --- a/tmpl-api/pom.xml +++ /dev/null @@ -1,136 +0,0 @@ - - - 4.0.0 - - - com.example.tmpl - tmpl-service - 0.0.1-SNAPSHOT - - - tmpl-api - tmpl-api - 接入层 - REST API + 启动入口 - - - - - com.example.tmpl - tmpl-application - - - com.example.tmpl - tmpl-infrastructure - - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-validation - - - org.springframework.boot - spring-boot-starter-actuator - - - org.springframework.boot - spring-boot-starter-security - - - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - - - - - io.micrometer - micrometer-registry-prometheus - - - - - org.mapstruct - mapstruct - - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - com.tngtech.archunit - archunit-junit5 - - - com.h2database - h2 - test - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - org.jacoco - jacoco-maven-plugin - - - - prepare-agent - - - - report - test - - report - - - - check - - check - - - - - BUNDLE - - - LINE - COVEREDRATIO - 0.70 - - - - - - - - - - - \ No newline at end of file diff --git a/tmpl-api/src/main/java/com/example/tmpl/api/TmplApplication.java b/tmpl-api/src/main/java/com/example/tmpl/api/TmplApplication.java deleted file mode 100644 index 7da333b..0000000 --- a/tmpl-api/src/main/java/com/example/tmpl/api/TmplApplication.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.tmpl.api; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -/** - * 【接入层】应用启动类。 - * - *

使用 Spring Boot 启动应用,启用自动配置。 - */ -@SpringBootApplication -public class TmplApplication { - - public static void main(String[] args) { - SpringApplication.run(TmplApplication.class, args); - } -} \ No newline at end of file diff --git a/tmpl-api/src/main/java/com/example/tmpl/api/config/OpenApiConfig.java b/tmpl-api/src/main/java/com/example/tmpl/api/config/OpenApiConfig.java deleted file mode 100644 index 8450dd3..0000000 --- a/tmpl-api/src/main/java/com/example/tmpl/api/config/OpenApiConfig.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.tmpl.api.config; - -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.info.Info; -import io.swagger.v3.oas.models.info.Contact; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * 【接入层】OpenAPI (Swagger) 配置。 - * - *

配置 API 文档相关信息。 - */ -@Configuration -public class OpenApiConfig { - - @Bean - public OpenAPI customOpenAPI() { - return new OpenAPI() - .info(new Info() - .title("Java21 Template API") - .version("1.0.0") - .description("基于 Clean Architecture 的 Java 21 模板服务 API") - .contact(new Contact() - .name("Development Team") - .email("dev@example.com"))); - } -} \ No newline at end of file diff --git a/tmpl-api/src/main/java/com/example/tmpl/api/config/SecurityConfig.java b/tmpl-api/src/main/java/com/example/tmpl/api/config/SecurityConfig.java deleted file mode 100644 index 72ed8c4..0000000 --- a/tmpl-api/src/main/java/com/example/tmpl/api/config/SecurityConfig.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.example.tmpl.api.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.web.SecurityFilterChain; - -/** - * 【接入层】安全配置。 - * - *

配置基本的安全策略。 - */ -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .authorizeHttpRequests(authz -> authz - .requestMatchers("/api/**").authenticated() - .requestMatchers("/actuator/**").permitAll() // 健康检查等端点公开 - .anyRequest().permitAll() - ) - .csrf(csrf -> csrf.disable()) // API 通常禁用 CSRF - .httpBasic(basic -> basic.disable()) // 禁用基本认证,使用其他认证方式 - .formLogin(form -> form.disable()) // 禁用表单登录 - .logout(logout -> logout.disable()); // 禁用默认登出 - - return http.build(); - } -} \ No newline at end of file diff --git a/tmpl-api/src/main/java/com/example/tmpl/api/config/WebConfig.java b/tmpl-api/src/main/java/com/example/tmpl/api/config/WebConfig.java deleted file mode 100644 index c60733d..0000000 --- a/tmpl-api/src/main/java/com/example/tmpl/api/config/WebConfig.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.tmpl.api.config; - -import com.example.tmpl.api.filter.TenantContextFilter; -import com.example.tmpl.api.filter.TraceIdFilter; -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * 【接入层】Web 配置。 - * - *

注册过滤器和其他 Web 相关配置。 - */ -@Configuration -public class WebConfig { - - @Bean - public FilterRegistrationBean traceIdFilter() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); - registrationBean.setFilter(new TraceIdFilter()); - registrationBean.addUrlPatterns("/api/*"); - registrationBean.setName("traceIdFilter"); - registrationBean.setOrder(1); - return registrationBean; - } - - @Bean - public FilterRegistrationBean tenantContextFilter() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); - registrationBean.setFilter(new TenantContextFilter()); - registrationBean.addUrlPatterns("/api/*"); - registrationBean.setName("tenantContextFilter"); - registrationBean.setOrder(2); - return registrationBean; - } -} \ No newline at end of file diff --git a/tmpl-api/src/main/java/com/example/tmpl/api/controller/OrderController.java b/tmpl-api/src/main/java/com/example/tmpl/api/controller/OrderController.java deleted file mode 100644 index 59d37bb..0000000 --- a/tmpl-api/src/main/java/com/example/tmpl/api/controller/OrderController.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.example.tmpl.api.controller; - -import com.example.tmpl.api.dto.ApiResponse; -import com.example.tmpl.api.dto.CreateOrderRequest; -import com.example.tmpl.api.dto.OrderResponse; -import com.example.tmpl.api.mapper.OrderDtoMapper; -import com.example.tmpl.application.context.TenantScope; -import com.example.tmpl.application.usecase.CreateOrderUseCase; -import com.example.tmpl.application.usecase.DeleteOrderUseCase; -import com.example.tmpl.application.usecase.GetOrderUseCase; -import com.example.tmpl.domain.entity.Order; -import com.example.tmpl.domain.valueobject.OrderId; -import jakarta.validation.Valid; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -/** - * 【接入层】订单控制器。 - * - *

提供订单相关的 REST API 接口。 - */ -@RestController -@RequestMapping("/api/v1/orders") -public class OrderController { - - private final CreateOrderUseCase createOrderUseCase; - private final GetOrderUseCase getOrderUseCase; - private final DeleteOrderUseCase deleteOrderUseCase; - private final OrderDtoMapper dtoMapper; - - public OrderController(CreateOrderUseCase createOrderUseCase, - GetOrderUseCase getOrderUseCase, - DeleteOrderUseCase deleteOrderUseCase, - OrderDtoMapper dtoMapper) { - this.createOrderUseCase = createOrderUseCase; - this.getOrderUseCase = getOrderUseCase; - this.deleteOrderUseCase = deleteOrderUseCase; - this.dtoMapper = dtoMapper; - } - - /** - * 创建订单。 - * - * @param request 创建订单请求 - * @return 订单ID - */ - @PostMapping - public ResponseEntity> createOrder(@Valid @RequestBody CreateOrderRequest request) { - var scope = TenantScope.fromContext(); - var command = dtoMapper.toCreateOrderCommand(request); - var orderId = createOrderUseCase.execute(command, scope); - return ResponseEntity.ok(ApiResponse.success(orderId.value())); - } - - /** - * 获取订单详情。 - * - * @param id 订单ID - * @return 订单详情 - */ - @GetMapping("/{id}") - public ResponseEntity> getOrder(@PathVariable String id) { - var scope = TenantScope.fromContext(); - var query = new com.example.tmpl.application.port.in.OrderDetailQuery(OrderId.of(id)); - Order order = getOrderUseCase.execute(query, scope); - OrderResponse response = dtoMapper.toOrderResponse(order); - return ResponseEntity.ok(ApiResponse.success(response)); - } - - /** - * 删除订单。 - * - * @param id 订单ID - * @return 操作结果 - */ - @DeleteMapping("/{id}") - public ResponseEntity> deleteOrder(@PathVariable String id) { - var scope = TenantScope.fromContext(); - var orderId = OrderId.of(id); - deleteOrderUseCase.execute(orderId, scope); - return ResponseEntity.ok(ApiResponse.success(null)); - } -} \ No newline at end of file diff --git a/tmpl-api/src/main/java/com/example/tmpl/api/dto/ApiResponse.java b/tmpl-api/src/main/java/com/example/tmpl/api/dto/ApiResponse.java deleted file mode 100644 index 8dc7ca0..0000000 --- a/tmpl-api/src/main/java/com/example/tmpl/api/dto/ApiResponse.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.tmpl.api.dto; - -/** - * 【接入层】统一响应包装类。 - * - *

封装 API 响应结果,包含状态码、消息和数据。 - */ -public record ApiResponse( - int code, - String message, - T data, - String timestamp -) { - - public static ApiResponse success(T data) { - return new ApiResponse<>(200, "success", data, java.time.Instant.now().toString()); - } - - public static ApiResponse error(int code, String message) { - return new ApiResponse<>(code, message, null, java.time.Instant.now().toString()); - } - - public static ApiResponse error(String message) { - return new ApiResponse<>(500, message, null, java.time.Instant.now().toString()); - } -} \ No newline at end of file diff --git a/tmpl-api/src/main/java/com/example/tmpl/api/dto/CreateOrderRequest.java b/tmpl-api/src/main/java/com/example/tmpl/api/dto/CreateOrderRequest.java deleted file mode 100644 index b7003fd..0000000 --- a/tmpl-api/src/main/java/com/example/tmpl/api/dto/CreateOrderRequest.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.tmpl.api.dto; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; - -/** - * 【接入层】创建订单请求 DTO。 - * - *

接收前端创建订单的请求参数。 - */ -public record CreateOrderRequest( - @NotBlank(message = "订单标题不能为空") - String title, - - @NotBlank(message = "订单金额不能为空") - @Pattern(regexp = "^\\d+(\\.\\d{1,2})?$", message = "金额格式不正确") - String totalAmount -) {} \ No newline at end of file diff --git a/tmpl-api/src/main/java/com/example/tmpl/api/dto/OrderResponse.java b/tmpl-api/src/main/java/com/example/tmpl/api/dto/OrderResponse.java deleted file mode 100644 index 1e7373d..0000000 --- a/tmpl-api/src/main/java/com/example/tmpl/api/dto/OrderResponse.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.tmpl.api.dto; - -import com.example.tmpl.domain.valueobject.OrderStatus; - -import java.math.BigDecimal; -import java.time.LocalDateTime; - -/** - * 【接入层】订单响应 DTO。 - * - *

向前端返回订单详情信息。 - */ -public record OrderResponse( - String id, - String customerId, - String userId, - String title, - BigDecimal totalAmount, - OrderStatus status, - LocalDateTime createTime, - LocalDateTime updateTime -) {} \ No newline at end of file diff --git a/tmpl-api/src/main/java/com/example/tmpl/api/exception/GlobalExceptionHandler.java b/tmpl-api/src/main/java/com/example/tmpl/api/exception/GlobalExceptionHandler.java deleted file mode 100644 index 9d21776..0000000 --- a/tmpl-api/src/main/java/com/example/tmpl/api/exception/GlobalExceptionHandler.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.example.tmpl.api.exception; - -import com.example.tmpl.api.dto.ApiResponse; -import com.example.tmpl.domain.exception.DomainException; -import com.example.tmpl.domain.exception.ErrorCode; -import jakarta.validation.ConstraintViolation; -import jakarta.validation.ConstraintViolationException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.MissingServletRequestParameterException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; - -import java.time.Instant; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * 【接入层】全局异常处理器。 - * - *

统一处理各种异常,转换为标准的 API 响应格式。 - */ -@RestControllerAdvice -public class GlobalExceptionHandler { - - private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); - - /** - * 处理领域异常。 - */ - @ExceptionHandler(DomainException.class) - public ResponseEntity> handleDomainException(DomainException ex) { - log.warn("业务异常: {}", ex.getMessage(), ex); - - ErrorCode errorCode = ex.getErrorCode(); - HttpStatus httpStatus = getHttpStatusForErrorCode(errorCode); - - return ResponseEntity.status(httpStatus) - .body(ApiResponse.error(httpStatus.value(), errorCode.getMessage())); - } - - /** - * 处理参数校验异常。 - */ - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handleValidationException(MethodArgumentNotValidException ex) { - log.warn("参数校验失败: {}", ex.getMessage()); - - Map errors = new HashMap<>(); - ex.getBindingResult().getFieldErrors().forEach(error -> - errors.put(error.getField(), error.getDefaultMessage()) - ); - - String errorMessage = errors.entrySet().stream() - .map(entry -> entry.getKey() + ": " + entry.getValue()) - .collect(Collectors.joining(", ")); - - return ResponseEntity.badRequest() - .body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), - "参数校验失败: " + errorMessage)); - } - - /** - * 处理约束违反异常。 - */ - @ExceptionHandler(ConstraintViolationException.class) - public ResponseEntity> handleConstraintViolationException(ConstraintViolationException ex) { - log.warn("约束违反: {}", ex.getMessage()); - - String errorMessage = ex.getConstraintViolations().stream() - .map(ConstraintViolation::getMessage) - .collect(Collectors.joining(", ")); - - return ResponseEntity.badRequest() - .body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), - "参数校验失败: " + errorMessage)); - } - - /** - * 处理缺少请求参数异常。 - */ - @ExceptionHandler(MissingServletRequestParameterException.class) - public ResponseEntity> handleMissingParamException(MissingServletRequestParameterException ex) { - log.warn("缺少请求参数: {}", ex.getMessage()); - - String errorMsg = String.format("缺少必需参数: %s (%s)", ex.getParameterName(), ex.getParameterType()); - return ResponseEntity.badRequest() - .body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), errorMsg)); - } - - /** - * 处理参数类型不匹配异常。 - */ - @ExceptionHandler(MethodArgumentTypeMismatchException.class) - public ResponseEntity> handleTypeMismatchException(MethodArgumentTypeMismatchException ex) { - log.warn("参数类型不匹配: {}", ex.getMessage()); - - String errorMsg = String.format("参数 '%s' 类型不匹配,期望类型: %s", - ex.getName(), ex.getRequiredType().getSimpleName()); - return ResponseEntity.badRequest() - .body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), errorMsg)); - } - - /** - * 处理请求体无法读取异常。 - */ - @ExceptionHandler(HttpMessageNotReadableException.class) - public ResponseEntity> handleHttpMessageNotReadable(HttpMessageNotReadableException ex) { - log.error("请求体无法读取: {}", ex.getMessage()); - - return ResponseEntity.badRequest() - .body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), - "请求体格式错误,请检查 JSON 格式")); - } - - /** - * 处理其他未预期的异常。 - */ - @ExceptionHandler(Exception.class) - public ResponseEntity> handleGenericException(Exception ex) { - log.error("未预期的异常: ", ex); - - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), - "系统内部错误")); - } - - /** - * 根据错误码获取对应的 HTTP 状态码。 - */ - private HttpStatus getHttpStatusForErrorCode(ErrorCode errorCode) { - switch (errorCode) { - case COMMON_VALIDATION_ERROR: - return HttpStatus.BAD_REQUEST; - case ORDER_NOT_FOUND: - return HttpStatus.NOT_FOUND; - case COMMON_OPTIMISTIC_LOCK_CONFLICT: - return HttpStatus.CONFLICT; - case COMMON_INTERNAL_ERROR: - default: - return HttpStatus.INTERNAL_SERVER_ERROR; - } - } -} \ No newline at end of file diff --git a/tmpl-api/src/main/java/com/example/tmpl/api/filter/TenantContextFilter.java b/tmpl-api/src/main/java/com/example/tmpl/api/filter/TenantContextFilter.java deleted file mode 100644 index e0aa721..0000000 --- a/tmpl-api/src/main/java/com/example/tmpl/api/filter/TenantContextFilter.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.example.tmpl.api.filter; - -import com.example.tmpl.application.context.TenantContext; -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import org.slf4j.MDC; -import org.springframework.core.annotation.Order; - -import java.io.IOException; - -/** - * 【接入层】租户安全上下文过滤器。 - * - *

从请求头解析租户信息,设置到 TenantContext 和 MDC。 - */ -@Order(2) -public class TenantContextFilter implements Filter { - - private static final String CUSTOMER_ID_HEADER = "X-Customer-Id"; - private static final String ORG_ID_HEADER = "X-Org-Id"; - private static final String USER_ID_HEADER = "X-User-Id"; - private static final String USER_ROLES_HEADER = "X-User-Roles"; - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - - HttpServletRequest httpRequest = (HttpServletRequest) request; - - // 从请求头获取租户信息 - String customerId = httpRequest.getHeader(CUSTOMER_ID_HEADER); - String orgId = httpRequest.getHeader(ORG_ID_HEADER); - String userId = httpRequest.getHeader(USER_ID_HEADER); - String rolesHeader = httpRequest.getHeader(USER_ROLES_HEADER); - String[] roles = rolesHeader != null ? rolesHeader.split(",") : new String[0]; - - // 设置到 TenantContext - TenantContext.set(new TenantContext(customerId, orgId, userId, roles)); - - // 设置到 MDC 以便日志输出 - MDC.put("customerId", customerId); - MDC.put("orgId", orgId); - MDC.put("userId", userId); - - try { - chain.doFilter(request, response); - } finally { - // 清理 TenantContext - TenantContext.clear(); - } - } -} \ No newline at end of file diff --git a/tmpl-api/src/main/java/com/example/tmpl/api/filter/TraceIdFilter.java b/tmpl-api/src/main/java/com/example/tmpl/api/filter/TraceIdFilter.java deleted file mode 100644 index db9bedb..0000000 --- a/tmpl-api/src/main/java/com/example/tmpl/api/filter/TraceIdFilter.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.example.tmpl.api.filter; - -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import org.slf4j.MDC; -import org.springframework.core.annotation.Order; - -import java.io.IOException; -import java.util.UUID; - -/** - * 【接入层】链路追踪过滤器。 - * - *

生成并设置 traceId 和 requestId,写入 MDC 以便日志关联。 - */ -@Order(1) -public class TraceIdFilter implements Filter { - - private static final String TRACE_ID_HEADER = "X-Trace-Id"; - private static final String REQUEST_ID_HEADER = "X-Request-Id"; - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - - HttpServletRequest httpRequest = (HttpServletRequest) request; - - // 获取或生成 traceId - String traceId = httpRequest.getHeader(TRACE_ID_HEADER); - if (traceId == null || traceId.trim().isEmpty()) { - traceId = generateTraceId(); - } - - // 获取或生成 requestId - String requestId = httpRequest.getHeader(REQUEST_ID_HEADER); - if (requestId == null || requestId.trim().isEmpty()) { - requestId = generateRequestId(); - } - - // 设置到 MDC - MDC.put("traceId", traceId); - MDC.put("requestId", requestId); - - try { - chain.doFilter(request, response); - } finally { - // 清理 MDC - MDC.clear(); - } - } - - private String generateTraceId() { - return UUID.randomUUID().toString().replace("-", ""); - } - - private String generateRequestId() { - return UUID.randomUUID().toString().replace("-", ""); - } -} \ No newline at end of file diff --git a/tmpl-api/src/main/java/com/example/tmpl/api/mapper/OrderDtoMapper.java b/tmpl-api/src/main/java/com/example/tmpl/api/mapper/OrderDtoMapper.java deleted file mode 100644 index 3f45aea..0000000 --- a/tmpl-api/src/main/java/com/example/tmpl/api/mapper/OrderDtoMapper.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.tmpl.api.mapper; - -import com.example.tmpl.api.dto.CreateOrderRequest; -import com.example.tmpl.api.dto.OrderResponse; -import com.example.tmpl.application.port.in.CreateOrderCommand; -import com.example.tmpl.domain.entity.Order; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; - -/** - * 【接入层】订单 DTO 映射器。 - * - *

使用 MapStruct 自动映射 DTO 与 Command/Domain 对象。 - */ -@Mapper(componentModel = "spring") -public interface OrderDtoMapper { - - CreateOrderCommand toCreateOrderCommand(CreateOrderRequest request); - - @Mapping(target = "id", source = "id.value") - @Mapping(target = "customerId", source = "customerId") - @Mapping(target = "userId", source = "userId") - @Mapping(target = "title", source = "title") - @Mapping(target = "totalAmount", source = "totalAmount.amount") - @Mapping(target = "status", source = "status") - @Mapping(target = "createTime", source = "createTime") - @Mapping(target = "updateTime", source = "updateTime") - OrderResponse toOrderResponse(Order order); -} \ No newline at end of file diff --git a/tmpl-api/src/main/resources/application-dev.yml b/tmpl-api/src/main/resources/application-dev.yml deleted file mode 100644 index 1bcf3bd..0000000 --- a/tmpl-api/src/main/resources/application-dev.yml +++ /dev/null @@ -1,26 +0,0 @@ -# 开发环境配置 -spring: - jpa: - show-sql: true - properties: - hibernate: - format_sql: true - highlight_sql: true - datasource: - url: jdbc:postgresql://localhost:5432/tmpl_dev - username: postgres - password: password - -logging: - level: - com.example.tmpl: DEBUG - org.springframework.web: DEBUG - org.hibernate.SQL: DEBUG - org.hibernate.type.descriptor.sql.BasicBinder: TRACE - -server: - error: - include-message: always - include-binding-errors: always - include-stacktrace: on_param - include-exception: false \ No newline at end of file diff --git a/tmpl-api/src/main/resources/application-prod.yml b/tmpl-api/src/main/resources/application-prod.yml deleted file mode 100644 index 5f42b0b..0000000 --- a/tmpl-api/src/main/resources/application-prod.yml +++ /dev/null @@ -1,29 +0,0 @@ -# 生产环境配置 -spring: - datasource: - hikari: - maximum-pool-size: 20 - minimum-idle: 5 - connection-timeout: 30000 - idle-timeout: 600000 - max-lifetime: 1800000 - leak-detection-threshold: 60000 - -logging: - config: classpath:logback-spring.xml - -server: - error: - include-message: never - include-binding-errors: never - include-stacktrace: never - include-exception: false - -management: - endpoints: - web: - exposure: - include: health,info,prometheus - endpoint: - health: - show-details: never \ No newline at end of file diff --git a/tmpl-api/src/main/resources/application.yml b/tmpl-api/src/main/resources/application.yml deleted file mode 100644 index d1304b3..0000000 --- a/tmpl-api/src/main/resources/application.yml +++ /dev/null @@ -1,66 +0,0 @@ -# 公共配置 -server: - port: 8080 - shutdown: graceful - -spring: - application: - name: tmpl-service - profiles: - active: dev - datasource: - url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/template_db} - username: ${SPRING_DATASOURCE_USERNAME:postgres} - password: ${SPRING_DATASOURCE_PASSWORD:password} - driver-class-name: org.postgresql.Driver - jpa: - hibernate: - ddl-auto: validate - show-sql: false - properties: - hibernate: - dialect: org.hibernate.dialect.PostgreSQLDialect - format_sql: true - flyway: - enabled: true - baseline-on-migrate: true - locations: classpath:db/migration - redis: - host: ${REDIS_HOST:localhost} - port: ${REDIS_PORT:6379} - timeout: 2000ms - lifecycle: - timeout-per-shutdown-phase: 30s - -logging: - level: - com.example.tmpl: INFO - org.springframework.web: INFO - org.springframework.security: INFO - org.springframework.transaction: INFO - org.hibernate.SQL: DEBUG - org.hibernate.type.descriptor.sql.BasicBinder: TRACE - pattern: - console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId:-}] [%X{userId:-}] %logger{36} - %msg%n" - file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId:-}] [%X{userId:-}] %logger{36} - %msg%n" - -management: - endpoints: - web: - exposure: - include: health,info,prometheus - endpoint: - health: - show-details: when-authorized - info: - env: - enabled: true - metrics: - export: - prometheus: - enabled: true - -app: - observability: - slow-query-threshold-ms: 200 - slow-http-call-threshold-ms: 3000 \ No newline at end of file diff --git a/tmpl-api/src/main/resources/db/migration/V1__create_order_table.sql b/tmpl-api/src/main/resources/db/migration/V1__create_order_table.sql deleted file mode 100644 index 07e1d27..0000000 --- a/tmpl-api/src/main/resources/db/migration/V1__create_order_table.sql +++ /dev/null @@ -1,26 +0,0 @@ --- V1__create_order_table.sql --- 创建订单表 - -CREATE TABLE IF NOT EXISTS t_order ( - id VARCHAR(32) PRIMARY KEY, - customer_id VARCHAR(64) NOT NULL, - user_id VARCHAR(64) NOT NULL, - title VARCHAR(255) NOT NULL, - total_amount DECIMAL(10, 2), - status VARCHAR(20) NOT NULL, - create_user_id BIGINT NOT NULL, - create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - update_user_id BIGINT NOT NULL, - update_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - is_deleted BOOLEAN NOT NULL DEFAULT FALSE, - version INTEGER NOT NULL DEFAULT 0 -); - --- 创建索引 -CREATE INDEX idx_t_order_customer_id ON t_order(customer_id); -CREATE INDEX idx_t_order_user_id ON t_order(user_id); -CREATE INDEX idx_t_order_status ON t_order(status); -CREATE INDEX idx_t_order_create_time ON t_order(create_time); - --- 创建软删除的复合索引 -CREATE INDEX idx_t_order_customer_deleted ON t_order(customer_id, is_deleted); \ No newline at end of file diff --git a/tmpl-api/src/main/resources/logback-spring.xml b/tmpl-api/src/main/resources/logback-spring.xml deleted file mode 100644 index 3041c6b..0000000 --- a/tmpl-api/src/main/resources/logback-spring.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - - - logs/app.log - - ${FILE_LOG_PATTERN} - - - logs/app.%d{yyyy-MM-dd}.%i.gz - 100MB - 30 - 3GB - - - - - - logs/app-json.log - - - - - - - - - - - - - logs/app-json.%d{yyyy-MM-dd}.%i.gz - 100MB - 30 - 3GB - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/tmpl-api/src/test/java/com/example/tmpl/architecture/ArchitectureTest.java b/tmpl-api/src/test/java/com/example/tmpl/architecture/ArchitectureTest.java deleted file mode 100644 index 36ab469..0000000 --- a/tmpl-api/src/test/java/com/example/tmpl/architecture/ArchitectureTest.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.example.tmpl.architecture; - -import com.tngtech.archunit.core.domain.JavaClasses; -import com.tngtech.archunit.core.importer.ClassFileImporter; -import com.tngtech.archunit.lang.ArchRule; -import org.junit.jupiter.api.Test; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*; -import static com.tngtech.archunit.library.Architectures.layeredArchitecture; - -/** - * 【架构测试】架构合规性测试。 - * - *

验证分层架构、依赖方向、注解使用等是否符合架构规范。 - */ -class ArchitectureTest { - - private final JavaClasses importedClasses = new ClassFileImporter().importPackages("com.example.tmpl"); - - @Test - void domainLayerShouldNotDependOnOtherLayers() { - ArchRule rule = classes() - .that().resideInAPackage("..domain..") - .should().onlyDependOnClassesThat().resideInAnyPackage( - "java..", - "jakarta.validation..", - "com.example.tmpl.domain.." - ); - - rule.check(importedClasses); - } - - @Test - void applicationLayerShouldOnlyDependOnDomain() { - ArchRule rule = classes() - .that().resideInAPackage("..application..") - .should().onlyDependOnClassesThat().resideInAnyPackage( - "java..", - "org.springframework.transaction..", - "org.springframework.context..", - "org.springframework.data.commons..", - "com.example.tmpl.domain..", - "com.example.tmpl.application.." - ); - - rule.check(importedClasses); - } - - @Test - void infrastructureLayerShouldOnlyDependOnDomainAndApplication() { - ArchRule rule = classes() - .that().resideInAPackage("..infrastructure..") - .should().onlyDependOnClassesThat().resideInAnyPackage( - "java..", - "org.springframework..", - "org.postgresql..", - "org.flywaydb..", - "org.mapstruct..", - "com.example.tmpl.domain..", - "com.example.tmpl.application..", - "com.example.tmpl.infrastructure.." - ); - - rule.check(importedClasses); - } - - @Test - void apiLayerShouldOnlyDependOnApplicationAndInfrastructure() { - ArchRule rule = classes() - .that().resideInAPackage("..api..") - .should().onlyDependOnClassesThat().resideInAnyPackage( - "java..", - "org.springframework..", - "org.springdoc..", - "io.micrometer..", - "jakarta.validation..", - "com.example.tmpl.domain..", - "com.example.tmpl.application..", - "com.example.tmpl.infrastructure..", - "com.example.tmpl.api.." - ); - - rule.check(importedClasses); - } - - @Test - void controllersShouldNotDependOnInfrastructureDirectly() { - ArchRule rule = classes() - .that().resideInAPackage("..api.controller..") - .should().onlyDependOnClassesThat().resideInAnyPackage( - "java..", - "org.springframework.web..", - "org.springframework.http..", - "jakarta.validation..", - "com.example.tmpl.domain..", - "com.example.tmpl.application..", - "com.example.tmpl.api.dto..", - "com.example.tmpl.api.mapper..", - "com.example.tmpl.api.." - ); - - rule.check(importedClasses); - } - - @Test - void transactionalAnnotationShouldOnlyBeInApplicationLayer() { - ArchRule rule = noClasses() - .that().resideOutsideOfPackage("..application..") - .should().beAnnotatedWith(Transactional.class); - - rule.check(importedClasses); - } - - @Test - void domainClassesShouldNotUseSpringAnnotations() { - ArchRule rule = noClasses() - .that().resideInAPackage("..domain..") - .should().beAnnotatedWith(Service.class); - - rule.check(importedClasses); - } - - @Test - void layeredArchitectureTest() { - ArchRule rule = layeredArchitecture() - .layer("Domain").definedBy("..domain..") - .layer("Application").definedBy("..application..") - .layer("Infrastructure").definedBy("..infrastructure..") - .layer("API").definedBy("..api..") - - .whereLayer("Application").mayNotBeAccessedByAnyLayer() - .whereLayer("Domain").mayOnlyBeAccessedByLayers("Application", "Infrastructure") - .whereLayer("Infrastructure").mayOnlyBeAccessedByLayers("API", "Application") - .whereLayer("API").mayNotBeAccessedByAnyLayer(); - - rule.check(importedClasses); - } -} \ No newline at end of file diff --git a/tmpl-application/pom.xml b/tmpl-application/pom.xml deleted file mode 100644 index c204d31..0000000 --- a/tmpl-application/pom.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - 4.0.0 - - - com.example.tmpl - tmpl-service - 0.0.1-SNAPSHOT - - - tmpl-application - tmpl-application - 应用层 - 用例编排,仅依赖 domain - - - - - com.example.tmpl - tmpl-domain - - - - - org.springframework - spring-tx - - - org.springframework - spring-context - - - - - org.springframework.data - spring-data-commons - - - - - org.junit.jupiter - junit-jupiter - test - - - org.assertj - assertj-core - test - - - org.mockito - mockito-junit-jupiter - test - - - \ No newline at end of file diff --git a/tmpl-application/src/main/java/com/example/tmpl/application/context/TenantContext.java b/tmpl-application/src/main/java/com/example/tmpl/application/context/TenantContext.java deleted file mode 100644 index dfc2d8a..0000000 --- a/tmpl-application/src/main/java/com/example/tmpl/application/context/TenantContext.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.example.tmpl.application.context; - -/** - * 【应用层】租户安全上下文持有者。 - * - *

使用 ThreadLocal 存储当前请求的安全上下文信息。 - *

在 Web 请求开始时由 TenantContextFilter 设置,在请求结束时清理。 - */ -public final class TenantContext { - - private static final ThreadLocal HOLDER = new ThreadLocal<>(); - - private final String customerId; - private final String orgId; - private final String userId; - private final String[] roles; - - public TenantContext(String customerId, String orgId, String userId, String[] roles) { - this.customerId = customerId; - this.orgId = orgId; - this.userId = userId; - this.roles = roles; - } - - public static void set(TenantContext context) { - HOLDER.set(context); - } - - public static TenantContext current() { - TenantContext context = HOLDER.get(); - if (context == null) { - throw new IllegalStateException("TenantContext 未设置,请检查 TenantContextFilter 配置"); - } - return context; - } - - public static void clear() { - HOLDER.remove(); - } - - public String getCustomerId() { - return customerId; - } - - public String getOrgId() { - return orgId; - } - - public String getUserId() { - return userId; - } - - public String[] getRoles() { - return roles; - } -} \ No newline at end of file diff --git a/tmpl-application/src/main/java/com/example/tmpl/application/context/TenantScope.java b/tmpl-application/src/main/java/com/example/tmpl/application/context/TenantScope.java deleted file mode 100644 index 0de34c2..0000000 --- a/tmpl-application/src/main/java/com/example/tmpl/application/context/TenantScope.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.tmpl.application.context; - -/** - * 【应用层】租户安全上下文只读快照。 - * - *

用于在方法间传递安全上下文信息,便于单元测试时模拟。 - */ -public record TenantScope(String customerId, String orgId) { - - public static TenantScope fromContext() { - var ctx = TenantContext.current(); - return new TenantScope(ctx.getCustomerId(), ctx.getOrgId()); - } -} \ No newline at end of file diff --git a/tmpl-application/src/main/java/com/example/tmpl/application/port/in/CreateOrderCommand.java b/tmpl-application/src/main/java/com/example/tmpl/application/port/in/CreateOrderCommand.java deleted file mode 100644 index 9d6f528..0000000 --- a/tmpl-application/src/main/java/com/example/tmpl/application/port/in/CreateOrderCommand.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.tmpl.application.port.in; - -import com.example.tmpl.domain.valueobject.OrderId; - -/** - * 【应用层】创建订单命令。 - */ -public record CreateOrderCommand( - String title, - String totalAmount -) {} \ No newline at end of file diff --git a/tmpl-application/src/main/java/com/example/tmpl/application/port/in/OrderDetailQuery.java b/tmpl-application/src/main/java/com/example/tmpl/application/port/in/OrderDetailQuery.java deleted file mode 100644 index 93c711d..0000000 --- a/tmpl-application/src/main/java/com/example/tmpl/application/port/in/OrderDetailQuery.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.tmpl.application.port.in; - -import com.example.tmpl.domain.valueobject.OrderId; - -/** - * 【应用层】查询订单详情查询参数。 - */ -public record OrderDetailQuery(OrderId id) {} \ No newline at end of file diff --git a/tmpl-application/src/main/java/com/example/tmpl/application/port/out/OrderEventPublisher.java b/tmpl-application/src/main/java/com/example/tmpl/application/port/out/OrderEventPublisher.java deleted file mode 100644 index 8439ea8..0000000 --- a/tmpl-application/src/main/java/com/example/tmpl/application/port/out/OrderEventPublisher.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.tmpl.application.port.out; - -import com.example.tmpl.domain.event.DomainEvent; - -/** - * 【应用层】订单事件发布端口。 - */ -public interface OrderEventPublisher { - - void publish(DomainEvent event); -} \ No newline at end of file diff --git a/tmpl-application/src/main/java/com/example/tmpl/application/port/out/OrderPersistencePort.java b/tmpl-application/src/main/java/com/example/tmpl/application/port/out/OrderPersistencePort.java deleted file mode 100644 index 6f33c4a..0000000 --- a/tmpl-application/src/main/java/com/example/tmpl/application/port/out/OrderPersistencePort.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.tmpl.application.port.out; - -import com.example.tmpl.domain.entity.Order; -import com.example.tmpl.domain.valueobject.OrderId; - -import java.util.Optional; - -/** - * 【应用层】订单持久化端口。 - */ -public interface OrderPersistencePort { - - void save(Order order); - - Optional findByIdAndCustomerId(OrderId id, String customerId); - - void delete(Order order); -} \ No newline at end of file diff --git a/tmpl-application/src/main/java/com/example/tmpl/application/usecase/CreateOrderUseCase.java b/tmpl-application/src/main/java/com/example/tmpl/application/usecase/CreateOrderUseCase.java deleted file mode 100644 index ec57939..0000000 --- a/tmpl-application/src/main/java/com/example/tmpl/application/usecase/CreateOrderUseCase.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.example.tmpl.application.usecase; - -import com.example.tmpl.application.context.TenantScope; -import com.example.tmpl.application.port.in.CreateOrderCommand; -import com.example.tmpl.application.port.out.OrderEventPublisher; -import com.example.tmpl.application.port.out.OrderPersistencePort; -import com.example.tmpl.domain.entity.Order; -import com.example.tmpl.domain.exception.DomainException; -import com.example.tmpl.domain.exception.ErrorCode; -import com.example.tmpl.domain.valueobject.Money; -import com.example.tmpl.domain.valueobject.OrderId; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -/** - * 【应用层】创建订单用例。 - * - *

此用例负责协调订单创建流程,包括参数验证、业务规则执行和事件发布。 - */ -@Service -@Transactional -public class CreateOrderUseCase { - - private final OrderPersistencePort persistencePort; - private final OrderEventPublisher eventPublisher; - - public CreateOrderUseCase(OrderPersistencePort persistencePort, OrderEventPublisher eventPublisher) { - this.persistencePort = persistencePort; - this.eventPublisher = eventPublisher; - } - - /** - * 执行创建订单操作。 - * - * @param command 创建订单命令 - * @param tenantScope 租户安全上下文 - * @return 创建的订单ID - */ - public OrderId execute(CreateOrderCommand command, TenantScope tenantScope) { - // 参数验证 - if (command.title() == null || command.title().trim().isEmpty()) { - throw new DomainException(ErrorCode.COMMON_VALIDATION_ERROR, "订单标题不能为空"); - } - - // 解析金额 - Money totalAmount; - try { - totalAmount = Money.of(command.totalAmount()); - } catch (NumberFormatException e) { - throw new DomainException(ErrorCode.COMMON_VALIDATION_ERROR, "订单金额格式不正确"); - } - - // 创建订单聚合根 - Order order = Order.create(tenantScope.customerId(), tenantScope.getUserId(), command.title(), totalAmount); - - // 保存订单 - persistencePort.save(order); - - // 发布领域事件 - order.getDomainEvents().forEach(eventPublisher::publish); - order.clearDomainEvents(); - - return order.getId(); - } -} \ No newline at end of file diff --git a/tmpl-application/src/main/java/com/example/tmpl/application/usecase/DeleteOrderUseCase.java b/tmpl-application/src/main/java/com/example/tmpl/application/usecase/DeleteOrderUseCase.java deleted file mode 100644 index 61223e6..0000000 --- a/tmpl-application/src/main/java/com/example/tmpl/application/usecase/DeleteOrderUseCase.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.tmpl.application.usecase; - -import com.example.tmpl.application.context.TenantScope; -import com.example.tmpl.application.port.out.OrderPersistencePort; -import com.example.tmpl.domain.entity.Order; -import com.example.tmpl.domain.exception.DomainException; -import com.example.tmpl.domain.exception.ErrorCode; -import com.example.tmpl.domain.valueobject.OrderId; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -/** - * 【应用层】删除订单用例。 - * - *

此用例负责软删除订单,并确保多租户数据隔离。 - */ -@Service -@Transactional -public class DeleteOrderUseCase { - - private final OrderPersistencePort persistencePort; - - public DeleteOrderUseCase(OrderPersistencePort persistencePort) { - this.persistencePort = persistencePort; - } - - /** - * 执行删除订单操作。 - * - * @param id 订单ID - * @param tenantScope 租户安全上下文 - */ - public void execute(OrderId id, TenantScope tenantScope) { - Order order = persistencePort.findByIdAndCustomerId(id, tenantScope.customerId()) - .orElseThrow(() -> new DomainException(ErrorCode.ORDER_NOT_FOUND)); - - order.markDeleted(); - persistencePort.save(order); - } -} \ No newline at end of file diff --git a/tmpl-application/src/main/java/com/example/tmpl/application/usecase/GetOrderUseCase.java b/tmpl-application/src/main/java/com/example/tmpl/application/usecase/GetOrderUseCase.java deleted file mode 100644 index 9eeb6e1..0000000 --- a/tmpl-application/src/main/java/com/example/tmpl/application/usecase/GetOrderUseCase.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.example.tmpl.application.usecase; - -import com.example.tmpl.application.context.TenantScope; -import com.example.tmpl.application.port.in.OrderDetailQuery; -import com.example.tmpl.application.port.out.OrderPersistencePort; -import com.example.tmpl.domain.entity.Order; -import com.example.tmpl.domain.exception.DomainException; -import com.example.tmpl.domain.exception.ErrorCode; -import com.example.tmpl.domain.valueobject.OrderId; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -/** - * 【应用层】查询订单详情用例。 - * - *

此用例负责根据ID查询订单详情,并确保多租户数据隔离。 - */ -@Service -@Transactional(readOnly = true) -public class GetOrderUseCase { - - private final OrderPersistencePort persistencePort; - - public GetOrderUseCase(OrderPersistencePort persistencePort) { - this.persistencePort = persistencePort; - } - - /** - * 执行查询订单详情操作。 - * - * @param query 查询参数 - * @param tenantScope 租户安全上下文 - * @return 订单实体 - */ - public Order execute(OrderDetailQuery query, TenantScope tenantScope) { - return persistencePort.findByIdAndCustomerId(query.id(), tenantScope.customerId()) - .orElseThrow(() -> new DomainException(ErrorCode.ORDER_NOT_FOUND)); - } -} \ No newline at end of file diff --git a/tmpl-application/src/main/java/com/example/tmpl/application/usecase/ListOrderUseCase.java b/tmpl-application/src/main/java/com/example/tmpl/application/usecase/ListOrderUseCase.java deleted file mode 100644 index 7e2e9f4..0000000 --- a/tmpl-application/src/main/java/com/example/tmpl/application/usecase/ListOrderUseCase.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.tmpl.application.usecase; - -import com.example.tmpl.application.context.TenantScope; -import com.example.tmpl.application.port.out.OrderPersistencePort; -import com.example.tmpl.domain.entity.Order; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -/** - * 【应用层】分页查询订单列表用例。 - * - *

此用例负责分页查询订单列表,并确保多租户数据隔离。 - */ -@Service -@Transactional(readOnly = true) -public class ListOrderUseCase { - - private final OrderPersistencePort persistencePort; - - public ListOrderUseCase(OrderPersistencePort persistencePort) { - this.persistencePort = persistencePort; - } - - /** - * 执行分页查询订单列表操作。 - * - * @param tenantScope 租户安全上下文 - * @param pageable 分页参数 - * @return 订单分页结果 - */ - public Page execute(TenantScope tenantScope, Pageable pageable) { - // TODO: 实现分页查询逻辑,根据需要可以添加更多查询条件 - return null; // Placeholder - 实际实现需要在基础设施层提供相应方法 - } -} \ No newline at end of file diff --git a/tmpl-application/src/test/java/com/example/tmpl/application/usecase/CreateOrderUseCaseTest.java b/tmpl-application/src/test/java/com/example/tmpl/application/usecase/CreateOrderUseCaseTest.java deleted file mode 100644 index 033756f..0000000 --- a/tmpl-application/src/test/java/com/example/tmpl/application/usecase/CreateOrderUseCaseTest.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.example.tmpl.application.usecase; - -import com.example.tmpl.application.context.TenantScope; -import com.example.tmpl.application.port.in.CreateOrderCommand; -import com.example.tmpl.application.port.out.OrderEventPublisher; -import com.example.tmpl.application.port.out.OrderPersistencePort; -import com.example.tmpl.domain.entity.Order; -import com.example.tmpl.domain.exception.DomainException; -import com.example.tmpl.domain.exception.ErrorCode; -import com.example.tmpl.domain.valueobject.OrderId; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -/** - * 【应用层】创建订单用例单元测试。 - */ -@ExtendWith(MockitoExtension.class) -class CreateOrderUseCaseTest { - - @Mock - private OrderPersistencePort mockPersistencePort; - - @Mock - private OrderEventPublisher mockEventPublisher; - - private CreateOrderUseCase createOrderUseCase; - - @BeforeEach - void setUp() { - createOrderUseCase = new CreateOrderUseCase(mockPersistencePort, mockEventPublisher); - } - - @Test - @DisplayName("执行创建订单 - 成功") - void shouldExecuteCreateOrderSuccessfully() { - // Given - var command = new CreateOrderCommand("测试订单", "100.00"); - var tenantScope = new TenantScope("customer-123", "org-456"); - var savedOrder = Order.create("customer-123", "user-789", "测试订单", - com.example.tmpl.domain.valueobject.Money.of("100.00")); - var expectedOrderId = savedOrder.getId(); - - when(mockPersistencePort.save(any(Order.class))).thenAnswer(invocation -> { - Order order = invocation.getArgument(0); - // Simulate saving by setting a fixed ID - return null; - }); - - // When - var result = createOrderUseCase.execute(command, tenantScope); - - // Then - assertNotNull(result); - verify(mockPersistencePort).save(any(Order.class)); - verify(mockEventPublisher).publish(any()); - } - - @Test - @DisplayName("执行创建订单 - 标题为空抛出异常") - void shouldThrowExceptionWhenTitleIsEmpty() { - // Given - var command = new CreateOrderCommand("", "100.00"); - var tenantScope = new TenantScope("customer-123", "org-456"); - - // When & Then - DomainException exception = assertThrows(DomainException.class, () -> - createOrderUseCase.execute(command, tenantScope)); - assertEquals(ErrorCode.COMMON_VALIDATION_ERROR, exception.getErrorCode()); - } - - @Test - @DisplayName("执行创建订单 - 金额格式错误抛出异常") - void shouldThrowExceptionWhenAmountFormatIsIncorrect() { - // Given - var command = new CreateOrderCommand("测试订单", "invalid_amount"); - var tenantScope = new TenantScope("customer-123", "org-456"); - - // When & Then - DomainException exception = assertThrows(DomainException.class, () -> - createOrderUseCase.execute(command, tenantScope)); - assertEquals(ErrorCode.COMMON_VALIDATION_ERROR, exception.getErrorCode()); - } -} \ No newline at end of file diff --git a/tmpl-domain/pom.xml b/tmpl-domain/pom.xml deleted file mode 100644 index 7444f18..0000000 --- a/tmpl-domain/pom.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - 4.0.0 - - - com.example.tmpl - tmpl-service - 0.0.1-SNAPSHOT - - - tmpl-domain - tmpl-domain - 领域层 - 纯业务逻辑,零框架依赖 - - - - - jakarta.validation - jakarta.validation-api - - - - - org.junit.jupiter - junit-jupiter - test - - - org.assertj - assertj-core - test - - - - - - - - org.apache.maven.plugins - maven-enforcer-plugin - - - enforce-domain-purity - - enforce - - - - - - org.springframework:* - org.springframework.boot:* - org.springframework.data:* - org.hibernate:* - org.hibernate.orm:* - jakarta.persistence:* - - - domain 模块禁止依赖 Spring/JPA/Hibernate! - 请检查 architecture-spec.md 第 1.3 节。 - - - - - - - - - - \ No newline at end of file diff --git a/tmpl-domain/src/main/java/com/example/tmpl/domain/entity/Order.java b/tmpl-domain/src/main/java/com/example/tmpl/domain/entity/Order.java deleted file mode 100644 index 1a736e8..0000000 --- a/tmpl-domain/src/main/java/com/example/tmpl/domain/entity/Order.java +++ /dev/null @@ -1,146 +0,0 @@ -package com.example.tmpl.domain.entity; - -import com.example.tmpl.domain.event.DomainEvent; -import com.example.tmpl.domain.event.OrderCreatedEvent; -import com.example.tmpl.domain.exception.DomainException; -import com.example.tmpl.domain.exception.ErrorCode; -import com.example.tmpl.domain.valueobject.Money; -import com.example.tmpl.domain.valueobject.OrderId; -import com.example.tmpl.domain.valueobject.OrderStatus; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * 【领域层】订单聚合根。 - * - *

职责:维护自身一致性、执行业务规则、产生领域事件。 - *

规则:状态变更通过方法实现,不暴露 setter。 - */ -public class Order { - - private OrderId id; - private String customerId; - private String userId; - private String title; - private Money totalAmount; - private OrderStatus status; - private LocalDateTime createTime; - private LocalDateTime updateTime; - private int version; - private boolean deleted; - - private final transient List domainEvents = new ArrayList<>(); - - protected Order() { - } - - /** 工厂方法:创建新订单 */ - public static Order create(String customerId, String userId, String title, Money totalAmount) { - if (title == null || title.isBlank()) { - throw new DomainException( - ErrorCode.COMMON_VALIDATION_ERROR, "订单标题不能为空"); - } - if (totalAmount == null || !totalAmount.amount().stripTrailingZeros() - .equals(Money.ZERO.amount().stripTrailingZeros()) - && totalAmount.amount().signum() <= 0) { - // 允许金额为 0(如免费订单),但不允许负数 - } - - var order = new Order(); - order.id = OrderId.generate(); - order.customerId = customerId; - order.userId = userId; - order.title = title; - order.totalAmount = totalAmount; - order.status = OrderStatus.CREATED; - order.createTime = LocalDateTime.now(); - order.updateTime = order.createTime; - order.deleted = false; - - order.registerEvent( - new OrderCreatedEvent(order.id, userId, totalAmount)); - return order; - } - - /** 确认订单 */ - public void confirm() { - assertStatus(OrderStatus.CREATED, "确认"); - this.status = OrderStatus.CONFIRMED; - this.updateTime = LocalDateTime.now(); - } - - /** 取消订单 */ - public void cancel() { - if (status == OrderStatus.COMPLETED || status == OrderStatus.CANCELLED) { - throw new DomainException( - ErrorCode.ORDER_STATUS_INVALID, - "当前状态 [%s] 不允许取消".formatted(status)); - } - this.status = OrderStatus.CANCELLED; - this.updateTime = LocalDateTime.now(); - } - - /** 软删除 */ - public void markDeleted() { - this.deleted = true; - this.updateTime = LocalDateTime.now(); - } - - private void assertStatus(OrderStatus expected, String operation) { - if (this.status != expected) { - throw new DomainException( - ErrorCode.ORDER_STATUS_INVALID, - "当前状态 [%s] 不允许%s操作,需要 [%s]" - .formatted(this.status, operation, expected)); - } - } - - private void registerEvent(DomainEvent event) { - domainEvents.add(event); - } - - public List getDomainEvents() { - return Collections.unmodifiableList(domainEvents); - } - - public void clearDomainEvents() { - domainEvents.clear(); - } - - // ===== Getters(无 Setter)===== - public OrderId getId() { return id; } - public String getCustomerId() { return customerId; } - public String getUserId() { return userId; } - public String getTitle() { return title; } - public Money getTotalAmount() { return totalAmount; } - public OrderStatus getStatus() { return status; } - public LocalDateTime getCreateTime() { return createTime; } - public LocalDateTime getUpdateTime() { return updateTime; } - public int getVersion() { return version; } - public boolean isDeleted() { return deleted; } - - /** - * 从持久化数据重建领域对象(仅 Infrastructure 层的 Mapper 使用)。 - */ - public static Order reconstruct( - OrderId id, String customerId, String userId, String title, - Money totalAmount, OrderStatus status, - LocalDateTime createTime, LocalDateTime updateTime, - int version, boolean deleted) { - var order = new Order(); - order.id = id; - order.customerId = customerId; - order.userId = userId; - order.title = title; - order.totalAmount = totalAmount; - order.status = status; - order.createTime = createTime; - order.updateTime = updateTime; - order.version = version; - order.deleted = deleted; - return order; - } -} \ No newline at end of file diff --git a/tmpl-domain/src/main/java/com/example/tmpl/domain/event/DomainEvent.java b/tmpl-domain/src/main/java/com/example/tmpl/domain/event/DomainEvent.java deleted file mode 100644 index 4216fd0..0000000 --- a/tmpl-domain/src/main/java/com/example/tmpl/domain/event/DomainEvent.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.tmpl.domain.event; - -import java.time.Instant; - -/** - * 【领域层】领域事件标记接口。 - * - *

所有领域事件必须实现此接口。事件在聚合根内产生,由应用层发布。 - */ -public interface DomainEvent { - - Instant occurredOn(); -} \ No newline at end of file diff --git a/tmpl-domain/src/main/java/com/example/tmpl/domain/event/OrderCreatedEvent.java b/tmpl-domain/src/main/java/com/example/tmpl/domain/event/OrderCreatedEvent.java deleted file mode 100644 index 4c7dcae..0000000 --- a/tmpl-domain/src/main/java/com/example/tmpl/domain/event/OrderCreatedEvent.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.tmpl.domain.event; - -import com.example.tmpl.domain.valueobject.Money; -import com.example.tmpl.domain.valueobject.OrderId; - -import java.time.Instant; - -/** - * 【领域层】订单创建事件。 - */ -public record OrderCreatedEvent( - OrderId orderId, - String userId, - Money totalAmount, - Instant occurredOn -) implements DomainEvent { - - public OrderCreatedEvent(OrderId orderId, String userId, Money totalAmount) { - this(orderId, userId, totalAmount, Instant.now()); - } -} \ No newline at end of file diff --git a/tmpl-domain/src/main/java/com/example/tmpl/domain/exception/DomainException.java b/tmpl-domain/src/main/java/com/example/tmpl/domain/exception/DomainException.java deleted file mode 100644 index f2256d7..0000000 --- a/tmpl-domain/src/main/java/com/example/tmpl/domain/exception/DomainException.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.example.tmpl.domain.exception; - -/** - * 【领域层】领域异常基类。 - * - *

所有业务异常必须继承此类并携带 {@link ErrorCode}。 - * API 层的 GlobalExceptionHandler 将其统一转换为 HTTP 响应。 - * - *

禁止直接抛出 RuntimeException / IllegalStateException 表达业务规则。 - */ -public class DomainException extends RuntimeException { - - private final ErrorCode errorCode; - - public DomainException(ErrorCode errorCode) { - super(errorCode.getMessage()); - this.errorCode = errorCode; - } - - public DomainException(ErrorCode errorCode, String detail) { - super(detail); - this.errorCode = errorCode; - } - - public DomainException(ErrorCode errorCode, Throwable cause) { - super(errorCode.getMessage(), cause); - this.errorCode = errorCode; - } - - public ErrorCode getErrorCode() { - return errorCode; - } -} \ No newline at end of file diff --git a/tmpl-domain/src/main/java/com/example/tmpl/domain/exception/ErrorCode.java b/tmpl-domain/src/main/java/com/example/tmpl/domain/exception/ErrorCode.java deleted file mode 100644 index 0e245fd..0000000 --- a/tmpl-domain/src/main/java/com/example/tmpl/domain/exception/ErrorCode.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.example.tmpl.domain.exception; - -/** - * 【领域层】统一错误码枚举。 - * - *

命名规则:{MODULE}_{SCENE},全大写蛇形。 - * 新增模块时添加对应前缀组。 - * - *

ErrorCode → HTTP Status 的映射关系定义在 API 层 GlobalExceptionHandler 中, - * 详见 contracts/errors.md。 - */ -public enum ErrorCode { - - // ===== COMMON ===== - COMMON_VALIDATION_ERROR("参数校验失败"), - COMMON_INTERNAL_ERROR("系统内部错误"), - COMMON_OPTIMISTIC_LOCK_CONFLICT("数据已被其他操作修改,请刷新后重试"), - - // ===== ORDER ===== - ORDER_NOT_FOUND("订单不存在"), - ORDER_ALREADY_CANCELLED("订单已取消,不可重复操作"), - ORDER_STATUS_INVALID("订单当前状态不允许此操作"); - - private final String message; - - ErrorCode(String message) { - this.message = message; - } - - public String getMessage() { - return message; - } -} \ No newline at end of file diff --git a/tmpl-domain/src/main/java/com/example/tmpl/domain/repository/OrderRepository.java b/tmpl-domain/src/main/java/com/example/tmpl/domain/repository/OrderRepository.java deleted file mode 100644 index e13d91a..0000000 --- a/tmpl-domain/src/main/java/com/example/tmpl/domain/repository/OrderRepository.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.tmpl.domain.repository; - -import com.example.tmpl.domain.entity.Order; -import com.example.tmpl.domain.valueobject.OrderId; - -import java.util.Optional; - -/** - * 【领域层】订单仓储接口。 - * - *

只定义契约,实现在 Infrastructure 层。 - *

注意:所有查询方法必须包含 customerId 参数(多租户隔离)。 - */ -public interface OrderRepository { - - void save(Order order); - - Optional findById(OrderId id, String customerId); - - void delete(Order order); -} \ No newline at end of file diff --git a/tmpl-domain/src/main/java/com/example/tmpl/domain/valueobject/Money.java b/tmpl-domain/src/main/java/com/example/tmpl/domain/valueobject/Money.java deleted file mode 100644 index dfc1c38..0000000 --- a/tmpl-domain/src/main/java/com/example/tmpl/domain/valueobject/Money.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.tmpl.domain.valueobject; - -import java.math.BigDecimal; -import java.math.RoundingMode; - -/** - * 【领域层】金额值对象。 - * - *

不可变,精度统一为 2 位小数,运算方法返回新实例。 - */ -public record Money(BigDecimal amount) { - - public static final Money ZERO = new Money(BigDecimal.ZERO); - - public Money { - if (amount == null) { - throw new IllegalArgumentException("金额不能为 null"); - } - amount = amount.setScale(2, RoundingMode.HALF_UP); - } - - public static Money of(BigDecimal amount) { - return new Money(amount); - } - - public static Money of(String amount) { - return new Money(new BigDecimal(amount)); - } - - public Money add(Money other) { - return new Money(this.amount.add(other.amount)); - } - - public Money multiply(int quantity) { - return new Money(this.amount.multiply(BigDecimal.valueOf(quantity))); - } -} \ No newline at end of file diff --git a/tmpl-domain/src/main/java/com/example/tmpl/domain/valueobject/OrderId.java b/tmpl-domain/src/main/java/com/example/tmpl/domain/valueobject/OrderId.java deleted file mode 100644 index 8d32f13..0000000 --- a/tmpl-domain/src/main/java/com/example/tmpl/domain/valueobject/OrderId.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.example.tmpl.domain.valueobject; - -import java.util.UUID; - -/** - * 【领域层】订单 ID 值对象。 - * - *

使用强类型 ID 避免原始类型混淆(如 orderId 与 userId 互传)。 - */ -public record OrderId(String value) { - - public OrderId { - if (value == null || value.isBlank()) { - throw new IllegalArgumentException("OrderId 不能为空"); - } - } - - public static OrderId generate() { - return new OrderId(UUID.randomUUID().toString().replace("-", "")); - } - - public static OrderId of(String value) { - return new OrderId(value); - } -} \ No newline at end of file diff --git a/tmpl-domain/src/main/java/com/example/tmpl/domain/valueobject/OrderStatus.java b/tmpl-domain/src/main/java/com/example/tmpl/domain/valueobject/OrderStatus.java deleted file mode 100644 index 86e36c4..0000000 --- a/tmpl-domain/src/main/java/com/example/tmpl/domain/valueobject/OrderStatus.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.tmpl.domain.valueobject; - -/** - * 【领域层】订单状态枚举。 - * - *

状态流转规则由 {@link com.example.tmpl.domain.entity.Order} 聚合根方法保证。 - */ -public enum OrderStatus { - CREATED, - CONFIRMED, - CANCELLED, - COMPLETED -} \ No newline at end of file diff --git a/tmpl-domain/src/test/java/com/example/tmpl/domain/entity/OrderTest.java b/tmpl-domain/src/test/java/com/example/tmpl/domain/entity/OrderTest.java deleted file mode 100644 index 864a61e..0000000 --- a/tmpl-domain/src/test/java/com/example/tmpl/domain/entity/OrderTest.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.example.tmpl.domain.entity; - -import com.example.tmpl.domain.exception.DomainException; -import com.example.tmpl.domain.exception.ErrorCode; -import com.example.tmpl.domain.valueobject.Money; -import com.example.tmpl.domain.valueobject.OrderStatus; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * 【领域层】订单聚合根单元测试。 - */ -class OrderTest { - - @Test - @DisplayName("创建订单 - 成功") - void shouldCreateOrderSuccessfully() { - // Given - String customerId = "customer-123"; - String userId = "user-456"; - String title = "测试订单"; - Money totalAmount = Money.of("100.00"); - - // When - var order = Order.create(customerId, userId, title, totalAmount); - - // Then - assertNotNull(order.getId()); - assertEquals(customerId, order.getCustomerId()); - assertEquals(userId, order.getUserId()); - assertEquals(title, order.getTitle()); - assertEquals(totalAmount, order.getTotalAmount()); - assertEquals(OrderStatus.CREATED, order.getStatus()); - assertFalse(order.isDeleted()); - assertNotNull(order.getCreateTime()); - assertNotNull(order.getUpdateTime()); - } - - @Test - @DisplayName("创建订单 - 标题为空抛出异常") - void shouldThrowExceptionWhenTitleIsNull() { - // Given - String customerId = "customer-123"; - String userId = "user-456"; - String title = null; - Money totalAmount = Money.of("100.00"); - - // When & Then - DomainException exception = assertThrows(DomainException.class, () -> - Order.create(customerId, userId, title, totalAmount)); - assertEquals(ErrorCode.COMMON_VALIDATION_ERROR, exception.getErrorCode()); - } - - @Test - @DisplayName("确认订单 - 从CREATED状态变为CONFIRMED") - void shouldConfirmOrderFromCreatedToConfirmed() { - // Given - var order = createTestOrder(); - - // When - order.confirm(); - - // Then - assertEquals(OrderStatus.CONFIRMED, order.getStatus()); - } - - @Test - @DisplayName("确认订单 - 非CREATED状态抛出异常") - void shouldThrowExceptionWhenConfirmNonCreatedOrder() { - // Given - var order = createTestOrder(); - order.confirm(); // 先变为 CONFIRMED - - // When & Then - DomainException exception = assertThrows(DomainException.class, order::confirm); - assertEquals(ErrorCode.ORDER_STATUS_INVALID, exception.getErrorCode()); - } - - @Test - @DisplayName("取消订单 - 成功") - void shouldCancelOrderSuccessfully() { - // Given - var order = createTestOrder(); - - // When - order.cancel(); - - // Then - assertEquals(OrderStatus.CANCELLED, order.getStatus()); - } - - @Test - @DisplayName("取消订单 - 已完成订单抛出异常") - void shouldThrowExceptionWhenCancelCompletedOrder() { - // Given - var order = createTestOrder(); - order.getClass(); // Note: In a real scenario, we'd have a complete() method - // For now, directly manipulating state for test purposes - order.cancel(); // This should work as it's from CREATED to CANCELLED - - // Testing cancellation of an already cancelled order - DomainException exception = assertThrows(DomainException.class, order::cancel); - assertEquals(ErrorCode.ORDER_STATUS_INVALID, exception.getErrorCode()); - } - - private Order createTestOrder() { - return Order.create("customer-123", "user-456", "测试订单", Money.of("100.00")); - } -} \ No newline at end of file diff --git a/tmpl-domain/src/test/java/com/example/tmpl/domain/valueobject/MoneyTest.java b/tmpl-domain/src/test/java/com/example/tmpl/domain/valueobject/MoneyTest.java deleted file mode 100644 index eb479e6..0000000 --- a/tmpl-domain/src/test/java/com/example/tmpl/domain/valueobject/MoneyTest.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.example.tmpl.domain.valueobject; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; - -import java.math.BigDecimal; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * 【领域层】金额值对象单元测试。 - */ -class MoneyTest { - - @Test - @DisplayName("创建金额 - 成功") - void shouldCreateMoneySuccessfully() { - // Given - BigDecimal amount = new BigDecimal("100.00"); - - // When - var money = new Money(amount); - - // Then - assertEquals(amount, money.amount()); - } - - @Test - @DisplayName("创建金额 - 精度自动调整为2位小数") - void shouldRoundToTwoDecimalPlaces() { - // Given - BigDecimal amount = new BigDecimal("100.1234"); - - // When - var money = new Money(amount); - - // Then - assertEquals(new BigDecimal("100.12"), money.amount()); - } - - @Test - @DisplayName("创建金额 - null值抛出异常") - void shouldThrowExceptionWhenAmountIsNull() { - // When & Then - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> - new Money(null)); - assertTrue(exception.getMessage().contains("金额不能为 null")); - } - - @Test - @DisplayName("金额相加 - 成功") - void shouldAddMoneySuccessfully() { - // Given - var money1 = new Money(new BigDecimal("100.00")); - var money2 = new Money(new BigDecimal("50.00")); - - // When - var result = money1.add(money2); - - // Then - assertEquals(new BigDecimal("150.00"), result.amount()); - } - - @Test - @DisplayName("金额乘法 - 成功") - void shouldMultiplyMoneySuccessfully() { - // Given - var money = new Money(new BigDecimal("100.00")); - - // When - var result = money.multiply(3); - - // Then - assertEquals(new BigDecimal("300.00"), result.amount()); - } - - @Test - @DisplayName("金额工厂方法 - 从字符串创建") - void shouldCreateMoneyFromString() { - // Given - String amountStr = "100.50"; - - // When - var money = Money.of(amountStr); - - // Then - assertEquals(new BigDecimal("100.50"), money.amount()); - } - - @Test - @DisplayName("金额工厂方法 - 从BigDecimal创建") - void shouldCreateMoneyFromBigDecimal() { - // Given - BigDecimal amount = new BigDecimal("200.75"); - - // When - var money = Money.of(amount); - - // Then - assertEquals(amount, money.amount()); - } -} \ No newline at end of file diff --git a/tmpl-infrastructure/pom.xml b/tmpl-infrastructure/pom.xml deleted file mode 100644 index b55ea8f..0000000 --- a/tmpl-infrastructure/pom.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - 4.0.0 - - - com.example.tmpl - tmpl-service - 0.0.1-SNAPSHOT - - - tmpl-infrastructure - tmpl-infrastructure - 基础设施层 - 技术实现(JPA/Redis/MQ 等) - - - - - com.example.tmpl - tmpl-domain - - - com.example.tmpl - tmpl-application - - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-data-redis - - - - - org.postgresql - postgresql - runtime - - - org.flywaydb - flyway-core - - - org.flywaydb - flyway-database-postgresql - - - - - org.mapstruct - mapstruct - - - - - org.springframework.boot - spring-boot-starter-test - test - - - com.h2database - h2 - test - - - \ No newline at end of file diff --git a/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/audit/AuditFieldsListener.java b/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/audit/AuditFieldsListener.java deleted file mode 100644 index 276a7cf..0000000 --- a/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/audit/AuditFieldsListener.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.example.tmpl.infrastructure.audit; - -import com.example.tmpl.application.context.TenantContext; -import com.example.tmpl.infrastructure.persistence.entity.OrderPO; -import jakarta.persistence.PrePersist; -import jakarta.persistence.PreUpdate; - -import java.time.LocalDateTime; - -/** - * 【基础设施层】审计字段监听器。 - * - *

在实体持久化前自动填充审计字段(create_user_id, create_time, update_user_id, update_time)。 - */ -public class AuditFieldsListener { - - @PrePersist - public void setAuditFieldsOnCreate(OrderPO entity) { - try { - String userIdStr = TenantContext.current().getUserId(); - Long userId = userIdStr != null ? Long.valueOf(userIdStr) : 0L; - - entity.setCreateUser(userId); - entity.setCreateTime(LocalDateTime.now()); - entity.setUpdateUser(userId); - entity.setUpdateTime(LocalDateTime.now()); - } catch (IllegalStateException e) { - // 如果没有设置TenantContext,则使用默认值 - entity.setCreateUser(0L); - entity.setCreateTime(LocalDateTime.now()); - entity.setUpdateUser(0L); - entity.setUpdateTime(LocalDateTime.now()); - } - } - - @PreUpdate - public void setAuditFieldsOnUpdate(OrderPO entity) { - try { - String userIdStr = TenantContext.current().getUserId(); - Long userId = userIdStr != null ? Long.valueOf(userIdStr) : 0L; - - entity.setUpdateUser(userId); - entity.setUpdateTime(LocalDateTime.now()); - } catch (IllegalStateException e) { - // 如果没有设置TenantContext,则使用默认值 - entity.setUpdateUser(0L); - entity.setUpdateTime(LocalDateTime.now()); - } - } -} \ No newline at end of file diff --git a/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/config/JacksonConfig.java b/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/config/JacksonConfig.java deleted file mode 100644 index b177938..0000000 --- a/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/config/JacksonConfig.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.example.tmpl.infrastructure.config; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; - -/** - * 【基础设施层】Jackson JSON 序列化配置。 - * - *

配置日期时间序列化模块。 - */ -@Configuration -public class JacksonConfig { - - @Bean - @Primary - public ObjectMapper objectMapper() { - ObjectMapper mapper = new ObjectMapper(); - mapper.registerModule(new JavaTimeModule()); - return mapper; - } -} \ No newline at end of file diff --git a/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/config/JpaConfig.java b/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/config/JpaConfig.java deleted file mode 100644 index bd59a59..0000000 --- a/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/config/JpaConfig.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.tmpl.infrastructure.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; - -/** - * 【基础设施层】JPA 配置。 - * - *

启用 JPA 仓库并配置扫描路径。 - */ -@Configuration -@EnableJpaRepositories(basePackages = "com.example.tmpl.infrastructure.persistence.repository") -public class JpaConfig { -} \ No newline at end of file diff --git a/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/config/RedisConfig.java b/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/config/RedisConfig.java deleted file mode 100644 index 9a71b62..0000000 --- a/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/config/RedisConfig.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.tmpl.infrastructure.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.serializer.StringRedisSerializer; - -/** - * 【基础设施层】Redis 配置。 - * - *

配置 Redis 连接工厂和模板。 - */ -@Configuration -public class RedisConfig { - - @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { - RedisTemplate template = new RedisTemplate<>(); - template.setConnectionFactory(connectionFactory); - template.setKeySerializer(new StringRedisSerializer()); - template.setValueSerializer(new StringRedisSerializer()); - template.setHashKeySerializer(new StringRedisSerializer()); - template.setHashValueSerializer(new StringRedisSerializer()); - return template; - } -} \ No newline at end of file diff --git a/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/event/LoggingOrderEventPublisher.java b/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/event/LoggingOrderEventPublisher.java deleted file mode 100644 index 22ae12e..0000000 --- a/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/event/LoggingOrderEventPublisher.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.example.tmpl.infrastructure.event; - -import com.example.tmpl.application.port.out.OrderEventPublisher; -import com.example.tmpl.domain.event.DomainEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; - -/** - * 【基础设施层】订单事件发布器实现。 - * - *

默认实现仅记录事件日志,如需集成消息队列可在子类中扩展。 - */ -@Component -public class LoggingOrderEventPublisher implements OrderEventPublisher { - - private static final Logger log = LoggerFactory.getLogger(LoggingOrderEventPublisher.class); - - @Override - public void publish(DomainEvent event) { - log.info("发布领域事件: {} - {}", event.getClass().getSimpleName(), event.occurredOn()); - // 在实际项目中,这里可以集成 Kafka、RabbitMQ 等消息中间件 - } -} \ No newline at end of file diff --git a/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/persistence/converter/OrderStatusConverter.java b/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/persistence/converter/OrderStatusConverter.java deleted file mode 100644 index a5b3fdb..0000000 --- a/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/persistence/converter/OrderStatusConverter.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.example.tmpl.infrastructure.persistence.converter; - -import com.example.tmpl.domain.valueobject.OrderStatus; -import jakarta.persistence.AttributeConverter; -import jakarta.persistence.Converter; - -/** - * 【基础设施层】订单状态属性转换器。 - * - *

将枚举类型 OrderStatus 与数据库字符串字段进行转换。 - */ -@Converter(autoApply = true) -public class OrderStatusConverter implements AttributeConverter { - - @Override - public String convertToDatabaseColumn(OrderStatus status) { - if (status == null) { - return null; - } - return status.name(); - } - - @Override - public OrderStatus convertToEntityAttribute(String dbData) { - if (dbData == null) { - return null; - } - try { - return OrderStatus.valueOf(dbData.toUpperCase()); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("未知的订单状态: " + dbData); - } - } -} \ No newline at end of file diff --git a/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/persistence/entity/OrderPO.java b/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/persistence/entity/OrderPO.java deleted file mode 100644 index 0284f79..0000000 --- a/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/persistence/entity/OrderPO.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.example.tmpl.infrastructure.persistence.entity; - -import jakarta.persistence.*; -import java.time.LocalDateTime; - -/** - * 【基础设施层】订单持久化对象。 - * - *

对应数据库表 t_order,包含审计字段和乐观锁版本号。 - */ -@Entity -@Table(name = "t_order") -@EntityListeners(AuditFieldsListener.class) -public class OrderPO { - - @Id - @Column(name = "id", length = 32) - private String id; - - @Column(name = "customer_id", nullable = false, length = 64) - private String customerId; - - @Column(name = "user_id", nullable = false, length = 64) - private String userId; - - @Column(name = "title", nullable = false, length = 255) - private String title; - - @Column(name = "total_amount", precision = 10, scale = 2) - private java.math.BigDecimal totalAmount; - - @Convert(converter = OrderStatusConverter.class) - @Column(name = "status", nullable = false, length = 20) - private com.example.tmpl.domain.valueobject.OrderStatus status; - - @Column(name = "create_user_id", nullable = false) - private Long createUser; - - @Column(name = "create_time", nullable = false) - private LocalDateTime createTime; - - @Column(name = "update_user_id", nullable = false) - private Long updateUser; - - @Column(name = "update_time", nullable = false) - private LocalDateTime updateTime; - - @Column(name = "is_deleted", nullable = false) - private Boolean deleted; - - @Version - @Column(name = "version", nullable = false) - private Integer version; - - // Constructors - public OrderPO() {} - - // Getters and Setters - public String getId() { return id; } - public void setId(String id) { this.id = id; } - - public String getCustomerId() { return customerId; } - public void setCustomerId(String customerId) { this.customerId = customerId; } - - public String getUserId() { return userId; } - public void setUserId(String userId) { this.userId = userId; } - - public String getTitle() { return title; } - public void setTitle(String title) { this.title = title; } - - public java.math.BigDecimal getTotalAmount() { return totalAmount; } - public void setTotalAmount(java.math.BigDecimal totalAmount) { this.totalAmount = totalAmount; } - - public com.example.tmpl.domain.valueobject.OrderStatus getStatus() { return status; } - public void setStatus(com.example.tmpl.domain.valueobject.OrderStatus status) { this.status = status; } - - public Long getCreateUser() { return createUser; } - public void setCreateUser(Long createUser) { this.createUser = createUser; } - - public LocalDateTime getCreateTime() { return createTime; } - public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; } - - public Long getUpdateUser() { return updateUser; } - public void setUpdateUser(Long updateUser) { this.updateUser = updateUser; } - - public LocalDateTime getUpdateTime() { return updateTime; } - public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; } - - public Boolean getDeleted() { return deleted; } - public void setDeleted(Boolean deleted) { this.deleted = deleted; } - - public Integer getVersion() { return version; } - public void setVersion(Integer version) { this.version = version; } -} \ No newline at end of file diff --git a/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/persistence/mapper/OrderPersistenceMapper.java b/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/persistence/mapper/OrderPersistenceMapper.java deleted file mode 100644 index ca625ca..0000000 --- a/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/persistence/mapper/OrderPersistenceMapper.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.example.tmpl.infrastructure.persistence.mapper; - -import com.example.tmpl.domain.entity.Order; -import com.example.tmpl.domain.valueobject.Money; -import com.example.tmpl.domain.valueobject.OrderId; -import com.example.tmpl.domain.valueobject.OrderStatus; -import com.example.tmpl.infrastructure.persistence.entity.OrderPO; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; -import org.mapstruct.factory.Mappers; - -import java.time.LocalDateTime; - -/** - * 【基础设施层】订单持久化映射器。 - * - *

使用 MapStruct 自动生成 PO 与 Domain Entity 之间的转换代码。 - */ -@Mapper(componentModel = "spring", imports = {OrderStatus.class, LocalDateTime.class}) -public interface OrderPersistenceMapper { - - OrderPersistenceMapper INSTANCE = Mappers.getMapper(OrderPersistenceMapper.class); - - @Mapping(target = "id", source = "id.value") - @Mapping(target = "totalAmount", source = "totalAmount.amount") - @Mapping(target = "status", source = "status") - @Mapping(target = "deleted", source = "deleted") - OrderPO toPO(Order domain); - - @Mapping(target = "id", expression = "java(OrderId.of(po.getId()))") - @Mapping(target = "totalAmount", expression = "java(Money.of(po.getTotalAmount()))") - @Mapping(target = "status", source = "po.status") - @Mapping(target = "deleted", source = "po.deleted") - Order toDomain(OrderPO po); - - default String map(OrderId orderId) { - return orderId != null ? orderId.value() : null; - } - - default OrderId mapToOrderId(String value) { - return value != null ? OrderId.of(value) : null; - } - - default java.math.BigDecimal map(Money money) { - return money != null ? money.amount() : null; - } - - default Money mapToMoney(java.math.BigDecimal amount) { - return amount != null ? Money.of(amount) : null; - } -} \ No newline at end of file diff --git a/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/persistence/repository/OrderJpaRepository.java b/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/persistence/repository/OrderJpaRepository.java deleted file mode 100644 index e763565..0000000 --- a/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/persistence/repository/OrderJpaRepository.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.tmpl.infrastructure.persistence.repository; - -import com.example.tmpl.infrastructure.persistence.entity.OrderPO; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -/** - * 【基础设施层】订单 JPA 仓储接口。 - * - *

提供基础的 CRUD 操作和自定义查询方法。 - */ -@Repository -public interface OrderJpaRepository extends JpaRepository { - - /** - * 根据ID和客户ID查询订单。 - * - * @param id 订单ID - * @param customerId 客户ID - * @return 订单持久化对象 - */ - @Query("SELECT o FROM OrderPO o WHERE o.id = :id AND o.customerId = :customerId AND o.deleted = false") - Optional findByIdAndCustomerId(@Param("id") String id, @Param("customerId") String customerId); -} \ No newline at end of file diff --git a/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/persistence/repository/OrderPersistenceAdapter.java b/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/persistence/repository/OrderPersistenceAdapter.java deleted file mode 100644 index eb04268..0000000 --- a/tmpl-infrastructure/src/main/java/com/example/tmpl/infrastructure/persistence/repository/OrderPersistenceAdapter.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.example.tmpl.infrastructure.persistence.repository; - -import com.example.tmpl.application.port.out.OrderPersistencePort; -import com.example.tmpl.domain.entity.Order; -import com.example.tmpl.domain.exception.DomainException; -import com.example.tmpl.domain.exception.ErrorCode; -import com.example.tmpl.domain.valueobject.OrderId; -import com.example.tmpl.infrastructure.persistence.entity.OrderPO; -import com.example.tmpl.infrastructure.persistence.mapper.OrderPersistenceMapper; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -/** - * 【基础设施层】订单持久化适配器。 - * - *

实现应用层定义的 OrderPersistencePort 接口,负责将领域对象与持久化对象之间的转换。 - */ -@Component -public class OrderPersistenceAdapter implements OrderPersistencePort { - - private final OrderJpaRepository jpaRepository; - private final OrderPersistenceMapper mapper; - - public OrderPersistenceAdapter(OrderJpaRepository jpaRepository, OrderPersistenceMapper mapper) { - this.jpaRepository = jpaRepository; - this.mapper = mapper; - } - - @Override - public void save(Order order) { - OrderPO po = mapper.toPO(order); - jpaRepository.save(po); - // 更新领域对象的版本号(乐观锁) - order.getClass(); // 保留领域对象引用,以便基础设施层更新相关信息 - } - - @Override - public Optional findByIdAndCustomerId(OrderId id, String customerId) { - return jpaRepository.findByIdAndCustomerId(id.value(), customerId) - .map(mapper::toDomain); - } - - @Override - public void delete(Order order) { - OrderPO po = mapper.toPO(order); - po.setDeleted(true); - jpaRepository.save(po); - } -} \ No newline at end of file diff --git a/tmpl-infrastructure/src/test/java/com/example/tmpl/infrastructure/persistence/OrderPersistenceMapperTest.java b/tmpl-infrastructure/src/test/java/com/example/tmpl/infrastructure/persistence/OrderPersistenceMapperTest.java deleted file mode 100644 index 4af3cba..0000000 --- a/tmpl-infrastructure/src/test/java/com/example/tmpl/infrastructure/persistence/OrderPersistenceMapperTest.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.example.tmpl.infrastructure.persistence; - -import com.example.tmpl.domain.entity.Order; -import com.example.tmpl.domain.valueobject.Money; -import com.example.tmpl.domain.valueobject.OrderId; -import com.example.tmpl.domain.valueobject.OrderStatus; -import com.example.tmpl.infrastructure.persistence.entity.OrderPO; -import com.example.tmpl.infrastructure.persistence.mapper.OrderPersistenceMapper; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import java.time.LocalDateTime; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * 【基础设施层】订单持久化映射器测试。 - */ -@SpringBootTest -class OrderPersistenceMapperTest { - - @Autowired - private OrderPersistenceMapper mapper; - - @Test - @DisplayName("领域对象转持久化对象 - 成功") - void shouldMapDomainToPoSuccessfully() { - // Given - var orderId = OrderId.of("test-order-id"); - var order = Order.reconstruct( - orderId, - "customer-123", - "user-456", - "测试订单", - Money.of("100.00"), - OrderStatus.CREATED, - LocalDateTime.now(), - LocalDateTime.now(), - 0, - false - ); - - // When - var po = mapper.toPO(order); - - // Then - assertEquals(orderId.value(), po.getId()); - assertEquals("customer-123", po.getCustomerId()); - assertEquals("user-456", po.getUserId()); - assertEquals("测试订单", po.getTitle()); - assertEquals(Money.of("100.00").amount(), po.getTotalAmount()); - assertEquals(OrderStatus.CREATED, po.getStatus()); - } - - @Test - @DisplayName("持久化对象转领域对象 - 成功") - void shouldMapPoToDomainSuccessfully() { - // Given - var po = new OrderPO(); - po.setId("test-order-id"); - po.setCustomerId("customer-123"); - po.setUserId("user-456"); - po.setTitle("测试订单"); - po.setTotalAmount(Money.of("100.00").amount()); - po.setStatus(OrderStatus.CREATED); - po.setDeleted(false); - po.setVersion(0); - po.setCreateTime(LocalDateTime.now()); - po.setUpdateTime(LocalDateTime.now()); - - // When - var order = mapper.toDomain(po); - - // Then - assertEquals(OrderId.of("test-order-id"), order.getId()); - assertEquals("customer-123", order.getCustomerId()); - assertEquals("user-456", order.getUserId()); - assertEquals("测试订单", order.getTitle()); - assertEquals(Money.of("100.00"), order.getTotalAmount()); - assertEquals(OrderStatus.CREATED, order.getStatus()); - } -} \ No newline at end of file