项目优化,构建文件Lombok异常问题修复
This commit is contained in:
@@ -2,58 +2,66 @@
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
Subdirectories have their own `CLAUDE.md` with module-specific details — see:
|
||||
- `system-admin/CLAUDE.md` — backend build, patterns, conventions, weather domain
|
||||
- `system-common/CLAUDE.md` — shared base classes, i18n
|
||||
- `weather-data-ui/CLAUDE.md` — frontend stack, patterns, critical rules
|
||||
## Project Overview
|
||||
|
||||
---
|
||||
Weather data management system -- a multi-module Maven project with a Spring Boot backend and Vue 3 frontend for managing meteorological monitoring data from weather stations.
|
||||
|
||||
## Project overview
|
||||
- **Backend**: Spring Boot 3.5.11, Java 17, Apache Shiro, MyBatis-Plus, Quartz
|
||||
- **Frontend**: Vue 3 + TypeScript + Vite 5, Element Plus, ECharts, Pinia
|
||||
- **Database**: MySQL 8.0 (primary), supports Oracle / SQL Server / PostgreSQL / Dameng via dynamic datasource
|
||||
- **Cache**: Redis (Lettuce client)
|
||||
|
||||
**weather-data** is a full-stack meteorological data analysis platform: Java 17 / Spring Boot 3.5 multi-module backend + Vue 3 / Vite 5 / TypeScript frontend. Forked from [renren-security](https://gitee.com/renrenio/renren-security).
|
||||
## Project Conventions
|
||||
|
||||
```
|
||||
weather-data/
|
||||
├── system-common/ → shared Java lib
|
||||
├── system-admin/ → admin backend (port 8080, /system-admin)
|
||||
├── system-dynamic-datasource → multi-DS routing via @DataSource annotation + AOP
|
||||
├── renren-generator/ → code generator (commented out of build)
|
||||
└── weather-data-ui/ → Vue 3 SPA frontend
|
||||
- **No emoji**: Avoid using emoji in code (including variable names, comments, commit messages) and in response messages. Use plain text instead.
|
||||
- **Keep CLAUDE.md files current**: Any operation that adds, deletes, or modifies files affecting project structure (new modules, new packages, renamed components, dependency changes, config changes, schema changes, etc.) must update the relevant CLAUDE.md file(s) -- top-level for cross-cutting changes, module-level for module-specific changes.
|
||||
|
||||
## Module Index
|
||||
|
||||
| Module | Path | Purpose |
|
||||
|--------|------|---------|
|
||||
| system-common | `system-common/` | Shared library: base classes, Redis, validation, XSS, utilities |
|
||||
| system-dynamic-datasource | `system-dynamic-datasource/` | Multi-datasource support via AbstractRoutingDataSource |
|
||||
| system-admin | `system-admin/` | Main Spring Boot application, REST API, jobs, security |
|
||||
| weather-data-ui | `weather-data-ui/` | Vue 3 frontend |
|
||||
|
||||
Each module has its own `CLAUDE.md` with module-specific architecture and conventions. When working in a module, read that module's CLAUDE.md for context.
|
||||
|
||||
## Build & Run Commands
|
||||
|
||||
### Backend
|
||||
|
||||
```bash
|
||||
# Build everything (skip tests)
|
||||
mvn clean package -DskipTests
|
||||
|
||||
# Build only system-admin module
|
||||
cd system-admin && mvn package -DskipTests
|
||||
|
||||
# Run with dev profile
|
||||
cd system-admin && mvn spring-boot:run -Dspring-boot.run.profiles=dev
|
||||
```
|
||||
|
||||
**Database**: `weather_data_system` (MySQL). Init from `system-admin/db/weather_data_system.sql` (includes schema + seed data). Default admin: `admin` / `admin` (BCrypt-encoded). Key custom tables: `weather_daily_data`, `weather_station`, `weather_file_scan_record`. No Flyway/Liquibase — all schema changes are manual SQL.
|
||||
Main entry: `system-admin/src/main/java/com/weather/AdminApplication.java`
|
||||
|
||||
### Port & context path reference
|
||||
### Frontend
|
||||
|
||||
| Service | Port | Context Path | App Class |
|
||||
|---|---|---|---|
|
||||
| Admin | 8080 | `/system-admin` | `AdminApplication` |
|
||||
| Frontend (dev) | 8001 | `/` | Vite dev server |
|
||||
| Frontend (prod) | 80 | `/` | Nginx via gateway |
|
||||
```bash
|
||||
cd weather-data-ui
|
||||
npm install
|
||||
npm run dev # dev server on port 8001
|
||||
npm run build # production build
|
||||
npm run lint # ESLint on src/**/*.{vue,ts}
|
||||
```
|
||||
|
||||
---
|
||||
### Docker Compose
|
||||
|
||||
## Code Style
|
||||
```bash
|
||||
docker compose up -d # MySQL + Redis + admin + api + UI + nginx gateway
|
||||
```
|
||||
|
||||
- **No emoji in UI strings.** Plain Chinese text for labels, buttons, status. Functional Unicode symbols are allowed and used: status dots `●`/`○`, checkmark `✓`, and box-drawing chars (`╔ ═ ╗ ━`) in log banners.
|
||||
- **No emoji in code comments or docstrings.** Plain text only.
|
||||
- **Keep CLAUDE.md current** — whenever code is modified, added, deleted, or any file change affects the project structure, build process, architecture, or conventions, update the relevant `CLAUDE.md` in the same commit to reflect the new state. This file and its children are the source of truth for both humans and Claude; stale documentation is a bug.
|
||||
## Key Configuration
|
||||
|
||||
---
|
||||
|
||||
## Docker deployment
|
||||
|
||||
Five services in `docker-compose.yml`: mysql (8.0), redis (7 Alpine + AOF), admin JAR, UI (Nginx + built frontend), gateway (Nginx reverse proxy on port 80). The `api` service definition still references the deleted `system-api` module — remove it before deploying.
|
||||
|
||||
**Important**: The `deploy/` directory referenced by Docker Compose (`deploy/mysql/init/`, `deploy/nginx/`) **does not exist locally** — it must be created for deployment.
|
||||
|
||||
Environment variables from `.env` at project root. Two frontend Dockerfiles: standard multi-stage (`Dockerfile`) and pre-built (`Dockerfile.offline`).
|
||||
|
||||
## Repository notes
|
||||
|
||||
- `README.md` does not contain substantive guidance — this file is the primary operational reference.
|
||||
- No CI configuration exists. Only pre-commit is frontend lint-staged via yarn git hooks.
|
||||
- `renren-generator` module exists but is commented out of the root POM build.
|
||||
- `system-dynamic-datasource` provides `@DataSource` annotation-driven multi-DS routing; the slave DS config in `application-dev.yml` is commented out.
|
||||
- `.gitignore` excludes `.idea/` but `.idea/` is tracked (committed IDE config; `.idea/.gitignore` only excludes local files like `workspace.xml`).
|
||||
- **Dev profile**: `system-admin/src/main/resources/application-dev.yml` -- local MySQL (weather_data_system), Redis 127.0.0.1:6379
|
||||
- **Prod profile**: `application-prod.yml` -- expects env variables for DB/Redis (see docker-compose.yml)
|
||||
- **project-options.redis.open**: toggles Redis caching globally (set via sys_params)
|
||||
|
||||
@@ -1,67 +1,78 @@
|
||||
# 气象数据分析平台
|
||||
# Weather Data Management System
|
||||
|
||||
基于 Spring Boot 3.5 / Vue 3 的全栈气象数据管理与分析系统,支持气象站点管理、逐日观测数据批量导入、数据统计分析与可视化展示。
|
||||
A full-stack meteorological data management and analysis platform built with Spring Boot 3.5 and Vue 3.
|
||||
|
||||
> **开发者文档** → [CLAUDE.md](CLAUDE.md)
|
||||
> Developer documentation: [CLAUDE.md](CLAUDE.md) | Module docs: [system-admin](system-admin/CLAUDE.md) | [system-common](system-common/CLAUDE.md) | [system-dynamic-datasource](system-dynamic-datasource/CLAUDE.md) | [weather-data-ui](weather-data-ui/CLAUDE.md)
|
||||
>
|
||||
> Chinese version: [README.zh-CN.md](README.zh-CN.md)
|
||||
|
||||
---
|
||||
|
||||
## 项目结构
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
weather-data/
|
||||
├── system-common/ → 公共模块:基础实体、工具类、Redis、校验
|
||||
├── system-admin/ → 管理后台 (port 8080, /system-admin)
|
||||
│ └── modules/weather/
|
||||
│ ├── dailydata/ → 逐日观测数据(Excel 批量导入、统计汇总)
|
||||
│ ├── station/ → 气象站点管理
|
||||
│ └── filescan/ → 气象资料文件监控与查阅
|
||||
├── system-api/ → 外部 API 服务 (port 8081, /renren-api)
|
||||
├── system-dynamic-datasource/ → 多数据源支持(预留)
|
||||
├── renren-generator/ → 代码生成器
|
||||
└── weather-data-ui/ → Vue 3 前端
|
||||
├── system-common/ Shared library (base classes, Redis, validation, utilities)
|
||||
├── system-dynamic-datasource/ Multi-datasource support (AbstractRoutingDataSource)
|
||||
├── system-admin/ Main application (REST API, security, jobs, file scanning)
|
||||
└── weather-data-ui/ Vue 3 frontend (Element Plus, ECharts, Pinia)
|
||||
```
|
||||
|
||||
### 模块说明
|
||||
### Module Relationships
|
||||
|
||||
| 模块 | 说明 |
|
||||
|---|---|
|
||||
| **system-admin** | 管理后台,包含气象数据管理、站点管理、文件查阅、系统管理(用户/角色/菜单/部门)、定时任务、日志管理 |
|
||||
| **system-api** | 对外开放的 REST API 服务,提供用户注册/登录等接口 |
|
||||
| **system-common** | 共享库,被所有子模块依赖 |
|
||||
| **weather-data-ui** | 前端 SPA,气象数据仪表盘、CRUD 页面、图表可视化 |
|
||||
```
|
||||
system-common <-- depended on by all backend modules
|
||||
system-dynamic-datasource <-- depended on by system-admin
|
||||
system-admin <-- runnable Spring Boot app, depends on both above
|
||||
weather-data-ui <-- independent frontend, communicates with system-admin via REST API
|
||||
```
|
||||
|
||||
---
|
||||
`system-common` provides the base class hierarchy (`BaseEntity`, `CrudService`, `CrudServiceImpl`), Redis utilities, validation framework, XSS protection, and shared utilities. All backend modules consume it.
|
||||
|
||||
## 技术栈
|
||||
`system-dynamic-datasource` provides the `@DataSource` annotation and `DynamicDataSource` (extends `AbstractRoutingDataSource`) for switching between multiple databases at runtime. Used by `system-admin` when queries need to target different data sources.
|
||||
|
||||
| 层级 | 技术 |
|
||||
|---|---|
|
||||
| 后端框架 | Spring Boot 3.5, MyBatis-Plus 3.5, Apache Shiro 1.12 |
|
||||
| 数据库 | MySQL 8.0(也支持达梦、Oracle、SQL Server、PostgreSQL) |
|
||||
| 缓存 | Redis 7(可选) |
|
||||
| 定时任务 | Quartz |
|
||||
| 接口文档 | Knife4j (Swagger) |
|
||||
| 前端框架 | Vue 3, TypeScript, Vite 5, Element Plus, Pinia, ECharts |
|
||||
`system-admin` is the main runnable application. It contains all business logic organized under `modules/`:
|
||||
|
||||
**环境要求**:JDK 17+, Maven 3.6+, Node.js 18+, MySQL 8.0+
|
||||
| Domain | Package | Description |
|
||||
|--------|---------|-------------|
|
||||
| Weather daily data | `modules/weather/dailydata/` | Excel batch import, CRUD, statistical summaries with Redis caching |
|
||||
| Weather stations | `modules/weather/station/` | Station registry, dept association for data scoping |
|
||||
| File scanning | `modules/weather/filescan/` | WatchService-based directory monitoring, auto-import of meteorological files |
|
||||
| Region management | `modules/region/` | Geographic region tree (province/city/county) |
|
||||
| System management | `modules/sys/` | Users, roles, menus, departments, dictionaries, parameters |
|
||||
| Alerts / notifications | `modules/sys/alert/` | SSE real-time push, Spring event-driven broadcast |
|
||||
| Security | `modules/security/` | Shiro + token-based auth, OAuth2 filter, password hashing |
|
||||
| Job scheduling | `modules/job/` | Quartz dynamic job management, online start/stop |
|
||||
| Audit logs | `modules/log/` | Operation log, login log, error log |
|
||||
| Cloud storage | `modules/oss/` | Alibaba Cloud / Qiniu / Tencent Cloud file storage |
|
||||
|
||||
---
|
||||
`weather-data-ui` is a Vue 3 SPA that communicates with `system-admin` via REST API. It features dynamic routing (routes loaded from server menus on login), tab-based navigation, ECharts visualization, and real-time SSE alert streaming.
|
||||
|
||||
## 快速启动(本地开发)
|
||||
## Tech Stack
|
||||
|
||||
### 1. 初始化数据库
|
||||
| Layer | Technology |
|
||||
|-------|-----------|
|
||||
| Backend framework | Spring Boot 3.5.11, MyBatis-Plus 3.5.8, Apache Shiro 1.12 |
|
||||
| Database | MySQL 8.0 (also supports Oracle, SQL Server, PostgreSQL, Dameng) |
|
||||
| Connection pool | Druid 1.2 |
|
||||
| Cache | Redis 7 (Lettuce client, optional via `project-options.redis.open`) |
|
||||
| Job scheduling | Quartz 2.3 |
|
||||
| API documentation | Knife4j 4.5 (Swagger) |
|
||||
| Excel processing | EasyExcel 3.2 |
|
||||
| Frontend | Vue 3.5, TypeScript 5.7, Vite 5.4, Element Plus 2.10, Pinia 2.3, ECharts 5 |
|
||||
|
||||
创建数据库 `weather_data_system`(UTF-8mb4),执行初始化脚本:
|
||||
**Requirements**: JDK 17+, Maven 3.6+, Node.js 18+, MySQL 8.0+
|
||||
|
||||
## Quick Start (Local Development)
|
||||
|
||||
### 1. Initialize Database
|
||||
|
||||
```bash
|
||||
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS weather_data_system DEFAULT CHARSET utf8mb4"
|
||||
mysql -u root -p weather_data_system < system-admin/db/weather_data_system.sql
|
||||
```
|
||||
|
||||
### 2. 配置数据源
|
||||
### 2. Configure Datasource
|
||||
|
||||
编辑 `system-admin/src/main/resources/application-dev.yml`,修改 MySQL 连接信息:
|
||||
Edit `system-admin/src/main/resources/application-dev.yml`, update MySQL and Redis connection settings:
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
@@ -69,24 +80,30 @@ spring:
|
||||
druid:
|
||||
url: jdbc:mysql://localhost:3306/weather_data_system?...
|
||||
username: root
|
||||
password: 你的密码
|
||||
password: your_password
|
||||
data:
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
port: 6379
|
||||
password:
|
||||
```
|
||||
|
||||
### 3. 启动后端
|
||||
### 3. Start Backend
|
||||
|
||||
```bash
|
||||
# 完整构建(跳过测试)
|
||||
# Full build
|
||||
mvn clean install -DskipTests
|
||||
|
||||
# IDEA 中直接运行 AdminApplication.java
|
||||
# Run from IDE: AdminApplication.java
|
||||
# Or from CLI:
|
||||
cd system-admin && mvn spring-boot:run -Dspring-boot.run.profiles=dev
|
||||
```
|
||||
|
||||
管理后台:http://localhost:8080/system-admin
|
||||
接口文档:http://localhost:8080/system-admin/doc.html(默认关闭,需在 `application.yml` 中启用 `knife4j.enable: true`)
|
||||
Admin backend: http://localhost:8080/system-admin
|
||||
API docs: http://localhost:8080/system-admin/doc.html
|
||||
Default account: `admin` / `admin`
|
||||
|
||||
默认账号:`admin` / `admin`
|
||||
|
||||
### 4. 启动前端
|
||||
### 4. Start Frontend
|
||||
|
||||
```bash
|
||||
cd weather-data-ui
|
||||
@@ -94,121 +111,47 @@ npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
前端开发服务器:http://localhost:8001
|
||||
Frontend dev server: http://localhost:8001
|
||||
|
||||
> **注意**:前端开发环境 `.env.development` 中 `VITE_APP_API` 默认指向 `http://192.168.2.186:8080/system-admin`,请按需修改为你的后端地址。
|
||||
Edit `weather-data-ui/.env.development` and set `VITE_APP_API` to your backend URL if needed.
|
||||
|
||||
---
|
||||
## Deployment
|
||||
|
||||
## 生产部署
|
||||
|
||||
项目提供两种部署方式,根据目标环境选用。
|
||||
|
||||
### Windows 一键部署
|
||||
|
||||
适用于 Windows Server 生产环境,自动安装 MySQL / Redis / Nginx 并部署前后端。
|
||||
|
||||
**部署包结构**(`deploy/` 目录,可独立拷贝到目标服务器):
|
||||
|
||||
```
|
||||
deploy/
|
||||
├── deploy.bat ← 一键部署(推荐,双击运行)
|
||||
├── deploy.ps1 ← PowerShell 版本
|
||||
├── templates/ ← 配置模板(deploy.bat 使用)
|
||||
├── project/
|
||||
│ ├── client-side/dist/ ← 前端构建产物
|
||||
│ └── server-side/
|
||||
│ ├── system-admin.jar ← 管理后台
|
||||
│ └── system-api.jar ← API 服务(可选)
|
||||
├── mysql/init/
|
||||
│ └── weather_data_system.sql ← 数据库初始化脚本
|
||||
├── nginx/nginx.conf ← Docker 网关配置
|
||||
└── software/ ← 第三方安装包
|
||||
├── mysql-installer-community-8.0.46.0.msi
|
||||
├── nginx-1.28.3.zip
|
||||
└── Redis-8.8.0-Windows-x64-Service.zip
|
||||
```
|
||||
|
||||
```cmd
|
||||
REM 以管理员身份运行命令提示符
|
||||
|
||||
deploy\deploy.bat :: 默认安装到 C:\weather-data
|
||||
|
||||
REM 部署完成后:
|
||||
C:\weather-data\start.bat :: 启动平台
|
||||
C:\weather-data\stop.bat :: 停止平台
|
||||
```
|
||||
|
||||
> **前置条件**:Windows 10 1803+ 或 Windows Server 2019+,需预装 JDK 17+。将 `deploy/` 目录完整拷贝至目标服务器后以管理员身份运行 `deploy.bat`。
|
||||
|
||||
### Docker 部署
|
||||
|
||||
适用于 Linux 服务器或支持 Docker 的 Windows 环境。编排包含 6 个服务:MySQL、Redis、Admin、API、UI、Gateway。
|
||||
|
||||
### 前置条件
|
||||
|
||||
1. 安装 Docker Engine 20.10+ 和 Docker Compose v2
|
||||
2. JAR 已构建到各模块的 `target/` 目录
|
||||
3. 前端已构建到 `weather-data-ui/dist/`
|
||||
|
||||
### 部署步骤
|
||||
### Docker Compose
|
||||
|
||||
```bash
|
||||
# 1. 构建所有模块
|
||||
# Build backend JARs and frontend dist
|
||||
mvn clean install -DskipTests
|
||||
cd weather-data-ui && npm run build && cd ..
|
||||
|
||||
# 2. 修改环境变量(密码等敏感信息)
|
||||
cp .env .env.local # 修改 .env.local 中的密码
|
||||
# Configure environment
|
||||
cp .env .env.local # Edit passwords in .env.local
|
||||
|
||||
# 3. 创建部署所需的 nginx 配置目录
|
||||
mkdir -p deploy/nginx deploy/mysql/init
|
||||
|
||||
# 4. 复制初始化 SQL
|
||||
cp system-admin/db/weather_data_system.sql deploy/mysql/init/
|
||||
|
||||
# 5. 启动
|
||||
# Start all services
|
||||
docker compose up -d
|
||||
|
||||
# 6. 访问
|
||||
# http://localhost → 前端页面
|
||||
# http://localhost:8080/system-admin → 管理后台
|
||||
# http://localhost:8081/renren-api → API 服务
|
||||
# Access
|
||||
# http://localhost -> Frontend (via nginx gateway)
|
||||
# http://localhost:8080 -> Admin backend
|
||||
```
|
||||
|
||||
### 常用命令
|
||||
Services: MySQL 8.0, Redis 7, Admin (Spring Boot), API (optional, port 8081), UI (nginx), Gateway (nginx reverse proxy, port 80).
|
||||
|
||||
```bash
|
||||
docker compose up -d # 启动所有服务
|
||||
docker compose down # 停止所有服务
|
||||
docker compose logs -f admin # 查看管理后台日志
|
||||
docker compose restart admin # 重启管理后台
|
||||
```
|
||||
### Windows Server
|
||||
|
||||
---
|
||||
See `deploy/` directory for one-click deployment scripts (`deploy.bat` / `deploy.ps1`). Requires Windows 10 1803+ or Windows Server 2019+, JDK 17+ pre-installed. Run as Administrator.
|
||||
|
||||
## 主要功能
|
||||
## Key Features
|
||||
|
||||
### 气象数据管理
|
||||
- **逐日观测数据**:支持 Excel 批量导入(异步双遍扫描,进度可查)、按站点/日期查询、统计分析、导出
|
||||
- **气象站点管理**:站点 CRUD,关联部门实现数据权限隔离
|
||||
- **气象资料查阅**:自动监控资料文件目录,支持在线查阅
|
||||
- **Weather data import**: Excel batch import with async two-pass processing, progress tracking, multi-row INSERT optimization
|
||||
- **Statistical analysis**: Same-date-across-years summarization with Redis caching, multi-dimensional queries
|
||||
- **File monitoring**: Automatic directory watching (WatchService), MD5 deduplication, file lifecycle management (receive -> display -> archive)
|
||||
- **Real-time notifications**: SSE-based alert streaming, Spring event-driven broadcast
|
||||
- **Data scoping**: Dept-level row-level security via MyBatis-Plus interceptor with `@DataFilter` annotation
|
||||
- **Dynamic datasource**: Runtime datasource switching via `@DataSource` annotation with ThreadLocal context
|
||||
- **Job scheduling**: Quartz-based dynamic job management with online start/stop/configure
|
||||
- **Cloud storage**: Alibaba Cloud OSS, Qiniu, Tencent Cloud COS integration
|
||||
|
||||
### 系统管理
|
||||
- 用户 / 角色 / 菜单 / 部门管理
|
||||
- 数据权限:`@DataFilter` 注解实现部门级数据隔离
|
||||
- 定时任务:Quartz 动态管理,支持在线启停
|
||||
- 操作日志 / 登录日志 / 异常日志
|
||||
- 文件存储(支持本地及云存储)
|
||||
## License
|
||||
|
||||
### 前端仪表盘
|
||||
- 气象数据多维度统计与图表可视化(ECharts)
|
||||
- 筛选、排序、导出(PNG/PDF)
|
||||
- 导入进度实时展示
|
||||
|
||||
---
|
||||
|
||||
## 参考链接
|
||||
|
||||
- [CLAUDE.md](CLAUDE.md) — 开发者文档(架构、规范、注意事项)
|
||||
- 基于 [renren-security](https://gitee.com/renrenio/renren-security) 二次开发
|
||||
Apache License 2.0
|
||||
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
# 气象数据管理系统
|
||||
|
||||
基于 Spring Boot 3.5 与 Vue 3 的全栈气象数据管理与分析平台。
|
||||
|
||||
> 开发者文档:[CLAUDE.md](CLAUDE.md) | 模块文档:[system-admin](system-admin/CLAUDE.md) | [system-common](system-common/CLAUDE.md) | [system-dynamic-datasource](system-dynamic-datasource/CLAUDE.md) | [weather-data-ui](weather-data-ui/CLAUDE.md)
|
||||
>
|
||||
> English version: [README.md](README.md)
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
weather-data/
|
||||
├── system-common/ 公共模块(基础类、Redis、校验、工具类)
|
||||
├── system-dynamic-datasource/ 多数据源支持(AbstractRoutingDataSource)
|
||||
├── system-admin/ 主应用(REST API、安全、定时任务、文件扫描)
|
||||
└── weather-data-ui/ Vue 3 前端(Element Plus、ECharts、Pinia)
|
||||
```
|
||||
|
||||
### 模块关系
|
||||
|
||||
```
|
||||
system-common <-- 被所有后端模块依赖
|
||||
system-dynamic-datasource <-- 被 system-admin 依赖
|
||||
system-admin <-- 可运行的 Spring Boot 应用,依赖上述两个模块
|
||||
weather-data-ui <-- 独立前端,通过 REST API 与 system-admin 通信
|
||||
```
|
||||
|
||||
**[system-common](system-common/)** 提供基础类层次([BaseEntity](system-common/src/main/java/com/weather/common/entity/BaseEntity.java)、[CrudService](system-common/src/main/java/com/weather/common/service/CrudService.java)、[CrudServiceImpl](system-common/src/main/java/com/weather/common/service/impl/CrudServiceImpl.java))、[Redis 工具](system-common/src/main/java/com/weather/common/redis/RedisUtils.java)、校验框架、XSS 防护及通用工具类。所有后端模块均依赖此模块。
|
||||
|
||||
**[system-dynamic-datasource](system-dynamic-datasource/)** 提供 [@DataSource](system-dynamic-datasource/src/main/java/com/weather/commons/dynamic/datasource/annotation/DataSource.java) 注解与 [DynamicDataSource](system-dynamic-datasource/src/main/java/com/weather/commons/dynamic/datasource/config/DynamicDataSource.java)(继承 `AbstractRoutingDataSource`),用于运行时切换多数据源。当 system-admin 需要查询不同数据库时使用。
|
||||
|
||||
**[system-admin](system-admin/)** 是主应用入口([AdminApplication.java](system-admin/src/main/java/com/weather/AdminApplication.java)),所有业务逻辑按 domain 组织在 `modules/` 下:
|
||||
|
||||
| 业务域 | 包路径 | 功能说明 |
|
||||
|--------|--------|----------|
|
||||
| 逐日气象数据 | [modules/weather/dailydata/](system-admin/src/main/java/com/weather/modules/weather/dailydata/) | Excel 批量导入、CRUD、Redis 缓存统计汇总 |
|
||||
| 气象站点 | [modules/weather/station/](system-admin/src/main/java/com/weather/modules/weather/station/) | 站点注册,关联部门实现数据权限隔离 |
|
||||
| 文件扫描 | [modules/weather/filescan/](system-admin/src/main/java/com/weather/modules/weather/filescan/) | WatchService 目录监控,气象资料自动入库 |
|
||||
| 区域管理 | [modules/region/](system-admin/src/main/java/com/weather/modules/region/) | 行政区域树(省/市/县) |
|
||||
| 系统管理 | [modules/sys/](system-admin/src/main/java/com/weather/modules/sys/) | 用户、角色、菜单、部门、字典、参数 |
|
||||
| 通知消息 | [modules/sys/alert/](system-admin/src/main/java/com/weather/modules/sys/alert/) | SSE 实时推送,Spring 事件驱动广播 |
|
||||
| 安全认证 | [modules/security/](system-admin/src/main/java/com/weather/modules/security/) | Shiro + Token 认证,OAuth2 过滤器,密码加密 |
|
||||
| 定时任务 | [modules/job/](system-admin/src/main/java/com/weather/modules/job/) | Quartz 动态任务管理,在线启停 |
|
||||
| 审计日志 | [modules/log/](system-admin/src/main/java/com/weather/modules/log/) | 操作日志、登录日志、异常日志 |
|
||||
| 云存储 | [modules/oss/](system-admin/src/main/java/com/weather/modules/oss/) | 阿里云 OSS / 七牛云 / 腾讯云 COS 文件存储 |
|
||||
|
||||
**[weather-data-ui](weather-data-ui/)** 是 Vue 3 单页应用,通过 REST API 与 system-admin 通信。特性包括:动态路由(登录时从服务端菜单加载)、标签页导航、ECharts 图表可视化、SSE 实时通知滚动。
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 层级 | 技术 |
|
||||
|------|------|
|
||||
| 后端框架 | Spring Boot 3.5.11, MyBatis-Plus 3.5.8, Apache Shiro 1.12 |
|
||||
| 数据库 | MySQL 8.0(同时支持 Oracle、SQL Server、PostgreSQL、达梦) |
|
||||
| 连接池 | Druid 1.2 |
|
||||
| 缓存 | Redis 7(Lettuce 客户端,通过 `project-options.redis.open` 控制开关) |
|
||||
| 定时任务 | Quartz 2.3 |
|
||||
| 接口文档 | Knife4j 4.5 (Swagger) |
|
||||
| Excel 处理 | EasyExcel 3.2 |
|
||||
| 前端 | Vue 3.5, TypeScript 5.7, Vite 5.4, Element Plus 2.10, Pinia 2.3, ECharts 5 |
|
||||
|
||||
**环境要求**:JDK 17+、Maven 3.6+、Node.js 18+、MySQL 8.0+
|
||||
|
||||
## 快速启动(本地开发)
|
||||
|
||||
### 1. 初始化数据库
|
||||
|
||||
```bash
|
||||
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS weather_data_system DEFAULT CHARSET utf8mb4"
|
||||
mysql -u root -p weather_data_system < system-admin/db/weather_data_system.sql
|
||||
```
|
||||
|
||||
数据库脚本:[system-admin/db/weather_data_system.sql](system-admin/db/weather_data_system.sql)
|
||||
|
||||
### 2. 配置数据源
|
||||
|
||||
编辑 [system-admin/src/main/resources/application-dev.yml](system-admin/src/main/resources/application-dev.yml),修改 MySQL 和 Redis 连接信息:
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
datasource:
|
||||
druid:
|
||||
url: jdbc:mysql://localhost:3306/weather_data_system?...
|
||||
username: root
|
||||
password: 你的密码
|
||||
data:
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
port: 6379
|
||||
password:
|
||||
```
|
||||
|
||||
### 3. 启动后端
|
||||
|
||||
```bash
|
||||
# 完整构建
|
||||
mvn clean install -DskipTests
|
||||
|
||||
# 在 IDE 中运行 AdminApplication.java
|
||||
# 或命令行启动:
|
||||
cd system-admin && mvn spring-boot:run -Dspring-boot.run.profiles=dev
|
||||
```
|
||||
|
||||
管理后台:<http://localhost:8080/system-admin>
|
||||
接口文档:<http://localhost:8080/system-admin/doc.html>
|
||||
默认账号:`admin` / `admin`
|
||||
|
||||
### 4. 启动前端
|
||||
|
||||
```bash
|
||||
cd weather-data-ui
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
前端开发服务器:<http://localhost:8001>
|
||||
|
||||
如需修改后端地址,编辑 [weather-data-ui/.env.development](weather-data-ui/.env.development) 中的 `VITE_APP_API`。
|
||||
|
||||
## 部署
|
||||
|
||||
### Docker Compose
|
||||
|
||||
编排文件:[docker-compose.yml](docker-compose.yml)
|
||||
|
||||
```bash
|
||||
# 构建后端 JAR 和前端产物
|
||||
mvn clean install -DskipTests
|
||||
cd weather-data-ui && npm run build && cd ..
|
||||
|
||||
# 配置环境变量
|
||||
cp .env .env.local # 在 .env.local 中修改密码等敏感配置
|
||||
|
||||
# 启动所有服务
|
||||
docker compose up -d
|
||||
|
||||
# 访问地址
|
||||
# http://localhost -> 前端页面(通过 nginx 网关)
|
||||
# http://localhost:8080 -> 管理后台
|
||||
```
|
||||
|
||||
服务清单:MySQL 8.0、Redis 7、Admin(Spring Boot)、API(可选,端口 8081)、UI(nginx)、Gateway(nginx 反向代理,端口 80)。
|
||||
|
||||
### Windows Server
|
||||
|
||||
参见 `deploy/` 目录下的一键部署脚本([deploy.bat](deploy/deploy.bat) / [deploy.ps1](deploy/deploy.ps1))。要求 Windows 10 1803+ 或 Windows Server 2019+,需预装 JDK 17+。以管理员身份运行。
|
||||
|
||||
## 核心功能
|
||||
|
||||
- **气象数据导入**:Excel 异步双遍扫描批量导入,进度可查,多行 INSERT 优化。入口:[WeatherDataImportManager](system-admin/src/main/java/com/weather/modules/weather/dailydata/WeatherDataImportManager.java)
|
||||
- **统计汇总**:历年同月同日数据汇总,Redis 缓存加速查询,缓存命中自动降级至数据库。缓存任务:[WeatherSummarizeCacheTask](system-admin/src/main/java/com/weather/modules/weather/dailydata/task/WeatherSummarizeCacheTask.java)
|
||||
- **文件监控**:基于 WatchService 自动监控资料目录,MD5 去重,文件生命周期管理(接收 -> 展示 -> 归档)。服务:[FileWatchServiceManager](system-admin/src/main/java/com/weather/modules/weather/filescan/FileWatchServiceManager.java)
|
||||
- **实时通知**:基于 SSE 的服务端推送,Spring 事件驱动广播。服务:[SseAlertService](system-admin/src/main/java/com/weather/modules/sys/alert/SseAlertService.java)
|
||||
- **数据权限**:基于 MyBatis-Plus 拦截器的部门级行级安全,通过 `@DataFilter` 注解启用。拦截器:[DataFilterInterceptor](system-admin/src/main/java/com/weather/common/interceptor/DataFilterInterceptor.java)
|
||||
- **动态数据源**:运行时通过 `@DataSource` 注解切换数据源,ThreadLocal 上下文传递。实现:[DynamicDataSource](system-dynamic-datasource/src/main/java/com/weather/commons/dynamic/datasource/config/DynamicDataSource.java)
|
||||
- **定时任务**:Quartz 动态任务管理,支持在线创建、启停、修改 Cron 表达式。配置:[ScheduleConfig](system-admin/src/main/java/com/weather/modules/job/config/ScheduleConfig.java)
|
||||
- **云存储**:支持阿里云 OSS、七牛云、腾讯云 COS 文件上传与管理。工厂:[OSSFactory](system-admin/src/main/java/com/weather/modules/oss/cloud/OSSFactory.java)
|
||||
|
||||
## 许可证
|
||||
|
||||
[Apache License 2.0](LICENSE)
|
||||
@@ -19,8 +19,6 @@
|
||||
<module>system-common</module>
|
||||
<module>system-dynamic-datasource</module>
|
||||
<module>system-admin</module>
|
||||
<!-- <module>system-api</module>-->
|
||||
<!-- <module>renren-generator</module>-->
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
@@ -135,6 +133,23 @@
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<!-- 阿里云maven仓库 -->
|
||||
<repositories>
|
||||
<repository>
|
||||
|
||||
+111
-109
@@ -1,144 +1,146 @@
|
||||
# CLAUDE.md — system-admin
|
||||
# CLAUDE.md - system-admin
|
||||
|
||||
Backend module: Spring Boot 3.5 admin application (port 8080, context `/system-admin`).
|
||||
This file provides guidance to Claude Code when working in the system-admin module.
|
||||
|
||||
---
|
||||
## Purpose
|
||||
|
||||
## Build & Run
|
||||
Main Spring Boot application module. Entry point: `com.weather.AdminApplication`. Depends on `system-common` and `system-dynamic-datasource`.
|
||||
|
||||
```bash
|
||||
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
|
||||
## Module Structure
|
||||
|
||||
```
|
||||
com.weather/
|
||||
├── AdminApplication.java # @SpringBootApplication, @EnableAsync
|
||||
├── common/ # Module-level shared code
|
||||
│ ├── annotation/ # @DataFilter, @LogOperation
|
||||
│ ├── aspect/ # DataFilterAspect, LogOperationAspect
|
||||
│ ├── config/ # JacksonConfig, MybatisPlusConfig, SwaggerConfig, FileServerConfig
|
||||
│ ├── exception/ # CustomExceptionHandler
|
||||
│ ├── handler/ # FieldMetaObjectHandler (MyBatis-Plus auto-fill)
|
||||
│ ├── interceptor/ # DataFilterInterceptor, DataScope
|
||||
│ ├── utils/ # ExcelUtils, TimeUtils
|
||||
│ └── validator/group/ # Cloud storage validator groups
|
||||
└── modules/
|
||||
├── job/ # Quartz scheduling
|
||||
├── log/ # Audit logs (operation, login, error)
|
||||
├── oss/ # Cloud file storage (Alibaba/Qiniu/Tencent)
|
||||
├── region/ # Geographic region tree
|
||||
├── security/ # Shiro auth, OAuth2 filter, login
|
||||
├── sys/ # System management (users, roles, menus, depts, dicts, params, alerts)
|
||||
└── weather/ # Weather domain
|
||||
├── dailydata/ # Daily weather data CRUD, import, caching
|
||||
├── filescan/ # WatchService file scanning pipeline
|
||||
└── station/ # Weather station management
|
||||
```
|
||||
|
||||
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.
|
||||
## Service Layer Pattern
|
||||
|
||||
Launch from IntelliJ:
|
||||
- `AdminApplication` (`system-admin/`) → port 8080, context `/system-admin`
|
||||
- `GeneratorApplication` (`renren-generator/`) — commented out of build
|
||||
All business modules follow this structure under `modules/<domain>/`:
|
||||
|
||||
## 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)
|
||||
controller/ → @RestController, delegates to service interface
|
||||
service/ → Interface extends CrudService<Entity, Dto>
|
||||
service/impl/ → Extends CrudServiceImpl<Dao, Entity, Dto>, implements getWrapper()
|
||||
dao/ → Interface extends BaseDao / BaseMapper
|
||||
entity/ → Extends BaseEntity, maps to DB table
|
||||
dto/ → Request/response DTOs
|
||||
excel/ → @ExcelProperty-annotated beans for EasyExcel import/export
|
||||
```
|
||||
|
||||
Mapper XMLs: `src/main/resources/mapper/<domain>/**/*.xml`
|
||||
Entity-to-DTO conversion uses `ConvertUtils.sourceToTarget()`.
|
||||
|
||||
### Conventions
|
||||
## Security / Auth
|
||||
|
||||
- Lombok used throughout: `@Data`, `@AllArgsConstructor`, `@Slf4j` are standard on entity/service classes.
|
||||
- DTO/Entity/VO separation per module — request DTOs often extend `BaseEntity`.
|
||||
- **Framework**: Apache Shiro with Jakarta-compatible artifacts (classifier: `jakarta`)
|
||||
- **Token flow**: Client sends `token` header/param -> `Oauth2Filter` extracts it -> `Oauth2Realm` validates against `sys_user_token` table -> returns `UserDetail` (user + dept scope + permissions)
|
||||
- **Public paths** (no auth): `/login`, `/captcha`, `/druid/**`, `/doc.html`, `/swagger/**`, `/v3/api-docs/**`, `/favicon.ico`
|
||||
- **Password hashing**: Custom `BCryptPasswordEncoder` with `PasswordUtils` (configurable rounds via sys_params)
|
||||
- **Async context**: `UserContextHolder` (ThreadLocal) preserves user context across async task boundaries (used in file import, scheduled tasks). `SecurityUser.getUser()` falls back to this when Shiro Subject is unavailable.
|
||||
|
||||
## PK & Auth
|
||||
## Data Scoping (Dept-Based Row-Level Security)
|
||||
|
||||
- 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.
|
||||
`DataFilterInterceptor` is a MyBatis-Plus `InnerInterceptor`. When a service method is annotated with `@DataFilter`, the `DataFilterAspect` injects a `DataScope` object containing a SQL filter clause into the query parameters. The interceptor then appends this WHERE clause to the SQL, restricting results to the user's authorized departments.
|
||||
|
||||
## Key cross-cutting mechanisms
|
||||
Example service usage:
|
||||
```java
|
||||
@DataFilter
|
||||
public PageData<Dto> page(Map<String, Object> params) {
|
||||
// SQL automatically filtered by dept scope
|
||||
}
|
||||
```
|
||||
|
||||
| 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`. |
|
||||
## File Scanning Pipeline
|
||||
|
||||
## Exception handling
|
||||
`FileWatchServiceManager` (`modules/weather/filescan/`) monitors `{FILE_SCAN_ROOT_PATH}/receive/` using `java.nio.file.WatchService`:
|
||||
|
||||
Single `@RestControllerAdvice` handler in the admin module:
|
||||
1. **Directory layout**: `receive/<deptName>/`, `display/<deptName>/`, `archive/<deptName>/`
|
||||
2. **Detection**: WatchService detects `ENTRY_CREATE` and `ENTRY_MODIFY` events
|
||||
3. **Wait**: `waitForFileReady()` polls file size stability + file lock to ensure write completion
|
||||
4. **Dedup**: MD5 hash check against `weather_file_scan_record` table
|
||||
5. **Parse**: `FileNameParser` extracts region, category, period from filename
|
||||
6. **Store**: Record inserted; file copied to `display/`; old version moved to `archive/`
|
||||
7. **Root files**: Files in `receive/` root (no dept subdir) are classified as "model forecast" with `deptId = null`
|
||||
|
||||
| Handler | Catches | Persists errors? |
|
||||
|---|---|---|
|
||||
| `CustomExceptionHandler` | `CommonException`, `DuplicateKeyException`, `UnauthorizedException`, generic `Exception` | **Yes** — saves to `SysLogErrorService` (IP, user-agent, URI, params, stack trace) |
|
||||
`FileScanStartupRunner` (ApplicationRunner) triggers full directory scan + WatchService registration on startup.
|
||||
|
||||
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`).
|
||||
## Weather Daily Data
|
||||
|
||||
## Logging
|
||||
Core table: `weather_daily_data` (entity: `WeatherDailyDataEntity`). Fields: stationId, observeDate, avgTemp, maxTemp/minTemp (with time), rainfall (20-20 and 08-08), relativeHumidity, atmospheres, wind (avg/max/extreme speed + direction + time), deptId.
|
||||
|
||||
- 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.
|
||||
### Import Flow
|
||||
|
||||
## Redis & Docs
|
||||
`WeatherDataImportManager` handles Excel uploads:
|
||||
1. Save MultipartFile to temp file
|
||||
2. Submit async task (`CompletableFuture`), returns taskId immediately
|
||||
3. First pass: `EasyExcel.read()` with count listener to get total rows
|
||||
4. Second pass: `WeatherDataListener` reads rows in batches, calls `insertBatch()`
|
||||
5. `insertBatch()` resolves station-to-dept mapping, fills audit fields, uses `insertBatchMultiRow()` (custom MySQL multi-row INSERT)
|
||||
6. Clears weather summarize Redis cache on completion
|
||||
7. Progress tracked in-memory (`ConcurrentHashMap<String, ImportProgress>`)
|
||||
|
||||
- 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.
|
||||
### Summarize Caching
|
||||
|
||||
## MyBatis-Plus gotchas
|
||||
`WeatherSummarizeCacheTask` (Quartz job) precomputes "same month-day across years" summary data per station into Redis. Cache key: `weather:summarize:{month}:{day}`. On cache hit, `getCachedSummarize()` filters by station + year range. On miss, falls back to `selectSummarizeByMonthDay()` DB query.
|
||||
|
||||
- **Batch inserts bypass auto-fill** — `FieldMetaObjectHandler` 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.
|
||||
## Alert / Notification System
|
||||
|
||||
## Weather domain
|
||||
- **Storage**: `SysAlertEntity` in `sys_alert` table
|
||||
- **Real-time push**: `SseAlertService` manages active SSE connections (`CopyOnWriteArraySet<SseEmitter>`), broadcasts on Spring events:
|
||||
- `AlertCreatedEvent` -> SSE event `alert`
|
||||
- `AlertWithdrawnEvent` -> SSE event `alert-withdrawn`
|
||||
- `AlertDeletedEvent` -> SSE event `alert-deleted`
|
||||
- **External sources**: `AlertSourcePollingTask` (Quartz) polls external services; `AlertSourceCollector` gathers results
|
||||
|
||||
Three sub-modules under `system-admin/.../modules/weather/`:
|
||||
## Job Scheduling (Quartz)
|
||||
|
||||
| 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` |
|
||||
Tables: `schedule_job`, `schedule_job_log`, plus standard `qrtz_*` tables.
|
||||
|
||||
### Weather data import flow
|
||||
- `ScheduleConfig`: Quartz `SchedulerFactoryBean` configuration
|
||||
- `ScheduleJob` entity: bean class, cron expression, params, status (PAUSE/NORMAL)
|
||||
- `ScheduleUtils`: Create/update/delete/pause/resume Quartz triggers
|
||||
- `JobCommandLineRunner`: On startup, restores all NORMAL-status jobs from DB
|
||||
- `ITask` interface: `run(String params)` method -- all job classes implement this
|
||||
- Concurrency control via `@DisallowConcurrentExecution`
|
||||
|
||||
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:*`).
|
||||
## Database
|
||||
|
||||
### Weather summarize cache
|
||||
Schema: `system-admin/db/weather_data_system.sql`
|
||||
|
||||
`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.
|
||||
### Core Tables
|
||||
|
||||
### 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.
|
||||
| Table | Purpose |
|
||||
|-------|---------|
|
||||
| `sys_user`, `sys_role`, `sys_menu`, `sys_dept` | RBAC |
|
||||
| `sys_role_user`, `sys_role_menu`, `sys_role_data_scope` | RBAC associations |
|
||||
| `sys_user_token` | Shiro auth tokens |
|
||||
| `sys_dict_type`, `sys_dict_data` | Dictionary system |
|
||||
| `sys_params` | Key-value system parameters |
|
||||
| `sys_alert` | Alert/notification records |
|
||||
| `weather_station` | Weather station registry |
|
||||
| `weather_daily_data` | Daily meteorological observations |
|
||||
| `weather_file_scan_record` | File tracking (receive -> display -> archive) |
|
||||
| `sys_region` | Geographic region tree (province/city/county) |
|
||||
| `schedule_job`, `schedule_job_log` | Quartz job definitions and execution history |
|
||||
| `qrtz_*` | Quartz internal scheduler tables |
|
||||
| `sys_log_operation`, `sys_log_login`, `sys_log_error` | Audit logs |
|
||||
| `sys_oss` | Cloud storage object records |
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
Target Server Version : 80044 (8.0.44)
|
||||
File Encoding : 65001
|
||||
|
||||
Date: 24/06/2026 13:44:11
|
||||
Date: 27/06/2026 22:38:33
|
||||
*/
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
@@ -276,9 +276,9 @@ CREATE TABLE `schedule_job` (
|
||||
-- ----------------------------
|
||||
-- Records of schedule_job
|
||||
-- ----------------------------
|
||||
INSERT INTO `schedule_job` VALUES (1067246875800000076, 'testTask', '123456', '0 0/30 * * * ?', 0, '有参测试,多个参数使用json', 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-06-16 19:18:58');
|
||||
INSERT INTO `schedule_job` VALUES (2029504525156528130, 'fileScanTask', '', '0 0/5 * * * ?', 0, '文件扫描定时任务', 1067246875800000001, '2026-03-05 18:29:20', 1067246875800000001, '2026-06-24 12:14:59');
|
||||
INSERT INTO `schedule_job` VALUES (2069614094374367234, 'weatherSummarizeCacheTask', '', '0 0 1 * * ?', 1, '天气汇总缓存', 1067246875800000001, '2026-06-24 10:50:26', 1067246875800000001, '2026-06-24 12:14:51');
|
||||
INSERT INTO `schedule_job` VALUES (2029504525156528130, 'fileScanTask', '', '0 0/1 * * * ?', 1, '文件扫描定时任务,每分钟', 1067246875800000001, '2026-03-05 18:29:20', 1067246875800000001, '2026-06-27 19:02:10');
|
||||
INSERT INTO `schedule_job` VALUES (2069614094374367234, 'weatherSummarizeCacheTask', '', '0 0 1 * * ?', 1, '天气汇总缓存,每天凌晨1点', 1067246875800000001, '2026-06-24 10:50:26', 1067246875800000001, '2026-06-27 19:02:02');
|
||||
INSERT INTO `schedule_job` VALUES (2070458228203012097, 'alertSourcePollingTask', '', '0 */10 * * * ?', 1, '通知数据源轮询,每10分钟', 1067246875800000001, '2026-06-26 18:44:44', 1067246875800000001, '2026-06-26 18:44:44');
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for schedule_job_log
|
||||
@@ -302,6 +302,33 @@ CREATE TABLE `schedule_job_log` (
|
||||
-- Records of schedule_job_log
|
||||
-- ----------------------------
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_alert
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_alert`;
|
||||
CREATE TABLE `sys_alert` (
|
||||
`id` bigint NOT NULL COMMENT 'Snowflake ID',
|
||||
`level` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'info' COMMENT '级别: info|warning|danger',
|
||||
`title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '通知标题',
|
||||
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '通知正文',
|
||||
`source_type` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'manual' COMMENT '来源: manual|import|schedule|datasource',
|
||||
`source_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '业务关联ID',
|
||||
`is_active` tinyint(1) NULL DEFAULT 1 COMMENT '1=有效 0=已撤回',
|
||||
`publish_time` datetime NOT NULL COMMENT '发布时间',
|
||||
`expire_time` datetime NULL DEFAULT NULL COMMENT '过期时间(null=永不过期)',
|
||||
`creator` bigint NULL DEFAULT NULL COMMENT '创建人ID',
|
||||
`create_date` datetime NULL DEFAULT NULL COMMENT '创建时间',
|
||||
`updater` bigint NULL DEFAULT NULL COMMENT '更新人ID',
|
||||
`update_date` datetime NULL DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_active_time`(`is_active` ASC, `publish_time` ASC) USING BTREE,
|
||||
INDEX `idx_source`(`source_type` ASC, `source_id` ASC) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '系统通知表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of sys_alert
|
||||
-- ----------------------------
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_dept
|
||||
-- ----------------------------
|
||||
@@ -516,7 +543,7 @@ INSERT INTO `sys_menu` VALUES (1067246875800000014, 1067246875800000012, '查看
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000015, 1067246875800000012, '新增', NULL, 'sys:dept:save', 1, NULL, 1, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000016, 1067246875800000012, '修改', NULL, 'sys:dept:update', 1, NULL, 2, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000017, 1067246875800000012, '删除', NULL, 'sys:dept:delete', 1, NULL, 3, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000025, 1067246875800000035, '菜单管理', 'sys/menu', NULL, 0, 'icon-unorderedlist', 0, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000025, 1067246875800000035, '菜单管理', 'sys/menu', NULL, 0, 'icon-unorderedlist', 1, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-06-26 18:49:40');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000026, 1067246875800000025, '查看', NULL, 'sys:menu:list,sys:menu:info', 1, NULL, 0, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000027, 1067246875800000025, '新增', NULL, 'sys:menu:save', 1, NULL, 1, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000028, 1067246875800000025, '修改', NULL, 'sys:menu:update', 1, NULL, 2, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
@@ -531,14 +558,14 @@ INSERT INTO `sys_menu` VALUES (1067246875800000036, 1067246875800000030, '暂停
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000037, 1067246875800000030, '恢复', NULL, 'sys:schedule:resume', 1, NULL, 5, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000038, 1067246875800000030, '立即执行', NULL, 'sys:schedule:run', 1, NULL, 6, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000039, 1067246875800000030, '日志列表', NULL, 'sys:schedule:log', 1, NULL, 7, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000040, 1067246875800000035, '参数管理', 'sys/params', '', 0, 'icon-fileprotect', 1, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000040, 1067246875800000035, '参数管理', 'sys/params', '', 0, 'icon-fileprotect', 4, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-06-26 18:49:56');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000041, 1067246875800000035, '字典管理', 'sys/dict-type', NULL, 0, 'icon-golden-fill', 2, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000042, 1067246875800000041, '查看', NULL, 'sys:dict:page,sys:dict:info', 1, NULL, 0, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000043, 1067246875800000041, '新增', NULL, 'sys:dict:save', 1, NULL, 1, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000044, 1067246875800000041, '修改', NULL, 'sys:dict:update', 1, NULL, 2, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000045, 1067246875800000041, '删除', NULL, 'sys:dict:delete', 1, NULL, 3, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000046, 0, '日志管理', NULL, NULL, 0, 'icon-container', 5, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 17:28:42');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000047, 1067246875800000035, '文件上传', 'oss/oss', 'sys:oss:all', 0, 'icon-upload', 4, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000047, 1067246875800000035, '文件上传', 'oss/oss', 'sys:oss:all', 0, 'icon-upload', 5, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-06-26 18:50:12');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000048, 1067246875800000046, '登录日志', 'sys/log-login', 'sys:log:login', 0, 'icon-filedone', 0, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000049, 1067246875800000046, '操作日志', 'sys/log-operation', 'sys:log:operation', 0, 'icon-solution', 1, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
INSERT INTO `sys_menu` VALUES (1067246875800000050, 1067246875800000046, '异常日志', 'sys/log-error', 'sys:log:error', 0, 'icon-file-exception', 2, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
@@ -573,12 +600,19 @@ INSERT INTO `sys_menu` VALUES (2029484183805616134, 2029484183805616129, '导出
|
||||
INSERT INTO `sys_menu` VALUES (2063582074925912065, 2029484183805616129, '汇总', '', 'dailyweather:weatherdailydata:query', 1, '', 5, 1067246875800000001, '2026-06-07 19:21:21', 1067246875800000001, '2026-06-07 19:21:29');
|
||||
INSERT INTO `sys_menu` VALUES (2064677221264756738, 2029421667205255169, '列表', '', 'station:weatherstation:list', 1, '', 5, 1067246875800000001, '2026-06-10 19:53:04', 1067246875800000001, '2026-06-10 19:53:04');
|
||||
INSERT INTO `sys_menu` VALUES (2065416928948871170, 0, '气象模块', '', '', 0, 'icon-ungroup', 0, 1067246875800000001, '2026-06-12 20:52:24', 1067246875800000001, '2026-06-12 21:14:12');
|
||||
INSERT INTO `sys_menu` VALUES (2065422543658115073, 2065416928948871170, '实时监测', '/weather/realtime-monitoring', '', 0, 'icon-time-circle-fill', 0, 1067246875800000001, '2026-06-12 21:14:43', 1067246875800000001, '2026-06-17 19:14:06');
|
||||
INSERT INTO `sys_menu` VALUES (2065422543658115073, 2065416928948871170, '实时监测', 'weather/realtime-monitoring', '', 0, 'icon-time-circle-fill', 0, 1067246875800000001, '2026-06-12 21:14:43', 1067246875800000001, '2026-06-26 18:52:32');
|
||||
INSERT INTO `sys_menu` VALUES (2065422609408024577, 2065416928948871170, '回波预测', 'weather/prediction', '', 0, 'icon-earth', 1, 1067246875800000001, '2026-06-12 21:14:58', 1067246875800000001, '2026-06-17 20:17:17');
|
||||
INSERT INTO `sys_menu` VALUES (2065422708557176834, 2065416928948871170, '631气象信息', 'weather/631weather-data', '', 0, 'icon-pic-left', 2, 1067246875800000001, '2026-06-12 21:15:22', 1067246875800000001, '2026-06-17 22:07:37');
|
||||
INSERT INTO `sys_menu` VALUES (2067153650086825985, 2065422543658115073, '获取实时监测', '', 'filescan:record:tree', 1, '', 0, 1067246875800000001, '2026-06-17 15:53:31', 1067246875800000001, '2026-06-17 15:53:31');
|
||||
INSERT INTO `sys_menu` VALUES (2067153761810501634, 2065422609408024577, '获取模式预测', '', 'filescan:record:model:list', 1, '', 0, 1067246875800000001, '2026-06-17 15:53:57', 1067246875800000001, '2026-06-17 20:17:47');
|
||||
INSERT INTO `sys_menu` VALUES (2067203040717520898, 2065416928948871170, '展示文件', '', 'filescan:record:display', 1, '', 0, 1067246875800000001, '2026-06-17 19:09:46', 1067246875800000001, '2026-06-17 19:09:46');
|
||||
INSERT INTO `sys_menu` VALUES (2070459758339641346, 1067246875800000035, '通知管理', 'sys/system-alert', '', 0, 'icon-sound-fill', 0, 1067246875800000001, '2026-06-26 18:50:48', 1067246875800000001, '2026-06-26 18:52:17');
|
||||
INSERT INTO `sys_menu` VALUES (2070462059406118913, 2070459758339641346, '查询', '', 'sys:alert:page', 1, '', 0, 1067246875800000001, '2026-06-26 18:59:57', 1067246875800000001, '2026-06-26 18:59:57');
|
||||
INSERT INTO `sys_menu` VALUES (2070462181372284929, 2070459758339641346, '详情', '', 'sys:alert:info', 1, '', 1, 1067246875800000001, '2026-06-26 19:00:26', 1067246875800000001, '2026-06-26 19:00:26');
|
||||
INSERT INTO `sys_menu` VALUES (2070462282153021442, 2070459758339641346, '新增', '', 'sys:alert:save', 1, '', 2, 1067246875800000001, '2026-06-26 19:00:50', 1067246875800000001, '2026-06-26 19:00:50');
|
||||
INSERT INTO `sys_menu` VALUES (2070462368132059138, 2070459758339641346, '修改', '', 'sys:alert:update', 1, '', 3, 1067246875800000001, '2026-06-26 19:01:11', 1067246875800000001, '2026-06-26 19:01:11');
|
||||
INSERT INTO `sys_menu` VALUES (2070462442044084226, 2070459758339641346, '删除', '', 'sys:alert:delete', 1, '', 4, 1067246875800000001, '2026-06-26 19:01:28', 1067246875800000001, '2026-06-26 19:01:28');
|
||||
INSERT INTO `sys_menu` VALUES (2070462786237059073, 2070459758339641346, '撤回', '', 'sys:alert:retract', 1, '', 5, 1067246875800000001, '2026-06-26 19:02:50', 1067246875800000001, '2026-06-26 19:02:50');
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_oss
|
||||
@@ -619,8 +653,8 @@ CREATE TABLE `sys_params` (
|
||||
-- ----------------------------
|
||||
-- Records of sys_params
|
||||
-- ----------------------------
|
||||
INSERT INTO `sys_params` VALUES (1067246875800000073, 'CLOUD_STORAGE_CONFIG_KEY', '{\"type\":1,\"qiniuDomain\":\"http://test.oss.renren.io\",\"qiniuPrefix\":\"upload\",\"qiniuAccessKey\":\"NrgMfABZxWLo5B-YYSjoE8-AZ1EISdi1Z3ubLOeZ\",\"qiniuSecretKey\":\"uIwJHevMRWU0VLxFvgy0tAcOdGqasdtVlJkdy6vV\",\"qiniuBucketName\":\"renren-oss\",\"aliyunDomain\":\"\",\"aliyunPrefix\":\"\",\"aliyunEndPoint\":\"\",\"aliyunAccessKeyId\":\"\",\"aliyunAccessKeySecret\":\"\",\"aliyunBucketName\":\"\",\"qcloudDomain\":\"\",\"qcloudPrefix\":\"\",\"qcloudSecretId\":\"\",\"qcloudSecretKey\":\"\",\"qcloudBucketName\":\"\"}', 0, '云存储配置信息', 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
INSERT INTO `sys_params` VALUES (2066834685586268161, 'FILE_SCAN_ROOT_PATH', 'D:/weather-data-files', 1, '气象文件扫描根路径', 1067246875800000001, '2026-06-16 18:46:04', 1067246875800000001, '2026-06-24 12:33:37');
|
||||
INSERT INTO `sys_params` VALUES (1067246875800000073, 'CLOUD_STORAGE_CONFIG_KEY', '{\"type\":1,\"qiniuDomain\":\"http://test.oss.com\",\"qiniuPrefix\":\"upload\",\"qiniuAccessKey\":\"AccessKey\",\"qiniuSecretKey\":\"SecretKey\",\"qiniuBucketName\":\"test-oss\",\"aliyunDomain\":\"\",\"aliyunPrefix\":\"\",\"aliyunEndPoint\":\"\",\"aliyunAccessKeyId\":\"\",\"aliyunAccessKeySecret\":\"\",\"aliyunBucketName\":\"\",\"qcloudDomain\":\"\",\"qcloudPrefix\":\"\",\"qcloudSecretId\":\"\",\"qcloudSecretKey\":\"\",\"qcloudBucketName\":\"\"}', 0, '云存储配置信息', 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
INSERT INTO `sys_params` VALUES (2066834685586268161, 'FILE_SCAN_ROOT_PATH', 0, 1, '气象文件扫描根路径', 1067246875800000001, '2026-06-16 18:46:04', 1067246875800000001, '2026-06-27 18:59:36');
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_region
|
||||
@@ -4106,7 +4140,7 @@ CREATE TABLE `sys_user` (
|
||||
-- ----------------------------
|
||||
-- Records of sys_user
|
||||
-- ----------------------------
|
||||
INSERT INTO `sys_user` VALUES (1067246875800000001, 'admin', '$2a$10$012Kx2ba5jzqr9gLlG4MX.bnQJTD9UWqF57XDo2N3.fPtLne02u/m', '管理员', NULL, 0, 'root@renren.io', '13612345678', 1067246875800000066, 1, 1, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
INSERT INTO `sys_user` VALUES (1067246875800000001, 'admin', '$2a$10$012Kx2ba5jzqr9gLlG4MX.bnQJTD9UWqF57XDo2N3.fPtLne02u/m', '管理员', NULL, 0, 'root@weather.com', '13111111111', 1067246875800000066, 1, 1, 1067246875800000001, '2026-03-04 11:24:04', 1067246875800000001, '2026-03-04 11:24:04');
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_user_token
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
///**
|
||||
// * Copyright (c) 2018 人人开源 All rights reserved.
|
||||
// *
|
||||
// * https://www.renren.io
|
||||
// *
|
||||
// * 版权所有,侵权必究!
|
||||
// */
|
||||
//
|
||||
//package io.weather.modules.job.config;
|
||||
//
|
||||
//import org.springframework.context.annotation.Bean;
|
||||
//import org.springframework.context.annotation.Configuration;
|
||||
//import org.springframework.scheduling.quartz.SchedulerFactoryBean;
|
||||
//
|
||||
//import javax.sql.DataSource;
|
||||
//import java.util.Properties;
|
||||
//
|
||||
///**
|
||||
// * 定时任务配置(备注:集群需要打开注释)
|
||||
// *
|
||||
// * @author 123
|
||||
// */
|
||||
//@Configuration
|
||||
//public class ScheduleConfig {
|
||||
//
|
||||
// @Bean
|
||||
// public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) {
|
||||
// SchedulerFactoryBean factory = new SchedulerFactoryBean();
|
||||
// factory.setDataSource(dataSource);
|
||||
//
|
||||
// //quartz参数
|
||||
// Properties prop = new Properties();
|
||||
// prop.put("org.quartz.scheduler.instanceName", "RenrenScheduler");
|
||||
// prop.put("org.quartz.scheduler.instanceId", "AUTO");
|
||||
// //线程池配置
|
||||
// prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
|
||||
// prop.put("org.quartz.threadPool.threadCount", "20");
|
||||
// prop.put("org.quartz.threadPool.threadPriority", "5");
|
||||
// //JobStore配置
|
||||
// prop.put("org.quartz.jobStore.class", "org.springframework.scheduling.quartz.LocalDataSourceJobStore");
|
||||
// //集群配置
|
||||
// prop.put("org.quartz.jobStore.isClustered", "true");
|
||||
// prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000");
|
||||
// prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1");
|
||||
//
|
||||
// prop.put("org.quartz.jobStore.misfireThreshold", "12000");
|
||||
// prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
|
||||
// prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?");
|
||||
//
|
||||
// //PostgreSQL数据库,需要打开此注释
|
||||
// //prop.put("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate");
|
||||
//
|
||||
// factory.setQuartzProperties(prop);
|
||||
//
|
||||
// factory.setSchedulerName("RenrenScheduler");
|
||||
// //延时启动
|
||||
// factory.setStartupDelay(30);
|
||||
// factory.setApplicationContextSchedulerContextKey("applicationContextKey");
|
||||
// //可选,QuartzScheduler 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了
|
||||
// factory.setOverwriteExistingJobs(true);
|
||||
// //设置自动启动,默认为true
|
||||
// factory.setAutoStartup(true);
|
||||
//
|
||||
// return factory;
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.weather.common.utils.SpringContextUtils;
|
||||
import com.weather.modules.job.entity.ScheduleJobEntity;
|
||||
import com.weather.modules.job.entity.ScheduleJobLogEntity;
|
||||
import com.weather.modules.job.service.ScheduleJobLogService;
|
||||
import org.quartz.DisallowConcurrentExecution;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -22,6 +23,7 @@ import java.util.Date;
|
||||
*
|
||||
* @author 123
|
||||
*/
|
||||
@DisallowConcurrentExecution
|
||||
public class ScheduleJob extends QuartzJobBean {
|
||||
private Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
|
||||
@@ -1,20 +1,3 @@
|
||||
/**
|
||||
* Copyright 2018 人人开源 https://www.renren.io
|
||||
* <p>
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* <p>
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
* <p>
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.weather.modules.security.password;
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,8 +15,9 @@ import jakarta.validation.constraints.Null;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 部门管理
|
||||
@@ -25,9 +26,9 @@ import java.util.Date;
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@EqualsAndHashCode
|
||||
@Schema(title = "部门管理")
|
||||
public class SysDeptDTO extends TreeNode implements Serializable {
|
||||
public class SysDeptDTO implements TreeNode<SysDeptDTO> {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(title = "id")
|
||||
@@ -39,6 +40,9 @@ public class SysDeptDTO extends TreeNode implements Serializable {
|
||||
@NotNull(message="{sysdept.pid.require}", groups = DefaultGroup.class)
|
||||
private Long pid;
|
||||
|
||||
@Schema(title = "子部门列表")
|
||||
private List<SysDeptDTO> children = new ArrayList<>();
|
||||
|
||||
@Schema(title = "部门名称")
|
||||
@NotBlank(message="{sysdept.name.require}", groups = DefaultGroup.class)
|
||||
private String name;
|
||||
|
||||
@@ -16,8 +16,9 @@ import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.hibernate.validator.constraints.Range;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单管理
|
||||
@@ -26,9 +27,9 @@ import java.util.Date;
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@EqualsAndHashCode
|
||||
@Schema(title = "菜单管理")
|
||||
public class SysMenuDTO extends TreeNode<SysMenuDTO> implements Serializable {
|
||||
public class SysMenuDTO implements TreeNode<SysMenuDTO> {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(title = "id")
|
||||
@@ -40,6 +41,9 @@ public class SysMenuDTO extends TreeNode<SysMenuDTO> implements Serializable {
|
||||
@NotNull(message="{sysmenu.pid.require}", groups = DefaultGroup.class)
|
||||
private Long pid;
|
||||
|
||||
@Schema(title = "子菜单列表")
|
||||
private List<SysMenuDTO> children = new ArrayList<>();
|
||||
|
||||
@Schema(title = "菜单名称")
|
||||
@NotBlank(message="sysmenu.name.require", groups = DefaultGroup.class)
|
||||
private String name;
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
/**
|
||||
* Copyright (c) 2019 人人开源 All rights reserved.
|
||||
* <p>
|
||||
* https://www.renren.io
|
||||
* <p>
|
||||
* 版权所有,侵权必究!
|
||||
*/
|
||||
|
||||
package com.weather.modules.sys.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
/**
|
||||
* Copyright (c) 2019 人人开源 All rights reserved.
|
||||
* <p>
|
||||
* https://www.renren.io
|
||||
* <p>
|
||||
* 版权所有,侵权必究!
|
||||
*/
|
||||
|
||||
package com.weather.modules.sys.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ public class WeatherDataListener extends AnalysisEventListener<WeatherExcelVO> {
|
||||
|
||||
try {
|
||||
LocalDate localDate = LocalDate.of(data.getYear(), data.getMonth(), data.getDay());
|
||||
entity.setObserveDate(Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
|
||||
entity.setObserveDate(Date.from(localDate.atStartOfDay(ZoneId.of("GMT+8")).toInstant()));
|
||||
} catch (Exception e) {
|
||||
log.warn("第{}行日期转换失败: {}年{}月{}日,已跳过", context.readRowHolder().getRowIndex(),
|
||||
data.getYear(), data.getMonth(), data.getDay());
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ public class WeatherSummarizeCacheTask implements ITask {
|
||||
.collect(Collectors.groupingBy(dto -> String.valueOf(dto.getStationId())));
|
||||
|
||||
String key = RedisKeys.getWeatherSummarizeKey(month, day);
|
||||
redisUtils.set(key, grouped, RedisUtils.NOT_EXPIRE);
|
||||
redisUtils.set(key, grouped, RedisUtils.HOUR_SIX_EXPIRE);
|
||||
|
||||
log.info("天气汇总缓存刷新完成,共 {} 条记录,{} 个站点", list.size(), grouped.size());
|
||||
} catch (Exception e) {
|
||||
|
||||
+2
-2
@@ -61,10 +61,10 @@ public class WeatherExcelVO {
|
||||
@ExcelProperty("最大风速出现时间")
|
||||
private Integer maxWindTime;
|
||||
|
||||
@ExcelProperty("极大风速的风向(角度)")
|
||||
@ExcelProperty("极大风速")
|
||||
private BigDecimal extremeWindSpeed;
|
||||
|
||||
@ExcelProperty("极大风速")
|
||||
@ExcelProperty("极大风速的风向(角度)")
|
||||
private Integer extremeWindDirection;
|
||||
|
||||
@ExcelProperty("极大风速出现时间")
|
||||
|
||||
@@ -2,7 +2,7 @@ spring:
|
||||
data:
|
||||
redis:
|
||||
database: 0
|
||||
host: 127.0.0.1
|
||||
host: 192.168.2.186
|
||||
port: 6379
|
||||
password: # 密码(默认为空)
|
||||
timeout: 6000ms # 连接超时时长(毫秒)
|
||||
@@ -16,7 +16,7 @@ spring:
|
||||
druid:
|
||||
#MySQL
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/weather_data_system?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||
url: jdbc:mysql://192.168.2.186:3306/weather_data_system?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||
username: root
|
||||
password: root
|
||||
initial-size: 10
|
||||
@@ -51,17 +51,3 @@ spring:
|
||||
project-options:
|
||||
redis:
|
||||
open: true
|
||||
|
||||
##多数据源的配置,需要引用renren-dynamic-datasource
|
||||
#dynamic:
|
||||
# datasource:
|
||||
# slave1:
|
||||
# driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
|
||||
# url: jdbc:sqlserver://123456:1433;DatabaseName=renren_security
|
||||
# username: sa
|
||||
# password: 123456
|
||||
# slave2:
|
||||
# driver-class-name: org.postgresql.Driver
|
||||
# url: jdbc:postgresql://123456:5432/renren_security
|
||||
# username: postgres
|
||||
# password: 123456
|
||||
|
||||
@@ -2,7 +2,7 @@ spring:
|
||||
data:
|
||||
redis:
|
||||
database: 0
|
||||
host: 127.0.0.1
|
||||
host: 192.168.2.186
|
||||
port: 6379
|
||||
password: # 生产环境请设置密码
|
||||
timeout: 6000ms # 连接超时时长(毫秒)
|
||||
@@ -16,7 +16,7 @@ spring:
|
||||
druid:
|
||||
#MySQL
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/weather_data_system?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||
url: jdbc:mysql://192.168.2.186:3306/weather_data_system?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||
username: weather
|
||||
password: 123456
|
||||
initial-size: 10
|
||||
|
||||
@@ -2,7 +2,7 @@ spring:
|
||||
data:
|
||||
redis:
|
||||
database: 0
|
||||
host: 127.0.0.1
|
||||
host: 192.168.2.186
|
||||
port: 6379
|
||||
password: # 密码(默认为空)
|
||||
timeout: 6000ms # 连接超时时长(毫秒)
|
||||
@@ -16,7 +16,7 @@ spring:
|
||||
druid:
|
||||
#MySQL
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/weather_data_system?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||
url: jdbc:mysql://192.168.2.186:3306/weather_data_system?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||
username: weather
|
||||
password: 123456
|
||||
initial-size: 10
|
||||
|
||||
@@ -5,7 +5,7 @@ server:
|
||||
threads:
|
||||
max: 1000
|
||||
min-spare: 30
|
||||
port: 8080
|
||||
port: 48080
|
||||
servlet:
|
||||
context-path: /system-admin
|
||||
session:
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
<springProfile name="dev,test">
|
||||
<logger name="org.springframework.web" level="TRACE"/>
|
||||
<logger name="org.springboot.sample" level="TRACE" />
|
||||
<logger name="io.renren" level="DEBUG" />
|
||||
<logger name="com.weather" level="DEBUG" />
|
||||
</springProfile>
|
||||
|
||||
<!-- 生产环境 -->
|
||||
<springProfile name="prod">
|
||||
<logger name="org.springframework.web" level="ERROR"/>
|
||||
<logger name="org.springboot.sample" level="ERROR" />
|
||||
<logger name="io.renren" level="ERROR" />
|
||||
<logger name="com.weather" level="ERROR" />
|
||||
</springProfile>
|
||||
|
||||
</configuration>
|
||||
+61
-12
@@ -1,20 +1,69 @@
|
||||
# CLAUDE.md — system-common
|
||||
# CLAUDE.md - system-common
|
||||
|
||||
Shared Java library used by all backend modules.
|
||||
This file provides guidance to Claude Code when working in the system-common module.
|
||||
|
||||
---
|
||||
## Purpose
|
||||
|
||||
## Service layer pattern
|
||||
Shared library module used by all other backend modules. Provides base classes, utilities, and cross-cutting concerns. This module has no main class and is not runnable on its own.
|
||||
|
||||
Two base classes defined here:
|
||||
## Base Class Hierarchy
|
||||
|
||||
| Base | Purpose |
|
||||
|---|---|
|
||||
| `CrudService<Dao, Entity, DTO>` | Generic CRUD: `page()`, `get()`, `save()`, `update()`, `delete()` |
|
||||
| `BaseService<Dao>` | Lighter base without DTO generic |
|
||||
### BaseEntity (`com.weather.common.entity.BaseEntity`)
|
||||
|
||||
These are used by all service implementations in `system-admin` and other modules. See `system-admin/CLAUDE.md` for the full module convention.
|
||||
All entities must extend this. Provides:
|
||||
- `id` (Long, `@TableId`)
|
||||
- `creator` (Long, `@TableField(fill = INSERT)`)
|
||||
- `createDate` (Date, `@TableField(fill = INSERT)`)
|
||||
|
||||
## i18n validation messages
|
||||
### BaseDao (`com.weather.common.dao.BaseDao<M, T>`)
|
||||
|
||||
Located at `src/main/resources/i18n/validation.properties`. Hibernate Validator messages used by `@RestControllerAdvice` exception handler in `system-admin`. Error codes follow `int` scheme: 5 digits, first 2 = module, last 3 = business (e.g. `10001`-`10029`).
|
||||
Extends MyBatis-Plus `BaseMapper<T>`. Provides `getById(Long id)` as an alias.
|
||||
|
||||
### Service Layer
|
||||
|
||||
```
|
||||
BaseService<T> # tag interface
|
||||
└── CrudService<T, D> # page/list/get/save/update/delete
|
||||
└── BaseServiceImpl<M, T> # dao injection + insert/updateById wrappers
|
||||
└── CrudServiceImpl<M, T, D> # generic CRUD with getWrapper()
|
||||
```
|
||||
|
||||
`CrudServiceImpl` is the key base class for all business services. Subclasses only need to implement `getWrapper(Map<String, Object> params)` to define query conditions. Entity-to-DTO conversion uses `ConvertUtils.sourceToTarget()`.
|
||||
|
||||
## Redis
|
||||
|
||||
- `RedisConfig`: Creates `RedisTemplate<String, Object>` with Jackson JSON serialization
|
||||
- `RedisUtils`: Wraps `RedisTemplate` operations -- `set/get/delete/deleteByPattern/hGet/hSet/hDelete/expire`
|
||||
- `RedisKeys`: Static factory for standardized key names (e.g. `getWeatherSummarizeKey()`, `getWeatherSummarizePattern()`)
|
||||
- `RedisAspect`: AOP aspect that logs Redis operation errors
|
||||
|
||||
## Validation
|
||||
|
||||
- `ValidatorUtils`: Bean Validation wrapper using Jakarta Validator
|
||||
- `AssertUtils`: Assertion helpers that throw `CommonException` on failure
|
||||
- `group/`: Validation groups -- `AddGroup`, `UpdateGroup`, `DefaultGroup`
|
||||
|
||||
## XSS Protection
|
||||
|
||||
- `XssFilter`: Servlet filter that wraps requests with `XssHttpServletRequestWrapper`
|
||||
- `XssUtils`: HTML entity encoding using Jsoup
|
||||
|
||||
## Exception Handling
|
||||
|
||||
- `CommonException`: Application-level runtime exception with error code
|
||||
- `ExceptionUtils`: Factory methods for creating typed exceptions
|
||||
- `ErrorCode`: Interface with error code constants
|
||||
|
||||
## Utilities
|
||||
|
||||
- `ConvertUtils`: Bean copy with recursive conversion support for nested objects
|
||||
- `DateUtils`: Date parsing/formatting
|
||||
- `JsonUtils`: JSON serialization via Jackson
|
||||
- `TreeUtils`: Build tree structures from flat lists (used for menus, depts, regions)
|
||||
- `HttpContextUtils`: Servlet request/response helpers
|
||||
- `IpUtils`: Extract client IP from request
|
||||
- `MessageUtils`: i18n message resolution
|
||||
- `SpringContextUtils`: Access Spring ApplicationContext statically
|
||||
- `Result<T>`: Standard API response wrapper with `code`, `msg`, `data`
|
||||
- `PageData<T>`: Paginated response with `total`, `list`
|
||||
- `TreeNode`: Tree node interface with `getId/setId/getPid/setPid/getChildren/setChildren`
|
||||
|
||||
@@ -2,30 +2,35 @@
|
||||
|
||||
package com.weather.common.utils;
|
||||
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 树节点,所有需要实现树节点的,都需要继承该类
|
||||
* 树节点接口,所有需要实现树节点的,都需要实现该接口
|
||||
*
|
||||
* @author 123
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Data
|
||||
public class TreeNode<T> implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
public interface TreeNode<T> extends Serializable {
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
private Long id;
|
||||
Long getId();
|
||||
|
||||
void setId(Long id);
|
||||
|
||||
/**
|
||||
* 上级ID
|
||||
*/
|
||||
private Long pid;
|
||||
Long getPid();
|
||||
|
||||
void setPid(Long pid);
|
||||
|
||||
/**
|
||||
* 子节点列表
|
||||
*/
|
||||
private List<T> children = new ArrayList<>();
|
||||
List<T> getChildren();
|
||||
|
||||
void setChildren(List<T> children);
|
||||
}
|
||||
@@ -20,7 +20,7 @@ public class TreeUtils {
|
||||
/**
|
||||
* 根据pid,构建树节点
|
||||
*/
|
||||
public static <T extends TreeNode> List<T> build(List<T> treeNodes, Long pid) {
|
||||
public static <T extends TreeNode<T>> List<T> build(List<T> treeNodes, Long pid) {
|
||||
//pid不能为空
|
||||
AssertUtils.isNull(pid, "pid");
|
||||
|
||||
@@ -37,7 +37,7 @@ public class TreeUtils {
|
||||
/**
|
||||
* 查找子节点
|
||||
*/
|
||||
private static <T extends TreeNode> T findChildren(List<T> treeNodes, T rootNode) {
|
||||
private static <T extends TreeNode<T>> T findChildren(List<T> treeNodes, T rootNode) {
|
||||
for(T treeNode : treeNodes) {
|
||||
if(rootNode.getId().equals(treeNode.getPid())) {
|
||||
rootNode.getChildren().add(findChildren(treeNodes, treeNode));
|
||||
@@ -49,7 +49,7 @@ public class TreeUtils {
|
||||
/**
|
||||
* 构建树节点
|
||||
*/
|
||||
public static <T extends TreeNode> List<T> build(List<T> treeNodes) {
|
||||
public static <T extends TreeNode<T>> List<T> build(List<T> treeNodes) {
|
||||
List<T> result = new ArrayList<>();
|
||||
|
||||
//list转map
|
||||
|
||||
@@ -6,12 +6,11 @@ import com.weather.common.exception.CommonException;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.Validation;
|
||||
import jakarta.validation.Validator;
|
||||
import jakarta.validation.ValidatorFactory;
|
||||
import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
import org.springframework.context.support.ResourceBundleMessageSource;
|
||||
import org.springframework.validation.beanvalidation.MessageSourceResourceBundleLocator;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -23,11 +22,16 @@ import java.util.Set;
|
||||
*/
|
||||
public class ValidatorUtils {
|
||||
|
||||
private static ResourceBundleMessageSource getMessageSource() {
|
||||
ResourceBundleMessageSource bundleMessageSource = new ResourceBundleMessageSource();
|
||||
bundleMessageSource.setDefaultEncoding("UTF-8");
|
||||
bundleMessageSource.setBasenames("i18n/validation");
|
||||
return bundleMessageSource;
|
||||
private static final ValidatorFactory VALIDATOR_FACTORY;
|
||||
|
||||
static {
|
||||
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
|
||||
messageSource.setDefaultEncoding("UTF-8");
|
||||
messageSource.setBasenames("i18n/validation");
|
||||
|
||||
VALIDATOR_FACTORY = Validation.byDefaultProvider().configure().messageInterpolator(
|
||||
new ResourceBundleMessageInterpolator(new MessageSourceResourceBundleLocator(messageSource)))
|
||||
.buildValidatorFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,14 +41,11 @@ public class ValidatorUtils {
|
||||
*/
|
||||
public static void validateEntity(Object object, Class<?>... groups)
|
||||
throws CommonException {
|
||||
Locale.setDefault(LocaleContextHolder.getLocale());
|
||||
Validator validator = Validation.byDefaultProvider().configure().messageInterpolator(
|
||||
new ResourceBundleMessageInterpolator(new MessageSourceResourceBundleLocator(getMessageSource())))
|
||||
.buildValidatorFactory().getValidator();
|
||||
Validator validator = VALIDATOR_FACTORY.getValidator();
|
||||
|
||||
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
|
||||
if (!constraintViolations.isEmpty()) {
|
||||
ConstraintViolation<Object> constraint = constraintViolations.iterator().next();
|
||||
ConstraintViolation<Object> constraint = constraintViolations.iterator().next();
|
||||
throw new CommonException(constraint.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# CLAUDE.md - system-dynamic-datasource
|
||||
|
||||
This file provides guidance to Claude Code when working in the system-dynamic-datasource module.
|
||||
|
||||
## Purpose
|
||||
|
||||
Provides dynamic multi-datasource switching at runtime using Spring's `AbstractRoutingDataSource`. This module has no main class and is not runnable on its own.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
@DataSource (annotation)
|
||||
↓ triggers
|
||||
DataSourceAspect (AOP around advice)
|
||||
↓ sets/clears
|
||||
DynamicContextHolder (ThreadLocal<Deque<String>>)
|
||||
↓ peek() returns key to
|
||||
DynamicDataSource (extends AbstractRoutingDataSource)
|
||||
↓ routes to
|
||||
Multiple DataSources (configured in DynamicDataSourceProperties)
|
||||
```
|
||||
|
||||
## Key Classes
|
||||
|
||||
### `@DataSource` annotation
|
||||
|
||||
Apply to methods or classes to specify which datasource to use. Takes a datasource name string.
|
||||
|
||||
### `DataSourceAspect`
|
||||
|
||||
AOP aspect that intercepts `@DataSource`-annotated methods. Before invocation, pushes the datasource name onto `DynamicContextHolder`. After invocation (finally block), pops it back off.
|
||||
|
||||
### `DynamicContextHolder`
|
||||
|
||||
Thread-safe holder using `ThreadLocal<Deque<String>>`. Uses a deque (stack) to support nested `@DataSource` calls -- each method restores the previous datasource on exit.
|
||||
- `push(String ds)` -- set current datasource
|
||||
- `poll()` -- restore previous
|
||||
- `peek()` -- get current (used by DynamicDataSource)
|
||||
|
||||
### `DynamicDataSource extends AbstractRoutingDataSource`
|
||||
|
||||
`determineCurrentLookupKey()` returns `DynamicContextHolder.peek()`. If the holder is empty, falls back to the default datasource.
|
||||
|
||||
### `DynamicDataSourceFactory`
|
||||
|
||||
Creates Druid datasource instances from configuration properties.
|
||||
|
||||
### Configuration Properties
|
||||
|
||||
`DynamicDataSourceProperties`: Maps to YAML `spring.datasource.dynamic.*` with multiple named datasource configs.
|
||||
`DataSourceProperties`: Individual datasource config (url, username, password, driver-class-name, etc.).
|
||||
|
||||
## Usage
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class SomeService {
|
||||
@DataSource("oracle")
|
||||
public List<Data> queryFromOracle() {
|
||||
// queries run against the "oracle" datasource
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When no `@DataSource` is present, queries use the default (primary) datasource configured in `application-*.yml`.
|
||||
@@ -1,3 +1,3 @@
|
||||
NODE_ENV=development
|
||||
VITE_APP_API=http://192.168.2.186:8080/system-admin
|
||||
VITE_APP_API=http://192.168.2.151:48080/system-admin
|
||||
|
||||
|
||||
+122
-158
@@ -1,184 +1,148 @@
|
||||
# CLAUDE.md — weather-data-ui
|
||||
# CLAUDE.md - weather-data-ui
|
||||
|
||||
Frontend module: Vue 3 / Vite 5 / TypeScript SPA.
|
||||
This file provides guidance to Claude Code when working in the weather-data-ui module.
|
||||
|
||||
---
|
||||
## Purpose
|
||||
|
||||
## Code Style
|
||||
Vue 3 + TypeScript frontend for the weather data management system. Single-page application built with Vite 5.
|
||||
|
||||
- **No emoji in UI strings.** Plain Chinese text for labels, buttons, status.
|
||||
- **No emoji in code comments or docstrings.** Plain text only.
|
||||
- **Keep CLAUDE.md current** — whenever code is modified, added, deleted, or any file change affects the module structure, build, conventions, or component patterns, update this file (and the root `CLAUDE.md` if cross-cutting) in the same commit to reflect the new state. Stale documentation is a bug.
|
||||
## Tech Stack
|
||||
|
||||
---
|
||||
- Vue 3.5 (Composition API), TypeScript 5.7, Vite 5.4
|
||||
- Element Plus 2.10 (UI library)
|
||||
- Pinia 2.3 (state management)
|
||||
- Vue Router 4.2 (hash mode routing)
|
||||
- ECharts 5 + vue-echarts 6 (charts)
|
||||
- Axios 1.11 (HTTP client)
|
||||
- Less/Sass for styling
|
||||
|
||||
## Commands
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
npm install # install dependencies
|
||||
npm run dev # Vite dev server (port 8001, host 0.0.0.0)
|
||||
npm run build / npm run build:prod # production build
|
||||
npm run serve # preview production build
|
||||
npm run lint # lint with autofix (ESLint)
|
||||
npx vue-tsc --noEmit # type-check (not in pre-commit)
|
||||
npm install
|
||||
npm run dev # dev server on port 8001, hot reload, proxies /api to backend
|
||||
npm run build # production build (vite build --mode production), outputs to dist/
|
||||
npm run lint # ESLint on src/**/*.{vue,ts} with --fix
|
||||
npm run serve # build + vite preview
|
||||
```
|
||||
|
||||
Pre-commit: `lint-staged` runs `eslint --fix` on `*.ts`/`*.vue` via `yorkie` git hooks (not husky). No test runner configured.
|
||||
## Project Structure
|
||||
|
||||
## Stack
|
||||
|
||||
- Vite 5 + Vue 3 + TypeScript SPA
|
||||
- Element Plus + Element Plus Icons (all icons registered globally)
|
||||
- `vue-router` with **hash history** (`createWebHashHistory`)
|
||||
- Pinia for state management
|
||||
- Axios via `src/utils/http.ts` + `src/service/baseService.ts`
|
||||
- API base URL: `VITE_APP_API` env var, overridable at runtime by `window.SITE_CONFIG.apiURL`
|
||||
|
||||
## Environment config
|
||||
|
||||
- Dev: `VITE_APP_API=http://192.168.2.186:8080/system-admin` (hardcoded IP — new devs must change)
|
||||
- Prod: `VITE_APP_API=/system-admin` (relative, proxied via Nginx)
|
||||
- Runtime override takes priority: `window.SITE_CONFIG.apiURL`
|
||||
|
||||
## Vite config
|
||||
|
||||
- `base: "./"` (relative paths), `chunkSizeWarningLimit: 1024`
|
||||
- Manual chunks: `lodash` and `vlib` (vue/vue-router/element-plus)
|
||||
- Dev: HMR overlay disabled, `host: "0.0.0.0"`, port 8001
|
||||
|
||||
## Axios HTTP pattern
|
||||
|
||||
- Success check: `response.data.code === 0` (not `=== 200`)
|
||||
- Request interceptor: adds `token` header, `X-Requested-With`, request timing, cache-busting `_t` on GET
|
||||
- On `code === 401`: auto-redirects to `/login`
|
||||
- Response unwrapped: callers receive `response.data`
|
||||
- File exports: bypass Axios, use `window.location.href` with token as query param
|
||||
- Uploads: no `Content-Type` set (browser auto-sets for `FormData`)
|
||||
|
||||
## Routing & state
|
||||
|
||||
- `src/router/base.ts`: 7 base routes (`/`, `/home`, `/login`, `/user/password`, `/iframe/:id?`, `/error`, 404 catch-all)
|
||||
- `src/router/index.ts`: `beforeEach` guard — auth check, dynamic route registration from backend menus, tab management. Routes are dynamically added via `addRoute` with **flattened nested routes** (keep-alive limitation). View components resolved via `import.meta.glob("/src/views/**/*.vue")`.
|
||||
- `src/store/index.ts` (`useAppStore`): monolithic store — all state nested in `state.state` (double nesting, e.g. `store.state.appIsLogin`). `initApp` fetches menus/permissions/user/dicts in 4 parallel requests.
|
||||
- `src/store/importTasks.ts`: separate store for import task tracking (computed getters: `activeTasks`, `hasActiveTasks`, `recentTasks`).
|
||||
- `src/utils/router.ts`: converts backend menu records → Vue router records, supports iframe/external links with `openStyle` flags.
|
||||
|
||||
Layout is event-driven: `src/layout/` shell + `mitt` event bus (`src/utils/emits.ts`). The `EMitt` enum defines events for sidebar, theme, tabs, layout changes. **Trace both the Pinia store and mitt events** when changing navigation/sidebar/tabs/theme.
|
||||
|
||||
Header right side: notification bell (combined badge) → `expand` (user menu). The old `import-task-indicator.vue` has been removed in favor of the notification center drawer.
|
||||
|
||||
## Common page pattern: `useView` hook
|
||||
|
||||
Admin CRUD pages use `src/hooks/useView.ts` for shared list-page workflow.
|
||||
|
||||
Key behaviors to know before refactoring:
|
||||
- `closeCurrentTab()`: if tabs enabled, emits `OnCloseCurrTab` mitt event; otherwise navigates to `/home`.
|
||||
- `exportHandle()`: uses `window.location.href` with token as query param (NOT Axios).
|
||||
- `dataListSortChangeHandle()`: converts camelCase → snake_case for backend (e.g. `stationId` → `station_id`).
|
||||
- `createdIsNeed: true` / `activatedIsNeed: false` by default. Pages needing refresh on tab activation must set `activatedIsNeed: true`.
|
||||
- Includes workflow helpers (`handleFlowRoute`, `flowDetailRoute`) hardcoded to `/flow/task-form`.
|
||||
|
||||
## Cache utility
|
||||
|
||||
All cache keys prefixed with `v1@` to avoid collisions. Supports `localStorage` and `sessionStorage` (token uses sessionStorage). JSON serialization is automatic. `getCache` supports auto-delete-after-read (`isDelete` flag).
|
||||
|
||||
## Weather frontend module
|
||||
|
||||
The home dashboard (`src/views/home.vue`) uses a **composable-based architecture**:
|
||||
|
||||
| Composable | Responsibility |
|
||||
|---|---|
|
||||
| `useWeatherConstants.ts` | Rain levels, temperature thresholds, filter field definitions, `fmtVal()`, level/class helpers |
|
||||
| `useWeatherFilter.ts` | Filter state, toggle/reset/match logic, `matchOp()` |
|
||||
| `useWeatherStats.ts` | `computeStats()`, `buildStatCards()`, `buildSummary()`, `rainLevelDistribution`, `WeatherDataRow` type |
|
||||
| `useWeatherChart.ts` | ECharts dynamic import, `buildChartOption()`, `ResizeObserver`, precise trigger key (not deep watch) |
|
||||
| `useWeatherExport.ts` | PNG/PDF export with dynamic `html2canvas`/`jspdf` imports, loading indicator |
|
||||
|
||||
Supporting utils: `src/utils/chartBuilder.ts`, `src/utils/exportReport.ts`.
|
||||
|
||||
## Critical rules
|
||||
|
||||
### 1. Null ≠ zero — missing data MUST be preserved as null
|
||||
When mapping backend API responses to frontend models, **never** default missing numeric values to `0`. Rainfall of `0mm` means "no rain that day" (valid measurement); `null` means "no data available" (missing record). Use `: null` not `: 0` in data mapping, and display `"—"` for null values via `fmtVal()`.
|
||||
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
rainfall: row.rain2020 != null ? +row.rain2020 : null,
|
||||
|
||||
// ❌ Wrong — confuses "no data" with "measured zero"
|
||||
rainfall: row.rain2020 != null ? +row.rain2020 : 0,
|
||||
```
|
||||
src/
|
||||
├── main.ts # App bootstrap, Pinia init
|
||||
├── App.vue # Root component
|
||||
├── assets/ # Static assets (css, icons, images, theme)
|
||||
├── components/ # Reusable components
|
||||
│ ├── alert-marquee/ # Alert scrolling marquee (SSE-driven)
|
||||
│ ├── base/ # Base table/form/dialog wrappers
|
||||
│ ├── sys-dept-tree/ # Department tree selector
|
||||
│ ├── sys-radio-group/ # Radio group with dict-driven options
|
||||
│ ├── sys-region-tree/ # Region tree selector
|
||||
│ ├── sys-select/ # Dict-driven select dropdown
|
||||
│ └── wang-editor/ # Rich text editor wrapper
|
||||
├── composables/ # Vue 3 composables
|
||||
│ ├── useAlertMarquee.ts # SSE alert streaming logic
|
||||
│ ├── useFloatingDrag.ts # Draggable floating panel logic
|
||||
│ ├── useWeatherChart.ts # ECharts chart configuration
|
||||
│ ├── useWeatherConstants.ts # Weather domain constants
|
||||
│ ├── useWeatherExport.ts # Export to Excel/PDF
|
||||
│ ├── useWeatherFilter.ts # Query filter state management
|
||||
│ └── useWeatherStats.ts # Statistical computation
|
||||
├── constants/ # Application constants
|
||||
│ ├── app.ts # API base URL, request timeout
|
||||
│ ├── cacheKey.ts # Cache key enums
|
||||
│ ├── config.ts # App config
|
||||
│ └── enum.ts # Enums (EMitt events, etc.)
|
||||
├── hooks/ # Legacy hooks
|
||||
│ └── useView.ts # View loader for dynamic routes
|
||||
├── layout/ # Layout components
|
||||
│ ├── index.vue # Main layout with sidebar + header
|
||||
│ ├── layout.vue # Alternative layout
|
||||
│ ├── fullscreen-layout.vue # Fullscreen page layout
|
||||
│ ├── header/ # Top header bar
|
||||
│ ├── sidebar/ # Left sidebar navigation
|
||||
│ └── view/ # Content view wrapper (tabs)
|
||||
├── router/
|
||||
│ ├── index.ts # Router instance + dynamic route registration
|
||||
│ └── base.ts # Static base routes (login, home, error, iframe)
|
||||
├── service/
|
||||
│ └── baseService.ts # HTTP helpers: get/post/put/delete/upload
|
||||
├── store/
|
||||
│ ├── index.ts # useAppStore (user, permissions, menus, routes, tabs)
|
||||
│ └── importTasks.ts # Import task progress store
|
||||
├── types/ # TypeScript type definitions
|
||||
├── utils/
|
||||
│ ├── cache.ts # Cookie/localStorage cache helpers (token storage)
|
||||
│ ├── chartBuilder.ts # ECharts option builder
|
||||
│ ├── emits.ts # Event bus (mitt)
|
||||
│ ├── exportReport.ts # PDF/Excel export utilities
|
||||
│ ├── http.ts # Axios instance with interceptors
|
||||
│ ├── router.ts # Route merging and registration helpers
|
||||
│ ├── theme.ts # Theme switching logic
|
||||
│ └── utils.ts # General utility functions
|
||||
└── views/ # Feature views
|
||||
├── dailyweather/ # Daily weather data query/import/charts
|
||||
├── home.vue # Dashboard home page
|
||||
├── iframe.vue # Iframe wrapper for external pages
|
||||
├── job/ # Scheduled job management
|
||||
├── login.vue # Login page
|
||||
├── oss/ # Cloud file storage management
|
||||
├── region/ # Region management
|
||||
├── station/ # Weather station management
|
||||
├── sys/ # System management (users, roles, menus, depts, dicts, alerts)
|
||||
├── tools/ # Utility tools
|
||||
└── weather/ # Weather data views
|
||||
```
|
||||
|
||||
All helper functions must accept `number | null` and return `"—"` or `""` for null. Stats computations must skip null values.
|
||||
## Router Architecture
|
||||
|
||||
### 2. Heavy libraries must use dynamic imports
|
||||
`html2canvas`, `jspdf`, and `echarts` are NOT imported at module level. Load them via `await import()` only when triggered by user action. This saves ~600KB from the initial bundle.
|
||||
- **Mode**: `createWebHashHistory()` (hash mode)
|
||||
- **Dynamic routing**: On login, `useAppStore.initApp()` fetches `/sys/menu/nav` (menu tree), `/sys/menu/permissions` (perms), `/sys/user/info` (user), `/sys/dict/type/all` (dicts). Menu data is merged with `src/views/**/*.vue` component map via `mergeServerRoute()` to build the full route table.
|
||||
- **Auto-registration**: `registerDynamicToRouterAndNext()` can register a new route at runtime by matching path to a view component file.
|
||||
- **Tab tracking**: Router `beforeEach` guard emits tab push events; tabs are tracked in store state.
|
||||
- **404 fallback**: Unmatched routes redirect to `/error` with `to=404` query param.
|
||||
|
||||
### 3. Fonts are self-hosted — no external network dependency
|
||||
Fonts (Noto Sans SC, JetBrains Mono) are bundled via `@fontsource/*` packages, imported in `src/main.ts`. Do NOT add Google Fonts `<link>` tags or `@import` back — the system runs on intranet where external network may be unavailable. To add a new font weight, import the corresponding fontsource CSS file in `main.ts`.
|
||||
## State Management (Pinia)
|
||||
|
||||
### 4. Export must show user feedback
|
||||
Always show `ElLoading.service` fullscreen and `ElMessage` success/failure. Disable the export button during rendering.
|
||||
`useAppStore` holds all application state:
|
||||
- `appIsLogin`, `appIsReady`, `appIsRender` -- lifecycle flags
|
||||
- `permissions[]` -- user permission set (strings)
|
||||
- `user` -- current user object
|
||||
- `dicts[]` -- dictionary data array
|
||||
- `routes[]` -- resolved route records
|
||||
- `routeToMeta` -- path-to-metadata mapping for tab titles
|
||||
- `tabs[]`, `activeTabName`, `closedTabs` -- tab management
|
||||
|
||||
### 5. Deep watchers on filter objects are banned
|
||||
Never use `watch(filters, callback, { deep: true })`. Derive a precise computed trigger key (e.g. `dataHash`, `filteredHash`, `extremesVersion`) and watch that instead.
|
||||
`initApp()` is called once on login -- returns merged routes that are then registered with the router.
|
||||
|
||||
## Notification center
|
||||
## HTTP Layer
|
||||
|
||||
The global notification system (`src/components/alert-marquee/index.vue`) aggregates two feed types:
|
||||
`utils/http.ts` creates an Axios instance:
|
||||
- **Request interceptor**: Injects `token` header from cache, adds `_t` timestamp to GET requests, handles form-urlencoded serialization
|
||||
- **Response interceptor**: `code === 0` means success; `code === 401` triggers redirect to `/login`; other codes show ElMessage error
|
||||
|
||||
| Feed | Source | Clickable? |
|
||||
|---|---|---|
|
||||
| System alerts | `useAlertMarquee.ts` composable (polls `/sys/alert/active/since` every 30s) | Yes — opens detail dialog |
|
||||
| Import tasks | `useImportTaskStore` Pinia store | No — shows real-time progress inline |
|
||||
`service/baseService.ts` wraps HTTP methods:
|
||||
- `get(path, params, headers)` -- adds cache-busting `_t`
|
||||
- `post(path, body, headers)` -- JSON content-type
|
||||
- `put(path, params, headers)` -- JSON content-type
|
||||
- `delete(path, params)` -- sends body
|
||||
- `upload(path, formData, headers)` -- multipart form upload
|
||||
|
||||
### Components & composables
|
||||
## SSE Alert Integration
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `src/composables/useAlertMarquee.ts` | Module-level singleton: message queue, drawer toggle, detail dialog state, scrollbar position. Real-time delivery: SSE (`/sys/alert/stream`) primary + 10s polling fallback. Exports danger-specific computed: `dangerMessages`, `latestDangerMessage`, `dangerCount`, `hasDangerMessages` |
|
||||
| `src/composables/useFloatingDrag.ts` | Pointer Events drag logic: `setPointerCapture`, viewport clamping, deferred `isDragging` (activates only on >3px move), `dragMoved` flag to distinguish drag vs click |
|
||||
| `src/components/alert-marquee/index.vue` | Floating scrollbar (640px wide, centered top, draggable) + `el-drawer` notification center + `el-dialog` detail popup |
|
||||
| `src/layout/header/base-header.vue` | Bell button with combined badge (alerts + active import tasks), toggles drawer |
|
||||
| `src/store/importTasks.ts` | Import task CRUD: `addTask`, `updateTask`, `removeTask`, `clearCompleted`; getters: `activeTasks`, `recentTasks` |
|
||||
`composables/useAlertMarquee.ts` manages SSE connection lifecycle. It connects to the backend SSE endpoint and dispatches `alert`, `alert-withdrawn`, and `alert-deleted` events to the `alert-marquee` component for real-time notification display.
|
||||
|
||||
### Level-based routing
|
||||
## Environment Variables
|
||||
|
||||
- **danger (紧急)**: triggers the floating scrollbar + appears in notification center drawer + bell badge
|
||||
- **warning / info (警告 / 提示)**: notification center drawer + bell badge only (no scrollbar)
|
||||
- The bell badge in base-header always shows total count (all levels + active import tasks)
|
||||
- `VITE_APP_API`: Backend API base URL (injected into `constants/app.ts` at build time)
|
||||
- `.env.development` / `.env.production`: Environment-specific configs
|
||||
|
||||
### Floating scrollbar behavior
|
||||
## Key Dependencies
|
||||
|
||||
- Visible only when logged in (`appStore.state.appIsLogin`) and has danger-level alerts (`hasDangerMessages`)
|
||||
- Positioned centered at top (`y: 56` below header), draggable to reposition, re-centers on window resize
|
||||
- Shows latest alert headline + count badge
|
||||
- **Close button** hides the bar; **auto-reappears** when new danger alerts arrive (watch on `dangerCount`)
|
||||
- **查看详情** button opens `el-dialog` with full alert text
|
||||
- The old `import-task-indicator.vue` (bell icon with popover in header) has been **removed**
|
||||
|
||||
### Import progress integration
|
||||
|
||||
1. Upload via `baseService.upload()` (FormData, no explicit Content-Type).
|
||||
2. On upload start, toast: `"导入已开始,可在通知中心查看进度"`.
|
||||
3. Poll `GET .../import/progress/{backendTaskId}` every 3 seconds.
|
||||
4. Update Pinia store (`useImportTaskStore`) — progress bar + status tag render reactively in the drawer.
|
||||
5. On completion: green checkmark; on failure: red cross + error message. Completed/failed tasks show a dismiss button.
|
||||
|
||||
### Alert management page
|
||||
|
||||
Manual alert CRUD at route `sys/system-alert`:
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `src/views/sys/system-alert.vue` | List page: `useView`-based, search by level/title/sourceType, batch delete, per-row withdraw |
|
||||
| `src/views/sys/system-alert-add-or-update.vue` | Add/edit dialog: level (info/warning/danger), title, content (textarea), sourceType, expireTime (datetime picker) |
|
||||
|
||||
Backend endpoints under `/sys/alert`:
|
||||
- `GET /page` — paginated list (`sys:alert:page`)
|
||||
- `GET /{id}` — detail (`sys:alert:info`)
|
||||
- `POST /` — create (`sys:alert:save`)
|
||||
- `PUT /` — update (`sys:alert:update`)
|
||||
- `DELETE /` — batch delete (`sys:alert:delete`)
|
||||
- `PUT /{id}/withdraw` — soft-withdraw (`sys:alert:update`)
|
||||
- `GET /active`, `GET /active/since` — frontend polling (no permission required)
|
||||
- `@vueuse/core`: Vue composition utilities
|
||||
- `mitt`: Lightweight event emitter
|
||||
- `nprogress`: Page load progress bar
|
||||
- `html2canvas` + `jspdf`: Client-side PDF export
|
||||
- `js-cookie`: Cookie management (token storage)
|
||||
- `qs`: Query string parsing/serialization
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# weather-data-ui
|
||||
|
||||
气象数据分析平台前端项目。
|
||||
|
||||
> 项目说明、快速启动、部署文档 → [../README.md](../README.md)
|
||||
> 开发者文档(架构、规范) → [../CLAUDE.md](../CLAUDE.md)
|
||||
Generated
+141
-125
File diff suppressed because it is too large
Load Diff
@@ -127,7 +127,7 @@
|
||||
margin: 8px 8px 0 0;
|
||||
border-radius: 2px;
|
||||
display: inline-block;
|
||||
box-shadow: 0 1px 3px rgba(0 0 0, 0.1);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
vertical-align: top;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -17,7 +17,7 @@ export default defineComponent({
|
||||
const store = useAppStore();
|
||||
return {
|
||||
value: computed(() => `${props.modelValue}`),
|
||||
dataList: getDictDataList(store.state.dicts, props.dictType)
|
||||
dataList: computed(() => getDictDataList(store.state.dicts, props.dictType))
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ export default defineComponent({
|
||||
const store = useAppStore();
|
||||
return {
|
||||
value: computed(() => `${props.modelValue}`),
|
||||
dataList: getDictDataList(store.state.dicts, props.dictType)
|
||||
dataList: computed(() => getDictDataList(store.state.dicts, props.dictType))
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// 消息队列 / 通知中心抽屉 / 详情弹窗 / 悬浮滚动条位置
|
||||
// 获取:SSE 主通道 + 10s 轮询降级 + 每60s全量对账
|
||||
|
||||
import { ref, computed } from "vue";
|
||||
import { ref, computed, onBeforeUnmount } from "vue";
|
||||
import baseService from "@/service/baseService";
|
||||
import app from "@/constants/app";
|
||||
import { getToken } from "@/utils/cache";
|
||||
@@ -329,6 +329,10 @@ function setPosition(pos: { x: number; y: number }): void {
|
||||
// ---------- composable 导出 ----------
|
||||
|
||||
export function useAlertMarquee() {
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling();
|
||||
});
|
||||
|
||||
return {
|
||||
// 消息
|
||||
messages,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// composables/useFloatingDrag.ts
|
||||
// 浮窗拖动逻辑 — Pointer Events 统一处理鼠标和触屏
|
||||
|
||||
import { ref, type Ref } from "vue";
|
||||
import { ref, onBeforeUnmount, type Ref } from "vue";
|
||||
|
||||
export interface DragPosition {
|
||||
x: number;
|
||||
@@ -16,6 +16,24 @@ export function useFloatingDrag(
|
||||
const dragMoved = ref(false);
|
||||
let dragResetTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
/** 清理拖拽期间注册的事件监听器(组件卸载时使用) */
|
||||
function cleanupDragListeners(): void {
|
||||
const el = elementRef.value;
|
||||
if (el) {
|
||||
el.removeEventListener("pointermove", onPointerMove);
|
||||
el.removeEventListener("pointerup", onPointerUp);
|
||||
el.removeEventListener("pointercancel", onPointerUp);
|
||||
}
|
||||
if (dragResetTimer) {
|
||||
clearTimeout(dragResetTimer);
|
||||
dragResetTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanupDragListeners();
|
||||
});
|
||||
|
||||
/** pointerdown 时记录的元素尺寸和指针在元素内的偏移 */
|
||||
let dragMeta: {
|
||||
width: number;
|
||||
|
||||
@@ -9,7 +9,7 @@ export const RAIN_LEVELS = [
|
||||
{ label: "大雨", min: 25, max: 50 },
|
||||
{ label: "暴雨", min: 50, max: 100 },
|
||||
{ label: "大暴雨", min: 100, max: 250 },
|
||||
{ label: "特大暴雨", min: 250, max: 9999 }
|
||||
{ label: "特大暴雨", min: 250, max: Infinity }
|
||||
] as const;
|
||||
|
||||
/** 可筛选的降雨等级(排除"无雨",因为无雨通常单独判断) */
|
||||
|
||||
@@ -58,7 +58,7 @@ export default defineComponent({
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<el-container :class="`rr ${containerClassNames}`" v-loading="state.loading" element-loading-background="#0000" element-loading-lock="true" element-loading-custom-class="rr-loading">
|
||||
<el-container :class="`rr ${containerClassNames}`" v-loading="state.loading" element-loading-background="transparent" element-loading-lock="true" element-loading-custom-class="rr-loading">
|
||||
<el-header class="rr-header" height="50px">
|
||||
<base-header></base-header>
|
||||
</el-header>
|
||||
|
||||
@@ -7,7 +7,7 @@ import emits from "@/utils/emits";
|
||||
import { toValidRoutes } from "@/utils/router";
|
||||
import { getThemeConfigCacheByKey } from "@/utils/theme";
|
||||
import { useWindowSize } from "@vueuse/core";
|
||||
import { defineComponent, onMounted, reactive, ref, watch } from "vue";
|
||||
import { defineComponent, onMounted, onBeforeUnmount, reactive, ref, watch } from "vue";
|
||||
import { RouteRecordRaw, useRoute, useRouter } from "vue-router";
|
||||
import { useAppStore } from "@/store";
|
||||
import SidebarMenusItems from "./sidebar-menus-items.vue";
|
||||
@@ -84,16 +84,25 @@ export default defineComponent({
|
||||
state.menus = ms;
|
||||
}
|
||||
);
|
||||
emits.on(EMitt.OnSwitchLeftSidebar, () => {
|
||||
const onSwitchLeftSidebar = () => {
|
||||
state.collapseSidebar = !state.collapseSidebar;
|
||||
});
|
||||
emits.on(EMitt.OnSetThemeNotUniqueOpened, (vl) => {
|
||||
};
|
||||
const onSetThemeNotUniqueOpened = (vl: boolean) => {
|
||||
state.uniqueOpened = vl;
|
||||
});
|
||||
emits.on(EMitt.OnSetTheme, ([vl]) => {
|
||||
};
|
||||
const onSetTheme = ([vl]: [string, string]) => {
|
||||
if (vl === EThemeSetting.Sidebar) {
|
||||
state.popClassName = getPopClassName();
|
||||
}
|
||||
};
|
||||
emits.on(EMitt.OnSwitchLeftSidebar, onSwitchLeftSidebar);
|
||||
emits.on(EMitt.OnSetThemeNotUniqueOpened, onSetThemeNotUniqueOpened);
|
||||
emits.on(EMitt.OnSetTheme, onSetTheme);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
emits.off(EMitt.OnSwitchLeftSidebar, onSwitchLeftSidebar);
|
||||
emits.off(EMitt.OnSetThemeNotUniqueOpened, onSetThemeNotUniqueOpened);
|
||||
emits.off(EMitt.OnSetTheme, onSetTheme);
|
||||
});
|
||||
watch(
|
||||
() => route.path,
|
||||
|
||||
@@ -15,7 +15,7 @@ export default defineComponent({
|
||||
setup(props) {
|
||||
const getStyle = (index: number): string => {
|
||||
const styles: Array<any> = [];
|
||||
const isHidden = props.hiddenIndex ? props.hiddenIndex > -1 && index > props.hiddenIndex : false;
|
||||
const isHidden = props.hiddenIndex != null ? props.hiddenIndex > -1 && index > props.hiddenIndex : false;
|
||||
styles.push("display:" + (isHidden ? "none" : "block"));
|
||||
return styles.join(";");
|
||||
};
|
||||
|
||||
@@ -27,6 +27,8 @@ const router = createRouter({
|
||||
routes: baseRoutes
|
||||
});
|
||||
|
||||
let initAppPromise: Promise<any> | null = null;
|
||||
|
||||
// 路由加载前
|
||||
router.beforeEach((to, from, next) => {
|
||||
//外链
|
||||
@@ -65,23 +67,28 @@ router.beforeEach((to, from, next) => {
|
||||
}
|
||||
} else {
|
||||
if (token) {
|
||||
store.initApp().then((res: Array<RouteRecordRaw>) => {
|
||||
const mergeRoute = baseRoutes.concat(res);
|
||||
router.options.routes = mergeRoute;
|
||||
registerToRouter(router, mergeRoute);
|
||||
if (!to.matched.length) {
|
||||
registerDynamicToRouterAndNext({ path: to.path, query: to.query });
|
||||
}
|
||||
store.updateState({
|
||||
appIsReady: true,
|
||||
routes: mergeRoute,
|
||||
routeToMeta: { ...store.state.routeToMeta, ...getBaseRouteToMeta(baseRoutes) }
|
||||
if (!initAppPromise) {
|
||||
initAppPromise = store.initApp().then((res: Array<RouteRecordRaw>) => {
|
||||
const mergeRoute = baseRoutes.concat(res);
|
||||
router.options.routes = mergeRoute;
|
||||
registerToRouter(router, mergeRoute);
|
||||
if (!to.matched.length) {
|
||||
registerDynamicToRouterAndNext({ path: to.path, query: to.query });
|
||||
}
|
||||
store.updateState({
|
||||
appIsReady: true,
|
||||
routes: mergeRoute,
|
||||
routeToMeta: { ...store.state.routeToMeta, ...getBaseRouteToMeta(baseRoutes) }
|
||||
});
|
||||
setTimeout(() => {
|
||||
store.updateState({ appIsRender: true, appIsLogin: true });
|
||||
}, 600);
|
||||
next({ ...to, replace: true });
|
||||
}).finally(() => {
|
||||
initAppPromise = null;
|
||||
});
|
||||
setTimeout(() => {
|
||||
store.updateState({ appIsRender: true, appIsLogin: true });
|
||||
}, 600);
|
||||
next({ ...to, replace: true });
|
||||
});
|
||||
}
|
||||
initAppPromise.then(() => {});
|
||||
} else {
|
||||
if (isPop) {
|
||||
if (!to.matched.length) {
|
||||
@@ -126,7 +133,7 @@ export const getSysRouteMap = (): IObject => {
|
||||
* @returns
|
||||
*/
|
||||
export const toSysViewComponentPath = (path: string): string => {
|
||||
path = path.replace("_", "-");
|
||||
path = path.replace(/_/g, "-");
|
||||
return `/src/views${path}.vue`;
|
||||
};
|
||||
/**
|
||||
|
||||
@@ -1,36 +1,2 @@
|
||||
// utils/chartBuilder.ts
|
||||
import type { WeatherRecord } from "../service/weatherDataService";
|
||||
import type { WeatherExtremes } from "../service/weatherStatsService";
|
||||
|
||||
export function buildWeatherOption(rows: WeatherRecord[], stats: WeatherExtremes) {
|
||||
return {
|
||||
tooltip: { trigger: "axis" },
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: rows.map((r) => r.year)
|
||||
},
|
||||
yAxis: {
|
||||
type: "value"
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "最高温",
|
||||
type: "bar",
|
||||
data: rows.map((r) => {
|
||||
const isMax = r.year === stats.maxTmaxYear;
|
||||
return {
|
||||
value: r.tmax,
|
||||
itemStyle: {
|
||||
color: isMax ? "#ff4d4f" : "#5b8ff9"
|
||||
},
|
||||
label: {
|
||||
show: isMax,
|
||||
position: "top",
|
||||
fontWeight: 600
|
||||
}
|
||||
};
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
// utils/chartBuilder.ts — 已废弃,请使用 composables/useWeatherChart.ts
|
||||
export {}
|
||||
|
||||
@@ -87,9 +87,9 @@ export const getThemeCluster = (theme: string): string[] => {
|
||||
green += Math.round(tint * (255 - green));
|
||||
blue += Math.round(tint * (255 - blue));
|
||||
|
||||
red = red.toString(16);
|
||||
green = green.toString(16);
|
||||
blue = blue.toString(16);
|
||||
red = red.toString(16).padStart(2, "0");
|
||||
green = green.toString(16).padStart(2, "0");
|
||||
blue = blue.toString(16).padStart(2, "0");
|
||||
|
||||
return `#${red}${green}${blue}`;
|
||||
}
|
||||
@@ -104,9 +104,9 @@ export const getThemeCluster = (theme: string): string[] => {
|
||||
green = Math.round((1 - shade) * green);
|
||||
blue = Math.round((1 - shade) * blue);
|
||||
|
||||
red = red.toString(16);
|
||||
green = green.toString(16);
|
||||
blue = blue.toString(16);
|
||||
red = red.toString(16).padStart(2, "0");
|
||||
green = green.toString(16).padStart(2, "0");
|
||||
blue = blue.toString(16).padStart(2, "0");
|
||||
|
||||
return `#${red}${green}${blue}`;
|
||||
};
|
||||
|
||||
@@ -237,19 +237,21 @@ export const treeDataTranslate = (data: IObject[], id?: string, pid?: string): I
|
||||
const temp: IObject = {};
|
||||
id = id || "id";
|
||||
pid = pid || "pid";
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
temp[data[i][id]] = data[i];
|
||||
// 浅拷贝避免修改输入数组元素
|
||||
const cloned = data.map((item) => ({ ...item }));
|
||||
for (let i = 0; i < cloned.length; i++) {
|
||||
temp[cloned[i][id]] = cloned[i];
|
||||
}
|
||||
for (let k = 0; k < data.length; k++) {
|
||||
if (!temp[data[k][pid]] || data[k][id] === data[k][pid]) {
|
||||
res.push(data[k]);
|
||||
for (let k = 0; k < cloned.length; k++) {
|
||||
if (!temp[cloned[k][pid]] || cloned[k][id] === cloned[k][pid]) {
|
||||
res.push(cloned[k]);
|
||||
continue;
|
||||
}
|
||||
if (!temp[data[k][pid]]["children"]) {
|
||||
temp[data[k][pid]]["children"] = [];
|
||||
if (!temp[cloned[k][pid]]["children"]) {
|
||||
temp[cloned[k][pid]]["children"] = [];
|
||||
}
|
||||
temp[data[k][pid]]["children"].push(data[k]);
|
||||
data[k]["_level"] = (temp[data[k][pid]]._level || 0) + 1;
|
||||
temp[cloned[k][pid]]["children"].push(cloned[k]);
|
||||
cloned[k]["_level"] = (temp[cloned[k][pid]]._level || 0) + 1;
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
<el-divider content-position="left">温湿度与气压</el-divider>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="相对湿度" prop="avgTemp" label-width="80px">
|
||||
<el-form-item label="相对湿度" prop="relativeHumidity" label-width="80px">
|
||||
<el-input-number v-model="dataForm.relativeHumidity" :precision="1" :step="0.1" controls-position="right" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="气压" prop="avgTemp" label-width="80px">
|
||||
<el-form-item label="气压" prop="atmospheres" label-width="80px">
|
||||
<el-input-number v-model="dataForm.atmospheres" :precision="1" :step="0.1" controls-position="right" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -80,13 +80,13 @@
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="日平均风速" prop="maxWindSpeed">
|
||||
<el-form-item label="日平均风速" prop="dayAvgWindSpeed">
|
||||
<el-input-number v-model="dataForm.dayAvgWindSpeed" :precision="1" :min="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="日平均风向" prop="maxWindSpeed">
|
||||
<el-form-item label="日平均风向" prop="dayAvgWindDirection">
|
||||
<el-input-number v-model="dataForm.dayAvgWindDirection" :precision="1" :min="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -103,23 +103,23 @@
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="极大风速" prop="maxWindSpeed">
|
||||
<el-form-item label="极大风速" prop="extremeWindSpeed">
|
||||
<el-input-number v-model="dataForm.extremeWindSpeed" :precision="1" :min="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="极大风速风向" prop="maxWindSpeed">
|
||||
<el-form-item label="极大风速风向" prop="extremeWindDirection">
|
||||
<el-input-number v-model="dataForm.extremeWindDirection" :precision="1" :min="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="极大风速出现时间" prop="minTempTime" label-width="125px">
|
||||
<el-form-item label="极大风速出现时间" prop="extremeWindTime" label-width="125px">
|
||||
<el-input v-model="dataForm.extremeWindTime" placeholder="HHmm"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最大风速出现时间" prop="minTempTime" label-width="125px">
|
||||
<el-form-item label="最大风速出现时间" prop="maxWindTime" label-width="125px">
|
||||
<el-input v-model="dataForm.maxWindTime" placeholder="HHmm"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
@@ -18,12 +18,20 @@ export default defineComponent({
|
||||
title: "404",
|
||||
message: "访问页面不存在"
|
||||
},
|
||||
403: {
|
||||
title: "403",
|
||||
message: "无权限访问"
|
||||
},
|
||||
500: {
|
||||
title: "500",
|
||||
message: "服务器错误"
|
||||
},
|
||||
error: {
|
||||
title: "错误",
|
||||
message: "访问出错了"
|
||||
}
|
||||
};
|
||||
const tip: ITip = tips[to?.toString() ?? "error"];
|
||||
const tip: ITip = tips[to?.toString()] || tips["error"];
|
||||
const onBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
@@ -273,7 +273,8 @@
|
||||
</section>
|
||||
|
||||
<!-- ==================== 主体 ==================== -->
|
||||
<div class="main-grid">
|
||||
<el-empty v-if="!loading && pageState.weatherData.length === 0" description="暂无历史气象数据" :image-size="120" style="margin-top: 60px" />
|
||||
<div class="main-grid" v-else>
|
||||
<div class="col-main">
|
||||
<el-card class="mod-card" shadow="never">
|
||||
<template #header>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<aside class="file-list-panel">
|
||||
<div v-for="(item, index) in state.fileItems" :key="item.anchorId" class="file-list-item" :class="{ 'is-active': state.selectedFileIndex === index }" @click="selectFile(index)">
|
||||
<div class="file-list-item__thumb">
|
||||
<img v-if="blobUrlCache[item.fileId]" :src="blobUrlCache[item.fileId]" class="file-list-item__img" alt="{{item.displayName}}}" />
|
||||
<img v-if="blobUrlCache[item.fileId]" :src="blobUrlCache[item.fileId]" class="file-list-item__img" :alt="item.displayName" />
|
||||
<span v-else-if="isImageFile(item.type)" class="file-list-item__icon file-list-item__icon--img">GIF</span>
|
||||
<span v-else class="file-list-item__icon file-list-item__icon--other">—</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user