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

159 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/`](../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
```bash
# 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.