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-adminGeneratorApplication(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,@Slf4jare standard on entity/service classes. - DTO/Entity/VO separation per module — request DTOs often extend
BaseEntity.
PK & Auth
- PK:
ASSIGN_ID(Snowflake) viaIdUtil.getSnowflakeNextId(). All entities extendBaseEntity. Exception:WeatherStationEntityusesAUTO_INCREMENT. - Auth: Apache Shiro 1.12 (Jakarta classifier) + OAuth2 token. Login →
tokenheader. - 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 useio.renren(fork legacy), notcom.weather. - Admin dev profile enables MyBatis SQL stdout logging (
StdOutImpl). - When adding
@Slf4jtocom.weather.*classes, add acom.weatherlevel override or change the existingio.renrenlogger scope.
Redis & Docs
- Redis:
project-options.redis.open: truein dev YAML controls whetherRedisAspectintercepts Redis calls (defaultfalse— Redis operations silently skipped when disabled). - Knife4j: disabled by default (
knife4j.enable: false). Docs at/doc.htmlwhen enabled. RedisAspectwraps@RedisCacheannotations with channel publish for cache invalidation.
MyBatis-Plus gotchas
- Batch inserts bypass auto-fill —
FieldMetaObjectHandleronly fires oninsert()/updateById(). Custom batch methods must manually setcreator,createDate,updater,updateDate,deptId. - Column names with special characters (e.g.
rain_20_20) require explicit@TableFieldannotations — MyBatis-Plus cannot auto-map them from camelCase. typeAliasesPackage: com.weather.modules.*.entity— all entity classes must reside under amodulessub-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
- First pass:
AnalysisEventListenercounts total rows. - Second pass:
WeatherDataListenerprocesses with batch insert (2000 records/batch). - Progress tracked in-memory via
ConcurrentHashMap<String, ImportProgress>(volatilefields +AtomicInteger). - Runs on
CompletableFuturewith manualUserContextHolderpropagation for security context. (Note:UserContextHolderis insystem-admin/.../security/user/, not the emptysystem-common/.../holder/package.) - 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.java — sys_alert table, Snowflake PK |
| DAO | dao/SysAlertDao.java + mapper/sys/SysAlertDao.xml — selectActiveAlerts, selectActiveAlertsSince, countBySource |
| DTO | dto/SysAlertDTO.java — publishTime/createDate/updateDate read-only |
| VO | vo/SysAlertVO.java — id 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.