Files
conti-backend/CLAUDE.md
T
Guangfei.ZhaoandClaude Code d78fcfc202 chore: 加 CLAUDE.md,忽略 agent 工作目录
CLAUDE.md(英文)给后续 AI 用:文档是约束来源而非描述、构建与测试命令、
architecture-test 三个测试类的不同机制(含源码扫描测试路径写错时静默扫 0 个
文件报绿的坑)、以及版本基线的硬约束。

.gitignore 忽略 .claude / .agents。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-30 16:01:51 +08:00

9.0 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Rule #0: the docs are the constraint, not a description

Architecture decisions live in ../conti-docs/backend/ (01–13, the single source of truth). This repo's docs/ is a copy of it, so people don't have to switch repos.

  • Edit docs in ../conti-docs/backend/, then sync into docs/ (sync commands — including two cross-repo link fixups — are in docs/README.md). Editing only one side causes drift.
  • When code and docs disagree, change the code, not the doc. In particular, never loosen a rule in architecture-test just to make code pass — loosening the rules is exactly what makes this whole structure worthless.
  • The few places where the scaffolding deliberately deviates from the docs are commented in the code itself.

Commands

# Everyday check: compile + unit tests + ArchUnit. No Docker needed (~1 min).
./gradlew build -PexcludeIntegration

# Full check: includes Testcontainers / WireMock. Needs a working Docker daemon. Run before an MR.
./gradlew build

# Single test class / single test method
./gradlew :architecture-test:test --tests "*KotlinSyntaxScanTest*"
./gradlew :domains:identity-store:test --tests "*StoreJpaRepositoryTest*"

# Run locally (only MySQL is in Docker; the app runs via Gradle so breakpoints/hot-reload work)
docker compose up -d mysql
SPRING_PROFILES_ACTIVE=local ./gradlew :bootstrap:bootRun   # Swagger: http://localhost:8080/swagger-ui.html

# Reset the database
docker compose down -v && docker compose up -d mysql

Tests tagged integration (real MySQL / WireMock) are skipped by -PexcludeIntegration. When you touch a repository layer or SQL, -PexcludeIntegration does not exercise what you changed — run the full build.

Architecture

Modular monolith: 14 Gradle modules, one deployable jar. Boundaries are enforced physically by the Gradle dependency graph, and statically by architecture-test.

bootstrap               the only module with @SpringBootApplication; the only one producing a jar;
                        the only one that applies the Boot plugin
platform/platform-*     cross-cutting infrastructure (web / security / persistence / observability / integration)
domains/*               business domains; identity-store is the only fully implemented vertical slice
integration/*           adapters to external systems (f6-adapter / mini-clients)
architecture-test       no production code; holds the architecture rules and source-scan tests

Dependency rules (enforced at compile time):

  • domains/x must never depend on domains/y — only on y-contract (a read-only contract module containing no Spring Web / JPA).
  • domains/* may depend on integration/* and platform-*.
  • Among platform-*, a module may only depend on platform-web.
  • Layering: api → application → domain, with infrastructure depended on by both. The domain layer must not reference Spring or JPA at all — its beans are wired explicitly via @Bean in infrastructure/config/. Entities live only in the infrastructure package; the api layer never sees them.

Adding a domain requires wiring 4 places. Miss one and the symptom is subtle (it compiles and boots, but Flyway doesn't run or the boundary is silently unguarded):

  1. settings.gradle: add the include, and write the module's build.gradle.
  2. bootstrap/build.gradle: add implementation project(...), or it never lands in the jar.
  3. DomainFlywayConfig.DOMAIN_SCHEMAS: add the schema name, create db/migration/<schema>/V1__init.sql (a Flyway location must not be an empty dir), and add the CREATE DATABASE to docker/mysql/init/01-create-databases.sql.
  4. ArchitectureRulesTest.domains: add the package name; NativeQueryScanTest.ownerByModule: add "xxx" to "<schema>".

Package name is the directory name with hyphens stripped (identity-store → identitystore); the sole exception is bff-orchestration → bff.

architecture-test: three test classes, three mechanisms

Know what's in here before changing production code, or you'll hit a red build and not know why:

Test class Mechanism What it catches
ArchitectureRulesTest ArchUnit over bytecode layer directions, Entity placement, field injection, java.util.Date, util-class naming, etc.
NativeQueryScanTest reads source text a native SQL string naming another domain's schema — schema names are string literals, invisible in bytecode
KotlinSyntaxScanTest reads source text !! / lateinit / suspend / kotlinx.coroutines / RxJava / Reactor / data class *Entity

Two traps in the source-scan tests (you only see them by reading the code):

  • They locate sources with relative paths (Path.of("../domains")), relying on Gradle running them with the module dir as working directory. If you add a new top-level source root, you must add it to KotlinSyntaxScanTest.productionSources() — otherwise the new code is never checked.
  • productionSources() contains filter { Files.exists(it) }: if the path is wrong it silently scans zero files and reports green. When writing a test like this, confirm it actually reads files by planting a deliberate violation and checking that it goes red — don't trust a green alone.

Kotlin coding constraints (see 13-kotlin-style.md)

!! / lateinit / coroutines / RxJava / Reactor / data class entities / any XxxUtils|Util|Helper|Common class are build gates — violating them fails test and therefore build. They are not suggestions.

Two things that are deliberately not automated:

  • companion object restricted to legitimate uses — left to review (all 14 existing uses are legitimate; a hard rule would only cause false positives).
  • Entities must be proxyable by Hibernate — currently NOT enforced (known gap): the root build.gradle does not apply kotlin("plugin.jpa"), so entities are final, Hibernate cannot proxy them, and fetch = LAZY silently degrades to eager. This hasn't blown up only because there are currently zero lazy associations. Read the "已知缺口(2026-09)" (known gap) section of 13-kotlin-style.md before adding anything like @ManyToOne(fetch = LAZY).

Hard version constraints

Component Version Constraint
Gradle 9.5.0 (wrapper) Two-sided: the Boot 4.1 plugin needs 8.14+ or 9.x, while KGP 2.4.10's fully-supported ceiling is 9.5.0. Bumping either Boot or Kotlin means recomputing the intersection.
Kotlin 2.4.10
Java 21 (pinned via toolchain)
Spring Boot / Cloud 4.1.0 / 2025.1.2
  • Version alignment always goes through Gradle's native platform() BOM; io.spring.dependency-management is deliberately not used (that plugin historically breaks on every Gradle major).
  • Gradle 9 no longer bundles the JUnit Platform launcher — deleting testRuntimeOnly 'org.junit.platform:junit-platform-launcher' from the root build.gradle makes every test task fail outright.
  • Boot 4 moved packages: @DataJpaTest → org.springframework.boot.data.jpa.test.autoconfigure, @AutoConfigureTestDatabase → org.springframework.boot.jdbc.test.autoconfigure (both need spring-boot-starter-data-jpa-test); spring-boot-starter-aop is now spring-boot-starter-aspectj.

Database conventions

  • One domain, one database (in MySQL, schema ≡ database), so DomainFlywayConfig starts one Flyway instance per entry in DOMAIN_SCHEMAS.
  • An already-executed Flyway script must never be edited (checksum mismatch → startup fails with Validate failed; this applies to the dev environment too). To change something, add a higher-versioned script.
  • Every migration must be forward-compatible: maxUnavailable: 0 means old and new pods share one database during a rolling update, so dropping or altering a column requires expand-contract across two releases.
  • Store-scoped queries must use StoreContextHolder.currentStoreId(), never a storeId request parameter — that parameter is the standard entry point for an authorization-bypass query.
  • Seed data (LocalSeedDataConfig) exists only under the local profile and must never be written into a Flyway script.

Deployment and misc

  • local on your own machine (Gradle); dev on internal k3s (./scripts/deploy-dev.sh <sha>, connect VPN first); uat/prod on Azure AKS (protected tag → manual Promote in GitLab).
  • Images don't compile code: the Dockerfile consumes the jar produced by the CI validate stage, so the bytes deployed are the bytes that were tested.
  • Real secrets are in no file in this repo; k8s/secret-uat.yaml holds only __injected_by_pipeline__ placeholders.
  • The lint (ktlint/detekt) and coverage jobs are currently left commented out in .gitlab-ci.yml — the plugins aren't wired up, so enabling them would go red immediately. .editorconfig already reserves the ij_kotlin_* section.