Files
weather-data/system-admin/CLAUDE.md
T

8.0 KiB

CLAUDE.md — system-admin

Backend module: Spring Boot 3.5 admin application (port 8080, context /system-admin).


Build & Run

mvn clean install -DskipTests           # full build (tests skipped by default)
mvn clean install -DskipTests=false     # build with tests
mvn -pl system-admin -DskipTests=false -Dtest=YourTestClass test  # single test

Tests use JUnit 4 (@RunWith(SpringRunner.class)), not JUnit 5. Only 2 test files exist (Redis, dynamic datasource). No Maven wrapper (mvnw). Maven remote repository: Aliyun mirror.

Launch from IntelliJ:

  • AdminApplication (system-admin/) → port 8080, context /system-admin
  • GeneratorApplication (renren-generator/) — commented out of build

Service layer pattern

Two base classes in system-common:

Base Purpose
CrudService<Dao, Entity, DTO> Generic CRUD: page(), get(), save(), update(), delete()
BaseService<Dao> Lighter base without DTO generic

Module convention:

modules/<name>/
├── controller/  → @RestController, returns Result
├── dao/         → extends BaseMapper<Entity> (MyBatis-Plus)
├── dto/         → request/query DTOs (often extends BaseEntity)
├── entity/      → @TableName JPA entity
├── service/     → interface extends CrudService/BaseService
│   └── impl/    → @Service implementation
├── excel/       → EasyExcel VO classes (optional)
└── vo/          → response VO classes (optional)

Mapper XMLs: src/main/resources/mapper/<domain>/**/*.xml

Conventions

  • Lombok used throughout: @Data, @AllArgsConstructor, @Slf4j are standard on entity/service classes.
  • DTO/Entity/VO separation per module — request DTOs often extend BaseEntity.

PK & Auth

  • PK: ASSIGN_ID (Snowflake) via IdUtil.getSnowflakeNextId(). All entities extend BaseEntity. Exception: WeatherStationEntity uses AUTO_INCREMENT.
  • Auth: Apache Shiro 1.12 (Jakarta classifier) + OAuth2 token. Login → token header.
  • Do not introduce Spring Security — the project uses Shiro exclusively.

Key cross-cutting mechanisms

Mechanism How
Data permissions @DataFilter on controller → DataFilterAspect → MyBatis interceptor injects dept-based SQL
Auto-fill FieldMetaObjectHandler fills creator/date via MyBatis-Plus. Only works with insert()/updateById() — batch inserts (e.g. insertBatchMultiRow) bypass auto-fill; fields must be set manually.
Scheduled jobs Quartz. schedule_job table, implements ITask, @Component("beanName"). Jobs auto-register at startup via JobCommandLineRunner. Seed data: testTask (paused, every 30 min), fileScanTask (paused, every 5 min), weatherSummarizeCacheTask (active, daily 1 AM).
File scanning WatchService (primary, background thread) + Quartz fallback (FileScanTask) + startup runner (FileScanStartupRunner). Files identified by MD5 hash.
Excel import EasyExcel + async dual-pass via WeatherDataImportManager. Progress tracked in-memory (ConcurrentHashMap), lost on restart.
API responses Always wrapped in Result. Frontend expects code === 0 for success.
Validation Hibernate Validator. XSS filter via XssFilter. i18n messages in system-common/src/main/resources/i18n/validation.properties.

Exception handling

Single @RestControllerAdvice handler in the admin module:

Handler Catches Persists errors?
CustomExceptionHandler CommonException, DuplicateKeyException, UnauthorizedException, generic Exception Yes — saves to SysLogErrorService (IP, user-agent, URI, params, stack trace)

Both return a generic error for caught Exception (not the exception message). CommonException uses i18n message lookup via MessageUtils.getMessage(code). Error codes follow int scheme: 5 digits, first 2 = module, last 3 = business (e.g. 10001-10029).

Logging

  • Logback-spring config in system-admin/src/main/resources/logback-spring.xml. Logger names use io.renren (fork legacy), not com.weather.
  • Admin dev profile enables MyBatis SQL stdout logging (StdOutImpl).
  • When adding @Slf4j to com.weather.* classes, add a com.weather level override or change the existing io.renren logger scope.

Redis & Docs

  • Redis: project-options.redis.open: true in dev YAML controls whether RedisAspect intercepts Redis calls (default false — Redis operations silently skipped when disabled).
  • Knife4j: disabled by default (knife4j.enable: false). Docs at /doc.html when enabled.
  • RedisAspect wraps @RedisCache annotations with channel publish for cache invalidation.

MyBatis-Plus gotchas

  • Batch inserts bypass auto-fillFieldMetaObjectHandler only fires on insert()/updateById(). Custom batch methods must manually set creator, createDate, updater, updateDate, deptId.
  • Column names with special characters (e.g. rain_20_20) require explicit @TableField annotations — MyBatis-Plus cannot auto-map them from camelCase.
  • typeAliasesPackage: com.weather.modules.*.entity — all entity classes must reside under a modules sub-package.

Weather domain

Three sub-modules under system-admin/.../modules/weather/:

Module Purpose
dailydata/ Daily observations, Excel batch import (async dual-pass), EasyExcel listener, summary export
station/ Weather station CRUD, linked to dept via dept_id
filescan/ File monitoring + serving. Format: <地区>地区-<指标>.png / <地区>地区631信息.txt

Weather data import flow

  1. First pass: AnalysisEventListener counts total rows.
  2. Second pass: WeatherDataListener processes with batch insert (2000 records/batch).
  3. Progress tracked in-memory via ConcurrentHashMap<String, ImportProgress> (volatile fields + AtomicInteger).
  4. Runs on CompletableFuture with manual UserContextHolder propagation for security context. (Note: UserContextHolder is in system-admin/.../security/user/, not the empty system-common/.../holder/ package.)
  5. On completion, clears Redis summary cache (weather:summarize:*).

Weather summarize cache

WeatherSummarizeCacheTask (Quartz job) pre-computes daily historical summaries into Redis. Uses MySQL-specific SQL functions (MONTH(), DAY() on observe_date). Cache key: weather:summarize:{month}:{day}, non-expiring. Service checks cache first for queries spanning same month/day across years.

Station priority ordering

WeatherDailyDataServiceImpl.page() uses custom CASE WHEN SQL to order stations belonging to the user's department + sub-departments first.

System alert module

CRUD + polling notification system under modules/sys/:

Layer File
Entity entity/SysAlertEntity.javasys_alert table, Snowflake PK
DAO dao/SysAlertDao.java + mapper/sys/SysAlertDao.xmlselectActiveAlerts, selectActiveAlertsSince, countBySource
DTO dto/SysAlertDTO.javapublishTime/createDate/updateDate read-only
VO vo/SysAlertVO.javaid as String (mapped from Long)
Service service/SysAlertService.java + impl/publishIfNotExists (dedup by source), withdraw (soft), CRUD
Controller controller/SysAlertController.java — polling: GET active, GET active/since (no auth); CRUD: page/get/save/update/delete/withdraw (Shiro permissions)
Enums enums/AlertLevelEnum.java (info/warning/danger), enums/AlertSourceTypeEnum.java (manual/import/schedule/datasource)

Data source extension

File Role
alert/AlertSource.java SPI interface: getName(), check() returning AlertMessage records
alert/AlertSourceCollector.java @Autowired(required=false) List<AlertSource> auto-discovery, calls publishIfNotExists for each
alert/task/AlertSourcePollingTask.java Quartz job (alertSourcePollingTask), default paused in schedule_job

To add a new alert source: implement AlertSource, register as @Component, enable the polling task.