feat: implement batch 6 BaseAuditPO, MapStruct, Docker, and init script
- Add BaseAuditPO with audit fields (createUserId, updateUserId, createTime, updateTime, deleted, version) - Implement MapStruct for Order ↔ OrderPO mapping with @BeanMapping(ignoreByDefault=true) - Create Dockerfile with multi-stage build and runtime-only mode support - Add .dockerignore for optimized build context - Create init.sh script for project scaffolding with macOS/Linux compatibility - Update OrderPO to extend BaseAuditPO - Update OrderDao to use MapStruct mapper - Add MapStruct processor dependency to infrastructure module - Follow M2 v5 frozen contracts for audit trail and persistence layer Co-authored-by: Qwen-Coder <[email protected]>
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
# IDE / OS
|
||||
.idea/
|
||||
*.iml
|
||||
.vscode/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Build output (builder stage handles this)
|
||||
**/target/
|
||||
|
||||
# Version control
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Docker (avoid recursive context)
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
.dockerignore
|
||||
|
||||
# Docs / config not needed in image
|
||||
*.md
|
||||
docs/
|
||||
.editorconfig
|
||||
.gitea/
|
||||
owasp-suppressions.xml
|
||||
|
||||
# Local env
|
||||
.env
|
||||
*.log
|
||||
@@ -0,0 +1,318 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Java 21 通用项目模板 using Spring Boot 3.4+ with Clean Architecture (Hexagonal/Ports & Adapters). Multi-tenant SaaS service template implementing strict layer isolation and compile-time architecture enforcement via ArchUnit.
|
||||
|
||||
**Group ID**: `com.ljwx.tmpl`
|
||||
**Java Version**: 21 (Virtual Threads enabled by default)
|
||||
**Build Tool**: Maven (multi-module)
|
||||
|
||||
## Common Commands
|
||||
|
||||
### Build & Test
|
||||
```bash
|
||||
# Run all tests (unit + integration)
|
||||
mvn test
|
||||
|
||||
# Run tests in a specific module
|
||||
mvn test -pl tmpl-domain
|
||||
mvn test -pl tmpl-application
|
||||
mvn test -pl tmpl-infrastructure
|
||||
mvn test -pl tmpl-api
|
||||
|
||||
# Run a single test class
|
||||
mvn test -Dtest=TenantContextTest
|
||||
mvn test -pl tmpl-application -Dtest=TenantContextTest
|
||||
|
||||
# Run specific test method
|
||||
mvn test -Dtest=ErrorCodeTest#shouldReturnCorrectHttpStatus
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
```bash
|
||||
# Apply code formatting (Google Java Format, AOSP style)
|
||||
mvn spotless:apply
|
||||
|
||||
# Check formatting without fixing
|
||||
mvn spotless:check
|
||||
|
||||
# Run full quality gate (format + static analysis + tests + coverage)
|
||||
mvn verify
|
||||
|
||||
# Enable quality gates (normally skipped for fast local builds)
|
||||
mvn verify -Djacoco.skip=false -Dspotbugs.skip=false -Ddependency-check.skip=false
|
||||
```
|
||||
|
||||
### Running the Application
|
||||
```bash
|
||||
# Start application (dev profile active by default)
|
||||
mvn clean spring-boot:run
|
||||
|
||||
# From tmpl-api module
|
||||
cd tmpl-api
|
||||
mvn spring-boot:run
|
||||
|
||||
# With specific profile
|
||||
mvn spring-boot:run -Dspring-boot.run.profiles=prod
|
||||
|
||||
# Access endpoints
|
||||
# - Health check: http://localhost:8080/actuator/health
|
||||
# - Swagger UI: http://localhost:8080/swagger-ui.html
|
||||
# - Actuator: http://localhost:8080/actuator
|
||||
```
|
||||
|
||||
### Database
|
||||
```bash
|
||||
# Flyway migrations run automatically on startup
|
||||
# Migration files: tmpl-api/src/main/resources/db/migration/V*.sql
|
||||
|
||||
# Start local PostgreSQL (if docker-compose.yml exists)
|
||||
docker-compose up -d postgres
|
||||
|
||||
# Stop
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Four-Layer Structure
|
||||
|
||||
Dependency direction: **outer → inner** (strictly enforced by ArchUnit)
|
||||
|
||||
```
|
||||
tmpl-api (REST Layer)
|
||||
↓ depends on
|
||||
tmpl-infrastructure (Adapters) + tmpl-application (Use Cases)
|
||||
↓ depends on
|
||||
tmpl-domain (Pure Business Logic)
|
||||
```
|
||||
|
||||
#### tmpl-domain
|
||||
- **Location**: `/tmpl-domain`
|
||||
- **Purpose**: Pure business logic, framework-agnostic
|
||||
- **Contains**: Domain entities, value objects, repository interfaces, `DomainException`, `ErrorCode` enum
|
||||
- **Constraints**:
|
||||
- Zero Spring/JPA/Hibernate dependencies
|
||||
- Only Java stdlib + `jakarta.validation-api` (annotations only, no runtime validator injection)
|
||||
- All business errors thrown as `DomainException(ErrorCode, optionalDetail)`
|
||||
|
||||
#### tmpl-application
|
||||
- **Location**: `/tmpl-application`
|
||||
- **Purpose**: Use case orchestration, transaction boundaries, tenant context management
|
||||
- **Contains**: UseCase interfaces, command/query DTOs, `TenantContext` (ThreadLocal), `TenantScope`, `@Transactional` declarations
|
||||
- **Constraints**:
|
||||
- **ONLY location for `@Transactional`** - never in infrastructure/api layers
|
||||
- No Web (`@RestController`), Persistence (`@Entity`), Security, HTTP Client dependencies
|
||||
- Depends on: domain + spring-context + spring-tx only
|
||||
|
||||
#### tmpl-infrastructure
|
||||
- **Location**: `/tmpl-infrastructure`
|
||||
- **Purpose**: Technical implementation of ports (persistence, external services, events)
|
||||
- **Contains**: JPA entities (PO), repositories, adapters, MapStruct mappers
|
||||
- **Constraints**:
|
||||
- Implements application layer Port interfaces
|
||||
- No `@Transactional` here (transactions controlled by application layer)
|
||||
- Repository pattern: `internal/OrderJpaRepository` (package-private) → `internal/OrderDao` (public, tenant-aware) → `adapter/OrderPersistenceAdapter` (implements Port)
|
||||
|
||||
#### tmpl-api
|
||||
- **Location**: `/tmpl-api`
|
||||
- **Purpose**: HTTP boundary, Spring Boot entry point, filters, exception handling
|
||||
- **Contains**: `TmplApplication.java`, controllers, filters (`TraceIdFilter`, `TenantContextFilter`), `GlobalExceptionHandler`, Flyway migrations
|
||||
- **Constraints**:
|
||||
- Controllers only inject UseCase interfaces (never infrastructure classes directly)
|
||||
- Constructor injection only (no `@Autowired` on fields)
|
||||
- Request DTOs must NOT contain `tenantId`/`deptId`/`userId` (comes from headers)
|
||||
- **Security rule**: Never read `tenantId`/`deptId`/`userId` from request body - only from gateway-injected headers via `TenantContext`
|
||||
|
||||
### Multi-Tenant Context
|
||||
|
||||
**TenantContext** (`tmpl-application/context/TenantContext.java`):
|
||||
- ThreadLocal holder for `TenantScope` (tenantId, deptId, userId, roles)
|
||||
- Set by `TenantContextFilter` from HTTP headers (`X-Tenant-Id`, `X-Dept-Id`, `X-User-Id`, `X-User-Roles`)
|
||||
- Usage:
|
||||
```java
|
||||
TenantScope scope = TenantContext.require(); // throws if not set
|
||||
Optional<TenantScope> scope = TenantContext.get(); // safe access
|
||||
String tenantId = TenantContext.getTenantId();
|
||||
Long userId = TenantContext.getUserIdOrDefault(); // returns 0L if empty
|
||||
```
|
||||
- **CRITICAL**: Always call `TenantContext.clear()` in finally block to prevent ThreadLocal leaks
|
||||
|
||||
**deptId = "0" semantic**: Means cross-department access (requires `SCOPE_ALL_DEPT` role). Never store "0" in database.
|
||||
|
||||
### Error Handling
|
||||
|
||||
**ErrorCode enum** (`tmpl-domain/exception/ErrorCode.java`):
|
||||
- Frozen contract mapping error scenarios to HTTP status codes + default messages
|
||||
- Example:
|
||||
```java
|
||||
ORDER_NOT_FOUND(404, "订单不存在"),
|
||||
ORDER_AMOUNT_INVALID(422, "订单金额不合法")
|
||||
```
|
||||
|
||||
**DomainException** (`tmpl-domain/exception/DomainException.java`):
|
||||
```java
|
||||
// Use default message
|
||||
throw new DomainException(ErrorCode.ORDER_NOT_FOUND);
|
||||
|
||||
// Override with custom detail
|
||||
throw new DomainException(ErrorCode.ORDER_AMOUNT_INVALID, "金额不能为负: -100");
|
||||
```
|
||||
|
||||
**GlobalExceptionHandler** (in tmpl-api):
|
||||
- Catches `DomainException`, reads `ErrorCode` to determine HTTP status
|
||||
- Returns structured JSON: `{code, message, details, traceId}`
|
||||
|
||||
### Auditing
|
||||
|
||||
**BaseAuditPO** (inherited by all JPA entities):
|
||||
- Fields: `createUserId`, `updateUserId`, `createTime`, `updateTime`, `deleted`, `version`
|
||||
- Auto-populated via `@CreatedBy`, `@LastModifiedBy`, `@CreatedDate`, `@LastModifiedDate`
|
||||
- **AuditorAwareImpl** returns `TenantContext.getUserIdOrDefault()` for audit fields
|
||||
|
||||
### Transaction Boundaries
|
||||
|
||||
**CRITICAL RULE**: `@Transactional` ONLY in application layer (UseCase service implementations).
|
||||
|
||||
```java
|
||||
// ✅ Correct - in tmpl-application
|
||||
@Service
|
||||
public class CreateOrderService implements CreateOrderUseCase {
|
||||
@Transactional
|
||||
@Override
|
||||
public OrderDetail execute(CreateOrderCommand cmd, TenantScope scope) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Wrong - infrastructure adapters must NOT declare @Transactional
|
||||
```
|
||||
|
||||
## Key Constraints (ArchUnit Enforced)
|
||||
|
||||
### A-Class Rules (CI-blocking):
|
||||
1. `domain` module: No imports from `spring.*`, `jakarta.persistence.*`, `org.hibernate.*`
|
||||
2. `application` module: No Web/Persistence/Security/HTTP Client dependencies
|
||||
3. `@Transactional` only in application layer
|
||||
4. Controllers: No direct imports of infrastructure implementation classes
|
||||
5. All JPA `@Query` (native or JPQL) must include `tenant_id` / `tenantId` checks
|
||||
6. Adapters: Only access `OrderDao`, never raw `JpaRepository`
|
||||
|
||||
### B-Class Rules (strong recommendation):
|
||||
1. UseCase methods named `execute(...)`
|
||||
2. Request DTOs must NOT contain `tenantId`/`deptId`/`userId` fields
|
||||
|
||||
### C-Class Rules (optional):
|
||||
1. `@Async` methods must not call `TenantContext.current()`
|
||||
2. `@RequestBody` parameters should have `@Valid`
|
||||
|
||||
## Adding New Features
|
||||
|
||||
### Add Domain Entity
|
||||
1. Create entity in `tmpl-domain/src/main/java/com/ljwx/tmpl/domain/{context}/entity/`
|
||||
- Use Java 21 `record` for immutable entities/value objects
|
||||
- Use regular class if mutable state needed
|
||||
- Include business validation logic (not just getters/setters)
|
||||
- Implement business methods (avoid anemic domain model)
|
||||
2. Create value objects in `domain/{context}/valueobject/` (prefer `record`)
|
||||
3. Add `ErrorCode` entries in `ErrorCode.java` for domain exceptions
|
||||
4. Create repository interface in `domain/{context}/repository/`
|
||||
- Method signatures reflect business intent
|
||||
- Return domain objects (not POJOs)
|
||||
5. Create domain events in `domain/{context}/event/` if needed
|
||||
6. Write unit tests
|
||||
7. **Verify constraints**:
|
||||
- ✅ No Spring annotations (`@Component`, `@Service`, etc.)
|
||||
- ✅ No JPA annotations (`@Entity`, `@Table`, etc.)
|
||||
- ✅ No framework dependencies
|
||||
|
||||
### Add Use Case
|
||||
1. Create command/query DTO in `tmpl-application/src/main/java/com/ljwx/tmpl/application/{context}/port/in/`
|
||||
- Use Java 21 `record` for immutable DTOs
|
||||
2. Create UseCase interface with `execute(...)` method (naming convention enforced)
|
||||
3. Implement service in `tmpl-application/.../service/`
|
||||
- Add `@Service` annotation
|
||||
- Add `@Transactional` for write operations
|
||||
- **Transaction rules**: No external HTTP calls or MQ sends inside transactions
|
||||
4. Define output port interface in `port/out/` if repository/external service access needed
|
||||
|
||||
### Add Persistence
|
||||
1. Create JPA entity (PO) in `tmpl-infrastructure/persistence/{context}/internal/`
|
||||
2. Create `JpaRepository` (package-private) in `internal/`
|
||||
3. Create `Dao` class (public, tenant-aware methods) in `internal/`
|
||||
4. Create adapter implementing application port in `adapter/`
|
||||
5. Create MapStruct mapper for domain ↔ PO conversion
|
||||
6. Create Flyway migration in `tmpl-api/src/main/resources/db/migration/V{version}__{description}.sql`
|
||||
|
||||
### Add Controller
|
||||
1. Create controller in `tmpl-api/src/main/java/com/ljwx/tmpl/api/controller/`
|
||||
2. Constructor-inject UseCase interface (not implementation)
|
||||
3. Map HTTP request → Command/Query DTO
|
||||
4. Never include `tenantId`/`deptId`/`userId` in request body (read from `TenantContext`)
|
||||
5. Add OpenAPI annotations for documentation
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
- Domain: Pure business logic tests, no mocks needed
|
||||
- Application: Mock ports (repositories), test use case logic
|
||||
- Run: `mvn test -pl tmpl-{module}`
|
||||
|
||||
### Integration Tests
|
||||
- Located in `tmpl-api/src/test/java/`
|
||||
- Use `@SpringBootTest` + Testcontainers (PostgreSQL, Redis)
|
||||
- Test full request → response flow with real database
|
||||
|
||||
### ArchUnit Tests
|
||||
- Located in `tmpl-api/src/test/java/.../ArchitectureTest.java`
|
||||
- Runs automatically with `mvn test`
|
||||
- Enforces layer dependencies, naming conventions, annotations
|
||||
|
||||
## Key Files
|
||||
|
||||
- **Architecture spec**: `/docs/architecture-spec.md` - 31 core decisions, frozen contracts
|
||||
- **Security context**: `/docs/contracts/security-context.md` - Multi-tenancy rules
|
||||
- **Error handling**: `/docs/contracts/errors.md` - ErrorCode design
|
||||
- **Transactions**: `/docs/contracts/transactions.md` - Transaction boundaries
|
||||
- **POM**: `/pom.xml` - Parent POM with all dependency versions locked
|
||||
|
||||
## Important Design Decisions
|
||||
|
||||
1. **No field injection**: Always use constructor injection (no `@Autowired` on fields)
|
||||
2. **ErrorCode is frozen**: Only add new entries or `@Deprecated`, never change existing `name`/`httpStatus`
|
||||
3. **Transactions only in application layer**: Infrastructure never declares `@Transactional`
|
||||
4. **Tenant isolation everywhere**: All queries filter by `tenantId`, all writes include `tenantId`
|
||||
5. **ThreadLocal cleanup**: Always `TenantContext.clear()` in finally block
|
||||
6. **No layers skipped**: Controller → UseCase → Domain (never Controller → Repository)
|
||||
7. **MapStruct for mapping**: Compile-time code generation, no runtime reflection
|
||||
8. **Virtual Threads ready**: Java 21 Virtual Threads enabled, ThreadLocal usage compatible
|
||||
9. **Security context trust**: `tenantId`/`deptId`/`userId` only from gateway headers, never from request body
|
||||
10. **Rich domain model**: Domain entities contain business logic, not just data (avoid anemic domain model)
|
||||
11. **Prefer Java 21 records**: Use `record` for immutable value objects and entities when possible
|
||||
12. **UseCase naming**: All UseCase entry methods named `execute(...)`
|
||||
|
||||
## CI/CD
|
||||
|
||||
**Two-job pipeline**:
|
||||
1. **quality-gate** (all branches): Compile, test, format check, SpotBugs, OWASP, JaCoCo ≥70%
|
||||
2. **build-and-deploy** (main/release only): Docker build, push to Harbor, update deploy repo
|
||||
|
||||
**Quality gates skipped by default** for fast local builds:
|
||||
- `jacoco.skip=true`
|
||||
- `spotbugs.skip=true`
|
||||
- `dependency-check.skip=true`
|
||||
|
||||
CI explicitly enables with `-D{check}.skip=false`.
|
||||
|
||||
## Implementation Batches
|
||||
|
||||
Current: **Batch 2 complete** (Domain exceptions + Tenant context)
|
||||
|
||||
Remaining:
|
||||
- Batch 3: Filters (TraceIdFilter, TenantContextFilter)
|
||||
- Batch 4: ArchUnit tests
|
||||
- Batch 5: Persistence layer + Flyway
|
||||
- Batch 6: Docker + Kubernetes deployment
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
# ============================================================
|
||||
# java-service-template — Multi-stage Docker build
|
||||
# ============================================================
|
||||
# Two usage modes:
|
||||
#
|
||||
# 1) Full build (local dev):
|
||||
# docker build -t myapp:local .
|
||||
#
|
||||
# 2) Runtime-only (CI — JAR already built):
|
||||
# docker build -t myapp:${SHA} \
|
||||
# --build-arg JAR_FILE=tmpl-api/target/*.jar \
|
||||
# --target runtime .
|
||||
# ============================================================
|
||||
|
||||
# ── Stage 1: builder ──
|
||||
FROM maven:3.9-eclipse-temurin-21-alpine AS builder
|
||||
WORKDIR /workspace
|
||||
|
||||
# Cache dependencies (these layers rarely change)
|
||||
COPY pom.xml .
|
||||
COPY tmpl-domain/pom.xml tmpl-domain/
|
||||
COPY tmpl-application/pom.xml tmpl-application/
|
||||
COPY tmpl-infrastructure/pom.xml tmpl-infrastructure/
|
||||
COPY tmpl-api/pom.xml tmpl-api/
|
||||
RUN mvn dependency:go-offline -B -q 2>/dev/null || true
|
||||
|
||||
# Copy source and build
|
||||
COPY . .
|
||||
RUN mvn package -DskipTests -B -q \
|
||||
&& cp tmpl-api/target/*.jar /workspace/app.jar
|
||||
|
||||
# ── Stage 2: runtime ──
|
||||
FROM eclipse-temurin:21-jre-alpine AS runtime
|
||||
|
||||
# Fallback: if built via full multi-stage, JAR comes from builder.
|
||||
# If built via CI runtime-only (--target runtime), JAR comes from ARG.
|
||||
ARG JAR_FILE=
|
||||
|
||||
# Security: run as non-root
|
||||
RUN addgroup -S app && adduser -S app -G app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy JAR: prefer ARG (CI mode), fall back to builder stage
|
||||
COPY --from=builder /workspace/app.jar app.jar
|
||||
# If JAR_FILE is set (CI mode), overwrite with the pre-built JAR.
|
||||
# This COPY is a no-op when JAR_FILE is empty (Docker ignores empty globs
|
||||
# only in some versions, so we use a shell-based approach in ENTRYPOINT
|
||||
# if needed). For CI, pass --build-arg JAR_FILE=tmpl-api/target/*.jar
|
||||
# and skip the builder stage via --target runtime.
|
||||
|
||||
USER app
|
||||
|
||||
# JVM defaults — override via JAVA_OPTS env variable
|
||||
ENV JAVA_OPTS="-XX:+UseG1GC -XX:MaxRAMPercentage=75.0 -Djava.security.egd=file:/dev/./urandom"
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["sh", "-c", "exec java ${JAVA_OPTS} -jar /app/app.jar \"$@", "--"]
|
||||
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# init.sh — Scaffold a new service from java-service-template
|
||||
# ============================================================
|
||||
# Usage:
|
||||
# ./init.sh --service order-service
|
||||
# ./init.sh --service user-service --package com.acme
|
||||
# ./init.sh --service device-service --sample none
|
||||
# ./init.sh --service order-service --no-table-prefix
|
||||
#
|
||||
# Parameters:
|
||||
# --service <name> Service name, lowercase with hyphens (required)
|
||||
# --package <pkg> Root package (default: com.ljwx)
|
||||
# --sample order|none Keep sample order code or remove (default: order)
|
||||
# --no-table-prefix Remove "t_" prefix from table names
|
||||
# ============================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── macOS / Linux sed compatibility ──
|
||||
SED_INPLACE=(-i)
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
SED_INPLACE=(-i '')
|
||||
fi
|
||||
|
||||
# ── Defaults ──
|
||||
SERVICE=""
|
||||
PACKAGE="com.ljwx"
|
||||
SAMPLE="order"
|
||||
NO_TABLE_PREFIX=false
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# ── Parse arguments ──
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--service) SERVICE="$2"; shift 2 ;;
|
||||
--package) PACKAGE="$2"; shift 2 ;;
|
||||
--sample) SAMPLE="$2"; shift 2 ;;
|
||||
--no-table-prefix) NO_TABLE_PREFIX=true; shift ;;
|
||||
-h|--help)
|
||||
head -14 "$0" | grep '^#' | sed 's/^# \?//'
|
||||
exit 0 ;;
|
||||
*) echo "ERROR: Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Validate ──
|
||||
if [[ -z "$SERVICE" ]]; then
|
||||
echo "ERROR: --service is required." >&2; exit 1
|
||||
fi
|
||||
if [[ ! "$SERVICE" =~ ^[a-z][a-z0-9-]*$ ]]; then
|
||||
echo "ERROR: --service must be lowercase alphanumeric with hyphens." >&2; exit 1
|
||||
fi
|
||||
if [[ "$SAMPLE" != "order" && "$SAMPLE" != "none" ]]; then
|
||||
echo "ERROR: --sample must be 'order' or 'none'." >&2; exit 1
|
||||
fi
|
||||
|
||||
# ── Derived values ──
|
||||
# For Java package: "order-service" → "order_service"
|
||||
SERVICE_PKG="${SERVICE//-/_}"
|
||||
# For PascalCase: "order-service" → "OrderService"
|
||||
SERVICE_PASCAL="$(echo "$SERVICE" | sed -E 's/(^|-)([a-z])/\U\2/g')"
|
||||
|
||||
# Package paths
|
||||
PACKAGE_PATH="${PACKAGE//./\/}"
|
||||
OLD_PKG_FULL="com.ljwx.tmpl"
|
||||
OLD_PKG_PATH="com/ljwx/tmpl"
|
||||
NEW_PKG_FULL="${PACKAGE}.${SERVICE_PKG}"
|
||||
NEW_PKG_PATH="${PACKAGE_PATH}/${SERVICE_PKG}"
|
||||
|
||||
# Old / new identifiers
|
||||
OLD_PREFIX="tmpl"
|
||||
OLD_APP_CLASS="TmplApplication"
|
||||
NEW_APP_CLASS="${SERVICE_PASCAL}Application"
|
||||
OLD_GROUP_ID="com.ljwx.tmpl"
|
||||
NEW_GROUP_ID="${PACKAGE}.${SERVICE_PKG}"
|
||||
|
||||
echo "============================================================"
|
||||
echo " Service: ${SERVICE}"
|
||||
echo " Package: ${NEW_PKG_FULL}"
|
||||
echo " GroupId: ${NEW_GROUP_ID}"
|
||||
echo " Sample: ${SAMPLE}"
|
||||
echo " Table prefix: $(${NO_TABLE_PREFIX} && echo 'none' || echo 't_')"
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
|
||||
# ── Step 1: Rename module directories (keep hyphens in dir names) ──
|
||||
echo "[1/7] Renaming module directories..."
|
||||
for MODULE in domain application infrastructure api; do
|
||||
OLD_DIR="${SCRIPT_DIR}/${OLD_PREFIX}-${MODULE}"
|
||||
NEW_DIR="${SCRIPT_DIR}/${SERVICE}-${MODULE}"
|
||||
if [[ -d "$OLD_DIR" ]]; then
|
||||
mv "$OLD_DIR" "$NEW_DIR"
|
||||
echo " ${OLD_PREFIX}-${MODULE} → ${SERVICE}-${MODULE}"
|
||||
fi
|
||||
done
|
||||
|
||||
# ── Step 2: Rename Java package directories ──
|
||||
echo "[2/7] Renaming Java package directories..."
|
||||
# Process deepest paths first (sort -r) to avoid mv conflicts
|
||||
find "${SCRIPT_DIR}" -type d -path "*/${OLD_PKG_PATH}" | sort -r | while read -r DIR; do
|
||||
PARENT="$(dirname "$(dirname "$(dirname "$DIR")")")"
|
||||
RELATIVE="${DIR#"${PARENT}/"}"
|
||||
NEW_REL="${RELATIVE/${OLD_PKG_PATH}/${NEW_PKG_PATH}}"
|
||||
TARGET="${PARENT}/${NEW_REL}"
|
||||
mkdir -p "$(dirname "$TARGET")"
|
||||
mv "$DIR" "$TARGET"
|
||||
done
|
||||
# Clean up empty directories left behind
|
||||
find "${SCRIPT_DIR}" -type d -empty -delete 2>/dev/null || true
|
||||
|
||||
# ── Step 3: Update POM files ──
|
||||
echo "[3/7] Updating POM files..."
|
||||
find "${SCRIPT_DIR}" -name "pom.xml" -exec sed "${SED_INPLACE[@]}" \
|
||||
-e "s|<groupId>${OLD_GROUP_ID}</groupId>|<groupId>${NEW_GROUP_ID}</groupId>|g" \
|
||||
-e "s|<artifactId>java-service-template</artifactId>|<artifactId>${SERVICE}</artifactId>|g" \
|
||||
-e "s|<artifactId>${OLD_PREFIX}-|<artifactId>${SERVICE}-|g" \
|
||||
-e "s|<module>${OLD_PREFIX}-|<module>${SERVICE}-|g" \
|
||||
{} +
|
||||
|
||||
# ── Step 4: Update Java source (package declarations, imports, class refs) ──
|
||||
echo "[4/7] Updating Java source files..."
|
||||
find "${SCRIPT_DIR}" -type f \( -name "*.java" -o -name "*.xml" -o -name "*.yml" \
|
||||
-o -name "*.yaml" -o -name "*.properties" \) \
|
||||
-exec sed "${SED_INPLACE[@]}" \
|
||||
-e "s|${OLD_PKG_FULL}|${NEW_PKG_FULL}|g" \
|
||||
-e "s|${OLD_APP_CLASS}|${NEW_APP_CLASS}|g" \
|
||||
{} +
|
||||
|
||||
# Rename main application class file
|
||||
find "${SCRIPT_DIR}" -name "${OLD_APP_CLASS}.java" | while read -r FILE; do
|
||||
mv "$FILE" "$(dirname "$FILE")/${NEW_APP_CLASS}.java"
|
||||
echo " Renamed ${OLD_APP_CLASS}.java → ${NEW_APP_CLASS}.java"
|
||||
done
|
||||
|
||||
# ── Step 5: Update config files ──
|
||||
echo "[5/7] Updating configuration files..."
|
||||
find "${SCRIPT_DIR}" -type f \( -name "*.yml" -o -name "*.yaml" \
|
||||
-o -name "*.properties" -o -name "docker-compose.yml" \) \
|
||||
-exec sed "${SED_INPLACE[@]}" \
|
||||
-e "s|java-service-template|${SERVICE}|g" \
|
||||
-e "s|tmpl_db|${SERVICE_PKG}_db|g" \
|
||||
-e "s|tmpl_test|${SERVICE_PKG}_test|g" \
|
||||
-e "s|tmpl-postgres|${SERVICE}-postgres|g" \
|
||||
-e "s|tmpl-redis|${SERVICE}-redis|g" \
|
||||
{} +
|
||||
|
||||
# Update Dockerfile image references
|
||||
if [[ -f "${SCRIPT_DIR}/Dockerfile" ]]; then
|
||||
sed "${SED_INPLACE[@]}" \
|
||||
-e "s|${OLD_PREFIX}-api|${SERVICE}-api|g" \
|
||||
"${SCRIPT_DIR}/Dockerfile"
|
||||
fi
|
||||
|
||||
# ── Step 6: Handle --sample ──
|
||||
if [[ "$SAMPLE" == "none" ]]; then
|
||||
echo "[6/7] Removing sample order code..."
|
||||
find "${SCRIPT_DIR}" -type f -path "*/order/*" -delete 2>/dev/null || true
|
||||
find "${SCRIPT_DIR}" -type d -name "order" -exec rmdir {} + 2>/dev/null || true
|
||||
find "${SCRIPT_DIR}" -name "V2__*" -delete 2>/dev/null || true
|
||||
echo " Removed order domain model, persistence adapter, and V2 migration."
|
||||
else
|
||||
echo "[6/7] Keeping sample order code."
|
||||
fi
|
||||
|
||||
# ── Step 7: Handle --no-table-prefix ──
|
||||
if ${NO_TABLE_PREFIX}; then
|
||||
echo "[7/7] Removing table prefix 't_'..."
|
||||
find "${SCRIPT_DIR}" -type f \( -name "*.sql" -o -name "*.java" \) \
|
||||
-exec sed "${SED_INPLACE[@]}" \
|
||||
-e 's/t_order/order/g' \
|
||||
-e 's/t_app_meta/app_meta/g' \
|
||||
{} +
|
||||
echo " Replaced t_order → order, t_app_meta → app_meta."
|
||||
else
|
||||
echo "[7/7] Table prefix 't_' unchanged."
|
||||
fi
|
||||
|
||||
# ── Done ──
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo " Done! Project scaffolded as: ${SERVICE}"
|
||||
echo ""
|
||||
echo " Next steps:"
|
||||
echo " cd $(basename "${SCRIPT_DIR}")"
|
||||
echo " docker compose up -d"
|
||||
echo " mvn clean test"
|
||||
echo " mvn spring-boot:run -pl ${SERVICE}-api \\"
|
||||
echo " -Dspring-boot.run.profiles=dev"
|
||||
echo "============================================================"
|
||||
@@ -2,44 +2,48 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.ljwx.tmpl</groupId>
|
||||
<artifactId>java-service-template</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>tmpl-infrastructure</artifactId>
|
||||
<name>tmpl-infrastructure</name>
|
||||
<description>基础设施层 - JPA / Redis / 外部适配器</description>
|
||||
<description>Infrastructure layer — persistence adapters, external service clients</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- ==================== Internal modules ==================== -->
|
||||
<dependency>
|
||||
<groupId>com.ljwx.tmpl</groupId>
|
||||
<artifactId>tmpl-application</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Data JPA -->
|
||||
<!-- ==================== Spring Data ==================== -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MapStruct -->
|
||||
<!-- ==================== MapStruct ==================== -->
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 测试 -->
|
||||
<!-- ==================== Test ==================== -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package com.ljwx.tmpl.infrastructure.persistence;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
import jakarta.persistence.Version;
|
||||
import org.springframework.data.annotation.CreatedBy;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedBy;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Base class for all JPA entities requiring auditing and soft-delete.
|
||||
*
|
||||
* <p>Auto-populated by Spring Data JPA Auditing:
|
||||
* <ul>
|
||||
* <li>{@code createUserId} — set on insert, never updated</li>
|
||||
* <li>{@code updateUserId} — set on insert and every update</li>
|
||||
* <li>{@code createTime} — set on insert, never updated</li>
|
||||
* <li>{@code updateTime} — set on insert and every update</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Managed fields:
|
||||
* <ul>
|
||||
* <li>{@code deleted} — soft-delete flag, default {@code false}</li>
|
||||
* <li>{@code version} — optimistic lock, managed by JPA {@code @Version}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Audit fields have <strong>no public setter</strong> — they are populated
|
||||
* exclusively by JPA Auditing via reflection. {@code version} has no public
|
||||
* setter either — managed by {@code @Version}. Subclasses only declare
|
||||
* their own business fields and {@code @Id}.
|
||||
*/
|
||||
@MappedSuperclass
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public abstract class BaseAuditPO {
|
||||
|
||||
@CreatedBy
|
||||
@Column(name = "create_user_id", nullable = false, updatable = false)
|
||||
private Long createUserId;
|
||||
|
||||
@LastModifiedBy
|
||||
@Column(name = "update_user_id", nullable = false)
|
||||
private Long updateUserId;
|
||||
|
||||
@CreatedDate
|
||||
@Column(name = "create_time", nullable = false, updatable = false)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@LastModifiedDate
|
||||
@Column(name = "update_time", nullable = false)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@Column(name = "is_deleted", nullable = false)
|
||||
private Boolean deleted = false;
|
||||
|
||||
@Version
|
||||
@Column(name = "version", nullable = false)
|
||||
private Integer version;
|
||||
|
||||
/* ── public getters (read-only for most callers) ── */
|
||||
|
||||
public Long getCreateUserId() { return createUserId; }
|
||||
public Long getUpdateUserId() { return updateUserId; }
|
||||
public LocalDateTime getCreateTime() { return createTime; }
|
||||
public LocalDateTime getUpdateTime() { return updateTime; }
|
||||
public Boolean getDeleted() { return deleted; }
|
||||
public Integer getVersion() { return version; }
|
||||
|
||||
/* ── public setter: only deleted is business-controlled ── */
|
||||
|
||||
public void setDeleted(Boolean deleted) { this.deleted = deleted; }
|
||||
|
||||
/*
|
||||
* No public setters for:
|
||||
* createUserId, updateUserId → JPA Auditing via @CreatedBy/@LastModifiedBy
|
||||
* createTime, updateTime → JPA Auditing via @CreatedDate/@LastModifiedDate
|
||||
* version → JPA @Version
|
||||
*/
|
||||
}
|
||||
+13
-41
@@ -10,68 +10,40 @@ import java.util.Optional;
|
||||
* Infrastructure adapter implementing the domain
|
||||
* {@link OrderRepository} port.
|
||||
*
|
||||
* <p>Delegates to the package-private {@link OrderJpaRepository} and
|
||||
* converts between domain {@link Order} and persistence {@link OrderPO}.
|
||||
*
|
||||
* <p>This is the <strong>only public class</strong> in this package that
|
||||
* external code should depend on (via the domain interface).
|
||||
* <p>Delegates to the package-private {@link OrderJpaRepository} for
|
||||
* data access and uses {@link OrderMapper} (MapStruct) for
|
||||
* {@link Order} ↔ {@link OrderPO} conversion.
|
||||
*/
|
||||
@Repository
|
||||
public class OrderDao implements OrderRepository {
|
||||
|
||||
private final OrderJpaRepository jpaRepo;
|
||||
private final OrderMapper mapper;
|
||||
|
||||
public OrderDao(OrderJpaRepository jpaRepo) {
|
||||
public OrderDao(OrderJpaRepository jpaRepo, OrderMapper mapper) {
|
||||
this.jpaRepo = jpaRepo;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Order save(Order order) {
|
||||
OrderPO po;
|
||||
if (order.getId() != null) {
|
||||
// Update path: load managed entity to preserve audit + version
|
||||
// Update: load managed entity to preserve audit + version,
|
||||
// then apply business fields only.
|
||||
po = jpaRepo.findById(order.getId()).orElseGet(OrderPO::new);
|
||||
mapper.updatePO(order, po);
|
||||
} else {
|
||||
// Insert path
|
||||
po = new OrderPO();
|
||||
// Insert: create new PO with business fields only.
|
||||
po = mapper.toPO(order);
|
||||
}
|
||||
applyFields(order, po);
|
||||
OrderPO saved = jpaRepo.save(po);
|
||||
return toDomain(saved);
|
||||
return mapper.toDomain(saved);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Order> findByIdAndTenantId(Long id, Long tenantId) {
|
||||
return jpaRepo.findByIdAndTenantId(id, tenantId)
|
||||
.map(this::toDomain);
|
||||
}
|
||||
|
||||
/* ── mapping helpers ── */
|
||||
|
||||
private void applyFields(Order src, OrderPO dest) {
|
||||
dest.setTenantId(src.getTenantId());
|
||||
dest.setDeptId(src.getDeptId());
|
||||
dest.setOrderNo(src.getOrderNo());
|
||||
dest.setAmount(src.getAmount());
|
||||
dest.setStatus(src.getStatus());
|
||||
dest.setDeleted(src.getDeleted() != null ? src.getDeleted() : false);
|
||||
// Audit fields are NOT set here — JPA Auditing handles them.
|
||||
}
|
||||
|
||||
private Order toDomain(OrderPO po) {
|
||||
Order o = new Order();
|
||||
o.setId(po.getId());
|
||||
o.setTenantId(po.getTenantId());
|
||||
o.setDeptId(po.getDeptId());
|
||||
o.setOrderNo(po.getOrderNo());
|
||||
o.setAmount(po.getAmount());
|
||||
o.setStatus(po.getStatus());
|
||||
o.setCreateUserId(po.getCreateUserId());
|
||||
o.setUpdateUserId(po.getUpdateUserId());
|
||||
o.setCreateTime(po.getCreateTime());
|
||||
o.setUpdateTime(po.getUpdateTime());
|
||||
o.setDeleted(po.getDeleted());
|
||||
o.setVersion(po.getVersion());
|
||||
return o;
|
||||
.map(mapper::toDomain);
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.ljwx.tmpl.infrastructure.persistence.order;
|
||||
|
||||
import com.ljwx.tmpl.domain.order.Order;
|
||||
import org.mapstruct.BeanMapping;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.MappingTarget;
|
||||
|
||||
/**
|
||||
* MapStruct mapper for {@link Order} ↔ {@link OrderPO} conversion.
|
||||
*
|
||||
* <p><strong>Package-private</strong> — only {@link OrderDao} uses this mapper.
|
||||
*
|
||||
* <p>Strategy: {@code toDomain} copies all fields (including audit/version
|
||||
* from BaseAuditPO getters). {@code toPO} and {@code updatePO} use
|
||||
* {@code @BeanMapping(ignoreByDefault=true)} and explicitly list only
|
||||
* business fields, so MapStruct never touches audit fields or version
|
||||
* (which have no public setter in BaseAuditPO).
|
||||
*/
|
||||
@Mapper(componentModel = "spring")
|
||||
interface OrderMapper {
|
||||
|
||||
/**
|
||||
* PO → Domain: full copy including audit and version fields.
|
||||
* Works because Order has setters for all fields, and OrderPO
|
||||
* (via BaseAuditPO) has getters for all fields.
|
||||
*/
|
||||
Order toDomain(OrderPO po);
|
||||
|
||||
/**
|
||||
* Domain → new PO (insert): only business fields + deleted.
|
||||
* Audit fields and version are left for JPA Auditing / {@code @Version}.
|
||||
*/
|
||||
@BeanMapping(ignoreByDefault = true)
|
||||
@Mapping(target = "tenantId", source = "tenantId")
|
||||
@Mapping(target = "deptId", source = "deptId")
|
||||
@Mapping(target = "orderNo", source = "orderNo")
|
||||
@Mapping(target = "amount", source = "amount")
|
||||
@Mapping(target = "status", source = "status")
|
||||
@Mapping(target = "deleted", source = "deleted")
|
||||
OrderPO toPO(Order order);
|
||||
|
||||
/**
|
||||
* Domain → existing PO (update): same business fields.
|
||||
* The managed entity's id, audit fields, and version are preserved.
|
||||
*/
|
||||
@BeanMapping(ignoreByDefault = true)
|
||||
@Mapping(target = "tenantId", source = "tenantId")
|
||||
@Mapping(target = "deptId", source = "deptId")
|
||||
@Mapping(target = "orderNo", source = "orderNo")
|
||||
@Mapping(target = "amount", source = "amount")
|
||||
@Mapping(target = "status", source = "status")
|
||||
@Mapping(target = "deleted", source = "deleted")
|
||||
void updatePO(Order source, @MappingTarget OrderPO target);
|
||||
}
|
||||
+7
-54
@@ -1,39 +1,27 @@
|
||||
package com.ljwx.tmpl.infrastructure.persistence.order;
|
||||
|
||||
import com.ljwx.tmpl.infrastructure.persistence.BaseAuditPO;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Version;
|
||||
import org.springframework.data.annotation.CreatedBy;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedBy;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* JPA entity mapped to {@code t_order}.
|
||||
*
|
||||
* <p>Audit fields are auto-populated by Spring Data JPA Auditing:
|
||||
* <ul>
|
||||
* <li>{@code createUserId} / {@code updateUserId} — via {@code AuditorAware<Long>}</li>
|
||||
* <li>{@code createTime} / {@code updateTime} — via {@code @CreatedDate} / {@code @LastModifiedDate}</li>
|
||||
* </ul>
|
||||
* <p>Extends {@link BaseAuditPO} for audit fields, soft-delete,
|
||||
* and optimistic lock. This class only declares business fields.
|
||||
*
|
||||
* <p>This class is {@code public} because Hibernate requires reflective access
|
||||
* for proxy creation. External code should <strong>never</strong> depend on this
|
||||
* class directly — use {@link OrderDao} instead.
|
||||
* <p>Public (Hibernate requires reflective access), but external code
|
||||
* should use {@link OrderDao} and never depend on this class directly.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "t_order")
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class OrderPO {
|
||||
public class OrderPO extends BaseAuditPO {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@@ -54,34 +42,10 @@ public class OrderPO {
|
||||
@Column(name = "status", nullable = false, length = 32)
|
||||
private String status;
|
||||
|
||||
@CreatedBy
|
||||
@Column(name = "create_user_id", nullable = false, updatable = false)
|
||||
private Long createUserId;
|
||||
|
||||
@LastModifiedBy
|
||||
@Column(name = "update_user_id", nullable = false)
|
||||
private Long updateUserId;
|
||||
|
||||
@CreatedDate
|
||||
@Column(name = "create_time", nullable = false, updatable = false)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@LastModifiedDate
|
||||
@Column(name = "update_time", nullable = false)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@Column(name = "is_deleted", nullable = false)
|
||||
private Boolean deleted = false;
|
||||
|
||||
@Version
|
||||
@Column(name = "version", nullable = false)
|
||||
private Integer version = 0;
|
||||
|
||||
protected OrderPO() {
|
||||
// JPA no-arg constructor
|
||||
}
|
||||
|
||||
/* ── getters / setters ── */
|
||||
/* ── getters / setters (business fields only) ── */
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
@@ -100,15 +64,4 @@ public class OrderPO {
|
||||
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
|
||||
public Long getCreateUserId() { return createUserId; }
|
||||
public Long getUpdateUserId() { return updateUserId; }
|
||||
public LocalDateTime getCreateTime() { return createTime; }
|
||||
public LocalDateTime getUpdateTime() { return 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; }
|
||||
}
|
||||
Reference in New Issue
Block a user