diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0ca3d29 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,144 @@ +# AGENTS.md + +This document provides guidance for agentic coding tools working on the UrbanOps (urbanops) codebase. + +## Build & Test Commands + +### Build Commands +```bash +# Clean and compile all modules +mvn clean compile + +# Build entire project (skipping tests) +mvn clean install -DskipTests + +# Build specific module with dependencies +mvn clean install -DskipTests -pl -am +``` + +### Test Commands +```bash +# Run all tests +mvn test + +# Run all tests for a specific module +mvn test -pl urbanops-module-system + +# Run a single test class +mvn test -Dtest=AdminUserServiceImplTest + +# Run a single test method +mvn test -Dtest=AdminUserServiceImplTest#testCreateUser + +# Run tests with specific profile +mvn test -Punit-test +``` + +## Project Structure + +- `urbanops-server` - Main Spring Boot application entry point +- `urbanops-module-system` - System management (users, roles, permissions, etc.) +- `urbanops-module-infra` - Infrastructure (files, jobs, configs, code generation) +- `urbanops-module-bpm` - Business Process Management (Flowable) +- `urbanops-module-xxx` - Business domain modules (garden, workorder, report, etc.) +- `urbanops-framework` - Shared framework components (security, redis, mybatis, etc.) +- `urbanops-dependencies` - Maven dependency version management + +## Code Style Guidelines + +### Package Structure +``` +com.zteits.urbanops.module.{module-name} +├── controller/admin - Admin API controllers +├── controller/app - App API controllers +├── controller/bridge - Legacy bridge controllers +├── service/.../impl - Service implementations +├── dal/dataobject - Database entities (DO classes) +├── dal/mysql - MyBatis mappers +├── controller/.../vo - View Objects (Request/Response VOs) +├── convert - MapStruct converters +└── enums - Module-specific enums +``` + +### Naming Conventions +- **Controllers**: `XxxController` (e.g., `UserController`) +- **Services**: `XxxService` (interface) and `XxxServiceImpl` (implementation) +- **Mappers**: `XxxMapper` (e.g., `AdminUserMapper`) +- **Data Objects**: `XxxDO` (e.g., `AdminUserDO`) +- **View Objects**: `XxxPageReqVO`, `XxxSaveReqVO`, `XxxRespVO`, `XxxSimpleRespVO` +- **Converters**: `XxxConvert` (MapStruct interface) + +### Code Organization +- DO classes extend `TenantBaseDO` or `BaseDO`, use `@TableName`, `@KeySequence`, `@Data`, `@EqualsAndHashCode(callSuper = true)`, `@Builder` +- VO classes use `@Schema` for documentation, `@NotBlank`/`@NotNull`/`@Size` for validation +- Controllers use `@Tag`, `@Operation`, `@PreAuthorize`, `@Valid`, `CommonResult` for responses +- Services use `@Service`, `@Slf4j`, `@Transactional(rollbackFor = Exception.class)`, `@Resource` + +### Error Handling +- Use `exception(ErrorCode)` from `ServiceExceptionUtil` to throw business exceptions +- Define error codes in module's `ErrorCodeConstants` interface +- Error code format: `1-002-xxx-xxx-xxx` (system module: 1-002-xxx-xxx-xxx) + +### Imports & Dependencies +- Organize: standard library → third-party → project packages +- No wildcard imports (e.g., avoid `import java.util.*`) +- Use Jakarta EE: `jakarta.*` imports +- Use Spring Boot 3.x and Spring Framework 6.x APIs + +### Validation +- Use Jakarta Bean Validation: `@NotNull`, `@NotBlank`, `@Size`, `@Pattern`, `@Email` +- Use `@Valid` for nested object validation +- Use `@AssertTrue` for complex validation with custom methods + +### Lombok Usage +- `@Data` for POJOs, `@Builder` for construction, `@EqualsAndHashCode(callSuper = true)` for DOs +- Configured in `lombok.config`: `lombok.accessors.chain=true`, `lombok.tostring.callsuper=CALL` + +### Testing Guidelines +- Extend `BaseMockitoUnitTest` (no DB) or `BaseDbUnitTest` (H2 database) +- Use JUnit 5: `@Test`, `@BeforeEach`, `@BeforeAll` +- Naming: `test{MethodName}_{scenario}` +- Use `assertPojoEquals()` for comparing DOs, `assertServiceException()` for exceptions +- Clean up test data via `@Sql` with `/sql/clean.sql` + +### Database +- Use MyBatis Plus, mappers extend `BaseMapper` +- Use `TenantBaseDO` for multi-tenant tables, `BaseDO` for single-tenant + +### Security +- Use `@PreAuthorize` with format `{module}:{resource}:{action}` (e.g., `system:user:create`) +- Inject current user: `SecurityFrameworkUtils.getLoginUserId()` + +### Date/Time +- Use `java.time`, `LocalDateTime` for timestamps +- Format: `DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND`, `@DateTimeFormat` + +### Logging +- Use `@Slf4j`, levels: `ERROR`, `WARN`, `INFO`, `DEBUG` +- Log meaningful context, avoid sensitive info (passwords, tokens, PII) + +### API Documentation +- Use OpenAPI 3.0: `@Tag`, `@Operation`, `@Parameter`, `@Schema` +- Chinese descriptions in `@Schema`, mark required with `requiredMode`, provide `example` values + +### Module Communication +- Use module APIs (`urbanops-module-api`) for cross-module communication +- Use `@Lazy` to avoid circular dependencies + +### Adding New Features +1. Create DO class in `dal/dataobject` +2. Create Mapper interface in `dal/mysql` +3. Create Service interface and implementation +4. Create VO classes in `controller/.../vo` +5. Create Controller class +6. Create Convert interface (MapStruct) +7. Write unit tests extending appropriate base class +8. Add error codes to `ErrorCodeConstants` + +### Common Pitfalls +- Forgetting `@Transactional` on modifying service methods +- Using wrong DO base class (`BaseDO` vs `TenantBaseDO`) +- Not using `@Valid` for request body validation +- Not handling exceptions appropriately +- Direct database access bypassing Service layer +- Using `System.out.println` instead of logger diff --git a/api_document.md b/api_document.md new file mode 100644 index 0000000..38f283a --- /dev/null +++ b/api_document.md @@ -0,0 +1,305 @@ +# API 接口文档 - 行道树巡检记录与安全风险评估 + +本文档用于指导微信小程序前端开发人员对接行道树巡检记录与安全风险评估接口。 +为完美契合小程序详情页中按部位折叠面板的设计,接口设计采用了**分层嵌套结构**。 + +--- + +## 接口基础信息 + +* **接口协议**:HTTP / HTTPS +* **请求格式**:`application/json;charset=utf-8` +* **响应格式**:`application/json;charset=utf-8` +* **接口基地址**:`/app-api/garden/tree-inspection` 或 `/app-api/business/tree-inspection` +* **认证方式**:请求头中需携带 `Authorization: Bearer ` + +--- + +## 1. 提交巡检记录并进行风险评估 + +小程序在巡检人员填写完成表单,点击提交按钮(图3)时调用该接口。 + +* **接口路径**:`POST /create` +* **请求方法**:`POST` + +### 请求 JSON 参数说明 (`TreeInspectionSaveReqVO`) + +| 一级属性名 | 二级字段名 | 类型 | 是否必填 | 枚举值 / 说明 | 字段描述 | +| :--- | :--- | :--- | :--- | :--- | :--- | +| **treeId** | - | Long | 是 | 关联的树木档案 ID,如 `1024` | 树木 ID | +| **inspectionTime** | - | String | 是 | 格式 `"yyyy-MM-dd HH:mm:ss"` | 巡检发生的时间 | +| **root** | - | Object | 是 | 对应“树根部位”折叠面板 | 树根评估指标组 | +| | **disease** | Integer | 是 | `0`(无真菌危害/腐朽), `8`(存在危害/腐朽) | 是否存在病害? | +| | **anchorage** | Integer | 是 | `0`(良好无盘根隆起), `7`(存在隆起或盘根) | 根系下扎情况 | +| | **cutting** | Integer | 是 | `0`(无工程切根), `5`(存在工程切根) | 是否存在工程切根 | +| **collar** | - | Object | 是 | 对应“根颈部位”折叠面板 | 根颈评估指标组 | +| | **woodDamage** | Integer | 是 | `0`(无), `5`(<10%), `15`(10%-30%), `25`(30%-50%), `70`(>=50% 一票否决) | 根颈木质部受损 | +| | **barkDamage** | Integer | 是 | `0`(<10%), `2`(10%-30%), `4`(30%-50%), `6`(>=50%) | 根颈树皮受损 | +| | **loosening** | Integer | 是 | `0`(不存在松动), `100`(存在松动 一票否决) | 根颈是否松动 | +| **trunk** | - | Object | 是 | 对应“主干部位”折叠面板 | 主干评估指标组 | +| | **woodDamage** | Integer | 是 | `0`(无), `5`(<10%), `12`(10%-30%), `20`(30%-50%), `70`(>=50% 一票否决) | 主干木质部受损 | +| | **tilt** | Integer | 是 | `0`(<10°), `3`(10°-20°), `8`(20°-30°), `70`(>=30° 一票否决) | 主干倾斜度 | +| | **barkDamage** | Integer | 是 | `0`(<10%), `1`(10%-30%), `3`(30%-50%), `5`(>=50%) | 主干树皮受损 | +| **crown** | - | Object | 是 | 对应“树冠部位”折叠面板 | 树冠评估指标组 | +| | **looseBranch** | Integer | 是 | `0`(无易落枝), `2`(占比<1/10), `3`(占比>=1/10) | 观察是否存在易落枝 | +| | **collarAbnormal** | Integer | 是 | `0`(无异常), `3`(龟裂/卷皮), `5`(腐烂尚未成洞), `70`(空洞/蛀干 一票否决) | 枝干结合部异常 | +| | **ventilationBalance**| Integer | 是 | `0`(好不偏冠), `1`(偏冠或透风差不偏冠), `2`(偏冠冠幅适中), `5`(透风差冠幅大不偏冠), `8`(透风差大且偏冠) | 树冠透风与平衡性 | +| **weight** | - | Object | 是 | 对应“权重因子评估”面板 | 生理与生境权重因子组 | +| | **treeSpeciesType** | String | 是 | `"深根性树种"`, `"浅根性树种"` | 树种类型 (深根性1.0 / 浅根性1.1) | +| | **plantingYears** | String | 是 | `"栽植 10 年以内"`, `"栽植 10-30 年"`, `"栽植 30 年以上"` | 栽植年限 (对应 1.0, 1.1, 1.2) | +| | **isWindCorridor** | Boolean | 是 | `true`(是), `false`(否) | 是否处于风口 (对应权重 2.0 / 1.0) | +| | **treePoolType** | String | 是 | `"联通树池"`, `"独立树池"`, `"树池硬化"` | 树池类型 (对应 1.0, 1.2, 1.5) | +| | **treePoolWidthDbhRatio**| String| 是 | `"7 倍及以上"`, `"5 倍-7 倍"`, `"3 倍-5 倍"`, `"3 倍以下"` | 树池宽胸径比 (对应 1.0-1.3) | +| **result** | - | Object | 是 | 对应“风险评估结果”面板 | 风险评估结果表单项 | +| | **isEmergency** | Boolean | 是 | `true`(是), `false`(否) | 是否展开应急评估 | +| | **windPower** | String | 否 | `"7 级及以下"`, `"8-9 级"`, `"10 级"`, `"10 级以上"` | 极端风力等级(`isEmergency`为`true`时必填) | +| | **defectScore** | Integer | 否 | 输入缺陷总分,如 `9` (若前端已计算则优先以前端为准) | 缺陷总得分 | +| | **normalResult** | Object | 否 | 常规风险评估计算结果对象 | 常规安全评估结果组 | +| | **normalResult.score**| Double | 否 | 常规安全评估得分,如 `10.89` | 常规安全得分 | +| | **normalResult.level**| String | 否 | 常规安全风险等级,如 `"II级 (轻度风险)"` | 常规风险等级 | +| | **emergencyResult**| Object | 否 | 应急风险评估计算结果对象 | 应急安全评估结果组 | +| | **emergencyResult.score**| Double| 否 | 应急安全评估得分,如 `16.34` | 应急安全得分 | +| | **emergencyResult.level**| String| 否 | 应急安全风险等级,如 `"II级 (轻度风险)"` | 应急风险等级 | +| **status** | - | Object | 是 | 对应“现状及处理措施”面板 | 现状及处理措施数据组 | +| | **photos** | List | 否 | 数组,最多 5 个图片 URL,超过 5 个会报错拦截 | 现场采集照片 (限制最多5张) | + +### 请求示例 JSON + +```json +{ + "treeId": 1024, + "inspectionTime": "2026-05-26 13:17:00", + "root": { + "disease": 0, + "anchorage": 0, + "cutting": 0 + }, + "collar": { + "woodDamage": 5, + "barkDamage": 0, + "loosening": 0 + }, + "trunk": { + "woodDamage": 0, + "tilt": 3, + "barkDamage": 0 + }, + "crown": { + "looseBranch": 0, + "collarAbnormal": 0, + "ventilationBalance": 1 + }, + "weight": { + "treeSpeciesType": "深根性树种", + "plantingYears": "栽植 10-30 年", + "isWindCorridor": false, + "treePoolType": "联通树池", + "treePoolWidthDbhRatio": "5 倍(含)-7 倍(不含)" + }, + "result": { + "isEmergency": true, + "windPower": "8-9 级", + "defectScore": 9, + "normalResult": { + "score": 10.89, + "level": "II级 (轻度风险)" + }, + "emergencyResult": { + "score": 16.34, + "level": "II级 (轻度风险)" + } + }, + "status": { + "photos": [ + "https://example.com/images/tree_whole.jpg", + "https://example.com/images/tree_detail1.jpg" + ] + } +} +``` + +### 响应示例 JSON + +```json +{ + "code": 0, + "data": 12, + "msg": "" +} +``` +*(注:返回的 `data` 值为新建的巡检评估记录 ID。)* + +--- + +## 2. 获得层级嵌套的巡检记录详情 + +进入巡检历史记录详情页(图2)时,获取该记录所有计算指标及打分结果。该接口返回完全**层次分明、与表单高度对称的结构**,并额外输出每项计算的权重常数、各分类得分与最终安全等级描述。 + +* **接口路径**:`GET /get` +* **请求方法**:`GET` +* **请求参数**: + +| 参数名 | 类型 | 是否必填 | 说明 | +| :--- | :--- | :--- | :--- | +| **id** | Long | 是 | 巡检记录 ID,例如 `12` | + +### 响应示例 JSON + +```json +{ + "code": 0, + "data": { + "id": 12, + "treeId": 1024, + "treenumber": "D0001-P1-0001", + "inspectionTime": "2026-05-26 13:17:00", + "inspectorId": 10001, + "inspectorName": "张三", + "root": { + "disease": 0, + "anchorage": 0, + "cutting": 0 + }, + "collar": { + "woodDamage": 5, + "barkDamage": 0, + "loosening": 0 + }, + "trunk": { + "woodDamage": 0, + "tilt": 3, + "barkDamage": 0 + }, + "crown": { + "looseBranch": 0, + "collarAbnormal": 0, + "ventilationBalance": 1 + }, + "weight": { + "treeSpeciesType": "深根性树种", + "treeSpeciesWeight": 1.0, + "plantingYears": "栽植 10-30 年", + "plantingYearsWeight": 1.1, + "isWindCorridor": false, + "windCorridorWeight": 1.0, + "treePoolType": "联通树池", + "treePoolWeight": 1.0, + "treePoolWidthDbhRatio": "5 倍(含)-7 倍(不含)", + "treePoolRatioWeight": 1.1 + }, + "result": { + "isEmergency": true, + "windPower": "8-9 级", + "windPowerWeight": 1.5, + "defectScore": 9, + "normalResult": { + "score": 10.89, + "level": "II级 (轻度风险)" + }, + "emergencyResult": { + "score": 16.34, + "level": "II级 (轻度风险)" + } + }, + "status": { + "photos": [ + "https://example.com/images/tree_whole.jpg", + "https://example.com/images/tree_detail1.jpg" + ] + } + }, + "msg": "" +} +``` + +--- + +## 3. 分页查询单株树木的历史巡检记录 + +对应“巡检记录列表”原型图(图1),按历史巡检时间倒序分页加载。 + +* **接口路径**:`GET /page` +* **请求方法**:`GET` +* **请求参数**: + +| 参数名 | 类型 | 是否必填 | 示例值 | 说明 | +| :--- | :--- | :--- | :--- | :--- | +| **treeId** | Long | 是 | `1024` | 对应树木档案 ID | +| **pageNo** | Integer | 否 | `1` | 页码,从 1 开始 | +| **pageSize** | Integer | 否 | `10` | 每页行数 | + +### 响应示例 JSON + +```json +{ + "code": 0, + "data": { + "list": [ + { + "id": 12, + "treeId": 1024, + "treenumber": "D0001-P1-0001", + "inspectionTime": "2026-05-26 13:17:00", + "inspectorName": "张三", + "normalLevel": "II级 (轻度风险)", + "emergencyLevel": "II级 (轻度风险)", + "isEmergency": true, + "isTreated": false + }, + { + "id": 2, + "treeId": 1024, + "treenumber": "D0001-P1-0001", + "inspectionTime": "2026-05-06 13:23:23", + "inspectorName": "李四", + "normalLevel": "I级 (基本无风险)", + "emergencyLevel": null, + "isEmergency": false, + "isTreated": true + } + ], + "total": 2 + }, + "msg": "" +} +``` + +--- + +## 3. 管理后台 - 行道树巡检与风险评估接口 + +管理后台的“巡检记录”页签(如图所示)包含“巡检历史列表(左侧)”和“单条巡检详情面板(右侧)”。这两个功能可以直接调用本组管理后台 API: + +* **接口基地址**:`/admin-api/garden/tree-inspection` +* **权限标识**:`garden:tree-inspection:query`(需在管理后台的角色权限中进行配置) + +### 3.1 获得单条巡检评估详情 + +对应管理后台点击左侧列表时,右侧展示的全部缺陷分、常规评估、应急评估以及现场照片(右侧详情面板)。 + +* **接口路径**:`GET /get` +* **请求方法**:`GET` +* **请求参数**:同小程序端,根据主键 `id` 检索。 +* **返回 JSON 结构**:与小程序端 `GET /get` 的响应示例格式**完全一致**。返回分层嵌套的 7 大模块,高度契合后台页面树根、根颈、主干、树冠、权重评估、风险评估结果、现状及照片的区块设计。 + +### 3.2 分页查询单株树木的历史巡检记录 + +对应管理后台中左侧用于折叠展示的多次巡检历史简要列表。 + +* **接口路径**:`GET /page` +* **请求方法**:`GET` +* **请求参数**:同小程序端 `GET /page`,按 `treeId` 进行分页拉取。 +* **返回 JSON 结构**:与小程序端 `GET /page` 的响应示例格式**完全一致**,按巡检时间由近及远倒序排列。 + +--- + +## 常见错误返回状态码 + +| HTTP状态码 / code 码 | 错误提示内容 | 触发原因 | +| :--- | :--- | :--- | +| 400 | `status.photos: 最多只能上传5张现场照片` | 现场采集照片 `photos` 数组长度大于 5 时级联强校验报错 | +| 400 | `treeId: 关联树木ID不能为空` | 请求体中缺失 `treeId` | +| 400 | `inspectionTime: 巡检时间不能为空` | 请求体中缺失 `inspectionTime` | +| 403 | `Forbidden` | 管理后台用户未被授权 `garden:tree-inspection:query` 权限 | +| 500 / `1-100-008-001` | `巡检评估记录不存在` | 查询详情时 ID 在数据库中被标记删除或不存在 | + diff --git a/deploy-test.sh b/deploy-test.sh new file mode 100755 index 0000000..bfea996 --- /dev/null +++ b/deploy-test.sh @@ -0,0 +1,68 @@ +#!/bin/bash +set -e + +# ==================== 配置 ==================== +SERVER_HOST="172.17.16.14" +SERVER_USER="root" +SERVER_PASS="JCSS@2025!@#$" +UPLOAD_DIR="/home/user/test_jar" +RESTART_SCRIPT="restart.sh" +JAR_NAME="urbanops-server.jar" +PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)" +JAR_PATH="$PROJECT_DIR/urbanops-server/target/$JAR_NAME" + +# ==================== 函数 ==================== + +check_sshpass() { + if ! command -v sshpass &> /dev/null; then + echo "[check] sshpass 未安装,尝试安装..." + if command -v brew &> /dev/null; then + brew install hudochenkov/sshpass/sshpass + else + echo "[ERROR] 请手动安装 sshpass: brew install hudochenkov/sshpass/sshpass" + exit 1 + fi + fi +} + +build() { + echo "[build] 开始 Maven 打包(跳过测试)..." + cd "$PROJECT_DIR" + mvn clean package -DskipTests + if [ -f "$JAR_PATH" ]; then + echo "[build] 打包完成: $JAR_PATH ($(du -sh "$JAR_PATH" | cut -f1))" + else + echo "[ERROR] 打包失败,未找到 $JAR_PATH" + exit 1 + fi +} + +upload() { + echo "[upload] 上传 JAR 到 $SERVER_HOST:$UPLOAD_DIR/ ..." + sshpass -p "$SERVER_PASS" scp -o StrictHostKeyChecking=no "$JAR_PATH" "$SERVER_USER@$SERVER_HOST:$UPLOAD_DIR/" + echo "[upload] 上传完成" +} + +restart() { + echo "[restart] 调用远程重启脚本..." + sshpass -p "$SERVER_PASS" ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_HOST" \ + "cd $UPLOAD_DIR && chmod +x $RESTART_SCRIPT && ./$RESTART_SCRIPT" + echo "[restart] 远程重启完成" +} + +# ==================== 主流程 ==================== + +echo "========================================" +echo " UrbanOps 测试环境部署" +echo " 目标: $SERVER_USER@$SERVER_HOST:$UPLOAD_DIR" +echo "========================================" + +check_sshpass +build +upload +restart + +echo "" +echo "========================================" +echo " 部署完成" +echo "========================================" diff --git a/guide.txt b/guide.txt new file mode 100644 index 0000000..d04003f --- /dev/null +++ b/guide.txt @@ -0,0 +1,1529 @@ +北京市园林绿化局 + +行道树安全风险评估指南 +(试行) +2024 年 11 月 + + 目 录 + +目 录 +前 言 .......................................................................................................1 +第一部分 概 述 ..................................................................................... 3 +1.1 基本概念 ............................................................................................................ 3 +1.2 目的及依据 ........................................................................................................ 3 +1.3 适用范围 ............................................................................................................ 3 +1.4 评估原则 ............................................................................................................ 4 +1.5 评估周期 ............................................................................................................ 5 +1.6 方法与工具 ........................................................................................................ 5 + +第二部分 评估体系 ............................................................................... 7 +2.1 评估指标体系 .................................................................................................... 7 +2.2 风险得分计算 .................................................................................................. 10 +2.3 风险等级判定 .................................................................................................. 11 + +第三部分 评估流程 ............................................................................. 13 +3.1 整体流程 .......................................................................................................... 13 +3.2 划分风险区域 .................................................................................................. 13 +3.3 道路初筛 .......................................................................................................... 14 +3.4 精细评估 .......................................................................................................... 17 +3.5 评估资料归档 .................................................................................................. 22 + +第四部分 风险控制 ............................................................................. 25 +4.1 风险防范原则 .................................................................................................. 25 +4.2 常规防范措施 .................................................................................................. 25 +4.3 风险处置措施 .................................................................................................. 26 +4.4 应急抢险 .......................................................................................................... 30 + +第五部分 附录 ..................................................................................... 31 +附录 1 风险指标判断示例 ..................................................................................... 31 + +i + + ii + + 前 言 +本指南由北京市园林绿化局组织实施。 +本指南起草单位:北京市园林绿化局城镇绿化处、北京 +民生智库科技信息咨询有限公司。 +本指南主要起草人:刘明星、周红英、朱永和、申明华、 +陈晓晶、姚士才、常广新、杨志华、吴斌、巢阳、张华 +伟、郭珺琪、张绮思、高天宇、胥心楠、池伯佳、胡嘉 +琪、陈季琴、刘丽婕、杨曦。 +本指南为首次发布。 + +1 + + 行道树安全风险评估指南(试行) + +2 + + 第一部分 概 述 + +第一部分 概 述 +1.1 基本概念 +行道树指种植于道路两侧及分车带、具有一定遮荫功能 +并构成街景的乔木。 +行道树安全风险是城市公共安全风险的一部分,本指南 +所指的行道树安全风险指行道树发生倒伏、断折、落枝等事 +件的可能性。 +行道树安全风险评估是识别、分析和评价行道树发生倒 +伏、断折、落枝可能性的过程。 +1.2 目的及依据 +为了科学评估行道树的潜在安全危险,及时采取处置措 +施,消除行道树安全隐患,依据《风险管理 + +风险评估技术》 + +(GBT27921-2023),参考《城市树木健康诊断技术规程》 +(DB11T 1692-2019)等规范文件,结合本市行道树实际情况, +编制本指南。 +1.3 适用范围 +本指南适用于本市范围内行道树(不含古树名木)的安 +全风险评估,具体评估对象包括种植于人行道、机非隔离带、 +主辅路隔离带、中央隔离带、其他道路两侧的乔木。 +行道树安全风险评估应由具有园林绿化中级及以上技 +术职称并有绿地养护管理经验的人员组织开展。 +3 + + 行道树安全风险评估指南(试行) + +1.4 评估原则 +北京市行道树数量庞大,在资源有限的情况下,应先聚 +焦重点区域,再聚焦单株树木开展安全风险评估。根据北京 +市气象特征,冬季大风和汛期为行道树安全事件多发时段, +在常规情况下定期开展安全风险评估的同时,还应在极端大 +风天气频发季节开展应急评估。 + +聚焦重点区域。根据行道树倒伏、断折、落枝等事件对 +公共安全影响的严重程度划分风险区域类别,其中,人员密 +集、交通流量大的主次干路、景区、公园周边道路、城市风 +口位置等区域为高风险区域,行道树安全风险评估应优先保 +障高风险区域。不同风险区域分类参考见表 1。 +表 1 行道树安全风险区域分类 +类别 + +解释 + +示例 +1.环路、主干路、次干路等重点道路; +2.环路、主干路、次干路以外人流量、车流量大的道路; + +高风险 + +频繁使用、人员密 + +区域 + +集的道路 + +3.位于政务活动场所、国际交往场所等重要区域的道路; +4.位于重点交通枢纽、商务区、会展区、商圈、重点公园等人 +员活动密集区域的道路; +5.河道两侧、十字路口、立交桥周边、城区高楼之间的狭窄地 +带等具有典型风口特征的位置。 + +中风险 +区域 + +使用频率中等、人 +员密集程度略低 +的道路 + +1.支路等车流量、人流量中等的道路; +2.位于一般公园、社区、普通医院等周边的道路。 + +低风险 + +使用频率较低的 + +1.街巷等非重点道路人流量、车流量较少的道路; + +区域 + +道路 + +2.位于开放区域、林地、边缘区域等远离人活动区域的道路。 + +聚焦单株树木精细评估。具体到每一条道路,首先应使 +用目视法或简单工具对行道树进行初筛,筛选出具有风险特 +征的单株行道树,对其进行精细评估。 +4 + + 第一部分 概 述 + +聚焦重要时段。冬季大风和汛期为行道树安全风险防范 +的重要时段,常规情况下的安全风险评估工作应在重要时段 +一个月前完成,重要时段应结合气象预警信息开展应急评估, +以便提前采取防范措施。 +1.5 评估周期 +常绿行道树安全风险评估全年均可进行,落叶行道树安 +全风险评估建议在生长期进行。评估开展频次根据实际管理 +需求确定,高风险区域建议每半年开展一次,且至少有一次 +于汛期前完成。 +1.6 方法与工具 +本指南以目视法为主,使用到的简易工具包括:胸径尺、 +卷尺、测高器、橡皮锤、量角器等。 + +5 + + 行道树安全风险评估指南(试行) + +6 + + 第二部分 评估体系 + +第二部分 评估体系 +2.1 评估指标体系 +行道树安全风险评估指标体系包括【常规情况下的评估 +指标】和【应急情况下的评估指标】。常规情况下的评估指 +标适用于非汛期常规气象条件下的评估,应急情况下的评估 +指标适用于汛期或极端天气来临之前的评估。 +评估指标体系的整体框架如图 1 所示。 + +图 1 行道树安全风险评估指标体系整体框架 + +7 + + 行道树安全风险评估指南(试行) + +2.1.1 常规情况下的评估指标体系 +常规情况下,行道树安全风险评估从【树木缺陷】【树 +木生理特性】和【树木生境】三个维度展开。 +【树木缺陷】维度,依据“根本末”对安全风险的影响 +力递减的原则,依次对“树根”“根颈”“主干”“树冠” +四个部位的 12 项指标进行赋分,分值越大,风险越高。满 +分为 100 分,其中,树根部位分值为 20 分,根颈部位分值 +为 31 分,主干部位分值为 33 分,树冠部位分值为 16 分, +各部位所占分值见图 2。 + +图 2 【树木缺陷】指标中各部位所占分值示意图 + +8 + + 第二部分 评估体系 + +其中,“树根”包括根部病害、根系下扎情况和工程切 +根 3 项指标;“根颈”包括木质部受损、树皮受损 、根颈松 +动 3 项指标;“主干”包括木质部受损、主干倾斜、树皮受 +损 3 项指标;“树冠”包括易落枝、枝干结合部异常、树冠 +透风情况及平衡性 3 项指标。 +将五种缺陷较为严重的情况设置为“一票否决”项,直 +接赋分为 100 分(判定为“极度风险”)或 70 分(判定为 +“重度风险”),详见表 2。 +表 2 【树木缺陷】指标中的“一票否决”项 +“一票否决”项 +根颈松动 + +赋分及对应的风险等级 +直接赋 100 分,归入极度风险 + +根颈木质部受损达到 50%及以上 + +直接赋 70 分,归入重度及以上风险 + +主干木质部受损达到 50%及以上 + +直接赋 70 分,归入重度及以上风险 + +主干倾斜≥30° + +直接赋 70 分,归入重度及以上风险 + +枝干结合部异常有明显空洞或蛀干痕迹 + +直接赋 70 分,归入重度及以上风险 + +【树木生理特性】维度包括“树种类型”和“栽植年限” +两项指标,均为权重指标,权重越高,对风险的影响越大。 +“树种类型”分为深根性树种和浅根性树种,后者权重较高; +“栽植年限”分为栽植 10 年以内、栽植 10-30 年、栽植 30 +年以上三档,年限越长,权重越高。 +【树木生境】维度包括“是否处于风口”“树池类型” +和“树池宽度与胸径比”三项指标,均为权重指标,权重越 +9 + + 行道树安全风险评估指南(试行) + +高,对风险的影响越大。处于风口位置的行道树在大风天气 +下承受了更大的风力,赋予较高权重;“树池类型”方面, +分联通树池、独立树池、树池硬化三种情况,赋予的权重依 +次增大;树池宽度与胸径的比值越大,赋予的权重越小。 +2.1.2 应急情况下的评估指标体系 +汛期或极端天气频发季节,应结合气象预警信息开展应 +急评估。应急情况下的评估指标体系包括“常规情况下安全 +风险评估的全部指标”和“极端天气指标”。极端天气指标 +中将“风力”作为权重因子进行赋权,风力越大,权重越高。 +其中,风力 7 级及以下、8-9 级、10 级和 10 级以上的权重 +值分别为 1.0、1.5、2.0、3.0。 +2.2 风险得分计算 +常规情况下,行道树安全风险评估得分计算公式为: +常规情况下安全风险得分 =【树木缺陷】各项指标得分相加 +×【树木生理特性】各因子权重 +×【树木生境】各因子权重 + +应急情况下,行道树安全风险评估得分计算公式为: +应急情况下安全风险得分 =【树木缺陷】各项指标得分相加 +×【树木生理特性】各因子权重 +×【树木生境】各因子权重 +×【极端天气】因子权重 + +10 + + 第二部分 评估体系 + +2.3 风险等级判定 +根据安全风险得分进行风险等级判定。行道树安全风险 +等级分 5 级,安全风险得分与风险等级对应关系见表 3。 +表 3 行道树安全风险等级划分 +安全风险得分 + +风险等级 + +风险等级描述 + +[0,10) + +I级 + +基本无风险 + +[10,30) + +II 级 + +轻度风险 + +[30,70) + +III 级 + +中度风险 + +[70,100) + +IV 级 + +重度风险 + +100+ + +V级 + +极度风险 + +说明:当风险得分为临界值时,归入下一个较高风险等级。 + +11 + + 行道树安全风险评估指南(试行) + +12 + + 第三部分 评估流程 + +第三部分 评估流程 +3.1 整体流程 +评估前应做好准备工作,确定每次评估实施范围,统筹 +安排人员和工具。由具有园林绿化中级及以上技术职称并有 +行道树养护管理经验的人员牵头,组织辖区内管养单位的骨 +干技术人员参与,并对参与人员进行充分培训。准备数量充 +足的工具和调查评估表格。 +具体实施时,先划分风险区域类别,对道路进行风险初 +筛,确定需要调查的道路后聚焦道路上“有风险”的单株树 +进行精细评估。评估完成后应将评估资料进行归档,建立风 +险评估台账。 + +图 3 行道树安全风险评估整体流程 + +3.2 划分风险区域 +综合考虑人员密集度、交通流量、道路等级等因素,将 +辖区内的道路划分为高风险区域、中风险区域和低风险区域, +划分示例参考《表 1 行道树安全风险区域分类》。有条件 +的应绘制不同风险区域的道路分布图,为评估工作提供可视 +化参考。风险区域分布图示例见图 4。 +13 + + 行道树安全风险评估指南(试行) + +图 4 不同风险区域的道路分布图 + +根据风险类别的优先级和现有人力物力条件,确定每次 +评估拟覆盖的范围,原则上高风险区域应先行实施,若一次 +评估无法覆盖所有道路,应制定分步评估计划,确保所有道 +路每年至少评估一次。 +在设计调查路线时,需兼顾科学性和合理性原则,以保 +证评估工作的效率。 +3.3 道路初筛 +优先对高风险区域道路开展全覆盖初筛,中风险和低风 +险区域根据安排适时开展。道路过长时,可分段进行。 +采用目视法结合简易工具,对道路上行道树进行观察, +快速筛选具有以下任意一种特征的行道树: +14 + + 第三部分 评估流程 + +(1)根颈松动; +(2)根颈或主干木质部受损 10%以上; +(3)主干倾斜 20 度以上; +(4)枝干结合部有明显异常; +(5)处于风口且树冠结构明显失衡。 +对筛选出的行道树涂抹标记,以便开展精细评估。道路 +初步筛查使用的调查表见表 4。 +表 4 行道树安全风险初步筛查表 +所在区:____________ +存在以下风险表征的行道树 +道路 + +道路 + +编号 + +名称 + +风险区域分类 根颈 +松动 + +根颈或主干 + +主干倾 + +枝干结合 处于风口且 + +木质部受损 斜 20 度 部有明显 树冠结构明 +10%以上 + +以上 + +异常 + +显失衡 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +评估时间: + +年 + +高风险区域 +中风险区域 +低风险区域 +高风险区域 +中风险区域 +低风险区域 +高风险区域 +中风险区域 +低风险区域 +高风险区域 +中风险区域 +低风险区域 +高风险区域 +中风险区域 +低风险区域 +高风险区域 +中风险区域 +低风险区域 + +是否纳 +入精细 +评估 + +是 +否 +是 +否 +是 +否 +是 +否 +是 +否 +是 +否 + +评估单位: +评估人: + +月 + +日 + +15 + + 行道树安全风险评估指南(试行) +填表说明: +(1)道路编号 +D+四位数字,从 D0001 开始,对辖区内的道路依次编号,确保每条道路有 +唯一编号。 +(2)风险区域分类 +参考本指南《表 1 行道树安全风险区域分类》,结合本区实际情况进行划 +分。 +(3)风口 +受狭管效应影响,气流由开阔地带流入风口时,风速会急剧增大,狭管效 +应示意图见图 5。 + +图 5 城市建筑群间的狭管效应示意图 +处于风口位置的行道树具有较高的安全风险,在行道树初筛时,准确判断 +“风口”位置至关重要。判断“风口”的一般性建议如下: +“风口”的判断 +风口一般位于受狭管效应影响的地带、水面附近等,包括但不限于以下地点: + +16 + + + +河道两侧; + + + +湖面周边的迎风面; + + + +十字路口各个方位第一株行道树,尤其是东南角; + + + +立交桥周边的各方位第一株行道树,尤其是东南角; + + + +城区内高楼大厦间瞬间风力加强的狭窄地带; + + + +开敞空间,周边无任何遮挡物。 + + 第三部分 评估流程 + +3.4 精细评估 +根据道路初筛结果,选择初步筛查标记出的风险树,开 +展精细评估,填写表 5。按照“树根-根颈-主干-树冠”的 +顺序,依次填写树木缺陷各项指标得分,记录树木生理特性 +和树木生境各项权重因子的权重值,根据各项得分或权重值 +计算风险得分,并判断对应的风险等级。 +表 5 行道树安全风险精细评估调查表 +风险树编号:D-P- +(一)基本信息表 +树种 + +栽植位置 + +树高(m) + + 人行道 + +胸径(cm) + +点位坐标 + +一级指标 二级指标 + +X: + +根系下扎情况 +工程切根 + +木质部受损 +树木缺陷 +根颈 +树皮受损 + +根颈松动 +主干 + +木质部受损 + + 路侧绿地 + +冠幅(m) +Y: + +(二)行道树缺陷评估 +三级级指标 +评分标准 +根部病害 + +树根 + + 分车带 + +赋分 + +无真菌危害或腐朽情况 + +0 + +存在真菌危害或腐朽情况 + +8 + +根部下扎良好,无盘根或隆起 + +0 + +存在根部隆起或盘根 + +7 + +无工程切根 + +0 + +存在工程切根 + +5 + +无受损情况 + +0 + +受损程度<10% + +5 + +受损程度介于 10%(含)-30%(不含) + +15 + +受损程度介于 30%(含)-50%(不含) + +25 + +受损程度≥50% + +70 + +受损程度<10% + +0 + +受损程度介于 10%-30% + +2 + +受损程度介于 30%-50% + +4 + +受损程度≥50% + +6 + +不存在根颈松动 + +0 + +存在根颈松动 + +100 + +无受损情况 + +0 + +受损程度<10% + +5 + +得分 + +17 + + 行道树安全风险评估指南(试行) +一级指标 二级指标 + +三级级指标 + +主干倾 + +树皮受损 + +易落枝 + +树冠 +(16) + +评分标准 + +赋分 + +受损程度介于 10%(含)-30%(不含) + +12 + +受损程度介于 30%(含)-50%(不含) + +20 + +受损程度≥50% + +70 + +倾斜度<10° + +0 + +倾斜度介于 10°(含)-20°(不含) + +3 + +倾斜度介于 20°(含)-30°(不含) + +8 + +倾斜度≥30° + +70 + +受损程度<10% + +0 + +受损程度介于 10%(含)-30%(不含) + +1 + +受损程度介于 30%(含)-50%(不含) + +3 + +受损程度≥50% + +5 + +未发现易落枝 + +0 + +易落枝占整个树冠枝条数量的比例< +1/10 + +2 + +易落枝占整个树冠枝条数量的比例≥ +1/10 + +3 + +无异常 + +0 + +枝干结合部异 有龟裂或卷皮情况 +常 +有腐烂现象但尚未形成明显空洞 + +3 +5 + +有明显空洞或蛀干痕迹 + +70 + +透风情况较好,不偏冠 + +0 + +【透风情况较好但有明显偏冠】或【透 +树冠透风情况 风性差但冠幅适中,不偏冠】 +透风性差且明显偏冠,但冠幅适中 +及平衡性 + +得分 + +1 +2 + +透风性差且冠幅较大,但不偏冠 + +5 + +透风性差、冠幅较大且明显偏冠 + +8 + +缺陷评估得分 +(三)权重因子评估 +一级权重因子 + +二级权重因子 +树种类型 + +树木生理特性 +栽植年限 + +树木生境 + +是否处于风口 +树池类型 + +18 + +权重标准 + +赋权 + +深根性树种 + +1.0 + +浅根性树种 + +1.1 + +栽植 10 年以内 + +1.0 + +栽植 10-30 年 + +1.1 + +栽植 30 年以上 + +1.2 + +否 + +1.0 + +是 + +2.0 + +联通树池 + +1.0 + +权重值 + + 第三部分 评估流程 +独立树池 + +1.2 + +树池硬化 + +1.5 + +7 倍及以上 + +1.0 + +树池宽度与胸径 5 倍(含)-7 倍(不含) +比 +3 倍(含)-5 倍(不含) + +1.1 + +3 倍以下 + +1.3 + +1.2 + +常规情况下的评估得分 +(四)应急评估增项指标 +一级权重因子 + +二级权重因子 + +极端天气 + +风力 + +权重标准 + +赋权 + +7 级及以下 + +1.0 + +8-9 级 + +1.5 + +10 级 + +2.0 + +10 级以上 + +3.0 + +权重值 + +(五)风险得分及等级判定 +计算安全风险得分 +风险得分所属区间 + +对应风险等级 + +□ 得分<10 + +□ 基本无安全风险 + +□ 10≤得分<30 + +□ 轻度安全风险 + +□ 30≤得分<70 + +□ 中度安全风险 + +□ 70≤得分<100 + +□ 重度安全风险 + +□ 得分≥100 + +□ 极度安全风险 + +选择安全风险等级 + +评估单位: +评估人: + +评估时间: + +年 + +月 + +日 + +填表说明: +(1)风险树编号 +按“道路编号-树群单元编号-行道树排号-顺序编号”的规则进行编号,即 +DXXXX-PX-XXXX。 +行道树排号编写规则:P+1 位数字,南北走向道路按照从东向西的顺序依 +次命名 P1、P2、P3… …,东西走向的道路按照从北向南的顺序依次命名 P1、 +P2、P3… …,示例见图 6。 + +19 + + 行道树安全风险评估指南(试行) + +图 6 行道树排号编写规则示例 +比如,道路编号为 D0601,排号为 P1,则该树群单元中的第一株风险树编号 +为 D0601-P1-0001。 +(2)冠幅(m) +东西方向和南北方向冠幅的平均值。 +(3)根部病害 +仔细检查树根部位是否存在大型真菌或有明显的腐朽症状,若有则此项指 +标得分为 8 分。 +(4)根系下扎情况 +观察根系是否存隆起或盘根现象,若有则此项指标得分为 7 分。 +(5)工程切根 +观察根部是否存在切口,调查人员也可以根据施工记录判定是否存在工程 +切根情况。 +(6)根颈木质部受损 +根颈部位木质部受损情形包括腐朽、空洞、木质部开裂、明显的蛀干害虫 +痕迹等,受损程度以最大受损截面占根颈截面面积的比例计算,根据测算的受 +损比例选择相应的分值填写,若木质部受损程度大于等于 50%,则达到“一票 +否决”标准,直接得分 70,归入“重度风险”。 +(7)根颈树皮受损 +根颈部树皮受损情形包括树皮脱落、木质部裸露、树皮机械损伤等,受损 +程度以受损最大弧长占根颈周长的比例计算,根据测算的受损比例选择相应的 +分值填写。 +(8)根颈松动 +用力推动树干,观察根颈部位是否有明显晃动,此项为“一票否决”项, +若存在根颈松动情况,直接得分 100,归入极度风险。 +20 + + 第三部分 评估流程 +(9)主干木质部受损 +主干部位木质部受损情形包括腐朽、空洞、木质部开裂、明显的蛀干害虫 +痕迹等。受损程度以最大受损截面占主干截面面积的比例计算,主干外观无异 +常,应使用橡皮锤敲击,如有不同于正常树干的声音,判断为存在空腐。根据 +测算的受损比例选择相应的分值填写。若木质部受损程度大于等于 50%,则达 +到“一票否决”标准,直接得分 70,归入“重度风险”。 +(10)主干倾斜 +使用量角器测量树干偏离竖直方向的度数,根据测量结果选择相应的分值 +填写,若主干倾斜程度大于等于 30°,则达到“一票否决”标准,直接得分 70, +归入“重度风险”。 +(11)主干树皮受损 +主干部树皮受损情形包括树皮脱落、木质部裸露、树皮机械损伤等,受损 +程度以受损最大弧长占主干周长的比例计算,根据测算的受损比例选择相应的 +分值填写。 +(12)易落枝 +观察是否存在易落枝,包括枯死枝、蛀干枝或已折断未掉落的枝条,估算 +易落枝数量占树冠枝条数量的比例。根据观察结果选择相应的分值填写。 +(13)枝干结合部异常 +观察枝干结合部位是否存在异常,包括是否有龟裂或卷皮、腐烂、空洞或 +蛀干痕迹。根据异常程度选择相应的分值填写,若发现“有明显空洞或蛀干痕 +迹”,则达到“一票否决”标准,直接得分 70,归入“重度风险”。 +(14)树冠透风情况及平衡性 +观察是否存在树冠密不透风、冠幅过大或偏冠的情况,根据观察结果选择 +相应的分值填写。 +(15)树种类型 +常见树种根系类型划分参见表 6。 +表 6 常见树种根系分类 +序号 + +树种 + +根系分类 + +序号 + +树种 + +根系分类 + +1 + +国槐 + +深根性 + +8 + +栾树 + +深根性 + +2 + +刺槐 + +浅根性 + +9 + +垂柳 + +浅根性 + +3 + +白蜡 + +深根性 + +10 + +旱柳 + +浅根性 + +4 + +银杏 + +深根性 + +11 + +千头椿 + +浅根性 + +5 + +悬铃木 + +浅根性 + +12 + +臭椿 + +浅根性 + +6 + +毛白杨 + +深根性 + +13 + +油松 + +深根性 + +7 + +加杨 + +深根性 + +14 + +毛泡桐 + +浅根性 + +21 + + 行道树安全风险评估指南(试行) + +3.5 评估资料归档 +3.5.1 照片的采集 +对开展精细评估的每株行道树,均需拍摄现状照片,照 +片数量不少于 3 张且包括以下角度取景: +整体取景:含周边参照物,以展示单株行道树的树体形 +态及具体位置; +树木生境照片:展示树池形态、周边设施、建筑等; +细节照片:从不同角度清晰展示树木存在的缺陷问题, +如行道树存在多处缺陷,则每种症状的特写均需采集。 +此外,如遇处于风口的行道树,则需拍摄周边风场环境 +的照片或视频。 +3.5.2 点位的采集 +采集完风险树的信息和照片后,采集风险树点位坐标, +以树木编号命名,并以矢量数据保存。 +3.5.3 资料归档 +将各项调查评估信息进行汇总整理,形成行道树风险管 +理档案,具体包括: + 风险区域划分图、道路初筛表、精细评估调查表; + 对调查评估表格进行电子化,形成风险台账(见表 7); + 照片文件夹以树木编号命名,形成照片库; + 点位以树木编号命名,以矢量格式储存,与调查信息 +关联,形成行道树风险点位矢量数据库。 +22 + + 第三部分 评估流程 + +表 7 行道树安全风险台账表 +基本信息 +风险树 + +所属 + +所属道 + +X坐 + +Y坐 + +编号 + +区 + +路 + +标 + +标 + +树种 + +风险评估结果 +树高 + +胸径 + +冠幅 + +栽植 + +(m) + +(cm) + +(m) + +位置 + +常规情况 + +应急情况 + +下的风险 + +下的风险 + +等级 + +等级 + +风险处置 +建议处置 +措施 + +是否已处 +置 + +评估记录 +评估 +时间 + +评估单位 + +评估人 + +23 + + 行道树安全风险评估指南(试行) + +24 + + 第四部分 风险控制 + +第四部分 风险控制 +4.1 风险防范原则 + +坚持保护优先的原则。行道树安全风险管理要严格坚持 +保护优先原则,重在找出问题,提出改善措施。 + +安全风险分级管控原则。重度及以上安全风险树木及早 +处置,中度、轻度安全风险树木密切监测,采取缓解和保护 +措施。 +4.2 常规防范措施 +行道树作为有生命的城市基础设施,其安全风险管理贯 +穿树种规划、设计、施工、管养的整个过程。 +行道树树种规划阶段,应因地制宜,适地适树,优选抗 +性强、耐修剪、易栽活的乡土树种;设计阶段宜考虑丰富树 +种多样性和合理布局与搭配,提高行道树抗风险能力;施工 +阶段应关注施工质量和树木保护,确保严格按照相关规范和 +标准进行,工程验收时将安全风险指标考虑在内;管养阶段, +除日常养护工作外,还应针对长势不良的行道树及时采取树 +池扩大、联通树池改造或土壤改良等复壮措施。 +建立常态化安全风险评估机制,实现行道树安全风险台 +账动态管理,并及时采取措施消除风险。注重收集的行道树 +安全事故数据,建立风险预警数据库,针对往年行道树安全 +事件频发的重点区域、位置设置警示标识,完善应急预案, +加强巡查监测,提升行道树安全风险管理的有效性。 + +25 + + 行道树安全风险评估指南(试行) + +4.3 风险处置措施 +行道树安全风险是行道树健康风险的重要组成部分,按 +照保护优先和分级管控的原则,对于不同风险等级的行道树 +采取不同措施进行管理。 +评估为“轻度风险”“中度风险”的行道树,应依托树 +木医开展健康管理,减缓风险发展速度。 +评估为“重度风险”和“极度风险”的行道树,应立即 +采取措施,消除风险隐患。 +表 8 行道树安全风险等级对应的处置措施 +风险等级 + +风险等级描述 + +I级 + +基本无风险 + +II 级 + +轻度风险 + +III 级 + +中度风险 + +IV 级 + +重度风险 + +V级 + +极度风险 + +处置建议 +正常养护,每年定期开展安全风险评估。 +根据道路实际情况,结合存在的风险隐患点,开展修剪、支撑、创 +面或空洞修复、病虫害防治等治理措施,每半年巡查 1 次。 +加强巡查频次,每季度巡查一次,适时采取风险减缓措施。 +在极端天气来临前,特别是处于人流量较大区域风险树,在完成评 +估后 4 周内采取措施。对此类树木密切监察,直至风险降级或解除。 +立即采取措施,进行树木移除、危险枝清理或其他减轻风险的措施。 + +4.3.1 针对特殊情形树木的移除 +一般情况下,能采取风险减缓措施的树木均不建议进行 +移除。 +针对风险等级达到重度安全风险及以上且不具备采取 +风险减缓措施条件的行道树,建议进行移除,并适时补植适 +宜规格的苗木,以保证景观效果。 + +26 + + 第四部分 风险控制 +建议进行树木移除的情形 + + +栽植 2 年以上且根颈出现松动时,应及时采取措施,进行树木移除; + + + +树根存在严重腐烂,无法起到支撑作用的行道树,应及时移除; + + + +木质部受损(空腐率)超过 50%且树势较弱的行道树; + + + +主干倾斜 30 度以上且无法通过缩冠减缓风险或不具备增加支撑条件的行道树; + + + +其他经过专家研讨一致认定应及时移除的行道树。 + +4.3.2 针对木质部受损的行道树进行空洞、腐朽处理 +根颈、主干及大枝的木质部受损,尤其是出现腐朽、空 +洞时,应及时采取处理措施。 +(1)木质部腐朽处理措施 +主干、主枝上有明显裸露腐朽木质部的,首先清除木质 +部表面的松软碎末等杂物,不损伤活组织的前提下,使用已 +消毒的工具修整至活组织,喷洒杀菌剂后,再均匀喷洒水溶 +性防腐剂,待自然风干后均匀涂抹纯熟桐油等天然环保的防 +腐材料。 +(2)木质部空洞处理措施 +主干或主枝上有明显树洞的,针对不同类型空洞,建议 +采取不同的处置措施。树体修复施工宜在树木休眠期、天气 +干燥时进行。 +建议采取的空洞修复方式 + + +不易积水、存水的树洞,做好防腐处理,不填充封堵。 + + + +易积水但不影响树体安全,可在适当位置设导流管(孔)顺利排出的树洞,做好防腐 +处理不填充封堵。 + + + +敞开式、贯通式树洞不填充封堵,做好导水、防腐及安全加固处理。 + + + +易进水、存水的树洞,应封堵洞口,做好排水、通风处理。 + +27 + + 行道树安全风险评估指南(试行) + +4.3.3 针对倾斜的行道树采取支撑措施 +针对倾斜且具备支撑、加固条件的行道树,在不妨碍车 +辆、行人通行的情况下进行支撑、加固。树体外观明显倾斜 +的行道树,宜采用“人字”硬支撑、拉纤等方法进行支撑、 +加固。主干有劈裂倾倒隐患或树冠上有断裂隐患的分枝间可 +采用抱箍和相互拉纤进行加固。 +4.3.4 针对偏冠、树冠过大的行道树开展缩冠修剪 +行道树的修剪以冬春季(休眠期)修剪为主,夏季(生 +长期)修剪为辅,结合大风汛期前的应急修剪进行。 +针对存在偏冠的行道树,适当进行缩冠修剪。修剪时, +应对生长势较弱一方的枝条适当长放或轻剪,对生长势较强 +一侧适当回缩,以此达到平衡生长势。 +树冠过大的行道树需进行缩冠修剪,尤其是针对处于风 +口树冠过大的行道树,必须进行缩冠修剪。按照“由外及里、 +由上到下”的顺序进行修剪,注意保留大部分枝条顶芽,避 +免出现截干、重修剪等现象。修剪时应保持树木冠幅及树冠 +高度与树干适当比例,冠幅宜占全树高度的 1/3~1/2,树冠 +高度宜占全树高度的 1/2~2/3。 +4.3.5 针对枝条过密的行道树进行疏枝 +过密枝条的疏枝按照“一知、二看、三剪、四拿、五处 +理、六保护”的程序进行操作: +一知:参加修剪的全体人员,应明确修剪原则,知道操 +作规程、技术规范及特殊要求; +28 + + 第四部分 风险控制 + +二看:修剪前先绕树观察,对树木的修剪方法做到心中 +有数; +三剪:根据因地制宜,因树修剪的原则,合理修剪; +四拿:修剪下来的枝条,及时清运,保证环境整洁; +五处理:剪下的枝条要及时处理,防止病虫害蔓延; +六保护:疏除大枝、粗枝时,应保护树体。 +4.3.6 针对枝干结合部异常的修剪 +针对枝干结合部存在异常,导致连接部位脆弱易发生断 +折的行道树,应及时去异常部位的枝条,避免产生大枝劈裂 +的风险。修剪时应注意避开枝领,降低剪切面受到病虫害侵 +染的几率,修剪后及时涂抹愈合剂。 +4.3.7 针对蛀干害虫的防治措施 +常见的蛀干害虫包括鞘翅目的吉丁虫、天牛、小蠹、象 +甲等,鳞翅目的木蠹蛾、小卷蛾、松梢螟、透翅蛾等,膜翅 +目的树蜂等。 +按照“预防为主,综合防治”的原则,做到安全、经济、 +及时、有效。及时采取物理防治手段,包括诱杀、阻止上树、 +人工捕捉、摘除网幕、剪除病虫枝等。宜采用生物防治手段, +保护和利用天敌。采用化学防治措施时,选择符合环保要求 +的低毒农药。交替使用不同的药剂,减少喷药次数。 + +29 + + 行道树安全风险评估指南(试行) + +4.4 应急抢险 +管理单位应建立组织全面、职能明确、运行有效的应急 +抢险组织架构(见图 7),确保行道树安全风险管理有效。 +组织架构设立应急抢险领导小组,下设应急抢险办公室、后 +勤保障组、现场调查组和应急抢险组。组织架构中相关人员 +各司其职,保障应急信息、应急指令的及时传递,并定期开 +展培训和演练。 + +图 7 应急抢险组织架构 + +行道树安全风险事故发生后,应根据险情不同及时启动 +相应的应急抢险,以保障抢险工作高效、有序进行。当树木 +完全倒伏至地面或高度较低,采用普通锯除,再将大枝、主 +枝、主干截段,进行运输处理;当树木倒伏或断枝的位置过 +高时,采用高空移除,先将中级枝以上分枝去除,再将大枝、 +主枝、主干截断,再截段运输;若发生倾斜或挤压建筑的树 +木过大过高,为了防止二次破坏,采用搭建脚手架辅助移除。 + +30 + + 第五部分 附录 + +第五部分 附录 +附录 1 风险指标判断示例 +5.1.1 根部病害示例 + +蜜环菌 + +多孔菌 + +鬼伞 + +鬼伞 + +图 8 根部病害示例 + +5.1.2 根系下扎不良示例 + +31 + + 行道树安全风险评估指南(试行) +根部隆起 + +根部隆起 + +盘根 + +盘根 + +图 9 根系下扎不良示例 + +5.1.3 工程切根示例 + +图 10 施工切根示例 + +32 + + 第五部分 附录 + +5.1.4 根颈木质部受损示例 + +图 11 根颈木质部受损示例 + +5.1.5 根颈树皮受损示例 + +图 12 根颈树皮受损示例 + +33 + + 行道树安全风险评估指南(试行) + +5.1.6 主干木质部受损示例 + +图 13 主干部位木质部受损示例 + +34 + + 第五部分 附录 + +5.1.7 主干倾斜示例 + +图 14 主干倾斜示例 + +5.1.8 主干树皮受损示例 + +图 15 主干部位树皮受损示例 + +5.1.9 易落枝示例 + +图 16 易落枝示例 + +35 + + 行道树安全风险评估指南(试行) + +5.1.10 枝干结合部异常示例 + +图 17 枝干结合部异常示例 + +5.1.11 树冠透风情况及平衡性示例 + +36 + + 第五部分 附录 + +树冠密不透风且冠幅大 + +偏冠 + +图 18 树冠透风情况及平衡性示例 + +37 + + 行道树安全风险评估指南(试行) + +38 + + \ No newline at end of file diff --git a/script/convert_gis_road_coords.py b/script/convert_gis_road_coords.py new file mode 100644 index 0000000..879e8f5 --- /dev/null +++ b/script/convert_gis_road_coords.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +""" +WGS84 → GCJ02(高德坐标系)坐标转换脚本 +目标表: garden_gis_road +转换字段: starting_latitude/longitude, end_latitude/longitude, gis_polygon_coords + +使用方法: + python3 convert_gis_road_coords.py + +前提: pip install pymysql +""" +import pymysql +import math +import re + +# ========== 数据库配置 ========== +DB_CONFIG = { + 'host': '172.17.16.15', + 'port': 3306, + 'user': 'root', + 'password': 'mysql2025!', + 'database': 'urban_ops_agent', + 'charset': 'utf8mb4', +} + +# ========== WGS84 → GCJ02 算法 ========== +PI = math.pi +A = 6378245.0 # 长半轴 +EE = 0.00669342162296594323 # 偏心率平方 + + +def wgs84_to_gcj02(lng, lat): + """WGS84 转 GCJ02(高德坐标系)""" + if lng < 72.004 or lng > 137.8347 or lat < 0.8293 or lat > 55.8271: + return lng, lat + + x, y = lng - 105.0, lat - 35.0 + + dlat = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * math.sqrt(abs(x)) + dlat += (20.0 * math.sin(6.0 * x * PI) + 20.0 * math.sin(2.0 * x * PI)) * 2.0 / 3.0 + dlat += (20.0 * math.sin(y * PI) + 40.0 * math.sin(y / 3.0 * PI)) * 2.0 / 3.0 + dlat += (160.0 * math.sin(y / 12.0 * PI) + 320.0 * math.sin(y * PI / 30.0)) * 2.0 / 3.0 + + dlng = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * math.sqrt(abs(x)) + dlng += (20.0 * math.sin(6.0 * x * PI) + 20.0 * math.sin(2.0 * x * PI)) * 2.0 / 3.0 + dlng += (20.0 * math.sin(x * PI) + 40.0 * math.sin(x / 3.0 * PI)) * 2.0 / 3.0 + dlng += (150.0 * math.sin(x / 12.0 * PI) + 300.0 * math.sin(x / 30.0 * PI)) * 2.0 / 3.0 + + rad_lat = lat / 180.0 * PI + magic = math.sin(rad_lat) + magic = 1 - EE * magic * magic + sqrt_magic = math.sqrt(magic) + + dlat = (dlat * 180.0) / ((A * (1 - EE)) / (magic * sqrt_magic) * PI) + dlng = (dlng * 180.0) / (A / sqrt_magic * math.cos(rad_lat) * PI) + + return lng + dlng, lat + dlat + + +def transform_wkt(wkt): + """转换 WKT 字符串中的所有坐标对(支持 POLYGON / MULTIPOLYGON)""" + if not wkt or not wkt.strip(): + return wkt + + def convert_pair(m): + nlng, nlat = wgs84_to_gcj02(float(m.group(1)), float(m.group(2))) + return f"{nlng:.6f} {nlat:.6f}" + + return re.sub(r'(\d+\.?\d*)\s+(\d+\.?\d*)', convert_pair, wkt) + + +def main(): + conn = pymysql.connect(**DB_CONFIG) + cur = conn.cursor() + + # 1. 转换起点/终点坐标(如果有的话) + cur.execute( + "SELECT id, starting_latitude, starting_longitude, end_latitude, end_longitude " + "FROM garden_gis_road " + "WHERE (starting_latitude IS NOT NULL AND starting_latitude != '') " + " OR (end_latitude IS NOT NULL AND end_latitude != '')" + ) + rows = cur.fetchall() + updated = 0 + for row in rows: + id_, slat, slng, elat, elng = row + updates = [] + params = [] + + if slat and slng and slat.strip(): + try: + nlng, nlat = wgs84_to_gcj02(float(slng), float(slat)) + updates.append('starting_longitude = %s') + updates.append('starting_latitude = %s') + params.append(str(round(nlng, 6))) + params.append(str(round(nlat, 6))) + except ValueError: + pass + + if elat and elng and elat.strip(): + try: + nlng, nlat = wgs84_to_gcj02(float(elng), float(elat)) + updates.append('end_longitude = %s') + updates.append('end_latitude = %s') + params.append(str(round(nlng, 6))) + params.append(str(round(nlat, 6))) + except ValueError: + pass + + if updates: + sql = 'UPDATE garden_gis_road SET ' + ', '.join(updates) + ' WHERE id = %s' + params.append(id_) + cur.execute(sql, params) + updated += 1 + + conn.commit() + print(f'起点/终点坐标: 转换 {updated} 条') + + # 2. 转换围栏坐标 + cur.execute( + "SELECT COUNT(*) FROM garden_gis_road " + "WHERE gis_polygon_coords IS NOT NULL AND gis_polygon_coords != ''" + ) + total = cur.fetchone()[0] + print(f'围栏坐标记录: {total} 条') + + batch_size = 500 + updated = 0 + for offset in range(0, total, batch_size): + cur.execute( + "SELECT id, gis_polygon_coords FROM garden_gis_road " + "WHERE gis_polygon_coords IS NOT NULL AND gis_polygon_coords != '' " + "LIMIT %s OFFSET %s", + (batch_size, offset) + ) + for id_, wkt in cur.fetchall(): + new_wkt = transform_wkt(wkt) + if new_wkt != wkt: + cur.execute( + "UPDATE garden_gis_road SET gis_polygon_coords = %s WHERE id = %s", + (new_wkt, id_) + ) + updated += 1 + conn.commit() + print(f' 进度: {min(offset + batch_size, total)}/{total}, 已更新: {updated}') + + print(f'围栏坐标: 转换 {updated} 条') + + # 3. 验证 + cur.execute( + "SELECT COUNT(*) FROM garden_gis_road " + "WHERE gis_polygon_coords IS NOT NULL AND gis_polygon_coords != ''" + ) + total = cur.fetchone()[0] + unconverted = 0 + cur.execute( + "SELECT id, gis_polygon_coords FROM garden_gis_road " + "WHERE gis_polygon_coords IS NOT NULL AND gis_polygon_coords != ''" + ) + for id_, wkt in cur.fetchall(): + m = re.search(r'(\d+\.?\d*)\s+(\d+\.?\d*)', wkt) + if m: + wlng, wlat = float(m.group(1)), float(m.group(2)) + nlng, nlat = wgs84_to_gcj02(wlng, wlat) + if abs(wlng - nlng) < 0.00001: + unconverted += 1 + + print(f'\n验证: {unconverted}/{total} 条未转换') + if unconverted == 0: + print('所有坐标已成功转换为 GCJ02(高德坐标系)') + + cur.close() + conn.close() + + +if __name__ == '__main__': + main() diff --git a/script/shell/deploy.sh b/script/shell/deploy.sh index 1f7a314..4d2a94e 100644 --- a/script/shell/deploy.sh +++ b/script/shell/deploy.sh @@ -1,5 +1,5 @@ #!/bin/bash -set -e +set -eo pipefail DATE=$(date +%Y%m%d%H%M) # 基础路径 @@ -9,9 +9,9 @@ SOURCE_PATH=$BASE_PATH/build # 服务名称。同时约定部署服务的 jar 包名字也为它。 SERVER_NAME=urbanops-server # 环境 -PROFILES_ACTIVE=development +PROFILES_ACTIVE=dev # 健康检查 URL -HEALTH_CHECK_URL=http://127.0.0.1:48080/actuator/health/ +HEALTH_CHECK_URL=http://127.0.0.1:48081/actuator/health/ # heapError 存放路径 HEAP_ERROR_PATH=$BASE_PATH/heapError @@ -33,7 +33,8 @@ function backup() { # 如果存在,则备份到 backup 目录下,使用时间作为后缀 else echo "[backup] 开始备份 $SERVER_NAME ..." - cp $BASE_PATH/$SERVER_NAME.jar $BASE_PATH/backup/$SERVER_NAME-$DATE.jar + mkdir -p "$BASE_PATH/backup" + cp "$BASE_PATH/$SERVER_NAME.jar" "$BASE_PATH/backup/$SERVER_NAME-$DATE.jar" echo "[backup] 备份 $SERVER_NAME 完成" fi } @@ -47,12 +48,12 @@ function transfer() { echo "[transfer] $BASE_PATH/$SERVER_NAME.jar 不存在,跳过删除" else echo "[transfer] 移除 $BASE_PATH/$SERVER_NAME.jar 完成" - rm $BASE_PATH/$SERVER_NAME.jar + rm "$BASE_PATH/$SERVER_NAME.jar" fi # 复制新 jar 包 echo "[transfer] 从 $SOURCE_PATH 中获取 $SERVER_NAME.jar 并迁移至 $BASE_PATH ...." - cp $SOURCE_PATH/$SERVER_NAME.jar $BASE_PATH + cp "$SOURCE_PATH/$SERVER_NAME.jar" "$BASE_PATH" echo "[transfer] 转移 $SERVER_NAME.jar 完成" } @@ -60,7 +61,7 @@ function transfer() { # 停止:优雅关闭之前已经启动的服务 function stop() { echo "[stop] 开始停止 $BASE_PATH/$SERVER_NAME" - PID=$(ps -ef | grep $BASE_PATH/$SERVER_NAME | grep -v "grep" | awk '{print $2}') + PID=$(ps -ef | grep "$BASE_PATH/$SERVER_NAME.jar" | grep -v "grep" | awk '{print $2}') # 如果 Java 服务启动中,则进行关闭 if [ -n "$PID" ]; then # 正常关闭 @@ -70,7 +71,7 @@ function stop() { for ((i = 0; i < 120; i++)) do sleep 1 - PID=$(ps -ef | grep $BASE_PATH/$SERVER_NAME | grep -v "grep" | awk '{print $2}') + PID=$(ps -ef | grep "$BASE_PATH/$SERVER_NAME.jar" | grep -v "grep" | awk '{print $2}') if [ -n "$PID" ]; then echo -e ".\c" else @@ -99,7 +100,8 @@ function start() { echo "[start] PROFILES: $PROFILES_ACTIVE" # 开始启动 - BUILD_ID=dontKillMe nohup java -server $JAVA_OPS $JAVA_AGENT -jar $BASE_PATH/$SERVER_NAME.jar --spring.profiles.active=$PROFILES_ACTIVE & + mkdir -p "$HEAP_ERROR_PATH" + nohup java -server $JAVA_OPS $JAVA_AGENT -jar "$BASE_PATH/$SERVER_NAME.jar" --spring.profiles.active=$PROFILES_ACTIVE >> "$BASE_PATH/nohup.out" 2>&1 & echo "[start] 启动 $BASE_PATH/$SERVER_NAME 完成" } @@ -112,7 +114,7 @@ function healthCheck() { for ((i = 0; i < 120; i++)) do # 请求健康检查地址,只获取状态码。 - result=`curl -I -m 10 -o /dev/null -s -w %{http_code} $HEALTH_CHECK_URL || echo "000"` + result=$(curl -I -m 10 -o /dev/null -s -w %{http_code} "$HEALTH_CHECK_URL" || echo "000") # 如果状态码为 200,则说明健康检查通过 if [ "$result" == "200" ]; then echo "[healthCheck] 健康检查通过"; @@ -127,24 +129,29 @@ function healthCheck() { # 健康检查未通过,则异常退出 shell 脚本,不继续部署。 if [ ! "$result" == "200" ]; then echo "[healthCheck] 健康检查不通过,可能部署失败。查看日志,自行判断是否启动成功"; - tail -n 10 nohup.out + tail -n 10 "$BASE_PATH/nohup.out" exit 1; # 健康检查通过,打印最后 10 行日志,可能部署的人想看下日志。 else - tail -n 10 nohup.out + tail -n 10 "$BASE_PATH/nohup.out" fi # 如果未配置健康检查,则 sleep 120 秒,人工看日志是否部署成功。 else echo "[healthCheck] HEALTH_CHECK_URL 未配置,开始 sleep 120 秒"; sleep 120 echo "[healthCheck] sleep 120 秒完成,查看日志,自行判断是否启动成功"; - tail -n 50 nohup.out + tail -n 50 "$BASE_PATH/nohup.out" fi } # 部署 function deploy() { - cd $BASE_PATH + cd "$BASE_PATH" || { echo "[deploy] 错误:$BASE_PATH 目录不存在"; exit 1; } + if [ ! -d "$SOURCE_PATH" ]; then + echo "[deploy] 错误:构建目录 $SOURCE_PATH 不存在" + exit 1 + fi + mkdir -p "$BASE_PATH/backup" "$HEAP_ERROR_PATH" # 备份原 jar backup # 停止 Java 服务 diff --git a/sql/dis-check-menu.sql b/sql/dis-check-menu.sql new file mode 100644 index 0000000..8b3ea59 --- /dev/null +++ b/sql/dis-check-menu.sql @@ -0,0 +1,74 @@ +-- ===================================================== +-- dis核对 完整部署脚本 +-- 数据库: urban_ops_agent +-- 包含: 表结构变更 + 菜单创建 + 角色授权 +-- 执行方式: 在对应环境的数据库中执行此 SQL +-- ===================================================== + +-- ========================================== +-- Part 0: garden_gis_road 表结构变更 +-- ========================================== +-- 新增多边形围栏边界坐标字段(MySQL 8.x 兼容写法) +SET @col_exists = ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'garden_gis_road' + AND COLUMN_NAME = 'gis_polygon_coords' +); + +SET @sql = IF(@col_exists = 0, + 'ALTER TABLE garden_gis_road ADD COLUMN gis_polygon_coords LONGTEXT NOT NULL COMMENT ''多边形/矩形围栏边界坐标,格式:POLYGON((x1 y1, x2 y2, ..., xn yn, x1 y1)) 存储的是WGS84坐标系''', + 'SELECT ''COLUMN gis_polygon_coords already exists'' AS info' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- ========================================== +-- Part 1: 菜单创建 +-- ========================================== + +-- 1. 新增「dis核对」子菜单(位于电子围栏下) +INSERT INTO system_menu +(name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) +VALUES +('dis核对', '', 2, 1, 5698, 'disCheck', 'ep:view', 'fence/dis-check/index', 'DisCheck', 0, 1, 0, 1, '1', NOW(), '1', NOW(), 0); + +-- 2. 获取上一步插入的菜单ID,用于后续按钮权限插入 +SET @dis_check_menu_id = LAST_INSERT_ID(); + +-- 3. 新增「导出」按钮权限(挂在新菜单下) +INSERT INTO system_menu +(name, permission, type, sort, parent_id, path, icon, component, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) +VALUES +('导出', 'garden:gis-road:export', 3, 0, @dis_check_menu_id, '', '', '', 0, 1, 0, 1, '1', NOW(), '1', NOW(), 0); + +-- 4. 新增「查询」按钮权限 +INSERT INTO system_menu +(name, permission, type, sort, parent_id, path, icon, component, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) +VALUES +('查询', 'garden:gis-road:query', 3, 1, @dis_check_menu_id, '', '', '', 0, 1, 0, 1, '1', NOW(), '1', NOW(), 0); + +-- 5. 给超级管理员(role_id=1)分配菜单权限 +INSERT INTO system_role_menu (role_id, menu_id, creator, create_time, updater, update_time) +VALUES +(1, @dis_check_menu_id, '1', NOW(), '1', NOW()); + +INSERT INTO system_role_menu (role_id, menu_id, creator, create_time, updater, update_time) +SELECT 1, id, '1', NOW(), '1', NOW() +FROM system_menu +WHERE parent_id = @dis_check_menu_id AND deleted = 0; + +-- ===================================================== +-- 验证插入结果 +-- ===================================================== +SELECT id, name, type, parent_id, path, component, permission +FROM system_menu +WHERE parent_id = 5698 OR id = @dis_check_menu_id +ORDER BY parent_id, sort; + +SELECT rm.role_id, rm.menu_id, m.name, m.permission +FROM system_role_menu rm +JOIN system_menu m ON rm.menu_id = m.id +WHERE rm.menu_id = @dis_check_menu_id + OR rm.menu_id IN (SELECT id FROM system_menu WHERE parent_id = @dis_check_menu_id); diff --git a/sql/garden_road_add_gis_fields.sql b/sql/garden_road_add_gis_fields.sql new file mode 100644 index 0000000..7673298 --- /dev/null +++ b/sql/garden_road_add_gis_fields.sql @@ -0,0 +1,5 @@ +-- garden_road 表新增 GIS 关联字段 +ALTER TABLE garden_road + ADD COLUMN gis_plot_name VARCHAR(200) DEFAULT NULL COMMENT 'GIS 图斑名称', + ADD COLUMN gis_plot_area FLOAT DEFAULT NULL COMMENT 'GIS 图斑面积', + ADD COLUMN gis_is_related TINYINT DEFAULT 0 COMMENT '是否关联GIS (0=未关联, 1=已关联)'; diff --git a/sql/mysql/garden_tree_inspection.sql b/sql/mysql/garden_tree_inspection.sql new file mode 100644 index 0000000..2847077 --- /dev/null +++ b/sql/mysql/garden_tree_inspection.sql @@ -0,0 +1,66 @@ +-- 行道树巡检与安全风险评估表 +CREATE TABLE IF NOT EXISTS `garden_tree_inspection` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `tree_id` bigint NOT NULL COMMENT '一树一档案ID', + `treenumber` varchar(64) DEFAULT NULL COMMENT '树木编号', + `inspection_time` datetime NOT NULL COMMENT '巡检时间', + `inspector_id` bigint DEFAULT NULL COMMENT '巡检人ID', + `inspector_name` varchar(64) DEFAULT NULL COMMENT '巡检人姓名', + `dept_id` bigint DEFAULT NULL COMMENT '部门ID', + + -- 缺陷评估指标得分 (数值直接存对应选项的分数) + `root_disease` int DEFAULT '0' COMMENT '根部病害:0-无真菌危害或腐朽,8-存在真菌危害或腐朽', + `root_anchorage` int DEFAULT '0' COMMENT '根系下扎情况:0-良好,7-存在隆起或盘根', + `root_cutting` int DEFAULT '0' COMMENT '工程切根:0-无,5-存在', + `collar_wood_damage` int DEFAULT '0' COMMENT '根颈木质部受损:0-无,5-<10%, 15-10%-30%, 25-30%-50%, 70->=50%', + `collar_bark_damage` int DEFAULT '0' COMMENT '根颈树皮受损:0-<10%, 2-10%-30%, 4-30%-50%, 6->=50%', + `collar_loosening` int DEFAULT '0' COMMENT '根颈松动:0-不存在,100-存在', + `trunk_wood_damage` int DEFAULT '0' COMMENT '主干木质部受损:0-无,5-<10%, 12-10%-30%, 20-30%-50%, 70->=50%', + `trunk_tilt` int DEFAULT '0' COMMENT '主干倾斜:0-<10度, 3-10-20度, 8-20-30度, 70->=30度', + `trunk_bark_damage` int DEFAULT '0' COMMENT '主干树皮受损:0-<10%, 1-10%-30%, 3-30%-50%, 5->=50%', + `crown_loose_branch` int DEFAULT '0' COMMENT '易落枝:0-未发现,2-<1/10, 3->=1/10', + `crown_collar_abnormal` int DEFAULT '0' COMMENT '枝干结合部异常:0-无,3-龟裂卷皮,5-腐烂未成洞,70-明显空洞或蛀干痕迹', + `crown_ventilation_balance` int DEFAULT '0' COMMENT '树冠透风及平衡性:0-较好不偏冠, 1-偏冠或透风差不偏冠, 2-透风差明显偏冠但冠幅适中, 5-透风差冠幅大不偏冠, 8-透风差冠幅大且明显偏冠', + + -- 权重因子指标值和赋权值 + `tree_species_type` varchar(32) DEFAULT NULL COMMENT '树种类型:深根性树种, 浅根性树种', + `tree_species_weight` double DEFAULT '1.0' COMMENT '树种类型权重:深根性-1.0, 浅根性-1.1', + `planting_years` varchar(32) DEFAULT NULL COMMENT '栽植年限:栽植 10 年以内, 栽植 10-30 年, 栽植 30 年以上', + `planting_years_weight` double DEFAULT '1.0' COMMENT '栽植年限权重:10年内-1.0, 10-30年-1.1, 30年以上-1.2', + `is_wind_corridor` tinyint DEFAULT '0' COMMENT '是否处于风口:0-否, 1-是', + `wind_corridor_weight` double DEFAULT '1.0' COMMENT '风口权重:否-1.0, 是-2.0', + `tree_pool_type` varchar(32) DEFAULT NULL COMMENT '树池类型:联通树池, 独立树池, 树池硬化', + `tree_pool_weight` double DEFAULT '1.0' COMMENT '树池类型权重:联通树池-1.0, 独立树池-1.2, 树池硬化-1.5', + `tree_pool_width_dbh_ratio` varchar(32) DEFAULT NULL COMMENT '树池宽度与胸径比:7 倍及以上, 5 倍-7 倍, 3 倍-5 倍, 3 倍以下', + `tree_pool_ratio_weight` double DEFAULT '1.0' COMMENT '树池比权重:7倍及以上-1.0, 5-7倍-1.1, 3-5倍-1.2, 3倍以下-1.3', + + -- 是否展开应急评估及应急权重 + `is_emergency` tinyint DEFAULT '0' COMMENT '是否进行应急评估:0-否, 1-是', + `wind_power` varchar(32) DEFAULT NULL COMMENT '应急评估极端天气风力:7 级及以下, 8-9 级, 10 级, 10 级以上', + `wind_power_weight` double DEFAULT '1.0' COMMENT '风力权重:7级及以下-1.0, 8-9级-1.5, 10级-2.0, 10级以上-3.0', + + -- 评估结果 + `defect_score` int DEFAULT '0' COMMENT '树木缺陷得分', + `normal_score` double DEFAULT '0' COMMENT '常规情况下安全风险得分', + `normal_level` varchar(64) DEFAULT NULL COMMENT '常规情况下风险等级', + `emergency_score` double DEFAULT NULL COMMENT '应急情况下安全风险得分', + `emergency_level` varchar(64) DEFAULT NULL COMMENT '应急情况下风险等级', + + -- 现状及处理措施 + `treatment_suggestion` varchar(512) DEFAULT NULL COMMENT '建议处置措施', + `is_treated` tinyint DEFAULT '0' COMMENT '是否已处置:0-未处置, 1-已处置', + `photos` varchar(1024) DEFAULT NULL COMMENT '现场照片链接(JSON 数组)', + `estimator_company` varchar(255) DEFAULT NULL COMMENT '评估单位(公司)', + `estimator` varchar(100) DEFAULT NULL COMMENT '评估人', + + + -- 基础审计与租户字段(继承 BaseDO / TenantBaseDO) + `creator` varchar(64) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` tinyint DEFAULT '0' COMMENT '是否删除:0-未删除, 1-已删除', + `tenant_id` bigint DEFAULT '0' COMMENT '租户ID', + PRIMARY KEY (`id`), + KEY `idx_tree_id` (`tree_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='行道树巡检与安全评估记录'; diff --git a/sql/sync_land_forest_to_road.sql b/sql/sync_land_forest_to_road.sql new file mode 100644 index 0000000..011fc63 --- /dev/null +++ b/sql/sync_land_forest_to_road.sql @@ -0,0 +1,59 @@ +-- ============================================================ +-- 将 garden_gis_land_forest_data 数据同步到 garden_gis_road +-- 按 greenname + greentype 分组聚合 +-- - xbmj 求和 → gis_plot_area +-- - geometry 所有 MULTIPOLYGON 环拼接 → gis_polygon_coords (WKT) +-- - 其他 gis_ 字段取分组内 MAX 值 +-- +-- 注意:MySQL 不支持 ST_Union 聚合函数,geometry 并集通过 +-- GROUP_CONCAT 拼接各 MULTIPOLYGON 的内部环来实现,不做空间融合。 +-- 如需真正融合重叠区域,建议在 Java 层用 JTS 处理。 +-- ============================================================ + +-- 避免 GROUP_CONCAT 默认 1024 字节限制导致 WKT 截断 +SET SESSION group_concat_max_len = 10485760; -- 10MB + +INSERT INTO garden_gis_road ( + gis_plot_name, + gis_green_type_name, + gis_plot_area, + gis_polygon_coords, + gis_owner_unit, + gis_property_unit, + gis_manage_unit, + gis_street, + gis_plot_code, + gis_green_type_code, + street_id, + company_id, + dept_id +) +SELECT + greenname AS gis_plot_name, + greentype AS gis_green_type_name, + ROUND(SUM(xbmj), 2) AS gis_plot_area, + CONCAT( + 'MULTIPOLYGON(', + GROUP_CONCAT( + SUBSTR( + ST_AsText(geometry), + 14, -- 跳过 'MULTIPOLYGON('(13字符) + LENGTH(ST_AsText(geometry)) - 14 -- 去掉首尾各1个括号 + ) + SEPARATOR ',' + ), + ')' + ) AS gis_polygon_coords, + MAX(cqdw) AS gis_owner_unit, + MAX(cqdw) AS gis_property_unit, + MAX(gl_dw) AS gis_manage_unit, + MAX(CONCAT_WS('-', sheng, shi, xian, xiang)) AS gis_street, + '' AS gis_plot_code, + '' AS gis_green_type_code, + '' AS street_id, + 0 AS company_id, + 0 AS dept_id +FROM garden_gis_land_forest_data +WHERE greenname IS NOT NULL + AND greentype IS NOT NULL +GROUP BY greenname, greentype; diff --git a/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/convert/BusinessLineConvert.java b/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/convert/BusinessLineConvert.java new file mode 100644 index 0000000..4781cd0 --- /dev/null +++ b/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/convert/BusinessLineConvert.java @@ -0,0 +1,112 @@ +package com.zteits.urbanops.framework.excel.core.convert; + +import cn.idev.excel.converters.Converter; +import cn.idev.excel.enums.CellDataTypeEnum; +import cn.idev.excel.metadata.GlobalConfiguration; +import cn.idev.excel.metadata.data.ReadCellData; +import cn.idev.excel.metadata.data.WriteCellData; +import cn.idev.excel.metadata.property.ExcelContentProperty; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BusinessLineConvert implements Converter { + + @Override + public Class supportJavaTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + /** + * 导入:业务线名称(园林,市政) → 编码(yl,sz) + */ + @Override + public Object convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + String cellValue = readCellData.getStringValue(); + if (cellValue == null || cellValue.isBlank()) { + return null; + } + + try { + // 按逗号切割 + String[] lineArray = cellValue.split(","); + StringBuilder codeSb = new StringBuilder(); + + for (String line : lineArray) { + String code = convertLineToCode(line.trim()); + if (codeSb.length() > 0) { + codeSb.append(","); + } + codeSb.append(code); + } + + return codeSb.toString(); + } catch (Exception e) { + log.error("[convertToJavaData][业务线转换异常] {}", cellValue, e); + return null; + } + } + + /** + * 导出:业务线编码(yl,sz) → 名称(园林,市政) + */ + @Override + public WriteCellData convertToExcelData(Object value, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + if (value == null) { + return new WriteCellData<>(""); + } + + try { + String codeValue = String.valueOf(value); + String[] codeArray = codeValue.split(","); + StringBuilder nameSb = new StringBuilder(); + + for (String code : codeArray) { + String name = convertCodeToLine(code.trim()); + if (nameSb.length() > 0) { + nameSb.append(","); + } + nameSb.append(name); + } + + return new WriteCellData<>(nameSb.toString()); + } catch (Exception e) { + log.error("[convertToExcelData][业务线转换异常] {}", value, e); + return new WriteCellData<>(""); + } + } + + /** + * 名称 → 编码 + */ + private String convertLineToCode(String line) { + return switch (line) { + case "园林" -> "yl"; + case "市政" -> "sz"; + case "物业" -> "wy"; + case "秩序管理" -> "zx"; + case "环境卫生" -> "hj"; + default -> ""; + }; + } + + /** + * 编码 → 名称 + */ + private String convertCodeToLine(String code) { + return switch (code) { + case "yl" -> "园林"; + case "sz" -> "市政"; + case "wy" -> "物业"; + case "zx" -> "秩序管理"; + case "hj" -> "环境卫生"; + default -> ""; + }; + } +} diff --git a/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/convert/ImageListConverter.java b/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/convert/ImageListConverter.java new file mode 100644 index 0000000..5dedf72 --- /dev/null +++ b/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/convert/ImageListConverter.java @@ -0,0 +1,122 @@ +package com.zteits.urbanops.framework.excel.core.convert; + +import cn.idev.excel.converters.Converter; +import cn.idev.excel.enums.CellDataTypeEnum; +import cn.idev.excel.metadata.GlobalConfiguration; +import cn.idev.excel.metadata.data.ImageData; +import cn.idev.excel.metadata.data.ReadCellData; +import cn.idev.excel.metadata.data.WriteCellData; +import cn.idev.excel.metadata.property.ExcelContentProperty; +import cn.idev.excel.util.FileUtils; +import cn.idev.excel.util.ListUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; + +@Slf4j +public class ImageListConverter implements Converter> { + + // 横向排列:每张图片宽度 + 间距 + private static final int IMAGE_WIDTH = 120; + private static final int IMAGE_GAP = 15; + private static final int MAX_IMAGE_SIZE = 10 * 1024 * 1024; + private static final int CONNECT_TIMEOUT = 5000; + private static final int READ_TIMEOUT = 10000; + + @Override + public Class supportJavaTypeKey() { + return List.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.EMPTY; + } + + @Override + public List convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + return null; + } + + @Override + public WriteCellData convertToExcelData(List imageList, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + try { + if (imageList == null || imageList.isEmpty()) { + return new WriteCellData<>("无图片"); + } + + List imageDataList = ListUtils.newArrayList(); + int leftOffset = 0; // 横向偏移量 + + for (String path : imageList) { + if (!StringUtils.hasText(path)) continue; + + byte[] imageBytes = path.startsWith("http") ? loadNetworkImage(path) : loadLocalImage(path); + if (imageBytes == null || imageBytes.length == 0 || imageBytes.length > MAX_IMAGE_SIZE) { + log.warn("跳过无效图片:{}", path); + continue; + } + + ImageData imageData = new ImageData(); + imageData.setImage(imageBytes); + imageData.setImageType(ImageData.ImageType.PICTURE_TYPE_PNG); + + // ====================== 核心:横向偏移 ====================== + imageData.setLeft(leftOffset); + // ============================================================= + + imageDataList.add(imageData); + leftOffset += IMAGE_WIDTH + IMAGE_GAP; + } + + if (imageDataList.isEmpty()) { + return new WriteCellData<>("无有效图片"); + } + + WriteCellData writeCellData = new WriteCellData<>(); + writeCellData.setType(CellDataTypeEnum.EMPTY); + writeCellData.setImageDataList(imageDataList); + + return writeCellData; + + } catch (Exception e) { + log.error("图片导出失败", e); + return new WriteCellData<>("图片加载失败"); + } + } + + private byte[] loadLocalImage(String imagePath) throws Exception { + File file = new File(imagePath); + if (!file.exists() || !file.isFile()) { + log.warn("本地图片不存在:{}", imagePath); + return null; + } + return FileUtils.readFileToByteArray(file); + } + + private byte[] loadNetworkImage(String imageUrl) throws Exception { + URL url = new URL(imageUrl); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setConnectTimeout(CONNECT_TIMEOUT); + conn.setReadTimeout(READ_TIMEOUT); + try { + if (conn.getResponseCode() != 200) return null; + try (InputStream in = conn.getInputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { + byte[] buffer = new byte[4096]; + int len; + while ((len = in.read(buffer)) != -1) out.write(buffer, 0, len); + return out.toByteArray(); + } + } finally { + conn.disconnect(); + } + } +} diff --git a/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/handler/ImageColumnWidthHandler.java b/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/handler/ImageColumnWidthHandler.java new file mode 100644 index 0000000..c8c49ae --- /dev/null +++ b/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/handler/ImageColumnWidthHandler.java @@ -0,0 +1,25 @@ +package com.zteits.urbanops.framework.excel.core.handler; + +import cn.idev.excel.write.handler.SheetWriteHandler; +import cn.idev.excel.write.handler.context.SheetWriteHandlerContext; +import lombok.RequiredArgsConstructor; +import org.apache.poi.ss.usermodel.Sheet; + +/** + * 图片列自动加宽处理器 + */ +@RequiredArgsConstructor +public class ImageColumnWidthHandler implements SheetWriteHandler { + + // 需要加宽的列索引(从0开始),比如你的图片列在第3列,就传2 + private final int imageColumnIndex; + // 加宽后的列宽(单位:字符宽度,Excel默认1个字符宽度≈256像素) + private final int columnWidth; + + @Override + public void afterSheetCreate(SheetWriteHandlerContext context) { + Sheet sheet = context.getWriteSheetHolder().getSheet(); + // 设置指定列的宽度 + sheet.setColumnWidth(imageColumnIndex, columnWidth); + } +} diff --git a/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/util/ExcelUtils.java b/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/util/ExcelUtils.java index 84dfded..7a02cdf 100644 --- a/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/util/ExcelUtils.java +++ b/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/util/ExcelUtils.java @@ -44,9 +44,27 @@ public class ExcelUtils { } public static List read(MultipartFile file, Class head) throws IOException { - return FastExcelFactory.read(file.getInputStream(), head, null) - .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 - .doReadAllSync(); + List list = FastExcelFactory.read(file.getInputStream(), head, null) + .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 + .doReadAllSync(); + + // 过滤:对象不为 null + 不是全字段 null + if (list != null) { + list.removeIf(item -> { + if (item == null) return true; + // 判断所有字段是否都为 null + for (java.lang.reflect.Field field : item.getClass().getDeclaredFields()) { + field.setAccessible(true); + try { + if (field.get(item) != null) { + return false; + } + } catch (Exception e) { } + } + return true; + }); + } + return list; } public static List read(MultipartFile file, Class head, Integer startIndex) throws IOException { diff --git a/urbanops-module-api/urbanops-module-workorder-api/src/main/java/com/zteits/urbanops/module/workorder/dto/AppGardenWorkOrderInspectorReqVO.java b/urbanops-module-api/urbanops-module-workorder-api/src/main/java/com/zteits/urbanops/module/workorder/dto/AppGardenWorkOrderInspectorReqVO.java index 264026d..85de164 100644 --- a/urbanops-module-api/urbanops-module-workorder-api/src/main/java/com/zteits/urbanops/module/workorder/dto/AppGardenWorkOrderInspectorReqVO.java +++ b/urbanops-module-api/urbanops-module-workorder-api/src/main/java/com/zteits/urbanops/module/workorder/dto/AppGardenWorkOrderInspectorReqVO.java @@ -63,6 +63,12 @@ public class AppGardenWorkOrderInspectorReqVO { @Schema(description = "工单名称", example = "绿地卫生") private String orderName; + @Schema(description = "三级编码") + private String orderCode; + + @Schema(description = "业务类型(一级编码)") + private String busiType; + @Schema(description = "来源ID 工单来源 1、巡查,2、游客居民,3、12345,4、网格", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") //@NotNull(message = "来源ID不能为空") private Integer sourceId; diff --git a/urbanops-module-api/urbanops-module-workorder-api/src/main/java/com/zteits/urbanops/module/workorder/dto/MainInfoRespVO.java b/urbanops-module-api/urbanops-module-workorder-api/src/main/java/com/zteits/urbanops/module/workorder/dto/MainInfoRespVO.java index 83722b7..26e4411 100644 --- a/urbanops-module-api/urbanops-module-workorder-api/src/main/java/com/zteits/urbanops/module/workorder/dto/MainInfoRespVO.java +++ b/urbanops-module-api/urbanops-module-workorder-api/src/main/java/com/zteits/urbanops/module/workorder/dto/MainInfoRespVO.java @@ -31,6 +31,13 @@ public class MainInfoRespVO { @ExcelProperty("工单名称") private String orderName; + @Schema(description = "三级编码") + private String orderCode; + + @Schema(description = "业务类型(一级编码)") + private String busiType; + + /** * 工单类型 Q:快速工单,C:普通工单,O:其他工单 */ diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskExcleRespVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskExcleRespVO.java index 022500e..41cf6d2 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskExcleRespVO.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskExcleRespVO.java @@ -2,8 +2,9 @@ package com.zteits.urbanops.module.garden.controller.admin.emergencytask.vo; import cn.idev.excel.annotation.ExcelIgnoreUnannotated; import cn.idev.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.write.style.ColumnWidth; +import com.zteits.urbanops.framework.excel.core.convert.ImageListConverter; import com.zteits.urbanops.framework.excel.core.convert.ListToStringConverter; -import com.zteits.urbanops.module.garden.dal.dataobject.emergencytask.EmergencyMachineryDO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -52,7 +53,9 @@ public class EmergencyTaskExcleRespVO { private LocalDateTime rescueStartTime; @Schema(description = "抢险中照片", requiredMode = Schema.RequiredMode.REQUIRED) - @ExcelProperty(value="抢险中照片", converter = ListToStringConverter.class) + //@ExcelProperty(value="抢险中照片", converter = ListToStringConverter.class) + @ExcelProperty(value = "抢险中照片", converter = ImageListConverter.class) + @ColumnWidth(70) private List emergencyImg; @Schema(description = "树种(如:国槐)") @@ -72,7 +75,9 @@ public class EmergencyTaskExcleRespVO { private Integer personnelCount; @Schema(description = "抢险结束照片", requiredMode = Schema.RequiredMode.REQUIRED) - @ExcelProperty(value="抢险结束照片", converter = ListToStringConverter.class) + //@ExcelProperty(value="抢险结束照片", converter = ListToStringConverter.class) + @ExcelProperty(value = "抢险结束照片", converter = ImageListConverter.class) + @ColumnWidth(70) private List emergencyEndImg; @Schema(description = "备注") @@ -111,4 +116,4 @@ public class EmergencyTaskExcleRespVO { @ExcelProperty("归属公司") private String companyName; -} \ No newline at end of file +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/gis/GisRoadController.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/gis/GisRoadController.java new file mode 100644 index 0000000..701cf71 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/gis/GisRoadController.java @@ -0,0 +1,127 @@ +package com.zteits.urbanops.module.garden.controller.admin.gis; + +import com.zteits.urbanops.framework.common.pojo.PageParam; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.*; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.security.access.prepost.PreAuthorize; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; + +import jakarta.validation.*; +import jakarta.servlet.http.*; +import java.io.IOException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.*; + +import com.zteits.urbanops.framework.common.pojo.PageResult; +import com.zteits.urbanops.framework.common.pojo.CommonResult; +import static com.zteits.urbanops.framework.common.pojo.CommonResult.success; + +import com.zteits.urbanops.framework.excel.core.util.ExcelUtils; +import com.zteits.urbanops.framework.apilog.core.annotation.ApiAccessLog; +import static com.zteits.urbanops.framework.apilog.core.enums.OperateTypeEnum.*; + +import com.zteits.urbanops.module.garden.controller.admin.gis.vo.*; +import com.zteits.urbanops.module.garden.service.gis.GisRoadService; + +@Tag(name = "管理后台 - GIS 道路管理") +@RestController +@RequestMapping("/garden/gis/road") +@Validated +public class GisRoadController { + private static final Logger log = LoggerFactory.getLogger(GisRoadController.class); + + @Resource + private GisRoadService gisRoadService; + + @GetMapping("/list") + @Operation(summary = "获得 GIS 道路分页列表") + @PreAuthorize("@ss.hasPermission('garden:gis-road:query')") + public CommonResult> getGisRoadList(@Valid GisRoadPageReqVO pageReqVO) { + PageResult pageResult = gisRoadService.getGisRoadPage(pageReqVO); + return success(pageResult); + } + + @GetMapping("/list-all") + @Operation(summary = "获得全部 GIS 道路列表(不分页,用于地图渲染)") + @PreAuthorize("@ss.hasPermission('garden:gis-road:query')") + public CommonResult> getGisRoadAllList(GisRoadPageReqVO reqVO) { + return success(gisRoadService.getGisRoadAllList(reqVO)); + } + + @GetMapping("/get") + @Operation(summary = "获得 GIS 道路详情") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('garden:gis-road:query')") + public CommonResult getGisRoad(@RequestParam("id") Long id) { + return success(gisRoadService.getGisRoad(id)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出 GIS 道路 Excel") + @PreAuthorize("@ss.hasPermission('garden:gis-road:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportGisRoadExcel(@Valid GisRoadPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = gisRoadService.getGisRoadPage(pageReqVO).getList(); + List exportList = new ArrayList<>(); + for (GisRoadRespVO vo : list) { + GisRoadExportVO exportVO = new GisRoadExportVO(); + exportVO.setGisPlotName(vo.getGisPlotName()); + exportVO.setRoadName(vo.getRoadName()); + exportVO.setGisPlotArea(vo.getGisPlotArea()); + exportVO.setTotalArea(vo.getTotalArea()); + exportVO.setGisIsRelatedStr(vo.getGisIsRelated() != null && vo.getGisIsRelated() == 1 ? "已关联" : "未关联"); + exportVO.setGisDiffArea(vo.getGisDiffArea()); + exportVO.setCompanyName(vo.getCompanyName()); + exportVO.setDeptName(vo.getDeptName()); + // 位置信息:起点描述 ~ 终点描述 + String positionInfo = ""; + if (vo.getStartingRemark() != null) { + positionInfo += vo.getStartingRemark(); + } + if (vo.getEndRemark() != null) { + if (!positionInfo.isEmpty()) { + positionInfo += " ~ "; + } + positionInfo += vo.getEndRemark(); + } + exportVO.setPositionInfo(positionInfo.isEmpty() ? null : positionInfo); + exportList.add(exportVO); + } + String filename = "十谱绿地核对" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + ".xls"; + ExcelUtils.write(response, filename, "数据", GisRoadExportVO.class, exportList); + } + + @PostMapping("/associate") + @Operation(summary = "关联道路到 GIS 图斑") +// @PreAuthorize("@ss.hasPermission('garden:gis-road:query')") + public CommonResult associateRoad(@Valid @RequestBody GisRoadAssociateReqVO reqVO) { + gisRoadService.associateRoad(reqVO); + return success(true); + } + + @PostMapping("/disassociate") + @Operation(summary = "取消 GIS 图斑与道路的关联") +// @PreAuthorize("@ss.hasPermission('garden:gis-road:query')") + public CommonResult disassociateRoad(@Valid @RequestBody GisRoadDisassociateReqVO reqVO) { + gisRoadService.disassociateRoad(reqVO); + return success(true); + } + + @PostMapping("/convert-coordinates") + @Operation(summary = "将全部道路坐标从 WGS84 转换为 GCJ02(高德坐标系)") +// @PreAuthorize("@ss.hasPermission('garden:gis-road:query')") + public CommonResult convertCoordinates() { + int count = gisRoadService.convertCoordinatesToGcj02(); + log.info("坐标转换完成,共转换 {} 条记录", count); + return success("坐标转换完成,共转换 " + count + " 条记录"); + } + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/gis/GisTypeController.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/gis/GisTypeController.java new file mode 100644 index 0000000..cba4017 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/gis/GisTypeController.java @@ -0,0 +1,55 @@ +package com.zteits.urbanops.module.garden.controller.admin.gis; + +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.web.bind.annotation.*; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import java.util.*; + +import com.zteits.urbanops.framework.common.pojo.CommonResult; +import static com.zteits.urbanops.framework.common.pojo.CommonResult.success; +import com.zteits.urbanops.module.garden.dal.dataobject.gis.GisTypeDO; +import com.zteits.urbanops.module.garden.service.gis.GisTypeService; + +@Tag(name = "管理后台 - GIS 绿地类型") +@RestController +@RequestMapping("/garden/gis/type") +public class GisTypeController { + + @Resource + private GisTypeService gisTypeService; + + @GetMapping("/list-by-parent") + @Operation(summary = "按父级编码查子类型(级联下拉用)") + public CommonResult>> listByParentCode( + @RequestParam(required = false) String parentCode) { + List list = gisTypeService.listByParentCode(parentCode); + List> result = new ArrayList<>(); + for (GisTypeDO t : list) { + Map m = new HashMap<>(); + m.put("code", t.getTypeCode()); + m.put("name", t.getTypeName()); + m.put("level", t.getLevel()); + m.put("parentCode", t.getParentCode()); + result.add(m); + } + return success(result); + } + + @GetMapping("/list-by-level") + @Operation(summary = "按层级查类型列表") + public CommonResult>> listByLevel(@RequestParam Integer level) { + List list = gisTypeService.listByLevel(level); + List> result = new ArrayList<>(); + for (GisTypeDO t : list) { + Map m = new HashMap<>(); + m.put("code", t.getTypeCode()); + m.put("name", t.getTypeName()); + m.put("level", t.getLevel()); + m.put("parentCode", t.getParentCode()); + result.add(m); + } + return success(result); + } +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/gis/vo/GisRoadAssociateReqVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/gis/vo/GisRoadAssociateReqVO.java new file mode 100644 index 0000000..dddea3f --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/gis/vo/GisRoadAssociateReqVO.java @@ -0,0 +1,19 @@ +package com.zteits.urbanops.module.garden.controller.admin.gis.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "管理后台 - GIS 道路关联 Request VO") +@Data +public class GisRoadAssociateReqVO { + + @Schema(description = "GIS道路ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "GIS道路ID不能为空") + private Long id; + + @Schema(description = "关联的道路ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "道路ID不能为空") + private Long roadId; + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/gis/vo/GisRoadDisassociateReqVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/gis/vo/GisRoadDisassociateReqVO.java new file mode 100644 index 0000000..47bd74e --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/gis/vo/GisRoadDisassociateReqVO.java @@ -0,0 +1,15 @@ +package com.zteits.urbanops.module.garden.controller.admin.gis.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "管理后台 - GIS 道路取消关联 Request VO") +@Data +public class GisRoadDisassociateReqVO { + + @Schema(description = "GIS道路ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "GIS道路ID不能为空") + private Long id; + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/gis/vo/GisRoadExportVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/gis/vo/GisRoadExportVO.java new file mode 100644 index 0000000..cb91a6c --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/gis/vo/GisRoadExportVO.java @@ -0,0 +1,54 @@ +package com.zteits.urbanops.module.garden.controller.admin.gis.vo; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.zteits.urbanops.module.garden.util.TwoDecimalFloatSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; + +@Schema(description = "十谱绿地核对导出 VO") +@Data +@ExcelIgnoreUnannotated +public class GisRoadExportVO { + + @Schema(description = "十谱绿地名称") + @ExcelProperty("十谱绿地名称") + private String gisPlotName; + + @Schema(description = "道路名称") + @ExcelProperty("道路名称") + private String roadName; + + @Schema(description = "十谱绿地面积") + @ExcelProperty("十谱绿地面积(㎡)") + @JsonSerialize(using = TwoDecimalFloatSerializer.class) + private Float gisPlotArea; + + @Schema(description = "道路面积") + @ExcelProperty("道路面积(㎡)") + @JsonSerialize(using = TwoDecimalFloatSerializer.class) + private Float totalArea; + + @Schema(description = "是否关联") + @ExcelProperty("是否关联") + private String gisIsRelatedStr; + + @Schema(description = "差异面积") + @ExcelProperty("差异面积(㎡)") + @JsonSerialize(using = TwoDecimalFloatSerializer.class) + private Float gisDiffArea; + + @Schema(description = "归属单位") + @ExcelProperty("归属单位") + private String companyName; + + @Schema(description = "归属班组") + @ExcelProperty("归属班组") + private String deptName; + + @Schema(description = "位置信息") + @ExcelProperty("位置信息") + private String positionInfo; + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/gis/vo/GisRoadPageReqVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/gis/vo/GisRoadPageReqVO.java new file mode 100644 index 0000000..8032502 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/gis/vo/GisRoadPageReqVO.java @@ -0,0 +1,58 @@ +package com.zteits.urbanops.module.garden.controller.admin.gis.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import com.zteits.urbanops.framework.common.pojo.PageParam; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +import static com.zteits.urbanops.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - GIS 道路核对分页 Request VO") +@Data +public class GisRoadPageReqVO extends PageParam { + + @Schema(description = "道路名称") + private String roadName; + + @Schema(description = "业务线") + private String busiLine; + + @Schema(description = "街道ID") + private String streetId; + + @Schema(description = "街道名称") + private String streetName; + + @Schema(description = "养护级别") + private Integer levelId; + + @Schema(description = "道路属性") + private String roadAttr; + + @Schema(description = "道路方位") + private String direction; + + @Schema(description = "GIS 图斑编号") + private String gisPlotCode; + + @Schema(description = "GIS 图斑名称") + private String gisPlotName; + + @Schema(description = "是否已关联 (0=未关联, 1=已关联)") + private Integer gisIsRelated; + + @Schema(description = "归属公司ID") + private Long companyId; + + @Schema(description = "部门ID") + private Long deptId; + + @Schema(description = "GIS绿地类型编码(级联查询用)") + private String gisTypeCode; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/gis/vo/GisRoadRespVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/gis/vo/GisRoadRespVO.java new file mode 100644 index 0000000..8ffcbf8 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/gis/vo/GisRoadRespVO.java @@ -0,0 +1,192 @@ +package com.zteits.urbanops.module.garden.controller.admin.gis.vo; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.zteits.urbanops.module.garden.util.TwoDecimalFloatSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; +import cn.idev.excel.annotation.*; + +@Schema(description = "管理后台 - GIS 道路核对 Response VO") +@Data +@ExcelIgnoreUnannotated +public class GisRoadRespVO { + + @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("ID") + private Long id; + + @Schema(description = "关联道路ID") + @ExcelProperty("关联道路ID") + private Long gisRoadId; + + @Schema(description = "道路名称") + @ExcelProperty("道路名称") + private String roadName; + + @Schema(description = "业务线") + @ExcelProperty("业务线") + private String busiLine; + + @Schema(description = "街道ID") + @ExcelProperty("街道ID") + private String streetId; + + @Schema(description = "街道名称") + @ExcelProperty("街道名称") + private String streetName; + + @Schema(description = "养护级别") + @ExcelProperty("养护级别") + private Integer levelId; + + @Schema(description = "道路属性") + @ExcelProperty("道路属性") + private String roadAttr; + + @Schema(description = "道路方位") + @ExcelProperty("道路方位") + private String direction; + + @Schema(description = "行道树绿化面积") + @ExcelProperty("行道树绿化面积") + @JsonSerialize(using = TwoDecimalFloatSerializer.class) + private Float greenBeltTreeArea; + + @Schema(description = "绿化带面积") + @ExcelProperty("绿化带面积") + @JsonSerialize(using = TwoDecimalFloatSerializer.class) + private Float greenBeltArea; + + @Schema(description = "总绿化带面积") + @ExcelProperty("总绿化带面积") + @JsonSerialize(using = TwoDecimalFloatSerializer.class) + private Float totalArea; + + @Schema(description = "行道树面积") + @ExcelProperty("行道树面积") + @JsonSerialize(using = TwoDecimalFloatSerializer.class) + private Float treeArea; + + @Schema(description = "起点经度") + @ExcelProperty("起点经度") + private String startingLatitude; + + @Schema(description = "起点纬度") + @ExcelProperty("起点纬度") + private String startingLongitude; + + @Schema(description = "起点描述") + @ExcelProperty("起点描述") + private String startingRemark; + + @Schema(description = "终点经度") + @ExcelProperty("终点经度") + private String endLatitude; + + @Schema(description = "终点纬度") + @ExcelProperty("终点纬度") + private String endLongitude; + + @Schema(description = "终点描述") + @ExcelProperty("终点描述") + private String endRemark; + + @Schema(description = "备注") + @ExcelProperty("备注") + private String remark; + + @Schema(description = "归属公司ID") + @ExcelProperty("归属公司ID") + private Long companyId; + + @Schema(description = "部门ID") + @ExcelProperty("部门ID") + private Long deptId; + + @Schema(description = "创建者名称") + @ExcelProperty("创建者") + private String creatorName; + + @Schema(description = "创建时间") + @ExcelProperty("创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新者名称") + @ExcelProperty("更新者") + private String updaterName; + + // ========== GIS 核对相关字段 ========== + + @Schema(description = "GIS 图斑编号") + @ExcelProperty("GIS图斑编号") + private String gisPlotCode; + + @Schema(description = "GIS 图斑名称") + @ExcelProperty("GIS图斑名称") + private String gisPlotName; + + @Schema(description = "GIS 图斑面积") + @ExcelProperty("GIS图斑面积") + @JsonSerialize(using = TwoDecimalFloatSerializer.class) + private Float gisPlotArea; + + @Schema(description = "GIS 绿地类型编码") + @ExcelProperty("GIS绿地类型编码") + private String gisGreenTypeCode; + + @Schema(description = "GIS 绿地类型名称") + @ExcelProperty("GIS绿地类型名称") + private String gisGreenTypeName; + + @Schema(description = "GIS 所属街道") + @ExcelProperty("GIS所属街道") + private String gisStreet; + + @Schema(description = "GIS 权属单位") + @ExcelProperty("GIS权属单位") + private String gisOwnerUnit; + + @Schema(description = "GIS 物业单位") + @ExcelProperty("GIS物业单位") + private String gisPropertyUnit; + + @Schema(description = "GIS 养护单位") + @ExcelProperty("GIS养护单位") + private String gisManageUnit; + + @Schema(description = "GIS 差异面积") + @ExcelProperty("GIS差异面积") + @JsonSerialize(using = TwoDecimalFloatSerializer.class) + private Float gisDiffArea; + + @Schema(description = "GIS 差异备注") + @ExcelProperty("GIS差异备注") + private String gisDiffRemark; + + @Schema(description = "是否已关联 (0=未关联, 1=已关联)") + @ExcelProperty("是否已关联") + private Integer gisIsRelated; + + @Schema(description = "关联人") + @ExcelProperty("关联人") + private String gisRelatedUser; + + @Schema(description = "关联时间") + @ExcelProperty("关联时间") + private LocalDateTime gisRelatedTime; + + @Schema(description = "多边形围栏边界坐标 WKT格式") + @ExcelProperty("围栏边界坐标") + private String gisPolygonCoords; + + // ========== 翻译字段 ========== + + @Schema(description = "公司名称") + private String companyName; + + @Schema(description = "部门名称") + private String deptName; + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/road/vo/RoadPageReqVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/road/vo/RoadPageReqVO.java index 2f224ed..5cd5de5 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/road/vo/RoadPageReqVO.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/road/vo/RoadPageReqVO.java @@ -86,6 +86,9 @@ public class RoadPageReqVO extends PageParam { @Schema(description = "更新者", example = "芋艿") private String updaterName; + @Schema(description = "是否关联GIS (0=未关联, 1=已关联)") + private Integer gisIsRelated; + //对应前端排序字段 public static final Map> SORT_FIELD_MAP = Map.of( "id", RoadDO::getId, diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/road/vo/RoadRespVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/road/vo/RoadRespVO.java index 51de8b8..786bbcd 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/road/vo/RoadRespVO.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/road/vo/RoadRespVO.java @@ -122,4 +122,17 @@ public class RoadRespVO { @ExcelProperty("部门(道路负责部门)") private String deptName; + @Schema(description = "GIS 图斑名称") + @ExcelProperty("GIS图斑名称") + private String gisPlotName; + + @Schema(description = "GIS 图斑面积") + @ExcelProperty("GIS图斑面积") + @JsonSerialize(using = TwoDecimalFloatSerializer.class) + private Float gisPlotArea; + + @Schema(description = "是否关联GIS (0=未关联, 1=已关联)") + @ExcelProperty("是否关联GIS") + private Integer gisIsRelated; + } \ No newline at end of file diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/treeinspection/TreeInspectionController.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/treeinspection/TreeInspectionController.java new file mode 100644 index 0000000..c22ebc0 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/treeinspection/TreeInspectionController.java @@ -0,0 +1,50 @@ +package com.zteits.urbanops.module.garden.controller.admin.treeinspection; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.security.access.prepost.PreAuthorize; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; + +import com.zteits.urbanops.framework.common.pojo.CommonResult; +import com.zteits.urbanops.framework.common.pojo.PageResult; +import com.zteits.urbanops.module.garden.dal.dataobject.treeinspection.TreeInspectionDO; +import com.zteits.urbanops.module.garden.service.treeinspection.TreeInspectionService; +import com.zteits.urbanops.module.garden.controller.app.treeinspection.vo.*; +import com.zteits.urbanops.module.garden.convert.treeinspection.TreeInspectionConvert; + +import static com.zteits.urbanops.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 行道树巡检记录与评估") +@RestController("adminTreeInspectionController") +@RequestMapping("/garden/tree-inspection") +@Validated +public class TreeInspectionController { + + @Resource + private TreeInspectionService treeInspectionService; + + @GetMapping("/get") + @Operation(summary = "获得巡检评估详情") + @Parameter(name = "id", description = "巡检记录编号", required = true, example = "12") + @PreAuthorize("@ss.hasPermission('garden:tree-inspection:query')") + public CommonResult getTreeInspection(@RequestParam("id") Long id) { + return success(treeInspectionService.getTreeInspection(id)); + } + + @GetMapping("/page") + @Operation(summary = "分页查询单株树木的历史巡检记录") + @PreAuthorize("@ss.hasPermission('garden:tree-inspection:query')") + public CommonResult> getTreeInspectionPage(@Valid TreeInspectionPageReqVO pageReqVO) { + PageResult pageResult = treeInspectionService.getTreeInspectionPage(pageReqVO); + PageResult respPageResult = new PageResult<>( + TreeInspectionConvert.INSTANCE.convertList(pageResult.getList()), + pageResult.getTotal() + ); + return success(respPageResult); + } + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/AppHomepageSummaryController.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/AppHomepageSummaryController.java index 3ff0794..dc2fcf6 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/AppHomepageSummaryController.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/AppHomepageSummaryController.java @@ -2,10 +2,7 @@ package com.zteits.urbanops.module.garden.controller.app.homepage; import com.zteits.urbanops.framework.common.pojo.CommonResult; import com.zteits.urbanops.framework.common.pojo.PageResult; -import com.zteits.urbanops.module.garden.controller.app.homepage.vo.AppHomePageSummaryPageReqVO; -import com.zteits.urbanops.module.garden.controller.app.homepage.vo.AppHomePageSummaryReqVO; -import com.zteits.urbanops.module.garden.controller.app.homepage.vo.TaskCompletionStatusRspVo; -import com.zteits.urbanops.module.garden.controller.app.homepage.vo.TaskDetailsRspVo; +import com.zteits.urbanops.module.garden.controller.app.homepage.vo.*; import com.zteits.urbanops.module.garden.service.homepage.HomepageSummaryService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -15,7 +12,6 @@ import jakarta.validation.Valid; import org.springframework.web.bind.annotation.*; import java.util.List; -import java.util.Map; import static com.zteits.urbanops.framework.common.pojo.CommonResult.success; @@ -51,4 +47,9 @@ public class AppHomepageSummaryController { homepageSummaryService.countTaskNumberByUserId(busiDate); } + @PostMapping("/iWorkOrderSummary") + @Operation(summary = "工单情况统计") + public CommonResult iWorkOrderSummary(@Valid @RequestBody AppWorkOrderSummaryReqVo req) { + return success(homepageSummaryService.iWorkOrderSummary(req)); + } } diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/AppWorkOrderSummaryReqVo.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/AppWorkOrderSummaryReqVo.java new file mode 100644 index 0000000..bf6cb7c --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/AppWorkOrderSummaryReqVo.java @@ -0,0 +1,49 @@ +package com.zteits.urbanops.module.garden.controller.app.homepage.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +/** + * @Classname AppWorkOrderSummaryReqVo + * @Description 全域工单统计 + * @Date 2026/5/26 23:42 + * @Created by wangqian + */ +@Data +public class AppWorkOrderSummaryReqVo { + + @NotEmpty + @Schema(description = "查询类型", example = "0:全部工单 1:全域工单") + public String queryType; + @NotEmpty + @Schema(description = "开始时间", example = "2025-01-01") + public String beginTime; + /*结束时间*/ + @NotEmpty + @Schema(description = "结束时间", example = "2025-12-10") + public String endTime; + + + // ===================== 自动拼接时间成完整格式 ===================== + /** + * 获取 开始时间 yyyy-MM-dd 00:00:00 + */ + public String getBeginTime() { + if (beginTime != null && beginTime.length() == 10) { + return beginTime + " 00:00:00"; + } + return beginTime; + } + + /** + * 获取 结束时间 yyyy-MM-dd 23:59:59 + */ + public String getEndTime() { + if (endTime != null && endTime.length() == 10) { + return endTime + " 23:59:59"; + } + return endTime; + } + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/AppWorkOrderSummaryRspVo.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/AppWorkOrderSummaryRspVo.java new file mode 100644 index 0000000..51b6af5 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/AppWorkOrderSummaryRspVo.java @@ -0,0 +1,24 @@ +package com.zteits.urbanops.module.garden.controller.app.homepage.vo; + +import lombok.Data; + +import java.util.List; + +/** + * @Classname TaskCompletionStatusRspVo + * @Description 任务完成情况 + * @Date 2026/1/7 9:44 + * @Created by wangqian + */ +@Data +public class AppWorkOrderSummaryRspVo { + /*单位名称列表*/ + private List legend; + /*总条数*/ + private String totalNum; + /*占比*/ + private List percents; + /*单位工单统计*/ + private List table; + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/AppWorkOrderSummaryVo.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/AppWorkOrderSummaryVo.java new file mode 100644 index 0000000..aecca2c --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/AppWorkOrderSummaryVo.java @@ -0,0 +1,24 @@ +package com.zteits.urbanops.module.garden.controller.app.homepage.vo; + +import lombok.Data; + +/** + * @Classname TaskCompletionStatusRspVo + * @Description 任务完成情况 + * @Date 2026/1/7 9:44 + * @Created by wangqian + */ +@Data +public class AppWorkOrderSummaryVo { + + /*公司ID*/ + private Long companyId; + /*公司名称*/ + private String companyName; + /*总工单数量*/ + private Integer total; + /* 已完成数量*/ + private Integer finish; + /*待完成数量*/ + private Integer ongoing; +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/problemtype/AppProblemTypeController.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/problemtype/AppProblemTypeController.java new file mode 100644 index 0000000..383c6f2 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/problemtype/AppProblemTypeController.java @@ -0,0 +1,34 @@ +package com.zteits.urbanops.module.garden.controller.app.problemtype; + +import com.zteits.urbanops.framework.common.pojo.CommonResult; +import com.zteits.urbanops.module.garden.controller.app.problemtype.vo.ProblemTypeCascadeRespVO; +import com.zteits.urbanops.module.garden.service.problemtype.GardenProblemTypeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static com.zteits.urbanops.framework.common.pojo.CommonResult.success; + +@Tag(name = "APP - 问题类型") +@RestController +@RequestMapping("/app/problem-type") +@Validated +public class AppProblemTypeController { + + @Resource + private GardenProblemTypeService gardenProblemTypeService; + + @GetMapping("/list-by-parent") + @Operation(summary = "查询问题类型级联列表(级联下拉用)") + public CommonResult> getProblemTypeListByParent( + @RequestParam(value = "parentCode", required = false) String parentCode) { + return success(gardenProblemTypeService.getProblemTypeTree(parentCode)); + } +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/problemtype/vo/ProblemTypeCascadeRespVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/problemtype/vo/ProblemTypeCascadeRespVO.java new file mode 100644 index 0000000..64e77cf --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/problemtype/vo/ProblemTypeCascadeRespVO.java @@ -0,0 +1,22 @@ +package com.zteits.urbanops.module.garden.controller.app.problemtype.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Schema(description = "APP - 问题类型级联响应 VO") +@Data +public class ProblemTypeCascadeRespVO { + + @Schema(description = "问题类型名称", example = "树木倒伏") + private String label; + + @Schema(description = "问题类型编码", example = "PT001") + private String value; + + @Schema(description = "子类型列表") + private List children = new ArrayList<>(); + +} \ No newline at end of file diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/treeinspection/AppTreeInspectionController.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/treeinspection/AppTreeInspectionController.java new file mode 100644 index 0000000..af631d6 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/treeinspection/AppTreeInspectionController.java @@ -0,0 +1,53 @@ +package com.zteits.urbanops.module.garden.controller.app.treeinspection; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; + +import com.zteits.urbanops.framework.common.pojo.CommonResult; +import com.zteits.urbanops.framework.common.pojo.PageResult; +import com.zteits.urbanops.module.garden.dal.dataobject.treeinspection.TreeInspectionDO; +import com.zteits.urbanops.module.garden.service.treeinspection.TreeInspectionService; +import com.zteits.urbanops.module.garden.controller.app.treeinspection.vo.*; +import com.zteits.urbanops.module.garden.convert.treeinspection.TreeInspectionConvert; + +import static com.zteits.urbanops.framework.common.pojo.CommonResult.success; + +@Tag(name = "App - 行道树巡检与风险评估") +@RestController +@RequestMapping({"/garden/tree-inspection", "/business/tree-inspection"}) +@Validated +public class AppTreeInspectionController { + + @Resource + private TreeInspectionService treeInspectionService; + + @PostMapping("/create") + @Operation(summary = "提交巡检记录并评估") + public CommonResult createTreeInspection(@Valid @RequestBody TreeInspectionSaveReqVO createReqVO) { + return success(treeInspectionService.createTreeInspection(createReqVO)); + } + + @GetMapping("/get") + @Operation(summary = "获得巡检评估详情") + @Parameter(name = "id", description = "巡检记录编号", required = true, example = "12") + public CommonResult getTreeInspection(@RequestParam("id") Long id) { + return success(treeInspectionService.getTreeInspection(id)); + } + + @GetMapping("/page") + @Operation(summary = "分页查询单株树木的历史巡检记录") + public CommonResult> getTreeInspectionPage(@Valid TreeInspectionPageReqVO pageReqVO) { + PageResult pageResult = treeInspectionService.getTreeInspectionPage(pageReqVO); + PageResult respPageResult = new PageResult<>( + TreeInspectionConvert.INSTANCE.convertList(pageResult.getList()), + pageResult.getTotal() + ); + return success(respPageResult); + } + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/treeinspection/vo/TreeInspectionPageReqVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/treeinspection/vo/TreeInspectionPageReqVO.java new file mode 100644 index 0000000..30243bd --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/treeinspection/vo/TreeInspectionPageReqVO.java @@ -0,0 +1,20 @@ +package com.zteits.urbanops.module.garden.controller.app.treeinspection.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import jakarta.validation.constraints.NotNull; +import com.zteits.urbanops.framework.common.pojo.PageParam; + +@Schema(description = "小程序端 - 行道树巡检历史分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TreeInspectionPageReqVO extends PageParam { + + @Schema(description = "一树一档案ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "关联树木ID不能为空") + private Long treeId; + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/treeinspection/vo/TreeInspectionRespVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/treeinspection/vo/TreeInspectionRespVO.java new file mode 100644 index 0000000..ef97121 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/treeinspection/vo/TreeInspectionRespVO.java @@ -0,0 +1,187 @@ +package com.zteits.urbanops.module.garden.controller.app.treeinspection.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.zteits.urbanops.framework.common.util.json.databind.LocalDateTimeStringSerializer; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "小程序端 - 行道树巡检记录与评估详情 Response VO") +@Data +public class TreeInspectionRespVO { + + @Schema(description = "主键ID", example = "12") + private Long id; + + @Schema(description = "一树一档案ID", example = "1024") + private Long treeId; + + @Schema(description = "树木编号", example = "D0001-P1-0001") + private String treenumber; + + @Schema(description = "巡检时间") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + @JsonSerialize(using = LocalDateTimeStringSerializer.class) + private LocalDateTime inspectionTime; + + @Schema(description = "巡检人ID", example = "10001") + private Long inspectorId; + + @Schema(description = "巡检人姓名", example = "张三") + private String inspectorName; + + @Schema(description = "树根部位指标组") + private RootRespDTO root; + + @Schema(description = "根颈部位指标组") + private CollarRespDTO collar; + + @Schema(description = "主干部位指标组") + private TrunkRespDTO trunk; + + @Schema(description = "树冠部位指标组") + private CrownRespDTO crown; + + @Schema(description = "权重因子评估组") + private WeightRespDTO weight; + + @Schema(description = "风险评估结果组 (折叠面板)") + private ResultRespDTO result; + + + @Schema(description = "现状及处理措施组") + private StatusRespDTO status; + + @Data + @Schema(description = "树根部位指标详情") + public static class RootRespDTO { + @Schema(description = "根部病害:0-无真菌危害或腐朽,8-存在真菌危害或腐朽", example = "0") + private Integer disease; + + @Schema(description = "根系下扎情况:0-良好,7-存在隆起或盘根", example = "0") + private Integer anchorage; + + @Schema(description = "工程切根:0-无,5-存在", example = "0") + private Integer cutting; + } + + @Data + @Schema(description = "根颈部位指标详情") + public static class CollarRespDTO { + @Schema(description = "根颈木质部受损:0-无,5-<10%, 15-10%-30%, 25-30%-50%, 70->=50%", example = "5") + private Integer woodDamage; + + @Schema(description = "根颈树皮受损:0-<10%, 2-10%-30%, 4-30%-50%, 6->=50%", example = "0") + private Integer barkDamage; + + @Schema(description = "根颈松动:0-不存在,100-存在", example = "0") + private Integer loosening; + } + + @Data + @Schema(description = "主干部位指标详情") + public static class TrunkRespDTO { + @Schema(description = "主干木质部受损:0-无,5-<10%, 12-10%-30%, 20-30%-50%, 70->=50%", example = "0") + private Integer woodDamage; + + @Schema(description = "主干倾斜:0-<10度, 3-10-20度, 8-20-30度, 70->=30度", example = "0") + private Integer tilt; + + @Schema(description = "主干树皮受损:0-<10%, 1-10%-30%, 3-30%-50%, 5->=50%", example = "0") + private Integer barkDamage; + } + + @Data + @Schema(description = "树冠部位指标详情") + public static class CrownRespDTO { + @Schema(description = "易落枝:0-未发现,2-<1/10, 3->=1/10", example = "0") + private Integer looseBranch; + + @Schema(description = "枝干结合部异常:0-无,3-龟裂卷皮,5-腐烂未成洞,70-明显空洞或蛀干痕迹", example = "0") + private Integer collarAbnormal; + + @Schema(description = "树冠透风及平衡性:0-较好不偏冠, 1-偏冠或透风差不偏冠, 2-透风差明显偏冠但冠幅适中, 5-透风差冠幅大不偏冠, 8-透风差冠幅大且明显偏冠", example = "0") + private Integer ventilationBalance; + } + + @Data + @Schema(description = "权重因子评估详情") + public static class WeightRespDTO { + @Schema(description = "树种类型:深根性树种, 浅根性树种", example = "深根性树种") + private String treeSpeciesType; + + @Schema(description = "树种类型权重常数", example = "1.0") + private Double treeSpeciesWeight; + + @Schema(description = "栽植年限:栽植 10 年以内, 栽植 10-30 年, 栽植 30 年以上", example = "栽植 10-30 年") + private String plantingYears; + + @Schema(description = "栽植年限权重常数", example = "1.1") + private Double plantingYearsWeight; + + @Schema(description = "是否处于风口", example = "false") + private Boolean isWindCorridor; + + @Schema(description = "风口权重常数", example = "1.0") + private Double windCorridorWeight; + + @Schema(description = "树池类型:联通树池, 独立树池, 树池硬化", example = "联通树池") + private String treePoolType; + + @Schema(description = "树池类型权重常数", example = "1.0") + private Double treePoolWeight; + + @Schema(description = "树池宽度与胸径比:7 倍及以上, 5 倍-7 倍, 3 倍-5 倍, 3 倍以下", example = "7 倍及以上") + private String treePoolWidthDbhRatio; + + @Schema(description = "树池比权重常数", example = "1.0") + private Double treePoolRatioWeight; + } + + @Data + @Schema(description = "风险评估结果详情 (折叠面板)") + public static class ResultRespDTO { + @Schema(description = "是否进行应急评估", example = "true") + private Boolean isEmergency; + + @Schema(description = "应急评估极端天气风力:7 级及以下, 8-9 级, 10 级, 10 级以上", example = "8-9 级") + private String windPower; + + @Schema(description = "风力权重常数", example = "1.5") + private Double windPowerWeight; + + @Schema(description = "树木缺陷得分", example = "9") + private Integer defectScore; + + @Schema(description = "常规安全风险评估结果") + private EvaluationResultDTO normalResult; + + @Schema(description = "应急安全风险评估结果") + private EvaluationResultDTO emergencyResult; + } + + @Data + @Schema(description = "安全风险评估得分与等级结果") + public static class EvaluationResultDTO { + @Schema(description = "风险得分", example = "87.12") + private Double score; + + @Schema(description = "风险等级", example = "重度风险") + private String level; + } + + + + @Data + @Schema(description = "现状及处理措施详情") + public static class StatusRespDTO { + @Schema(description = "现场照片(最多5张)") + private List photos; + } + + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/treeinspection/vo/TreeInspectionSaveReqVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/treeinspection/vo/TreeInspectionSaveReqVO.java new file mode 100644 index 0000000..fe973ed --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/treeinspection/vo/TreeInspectionSaveReqVO.java @@ -0,0 +1,194 @@ +package com.zteits.urbanops.module.garden.controller.app.treeinspection.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import jakarta.validation.Valid; +import jakarta.validation.constraints.*; +import org.springframework.format.annotation.DateTimeFormat; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "小程序端 - 行道树巡检记录与评估新增/修改 Request VO") +@Data +public class TreeInspectionSaveReqVO { + + @Schema(description = "一树一档案ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "关联树木ID不能为空") + private Long treeId; + + @Schema(description = "巡检时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "巡检时间不能为空") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + private LocalDateTime inspectionTime; + + @Schema(description = "树根部位指标组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "树根部位指标不能为空") + @Valid + private RootDTO root; + + @Schema(description = "根颈部位指标组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "根颈部位指标不能为空") + @Valid + private CollarDTO collar; + + @Schema(description = "主干部位指标组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "主干部位指标不能为空") + @Valid + private TrunkDTO trunk; + + @Schema(description = "树冠部位指标组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "树冠部位指标不能为空") + @Valid + private CrownDTO crown; + + @Schema(description = "权重因子评估组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "权重因子评估组不能为空") + @Valid + private WeightDTO weight; + + @Schema(description = "风险评估结果组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "风险评估结果组不能为空") + @Valid + private ResultDTO result; + + + @Schema(description = "现状及处理措施组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "现状及处理措施组不能为空") + @Valid + private StatusDTO status; + + @Data + @Schema(description = "树根部位指标 DTO") + public static class RootDTO { + @Schema(description = "根部病害:0-无真菌危害或腐朽,8-存在真菌危害或腐朽", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "根部病害指标不能为空") + private Integer disease; + + @Schema(description = "根系下扎情况:0-良好,7-存在隆起或盘根", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "根系下扎情况指标不能为空") + private Integer anchorage; + + @Schema(description = "工程切根:0-无,5-存在", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "工程切根指标不能为空") + private Integer cutting; + } + + @Data + @Schema(description = "根颈部位指标 DTO") + public static class CollarDTO { + @Schema(description = "根颈木质部受损:0-无,5-<10%, 15-10%-30%, 25-30%-50%, 70->=50%", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + @NotNull(message = "根颈木质部受损指标不能为空") + private Integer woodDamage; + + @Schema(description = "根颈树皮受损:0-<10%, 2-10%-30%, 4-30%-50%, 6->=50%", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "根颈树皮受损指标不能为空") + private Integer barkDamage; + + @Schema(description = "根颈松动:0-不存在,100-存在", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "根颈松动指标不能为空") + private Integer loosening; + } + + @Data + @Schema(description = "主干部位指标 DTO") + public static class TrunkDTO { + @Schema(description = "主干木质部受损:0-无,5-<10%, 12-10%-30%, 20-30%-50%, 70->=50%", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "主干木质部受损指标不能为空") + private Integer woodDamage; + + @Schema(description = "主干倾斜:0-<10度, 3-10-20度, 8-20-30度, 70->=30度", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "主干倾斜指标不能为空") + private Integer tilt; + + @Schema(description = "主干树皮受损:0-<10%, 1-10%-30%, 3-30%-50%, 5->=50%", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "主干树皮受损指标不能为空") + private Integer barkDamage; + } + + @Data + @Schema(description = "树冠部位指标 DTO") + public static class CrownDTO { + @Schema(description = "易落枝:0-未发现,2-<1/10, 3->=1/10", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "易落枝指标不能为空") + private Integer looseBranch; + + @Schema(description = "枝干结合部异常:0-无,3-龟裂卷皮,5-腐烂未成洞,70-明显空洞或蛀干痕迹", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "枝干结合部异常指标不能为空") + private Integer collarAbnormal; + + @Schema(description = "树冠透风及平衡性:0-较好不偏冠, 1-偏冠或透风差不偏冠, 2-透风差明显偏冠但冠幅适中, 5-透风差冠幅大不偏冠, 8-透风差冠幅大且明显偏冠", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "树冠透风及平衡性指标不能为空") + private Integer ventilationBalance; + } + + @Data + @Schema(description = "权重因子评估 DTO") + public static class WeightDTO { + @Schema(description = "树种类型:深根性树种, 浅根性树种", requiredMode = Schema.RequiredMode.REQUIRED, example = "深根性树种") + @NotBlank(message = "树种类型不能为空") + private String treeSpeciesType; + + @Schema(description = "栽植年限:栽植 10 年以内, 栽植 10-30 年, 栽植 30 年以上", requiredMode = Schema.RequiredMode.REQUIRED, example = "栽植 10-30 年") + @NotBlank(message = "栽植年限不能为空") + private String plantingYears; + + @Schema(description = "是否处于风口", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否处于风口标识不能为空") + private Boolean isWindCorridor; + + @Schema(description = "树池类型:联通树池, 独立树池, 树池硬化", requiredMode = Schema.RequiredMode.REQUIRED, example = "联通树池") + @NotBlank(message = "树池类型不能为空") + private String treePoolType; + + @Schema(description = "树池宽度与胸径比:7 倍及以上, 5 倍-7 倍, 3 倍-5 倍, 3 倍以下", requiredMode = Schema.RequiredMode.REQUIRED, example = "7 倍及以上") + @NotBlank(message = "树池宽度与胸径比不能为空") + private String treePoolWidthDbhRatio; + } + + @Data + @Schema(description = "风险评估结果 DTO (折叠面板)") + public static class ResultDTO { + @Schema(description = "是否进行应急评估", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否应急评估标识不能为空") + private Boolean isEmergency; + + @Schema(description = "应急评估极端天气风力:7 级及以下, 8-9 级, 10 级, 10 级以上", example = "8-9 级") + private String windPower; + + @Schema(description = "树木缺陷得分", example = "9") + private Integer defectScore; + + @Schema(description = "常规安全风险评估结果") + private EvaluationResultDTO normalResult; + + @Schema(description = "应急安全风险评估结果") + private EvaluationResultDTO emergencyResult; + } + + @Data + @Schema(description = "安全风险评估得分与等级结果") + public static class EvaluationResultDTO { + @Schema(description = "风险得分", example = "87.12") + private Double score; + + @Schema(description = "风险等级", example = "重度风险") + private String level; + } + + + + @Data + @Schema(description = "现状及处理措施 DTO") + public static class StatusDTO { + @Schema(description = "现场照片(最多5张)") + @Size(max = 5, message = "最多只能上传5张现场照片") + private List photos; + } + + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/convert/treeinspection/TreeInspectionConvert.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/convert/treeinspection/TreeInspectionConvert.java new file mode 100644 index 0000000..59d8f1b --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/convert/treeinspection/TreeInspectionConvert.java @@ -0,0 +1,84 @@ +package com.zteits.urbanops.module.garden.convert.treeinspection; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; +import com.zteits.urbanops.module.garden.dal.dataobject.treeinspection.TreeInspectionDO; +import com.zteits.urbanops.module.garden.controller.app.treeinspection.vo.TreeInspectionSaveReqVO; +import com.zteits.urbanops.module.garden.controller.app.treeinspection.vo.TreeInspectionRespVO; +import java.util.List; + +@Mapper +public interface TreeInspectionConvert { + + TreeInspectionConvert INSTANCE = Mappers.getMapper(TreeInspectionConvert.class); + + @Mappings({ + @Mapping(source = "root.disease", target = "rootDisease"), + @Mapping(source = "root.anchorage", target = "rootAnchorage"), + @Mapping(source = "root.cutting", target = "rootCutting"), + @Mapping(source = "collar.woodDamage", target = "collarWoodDamage"), + @Mapping(source = "collar.barkDamage", target = "collarBarkDamage"), + @Mapping(source = "collar.loosening", target = "collarLoosening"), + @Mapping(source = "trunk.woodDamage", target = "trunkWoodDamage"), + @Mapping(source = "trunk.tilt", target = "trunkTilt"), + @Mapping(source = "trunk.barkDamage", target = "trunkBarkDamage"), + @Mapping(source = "crown.looseBranch", target = "crownLooseBranch"), + @Mapping(source = "crown.collarAbnormal", target = "crownCollarAbnormal"), + @Mapping(source = "crown.ventilationBalance", target = "crownVentilationBalance"), + @Mapping(source = "weight.treeSpeciesType", target = "treeSpeciesType"), + @Mapping(source = "weight.plantingYears", target = "plantingYears"), + @Mapping(source = "weight.isWindCorridor", target = "isWindCorridor"), + @Mapping(source = "weight.treePoolType", target = "treePoolType"), + @Mapping(source = "weight.treePoolWidthDbhRatio", target = "treePoolWidthDbhRatio"), + @Mapping(source = "result.isEmergency", target = "isEmergency"), + @Mapping(source = "result.windPower", target = "windPower"), + @Mapping(source = "status.photos", target = "photos"), + @Mapping(source = "result.defectScore", target = "defectScore"), + @Mapping(source = "result.normalResult.score", target = "normalScore"), + @Mapping(source = "result.normalResult.level", target = "normalLevel"), + @Mapping(source = "result.emergencyResult.score", target = "emergencyScore"), + @Mapping(source = "result.emergencyResult.level", target = "emergencyLevel") + }) + TreeInspectionDO convert(TreeInspectionSaveReqVO bean); + + @Mappings({ + @Mapping(source = "rootDisease", target = "root.disease"), + @Mapping(source = "rootAnchorage", target = "root.anchorage"), + @Mapping(source = "rootCutting", target = "root.cutting"), + @Mapping(source = "collarWoodDamage", target = "collar.woodDamage"), + @Mapping(source = "collarBarkDamage", target = "collar.barkDamage"), + @Mapping(source = "collarLoosening", target = "collar.loosening"), + @Mapping(source = "trunkWoodDamage", target = "trunk.woodDamage"), + @Mapping(source = "trunkTilt", target = "trunk.tilt"), + @Mapping(source = "trunkBarkDamage", target = "trunk.barkDamage"), + @Mapping(source = "crownLooseBranch", target = "crown.looseBranch"), + @Mapping(source = "crownCollarAbnormal", target = "crown.collarAbnormal"), + @Mapping(source = "crownVentilationBalance", target = "crown.ventilationBalance"), + @Mapping(source = "treeSpeciesType", target = "weight.treeSpeciesType"), + @Mapping(source = "treeSpeciesWeight", target = "weight.treeSpeciesWeight"), + @Mapping(source = "plantingYears", target = "weight.plantingYears"), + @Mapping(source = "plantingYearsWeight", target = "weight.plantingYearsWeight"), + @Mapping(source = "isWindCorridor", target = "weight.isWindCorridor"), + @Mapping(source = "windCorridorWeight", target = "weight.windCorridorWeight"), + @Mapping(source = "treePoolType", target = "weight.treePoolType"), + @Mapping(source = "treePoolWeight", target = "weight.treePoolWeight"), + @Mapping(source = "treePoolWidthDbhRatio", target = "weight.treePoolWidthDbhRatio"), + @Mapping(source = "treePoolRatioWeight", target = "weight.treePoolRatioWeight"), + @Mapping(source = "isEmergency", target = "result.isEmergency"), + @Mapping(source = "windPower", target = "result.windPower"), + @Mapping(source = "windPowerWeight", target = "result.windPowerWeight"), + @Mapping(source = "defectScore", target = "result.defectScore"), + @Mapping(source = "normalScore", target = "result.normalResult.score"), + @Mapping(source = "normalLevel", target = "result.normalResult.level"), + @Mapping(source = "emergencyScore", target = "result.emergencyResult.score"), + @Mapping(source = "emergencyLevel", target = "result.emergencyResult.level"), + @Mapping(source = "photos", target = "status.photos") + }) + TreeInspectionRespVO convert(TreeInspectionDO bean); + + + List convertList(List list); + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/gis/GisRoadDO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/gis/GisRoadDO.java new file mode 100644 index 0000000..bcf6b03 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/gis/GisRoadDO.java @@ -0,0 +1,149 @@ +package com.zteits.urbanops.module.garden.dal.dataobject.gis; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.zteits.urbanops.module.garden.util.TwoDecimalFloatSerializer; +import lombok.*; +import com.baomidou.mybatisplus.annotation.*; +import com.zteits.urbanops.framework.mybatis.core.dataobject.BaseDO; + +/** + * GIS 道路核对 DO - garden_gis_road 表 + * + * @author UrbanOps + */ +@TableName("garden_gis_road") +@KeySequence("garden_gis_road_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GisRoadDO extends BaseDO { + + @TableId + private Long id; + + /** 关联道路ID */ + private Long gisRoadId; + + /** 道路名称 */ + private String roadName; + + /** 业务线:yl-园林;wy-物业;sz-市政 */ + private String busiLine; + + /** 街道ID */ + private String streetId; + + /** 街道名称 */ + private String streetName; + + /** 养护级别: 10:特级;11:一级:12:二级;13:三级 */ + private Integer levelId; + + /** 道路属性 */ + private String roadAttr; + + /** 道路方位 W-西 E-东 N-北 S-南 */ + private String direction; + + /** 行道树绿化面积 */ + @JsonSerialize(using = TwoDecimalFloatSerializer.class) + private Float greenBeltTreeArea; + + /** 绿化带面积 */ + @JsonSerialize(using = TwoDecimalFloatSerializer.class) + private Float greenBeltArea; + + /** 总绿化带面积 */ + @JsonSerialize(using = TwoDecimalFloatSerializer.class) + private Float totalArea; + + /** 行道树(棵)面积 */ + @JsonSerialize(using = TwoDecimalFloatSerializer.class) + private Float treeArea; + + /** 起点经度 */ + private String startingLatitude; + + /** 起点纬度 */ + private String startingLongitude; + + /** 起点描述 */ + private String startingRemark; + + /** 终点经度 */ + private String endLatitude; + + /** 终点纬度 */ + private String endLongitude; + + /** 终点描述 */ + private String endRemark; + + /** 备注 */ + private String remark; + + /** 归属公司ID */ + private Long companyId; + + /** 部门ID */ + private Long deptId; + + /** 创建者名称 */ + private String creatorName; + + /** 更新者名称 */ + private String updaterName; + + // ========== GIS 核对相关字段 ========== + + /** GIS 图斑编号 */ + private String gisPlotCode; + + /** GIS 图斑名称 */ + private String gisPlotName; + + /** GIS 图斑面积 */ + @JsonSerialize(using = TwoDecimalFloatSerializer.class) + private Float gisPlotArea; + + /** GIS 绿地类型编码 */ + private String gisGreenTypeCode; + + /** GIS 绿地类型名称 */ + private String gisGreenTypeName; + + /** GIS 所属街道 */ + private String gisStreet; + + /** GIS 权属单位 */ + private String gisOwnerUnit; + + /** GIS 物业单位 */ + private String gisPropertyUnit; + + /** GIS 养护单位 */ + private String gisManageUnit; + + /** GIS 差异面积 */ + @JsonSerialize(using = TwoDecimalFloatSerializer.class) + private Float gisDiffArea; + + /** GIS 差异备注 */ + private String gisDiffRemark; + + /** 是否已关联 (0=未关联, 1=已关联) */ + private Integer gisIsRelated; + + /** 关联人 */ + private String gisRelatedUser; + + /** 关联时间 */ + private java.time.LocalDateTime gisRelatedTime; + + /** 多边形围栏边界坐标 WKT格式: POLYGON((x1 y1, x2 y2, ...)) WGS84坐标系 */ + private String gisPolygonCoords; + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/gis/GisTypeDO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/gis/GisTypeDO.java new file mode 100644 index 0000000..197ce5b --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/gis/GisTypeDO.java @@ -0,0 +1,37 @@ +package com.zteits.urbanops.module.garden.dal.dataobject.gis; + +import lombok.*; +import com.baomidou.mybatisplus.annotation.*; +import com.zteits.urbanops.framework.mybatis.core.dataobject.BaseDO; + +/** + * 公园绿地类型字典 DO + */ +@TableName("garden_gis_type") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GisTypeDO extends BaseDO { + + @TableId + private Long id; + + /** 类型编码 */ + private String typeCode; + + /** 类型名称 */ + private String typeName; + + /** 父级编码 */ + private String parentCode; + + /** 层级 */ + private Integer level; + + /** 排序 */ + private Integer sort; + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/road/RoadDO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/road/RoadDO.java index 14a06b7..34ea43b 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/road/RoadDO.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/road/RoadDO.java @@ -122,5 +122,16 @@ public class RoadDO extends BaseDO { */ private String updaterName; + // ========== GIS 关联字段 ========== + + /** GIS 图斑名称 */ + private String gisPlotName; + + /** GIS 图斑面积 */ + @JsonSerialize(using = TwoDecimalFloatSerializer.class) + private Float gisPlotArea; + + /** 是否关联GIS (0=未关联, 1=已关联) */ + private Integer gisIsRelated; } \ No newline at end of file diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/treeinspection/TreeInspectionDO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/treeinspection/TreeInspectionDO.java new file mode 100644 index 0000000..b24ac45 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/treeinspection/TreeInspectionDO.java @@ -0,0 +1,248 @@ +package com.zteits.urbanops.module.garden.dal.dataobject.treeinspection; + +import lombok.*; +import java.time.LocalDateTime; +import java.util.List; +import com.baomidou.mybatisplus.annotation.*; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import com.zteits.urbanops.framework.tenant.core.db.TenantBaseDO; + +/** + * 行道树巡检与安全风险评估记录 DO + * + * @author Antigravity + */ +@TableName(value = "garden_tree_inspection", autoResultMap = true) +@KeySequence("garden_tree_inspection_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TreeInspectionDO extends TenantBaseDO { + + /** + * 主键 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 一树一档案ID + */ + private Long treeId; + + /** + * 树木编号 + */ + private String treenumber; + + /** + * 巡检时间 + */ + private LocalDateTime inspectionTime; + + /** + * 巡检人ID + */ + private Long inspectorId; + + /** + * 巡检人姓名 + */ + private String inspectorName; + + /** + * 部门ID + */ + private Long deptId; + + // ========== 缺陷评估指标得分 ========== + + /** + * 根部病害:0-无真菌危害或腐朽,8-存在真菌危害或腐朽 + */ + private Integer rootDisease; + + /** + * 根系下扎情况:0-良好,7-存在隆起或盘根 + */ + private Integer rootAnchorage; + + /** + * 工程切根:0-无,5-存在 + */ + private Integer rootCutting; + + /** + * 根颈木质部受损:0-无,5-<10%, 15-10%-30%, 25-30%-50%, 70->=50% + */ + private Integer collarWoodDamage; + + /** + * 根颈树皮受损:0-<10%, 2-10%-30%, 4-30%-50%, 6->=50% + */ + private Integer collarBarkDamage; + + /** + * 根颈松动:0-不存在,100-存在 + */ + private Integer collarLoosening; + + /** + * 主干木质部受损:0-无,5-<10%, 12-10%-30%, 20-30%-50%, 70->=50% + */ + private Integer trunkWoodDamage; + + /** + * 主干倾斜:0-<10度, 3-10-20度, 8-20-30度, 70->=30度 + */ + private Integer trunkTilt; + + /** + * 主干树皮受损:0-<10%, 1-10%-30%, 3-30%-50%, 5->=50% + */ + private Integer trunkBarkDamage; + + /** + * 易落枝:0-未发现,2-<1/10, 3->=1/10 + */ + private Integer crownLooseBranch; + + /** + * 枝干结合部异常:0-无,3-龟裂卷皮,5-腐烂未成洞,70-明显空洞或蛀干痕迹 + */ + private Integer crownCollarAbnormal; + + /** + * 树冠透风及平衡性:0-较好不偏冠, 1-偏冠或透风差不偏冠, 2-透风差明显偏冠但冠幅适中, 5-透风差冠幅大不偏冠, 8-透风差冠幅大且明显偏冠 + */ + private Integer crownVentilationBalance; + + // ========== 权重因子指标值和赋权值 ========== + + /** + * 树种类型:深根性树种, 浅根性树种 + */ + private String treeSpeciesType; + + /** + * 树种类型权重:深根性-1.0, 浅根性-1.1 + */ + private Double treeSpeciesWeight; + + /** + * 栽植年限:栽植 10 年以内, 栽植 10-30 年, 栽植 30 年以上 + */ + private String plantingYears; + + /** + * 栽植年限权重:10年内-1.0, 10-30年-1.1, 30年以上-1.2 + */ + private Double plantingYearsWeight; + + /** + * 是否处于风口:0-否, 1-是 + */ + private Boolean isWindCorridor; + + /** + * 风口权重:否-1.0, 是-2.0 + */ + private Double windCorridorWeight; + + /** + * 树池类型:联通树池, 独立树池, 树池硬化 + */ + private String treePoolType; + + /** + * 树池类型权重:联通树池-1.0, 独立树池-1.2, 树池硬化-1.5 + */ + private Double treePoolWeight; + + /** + * 树池宽度与胸径比:7 倍及以上, 5 倍-7 倍, 3 倍-5 倍, 3 倍以下 + */ + private String treePoolWidthDbhRatio; + + /** + * 树池比权重:7倍及以上-1.0, 5-7倍-1.1, 3-5倍-1.2, 3倍以下-1.3 + */ + private Double treePoolRatioWeight; + + // ========== 应急评估及加权 ========== + + /** + * 是否进行应急评估:0-否, 1-是 + */ + private Boolean isEmergency; + + /** + * 应急评估极端天气风力:7 级及以下, 8-9 级, 10 级, 10 级以上 + */ + private String windPower; + + /** + * 风力权重:7级及以下-1.0, 8-9级-1.5, 10级-2.0, 10级以上-3.0 + */ + private Double windPowerWeight; + + // ========== 评估计算结果 ========== + + /** + * 树木缺陷得分 + */ + private Integer defectScore; + + /** + * 常规情况下安全风险得分 + */ + private Double normalScore; + + /** + * 常规情况下风险等级 + */ + private String normalLevel; + + /** + * 应急情况下安全风险得分 + */ + private Double emergencyScore; + + /** + * 应急情况下风险等级 + */ + private String emergencyLevel; + + // ========== 现状及处理措施 ========== + + /** + * 建议处置措施 + */ + private String treatmentSuggestion; + + /** + * 是否已处置:0-未处置, 1-已处置 + */ + private Boolean isTreated; + + /** + * 现场照片链接(JSON 数组) + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List photos; + + /** + * 评估单位(公司) + */ + private String estimatorCompany; + + /** + * 评估人 + */ + private String estimator; + + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/gis/GisRoadMapper.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/gis/GisRoadMapper.java new file mode 100644 index 0000000..3f5c1da --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/gis/GisRoadMapper.java @@ -0,0 +1,55 @@ +package com.zteits.urbanops.module.garden.dal.mysql.gis; + +import com.zteits.urbanops.framework.common.pojo.PageResult; +import com.zteits.urbanops.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.zteits.urbanops.framework.mybatis.core.mapper.BaseMapperX; +import com.zteits.urbanops.module.garden.dal.dataobject.gis.GisRoadDO; +import com.zteits.urbanops.module.garden.controller.admin.gis.vo.*; +import org.apache.ibatis.annotations.Mapper; +import java.util.*; + +/** + * GIS 道路核对 Mapper + */ +@Mapper +public interface GisRoadMapper extends BaseMapperX { + + /** XG 大类的子代码(不以 XG 开头,需特殊处理) */ + List XG_CHILD_CODES = Arrays.asList("RG", "AG", "BG", "MG", "WG", "SG", "UG"); + + /** 构建公共查询条件 */ + private LambdaQueryWrapperX buildQueryWrapper(GisRoadPageReqVO reqVO) { + LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX() + .likeIfPresent(GisRoadDO::getRoadName, reqVO.getRoadName()) + .eqIfPresent(GisRoadDO::getBusiLine, reqVO.getBusiLine()) + .eqIfPresent(GisRoadDO::getStreetId, reqVO.getStreetId()) + .likeIfPresent(GisRoadDO::getStreetName, reqVO.getStreetName()) + .eqIfPresent(GisRoadDO::getLevelId, reqVO.getLevelId()) + .eqIfPresent(GisRoadDO::getRoadAttr, reqVO.getRoadAttr()) + .eqIfPresent(GisRoadDO::getDirection, reqVO.getDirection()) + .likeIfPresent(GisRoadDO::getGisPlotCode, reqVO.getGisPlotCode()) + .likeIfPresent(GisRoadDO::getGisPlotName, reqVO.getGisPlotName()) + .eqIfPresent(GisRoadDO::getGisIsRelated, reqVO.getGisIsRelated()) + .eqIfPresent(GisRoadDO::getCompanyId, reqVO.getCompanyId()) + .eqIfPresent(GisRoadDO::getDeptId, reqVO.getDeptId()); + + if (reqVO.getGisTypeCode() != null) { + if ("XG".equals(reqVO.getGisTypeCode())) { + wrapper.in(GisRoadDO::getGisGreenTypeCode, XG_CHILD_CODES); + } else { + wrapper.likeRight(GisRoadDO::getGisGreenTypeCode, reqVO.getGisTypeCode()); + } + } + return wrapper.orderByDesc(GisRoadDO::getId); + } + + default PageResult selectPage(GisRoadPageReqVO reqVO) { + return selectPage(reqVO, buildQueryWrapper(reqVO)); + } + + /** 不分页查询全部记录(用于地图渲染) */ + default List selectAllList(GisRoadPageReqVO reqVO) { + return selectList(buildQueryWrapper(reqVO)); + } + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/gis/GisTypeMapper.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/gis/GisTypeMapper.java new file mode 100644 index 0000000..0227bc9 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/gis/GisTypeMapper.java @@ -0,0 +1,9 @@ +package com.zteits.urbanops.module.garden.dal.mysql.gis; + +import com.zteits.urbanops.framework.mybatis.core.mapper.BaseMapperX; +import com.zteits.urbanops.module.garden.dal.dataobject.gis.GisTypeDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface GisTypeMapper extends BaseMapperX { +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/homepage/HomepageSummaryMapper.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/homepage/HomepageSummaryMapper.java index c811deb..40d10be 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/homepage/HomepageSummaryMapper.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/homepage/HomepageSummaryMapper.java @@ -6,6 +6,7 @@ import com.zteits.urbanops.module.garden.controller.admin.homepage.vo.CommitTask import com.zteits.urbanops.module.garden.controller.admin.homepage.vo.CommitWorkOrderRespVo; import com.zteits.urbanops.module.garden.controller.admin.homepage.vo.CommitWorkOrderSourceRespVo; import com.zteits.urbanops.module.garden.controller.admin.homepage.vo.WorkOrderTrendRespVo; +import com.zteits.urbanops.module.garden.controller.app.homepage.vo.AppWorkOrderSummaryVo; import com.zteits.urbanops.module.garden.controller.app.homepage.vo.CommonTaskStatusVo; import com.zteits.urbanops.module.garden.controller.app.homepage.vo.TaskCompletionStatusRspVo; import com.zteits.urbanops.module.garden.controller.app.homepage.vo.TaskDetailsRspVo; @@ -206,4 +207,13 @@ public interface HomepageSummaryMapper extends BaseMapperX { * @Date 2026/1/12 0:45 */ List getUserAll(@Param("busiDate") String busiDate); + + /** + * @Author wangqian + * @Description 按单位+时间统计工单数量 总量 | 已完成| + * @Date 2026/5/26 22:46 + * @Param null + * @Return + */ + List countWorkOrder(@Param("queryType") String queryType,@Param("userId") Long userId, @Param("beginTime") String beginTime, @Param("endTime") String endTime); } diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/problemtype/GardenProblemTypeMapper.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/problemtype/GardenProblemTypeMapper.java index 8805a5a..25e0940 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/problemtype/GardenProblemTypeMapper.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/problemtype/GardenProblemTypeMapper.java @@ -6,6 +6,8 @@ import com.zteits.urbanops.framework.mybatis.core.mapper.BaseMapperX; import com.zteits.urbanops.module.garden.controller.admin.problemtype.vo.ProblemTypePageReqVO; import com.zteits.urbanops.module.garden.dal.dataobject.problemtype.GardenProblemTypeDO; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; import java.util.ArrayList; import java.util.List; @@ -63,25 +65,55 @@ public interface GardenProblemTypeMapper extends BaseMapperX() + .eq(GardenProblemTypeDO::getTypeName, typeName) + .eq(GardenProblemTypeDO::getParentCode, parentCode) + .eq(GardenProblemTypeDO::getLevel, level) + .last("LIMIT 1")); + } + + /** + * 绕过逻辑删除,直接查 typeCode 是否物理存在(用于编码生成时避免唯一键冲突) + */ + @Select("SELECT COUNT(1) FROM garden_problem_type WHERE type_code = #{code}") + int countByTypeCodeRaw(@Param("code") String code); + + /** * 生成下一个编码:父级编码 + 5位递增序号 * 如 parentCode=BIZ_GARDEN → BIZ_GARDEN00001, BIZ_GARDEN00002 + * 注意:加 LENGTH 条件避免匹配到子级编码;绕过逻辑删除检查确保唯一键可用 */ default String generateNextCode(String parentCode) { + int expectedLength = parentCode.length() + 5; List list = selectList(new LambdaQueryWrapperX() .likeRight(GardenProblemTypeDO::getTypeCode, parentCode) + .apply("LENGTH(type_code) = {0}", expectedLength) .orderByDesc(GardenProblemTypeDO::getTypeCode) .last("LIMIT 1")); + int seq; if (list == null || list.isEmpty()) { - return parentCode + "00001"; - } - String maxCode = list.get(0).getTypeCode(); - String seqStr = maxCode.substring(parentCode.length()); - try { - int seq = Integer.parseInt(seqStr) + 1; - return parentCode + String.format("%05d", seq); - } catch (NumberFormatException e) { - return parentCode + "00001"; + seq = 1; + } else { + String maxCode = list.get(0).getTypeCode(); + String seqStr = maxCode.substring(parentCode.length()); + try { + seq = Integer.parseInt(seqStr) + 1; + } catch (NumberFormatException e) { + seq = 1; + } } + // 跳过被逻辑删除记录占用的编码 + int maxAttempts = 100; + String nextCode; + do { + nextCode = parentCode + String.format("%05d", seq); + seq++; + } while (countByTypeCodeRaw(nextCode) > 0 && --maxAttempts > 0); + return nextCode; } } diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/road/RoadMapper.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/road/RoadMapper.java index 3cb4265..fe78a2b 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/road/RoadMapper.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/road/RoadMapper.java @@ -42,6 +42,7 @@ public interface RoadMapper extends BaseMapperX { .likeIfPresent(RoadDO::getCreatorName, reqVO.getCreatorName()) .betweenIfPresent(RoadDO::getCreateTime, reqVO.getCreateTime()) .likeIfPresent(RoadDO::getUpdaterName, reqVO.getUpdaterName()) + .eqIfPresent(RoadDO::getGisIsRelated, reqVO.getGisIsRelated()) .orderByDesc(RoadDO::getId)); } diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/treeinspection/TreeInspectionMapper.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/treeinspection/TreeInspectionMapper.java new file mode 100644 index 0000000..576664b --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/treeinspection/TreeInspectionMapper.java @@ -0,0 +1,26 @@ +package com.zteits.urbanops.module.garden.dal.mysql.treeinspection; + +import com.zteits.urbanops.framework.common.pojo.PageResult; +import com.zteits.urbanops.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.zteits.urbanops.framework.mybatis.core.mapper.BaseMapperX; +import com.zteits.urbanops.module.garden.dal.dataobject.treeinspection.TreeInspectionDO; +import com.zteits.urbanops.module.garden.controller.app.treeinspection.vo.TreeInspectionPageReqVO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 行道树巡检与安全风险评估记录 Mapper + * + * @author Antigravity + */ +@Mapper +public interface TreeInspectionMapper extends BaseMapperX { + + default PageResult selectPage(TreeInspectionPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(TreeInspectionDO::getTreeId, reqVO.getTreeId()) + .orderByDesc(TreeInspectionDO::getInspectionTime) + .orderByDesc(TreeInspectionDO::getId) + ); + } + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/enums/ErrorCodeConstants.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/enums/ErrorCodeConstants.java index 5cb7aa7..a7f3906 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/enums/ErrorCodeConstants.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/enums/ErrorCodeConstants.java @@ -150,5 +150,9 @@ public interface ErrorCodeConstants { // ========== 问题类型配置 1-100-009-000 ========== ErrorCode PROBLEM_TYPE_NOT_EXISTS = new ErrorCode(1100009000, "问题类型不存在"); ErrorCode PROBLEM_TYPE_CODE_EXISTS = new ErrorCode(1100009001, "问题类型编码已存在"); + ErrorCode PROBLEM_TYPE_NAME_DUPLICATE = new ErrorCode(1100009002, "同级下问题类型名称已存在,一级、二级、三级类型名称不能同时重复"); + + // ========== 一树一档案巡检与风险评估 1-100-008-000 ========== + ErrorCode TREE_INSPECTION_NOT_EXISTS = new ErrorCode(1100008001, "巡检评估记录不存在"); } diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/gis/GisRoadService.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/gis/GisRoadService.java new file mode 100644 index 0000000..5c96957 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/gis/GisRoadService.java @@ -0,0 +1,59 @@ +package com.zteits.urbanops.module.garden.service.gis; + +import java.util.*; +import com.zteits.urbanops.module.garden.controller.admin.gis.vo.*; +import com.zteits.urbanops.framework.common.pojo.PageResult; + +/** + * GIS 道路 Service 接口 + * + * @author UrbanOps + */ +public interface GisRoadService { + + /** + * 获得 GIS 道路分页 + * + * @param pageReqVO 分页查询 + * @return GIS 道路分页 + */ + PageResult getGisRoadPage(GisRoadPageReqVO pageReqVO); + + /** + * 获得 GIS 道路 + * + * @param id 编号 + * @return GIS 道路 + */ + GisRoadRespVO getGisRoad(Long id); + + /** + * 获得全部 GIS 道路列表(不分页,用于地图渲染) + * + * @param reqVO 查询条件 + * @return GIS 道路列表 + */ + List getGisRoadAllList(GisRoadPageReqVO reqVO); + + /** + * 关联道路到 GIS 图斑 + * + * @param reqVO 关联请求 + */ + void associateRoad(GisRoadAssociateReqVO reqVO); + + /** + * 取消 GIS 图斑与道路的关联 + * + * @param reqVO 取消关联请求 + */ + void disassociateRoad(GisRoadDisassociateReqVO reqVO); + + /** + * 将 garden_gis_road 表中所有坐标从 WGS84 转换为 GCJ02(高德坐标系) + * + * @return 转换记录数 + */ + int convertCoordinatesToGcj02(); + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/gis/GisRoadServiceImpl.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/gis/GisRoadServiceImpl.java new file mode 100644 index 0000000..4687d58 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/gis/GisRoadServiceImpl.java @@ -0,0 +1,230 @@ +package com.zteits.urbanops.module.garden.service.gis; + +import com.zteits.urbanops.module.system.api.dept.DeptApi; +import com.zteits.urbanops.module.system.api.dept.dto.DeptRespDTO; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import jakarta.annotation.Resource; +import org.springframework.util.CollectionUtils; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +import com.zteits.urbanops.module.garden.controller.admin.gis.vo.*; +import com.zteits.urbanops.module.garden.dal.dataobject.gis.GisRoadDO; +import com.zteits.urbanops.module.garden.dal.dataobject.road.RoadDO; +import com.zteits.urbanops.framework.common.pojo.PageResult; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.zteits.urbanops.framework.common.util.object.BeanUtils; +import com.zteits.urbanops.framework.security.core.util.SecurityFrameworkUtils; +import com.zteits.urbanops.module.garden.dal.mysql.gis.GisRoadMapper; +import com.zteits.urbanops.module.garden.dal.mysql.road.RoadMapper; +import com.zteits.urbanops.module.garden.util.GisCoordinateUtil; + +/** + * GIS 道路 Service 实现类 + * + * @author UrbanOps + */ +@Service +@Validated +public class GisRoadServiceImpl implements GisRoadService { + + @Resource + private GisRoadMapper gisRoadMapper; + + @Resource + private RoadMapper roadMapper; + + @Resource + private DeptApi deptApi; + + @Override + public PageResult getGisRoadPage(GisRoadPageReqVO pageReqVO) { + PageResult pageResult = gisRoadMapper.selectPage(pageReqVO); + PageResult page = BeanUtils.toBean(pageResult, GisRoadRespVO.class); + fillDeptAndCompanyName(page.getList()); + return page; + } + + @Override + public GisRoadRespVO getGisRoad(Long id) { + GisRoadDO gisRoadDO = gisRoadMapper.selectById(id); + if (gisRoadDO == null) { + return null; + } + GisRoadRespVO respVO = BeanUtils.toBean(gisRoadDO, GisRoadRespVO.class); + List list = new ArrayList<>(); + list.add(respVO); + fillDeptAndCompanyName(list); + return respVO; + } + + @Override + public List getGisRoadAllList(GisRoadPageReqVO reqVO) { + List list = gisRoadMapper.selectAllList(reqVO); + List voList = BeanUtils.toBean(list, GisRoadRespVO.class); + fillDeptAndCompanyName(voList); + return voList; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void associateRoad(GisRoadAssociateReqVO reqVO) { + GisRoadDO gisRoad = gisRoadMapper.selectById(reqVO.getId()); + if (gisRoad == null) { + throw new IllegalArgumentException("GIS道路不存在: id=" + reqVO.getId()); + } + RoadDO road = roadMapper.selectById(reqVO.getRoadId()); + if (road == null) { + throw new IllegalArgumentException("道路不存在: id=" + reqVO.getRoadId()); + } + + gisRoad.setGisRoadId(road.getId()); + gisRoad.setRoadName(road.getRoadName()); + gisRoad.setTotalArea(road.getTotalArea()); + gisRoad.setCompanyId(road.getCompanyId()); + gisRoad.setDeptId(road.getDeptId()); + + // 差异面积 = GIS面积 - 道路面积 + float gisArea = gisRoad.getGisPlotArea() != null ? gisRoad.getGisPlotArea() : 0; + float roadArea = road.getTotalArea() != null ? road.getTotalArea() : 0; + gisRoad.setGisDiffArea(gisArea - roadArea); + + gisRoad.setGisIsRelated(1); + gisRoad.setGisRelatedUser(SecurityFrameworkUtils.getLoginUserNickname()); + gisRoad.setGisRelatedTime(LocalDateTime.now()); + + gisRoadMapper.updateById(gisRoad); + + // 同步更新 garden_road 的 GIS 关联字段 + road.setGisPlotName(gisRoad.getGisPlotName()); + road.setGisPlotArea(gisRoad.getGisPlotArea()); + road.setGisIsRelated(1); + roadMapper.updateById(road); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void disassociateRoad(GisRoadDisassociateReqVO reqVO) { + GisRoadDO gisRoad = gisRoadMapper.selectById(reqVO.getId()); + if (gisRoad == null) { + throw new IllegalArgumentException("GIS道路不存在: id=" + reqVO.getId()); + } + + // 同步清空 garden_road 的 GIS 关联字段(LambdaUpdateWrapper 才能将字段设为 null) + if (gisRoad.getGisRoadId() != null) { + roadMapper.update(null, + new LambdaUpdateWrapper() + .eq(RoadDO::getId, gisRoad.getGisRoadId()) + .set(RoadDO::getGisPlotName, null) + .set(RoadDO::getGisPlotArea, null) + .set(RoadDO::getGisIsRelated, 0)); + } + + // 取消关联:仅将关联状态置为 0,其他字段因数据库 NOT NULL 约束保留原值 + gisRoadMapper.update(null, + new LambdaUpdateWrapper() + .eq(GisRoadDO::getId, gisRoad.getId()) + .set(GisRoadDO::getGisIsRelated, 0)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int convertCoordinatesToGcj02() { + List all = gisRoadMapper.selectList(); + if (CollectionUtils.isEmpty(all)) { + return 0; + } + int count = 0; + for (GisRoadDO road : all) { + boolean updated = false; + + // 转换起点坐标 + if (road.getStartingLatitude() != null && !road.getStartingLatitude().isBlank() + && road.getStartingLongitude() != null && !road.getStartingLongitude().isBlank()) { + try { + double lng = Double.parseDouble(road.getStartingLongitude()); + double lat = Double.parseDouble(road.getStartingLatitude()); + double[] gcj = GisCoordinateUtil.wgs84ToGcj02(lng, lat); + road.setStartingLongitude(String.format("%.6f", gcj[0])); + road.setStartingLatitude(String.format("%.6f", gcj[1])); + updated = true; + } catch (NumberFormatException ignored) { + } + } + + // 转换终点坐标 + if (road.getEndLatitude() != null && !road.getEndLatitude().isBlank() + && road.getEndLongitude() != null && !road.getEndLongitude().isBlank()) { + try { + double lng = Double.parseDouble(road.getEndLongitude()); + double lat = Double.parseDouble(road.getEndLatitude()); + double[] gcj = GisCoordinateUtil.wgs84ToGcj02(lng, lat); + road.setEndLongitude(String.format("%.6f", gcj[0])); + road.setEndLatitude(String.format("%.6f", gcj[1])); + updated = true; + } catch (NumberFormatException ignored) { + } + } + + // 转换多边形围栏坐标 + if (road.getGisPolygonCoords() != null && !road.getGisPolygonCoords().isBlank()) { + String converted = GisCoordinateUtil.transformPolygonWkt(road.getGisPolygonCoords()); + if (!converted.equals(road.getGisPolygonCoords())) { + road.setGisPolygonCoords(converted); + updated = true; + } + } + + if (updated) { + gisRoadMapper.updateById(road); + count++; + } + } + return count; + } + + /** + * 回填部门/公司名称 + */ + private void fillDeptAndCompanyName(List list) { + if (CollectionUtils.isEmpty(list)) { + return; + } + // 收集所有公司ID和部门ID + Set companyIds = list.stream() + .map(GisRoadRespVO::getCompanyId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + Set deptIds = list.stream() + .map(GisRoadRespVO::getDeptId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + deptIds.addAll(companyIds); + + if (deptIds.isEmpty()) { + return; + } + + Map deptMap = deptApi.getDeptMap(deptIds); + + list.forEach(vo -> { + if (vo.getDeptId() != null) { + DeptRespDTO dept = deptMap.get(vo.getDeptId()); + if (dept != null) { + vo.setDeptName(dept.getName()); + } + } + if (vo.getCompanyId() != null) { + DeptRespDTO company = deptMap.get(vo.getCompanyId()); + if (company != null) { + vo.setCompanyName(company.getName()); + } + } + }); + } + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/gis/GisTypeService.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/gis/GisTypeService.java new file mode 100644 index 0000000..ea83516 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/gis/GisTypeService.java @@ -0,0 +1,13 @@ +package com.zteits.urbanops.module.garden.service.gis; + +import java.util.List; +import com.zteits.urbanops.module.garden.dal.dataobject.gis.GisTypeDO; + +public interface GisTypeService { + + /** 按父级编码查子类型列表 */ + List listByParentCode(String parentCode); + + /** 按层级查类型列表 */ + List listByLevel(Integer level); +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/gis/GisTypeServiceImpl.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/gis/GisTypeServiceImpl.java new file mode 100644 index 0000000..25ac9db --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/gis/GisTypeServiceImpl.java @@ -0,0 +1,33 @@ +package com.zteits.urbanops.module.garden.service.gis; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.zteits.urbanops.module.garden.dal.dataobject.gis.GisTypeDO; +import com.zteits.urbanops.module.garden.dal.mysql.gis.GisTypeMapper; +import org.springframework.stereotype.Service; +import jakarta.annotation.Resource; +import java.util.List; + +@Service +public class GisTypeServiceImpl implements GisTypeService { + + @Resource + private GisTypeMapper gisTypeMapper; + + @Override + public List listByParentCode(String parentCode) { + return gisTypeMapper.selectList( + new LambdaQueryWrapper() + .eq(GisTypeDO::getParentCode, parentCode == null ? "" : parentCode) + .orderByAsc(GisTypeDO::getSort) + ); + } + + @Override + public List listByLevel(Integer level) { + return gisTypeMapper.selectList( + new LambdaQueryWrapper() + .eq(GisTypeDO::getLevel, level) + .orderByAsc(GisTypeDO::getSort) + ); + } +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/homepage/HomepageSummaryService.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/homepage/HomepageSummaryService.java index a3ab4b9..c535741 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/homepage/HomepageSummaryService.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/homepage/HomepageSummaryService.java @@ -1,13 +1,9 @@ package com.zteits.urbanops.module.garden.service.homepage; import com.zteits.urbanops.framework.common.pojo.PageResult; -import com.zteits.urbanops.module.garden.controller.app.homepage.vo.AppHomePageSummaryPageReqVO; -import com.zteits.urbanops.module.garden.controller.app.homepage.vo.AppHomePageSummaryReqVO; -import com.zteits.urbanops.module.garden.controller.app.homepage.vo.TaskCompletionStatusRspVo; -import com.zteits.urbanops.module.garden.controller.app.homepage.vo.TaskDetailsRspVo; +import com.zteits.urbanops.module.garden.controller.app.homepage.vo.*; import java.util.List; -import java.util.Map; /** * @Classname HomepageSummaryService @@ -42,4 +38,14 @@ public interface HomepageSummaryService { * @Return void */ public void countTaskNumberByUserId(String busiDate); + /** + * @Author wangqian + * @Description 工单情况统计 + * @Date 2026/5/26 22:34 + * @Param req + * @Return java.util.List + */ + AppWorkOrderSummaryRspVo iWorkOrderSummary(AppWorkOrderSummaryReqVo req); + + } diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/homepage/HomepageSummaryServiceImpl.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/homepage/HomepageSummaryServiceImpl.java index ccc02bc..4b8daff 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/homepage/HomepageSummaryServiceImpl.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/homepage/HomepageSummaryServiceImpl.java @@ -5,9 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.esotericsoftware.minlog.Log; import com.zteits.urbanops.framework.common.pojo.PageResult; -import com.zteits.urbanops.framework.common.util.date.DateUtils; import com.zteits.urbanops.framework.common.util.date.LocalDateTimeUtils; -import com.zteits.urbanops.framework.mybatis.core.query.QueryWrapperX; import com.zteits.urbanops.framework.mybatis.core.util.MyBatisUtils; import com.zteits.urbanops.framework.security.core.util.SecurityFrameworkUtils; import com.zteits.urbanops.module.garden.controller.app.homepage.vo.*; @@ -17,7 +15,7 @@ import com.zteits.urbanops.module.garden.dal.mysql.taskstatistics.TaskStatistics import com.zteits.urbanops.module.system.api.dept.DeptApi; import com.zteits.urbanops.module.system.api.permission.RoleApi; import com.zteits.urbanops.module.system.api.user.AdminUserApi; -import com.zteits.urbanops.module.system.api.user.dto.AdminUserRespDTO; +import com.zteits.urbanops.module.system.enums.common.CommonConstants; import jakarta.annotation.Resource; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -28,11 +26,12 @@ import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.LocalDateTime; -import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; +import static com.zteits.urbanops.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + /** * @Classname HomepageSummaryServiceImpl * @Description 首页统计实现 @@ -60,7 +59,7 @@ public class HomepageSummaryServiceImpl implements HomepageSummaryService{ @Override public List taskCompletionSummary(AppHomePageSummaryReqVO req) { - Long userId = SecurityFrameworkUtils.getLoginUserId(); + Long userId = getLoginUserId(); //巡查任务和养护任务已完成,未完成数量统计 //返回数据初始化 List days = getDays(req.getBeginTime(), req.getEndTime()); @@ -106,7 +105,7 @@ public class HomepageSummaryServiceImpl implements HomepageSummaryService{ @Override public PageResult queryTaskDetails(AppHomePageSummaryPageReqVO req) { - Long userId = SecurityFrameworkUtils.getLoginUserId(); + Long userId = getLoginUserId(); IPage mpPage = MyBatisUtils.buildPage(req); if (req.getQueryType() == 2) { //已完成 @@ -114,7 +113,7 @@ public class HomepageSummaryServiceImpl implements HomepageSummaryService{ } else { //待办 Long deptId = SecurityFrameworkUtils.getDeptId(); - List roleIds = roleApi.getRoleIdsByUserId(SecurityFrameworkUtils.getLoginUserId()); + List roleIds = roleApi.getRoleIdsByUserId(getLoginUserId()); mpPage = homepageSummaryMapper.queryPendingTaskDetails(mpPage, roleIds, userId, deptId, LocalDateTime.now()); } return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); @@ -287,6 +286,66 @@ public class HomepageSummaryServiceImpl implements HomepageSummaryService{ //批量保存 taskStatisticsMapper.insertBatch(taskStatisticsDOList); } + + @Override + public AppWorkOrderSummaryRspVo iWorkOrderSummary(AppWorkOrderSummaryReqVo req) { + // 1. 创建返回对象 + AppWorkOrderSummaryRspVo respVo = new AppWorkOrderSummaryRspVo(); + + Long userId = null; + //部派工单 + if ("1".equals(req.getQueryType())) { + // 全域督察员查询自己发起的工单 + List userIds = roleApi.getUserIdsByRoleCode(CommonConstants.INSPECTOR_ROLE_KEY); + if (!userIds.contains(getLoginUserId())) { + userId = getLoginUserId(); + } + } + // 2. 查询工单列表 + List workOrderList = homepageSummaryMapper.countWorkOrder( + req.getQueryType(), + userId, + req.getBeginTime(), + req.getEndTime() + ); + + // 3. 提取公司名称列表(修复:toList() 或 toList()) + List companyNameList = workOrderList.stream() + .map(AppWorkOrderSummaryVo::getCompanyName) + .collect(Collectors.toList()); + + // 4. 总数量求和 + int totalNum = workOrderList.stream() + .mapToInt(AppWorkOrderSummaryVo::getTotal) + .sum(); + + List percents; + if (totalNum <= 0) { + percents = Collections.singletonList(0); + } else { + // 3. 先计算四舍五入占比 + List tempPercents = workOrderList.stream() + .map(vo -> (int) Math.round((vo.getTotal() * 100.0) / totalNum)) + .collect(Collectors.toList()); + + // 4. 核心:修正总和 = 100%(把差值补到最后一项) + int sum = tempPercents.stream().mapToInt(Integer::intValue).sum(); + int diff = 100 - sum; + if (diff != 0 && !tempPercents.isEmpty()) { + int lastIndex = tempPercents.size() - 1; + tempPercents.set(lastIndex, tempPercents.get(lastIndex) + diff); + } + percents = tempPercents; + } + // 6. 封装返回数据 + respVo.setLegend(companyNameList); + respVo.setTable(workOrderList); + respVo.setTotalNum(String.valueOf(totalNum)); // 转字符串 + respVo.setPercents(percents); //占比 + // 7. 包装成 List 返回(因为方法声明返回 List) + return respVo; + } + /** * @Author wangqian * @Description 初始化对象集合 diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/problemtype/GardenProblemTypeService.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/problemtype/GardenProblemTypeService.java index 3c3bcc7..fc18530 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/problemtype/GardenProblemTypeService.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/problemtype/GardenProblemTypeService.java @@ -2,7 +2,9 @@ package com.zteits.urbanops.module.garden.service.problemtype; import com.zteits.urbanops.module.garden.controller.admin.problemtype.vo.ProblemTypePageReqVO; import com.zteits.urbanops.module.garden.controller.admin.problemtype.vo.ProblemTypeSaveReqVO; +import com.zteits.urbanops.module.garden.controller.app.problemtype.vo.ProblemTypeCascadeRespVO; import com.zteits.urbanops.module.garden.dal.dataobject.problemtype.GardenProblemTypeDO; +import com.zteits.urbanops.framework.common.pojo.CommonResult; import com.zteits.urbanops.framework.common.pojo.PageResult; import jakarta.validation.Valid; @@ -63,6 +65,14 @@ public interface GardenProblemTypeService { List getProblemTypeByParentCode(String parentCode, Integer level); /** + * 获取问题类型级联树 + * + * @param parentCode 父级编码(为空则返回顶级树) + * @return 级联树列表 + */ + List getProblemTypeTree(String parentCode); + + /** * 根据父级编码生成下一个类型编码 * * @param parentCode 父级编码 diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/problemtype/GardenProblemTypeServiceImpl.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/problemtype/GardenProblemTypeServiceImpl.java index 89e4928..4cc858e 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/problemtype/GardenProblemTypeServiceImpl.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/problemtype/GardenProblemTypeServiceImpl.java @@ -3,13 +3,18 @@ package com.zteits.urbanops.module.garden.service.problemtype; import com.zteits.urbanops.framework.common.enums.CommonStatusEnum; import com.zteits.urbanops.module.garden.controller.admin.problemtype.vo.ProblemTypePageReqVO; import com.zteits.urbanops.module.garden.controller.admin.problemtype.vo.ProblemTypeSaveReqVO; +import com.zteits.urbanops.module.garden.controller.app.problemtype.vo.ProblemTypeCascadeRespVO; import com.zteits.urbanops.module.system.dal.dataobject.dict.DictDataDO; import com.zteits.urbanops.module.system.service.dict.DictDataService; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; +import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; import com.zteits.urbanops.module.garden.dal.dataobject.problemtype.GardenProblemTypeDO; import com.zteits.urbanops.framework.common.pojo.PageResult; @@ -39,6 +44,9 @@ public class GardenProblemTypeServiceImpl implements GardenProblemTypeService { public Long createProblemType(ProblemTypeSaveReqVO createReqVO) { // 校验编码唯一性 validateTypeCodeUnique(createReqVO.getTypeCode(), null); + // 校验名称在同级父节点下唯一 + validateTypeNameUniqueUnderParent(createReqVO.getTypeName(), + createReqVO.getParentCode(), createReqVO.getLevel(), null); // 插入 GardenProblemTypeDO problemType = BeanUtils.toBean(createReqVO, GardenProblemTypeDO.class); gardenProblemTypeMapper.insert(problemType); @@ -77,6 +85,15 @@ public class GardenProblemTypeServiceImpl implements GardenProblemTypeService { } } + private void validateTypeNameUniqueUnderParent(String typeName, String parentCode, + Integer level, Long excludeId) { + GardenProblemTypeDO exist = gardenProblemTypeMapper.selectByNameAndParentAndLevel( + typeName, parentCode, level); + if (exist != null && (excludeId == null || !exist.getId().equals(excludeId))) { + throw exception(PROBLEM_TYPE_NAME_DUPLICATE); + } + } + @Override public GardenProblemTypeDO getProblemType(Long id) { return gardenProblemTypeMapper.selectById(id); @@ -93,6 +110,74 @@ public class GardenProblemTypeServiceImpl implements GardenProblemTypeService { } @Override + public List getProblemTypeTree(String parentCode) { + List allList; + + if (parentCode == null) { + allList = gardenProblemTypeMapper.selectList( + new com.zteits.urbanops.framework.mybatis.core.query.LambdaQueryWrapperX() + .eq(GardenProblemTypeDO::getStatus, 1) + .orderByAsc(GardenProblemTypeDO::getSort) + .orderByAsc(GardenProblemTypeDO::getId)); + } else { + List level2List = gardenProblemTypeMapper.selectByParentCodeAndLevel(parentCode, 2); + + List level2Codes = level2List.stream() + .map(GardenProblemTypeDO::getTypeCode) + .collect(Collectors.toList()); + + List level3List = new ArrayList<>(); + if (!level2Codes.isEmpty()) { + level3List = gardenProblemTypeMapper.selectList( + new com.zteits.urbanops.framework.mybatis.core.query.LambdaQueryWrapperX() + .eq(GardenProblemTypeDO::getStatus, 1) + .in(GardenProblemTypeDO::getParentCode, level2Codes)); + } + + allList = new ArrayList<>(); + allList.addAll(level2List); + allList.addAll(level3List); + } + + Map> childrenMap = allList.stream() + .filter(item -> item.getParentCode() != null && !item.getParentCode().equals(item.getTypeCode())) + .collect(Collectors.groupingBy(GardenProblemTypeDO::getParentCode)); + + List result = new ArrayList<>(); + if (parentCode == null) { + List rootList = allList.stream() + .filter(item -> item.getParentCode() == null || item.getParentCode().equals(item.getTypeCode())) + .collect(Collectors.toList()); + for (GardenProblemTypeDO root : rootList) { + ProblemTypeCascadeRespVO vo = buildTreeNode(root, childrenMap); + result.add(vo); + } + } else { + List childrenList = allList.stream() + .filter(item -> parentCode.equals(item.getParentCode())) + .collect(Collectors.toList()); + for (GardenProblemTypeDO child : childrenList) { + ProblemTypeCascadeRespVO vo = buildTreeNode(child, childrenMap); + result.add(vo); + } + } + return result; + } + + private ProblemTypeCascadeRespVO buildTreeNode(GardenProblemTypeDO item, Map> childrenMap) { + ProblemTypeCascadeRespVO vo = new ProblemTypeCascadeRespVO(); + vo.setLabel(item.getTypeName()); + vo.setValue(item.getTypeCode()); + List children = childrenMap.get(item.getTypeCode()); + if (children != null && !children.isEmpty()) { + for (GardenProblemTypeDO child : children) { + vo.getChildren().add(buildTreeNode(child, childrenMap)); + } + } + return vo; + } + + @Override public String generateNextCode(String parentCode) { return gardenProblemTypeMapper.generateNextCode(parentCode); } @@ -114,6 +199,10 @@ public class GardenProblemTypeServiceImpl implements GardenProblemTypeService { entity.setStatus(CommonStatusEnum.ENABLE.getStatus()); gardenProblemTypeMapper.insert(entity); count++; + } else if (!Objects.equals(exist.getTypeName(), dict.getLabel())) { + exist.setTypeName(dict.getLabel()); + gardenProblemTypeMapper.updateById(exist); + count++; } } return count; diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/treeinspection/TreeInspectionService.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/treeinspection/TreeInspectionService.java new file mode 100644 index 0000000..a786c9b --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/treeinspection/TreeInspectionService.java @@ -0,0 +1,39 @@ +package com.zteits.urbanops.module.garden.service.treeinspection; + +import jakarta.validation.Valid; +import com.zteits.urbanops.framework.common.pojo.PageResult; +import com.zteits.urbanops.module.garden.dal.dataobject.treeinspection.TreeInspectionDO; +import com.zteits.urbanops.module.garden.controller.app.treeinspection.vo.*; + +/** + * 行道树巡检与安全风险评估业务 Service 接口 + * + * @author Antigravity + */ +public interface TreeInspectionService { + + /** + * 创建行道树巡检与评估记录 + * + * @param createReqVO 创建信息 + * @return 记录ID + */ + Long createTreeInspection(@Valid TreeInspectionSaveReqVO createReqVO); + + /** + * 获得行道树巡检与评估详情 + * + * @param id 编号 + * @return 嵌套结构的详情 VO + */ + TreeInspectionRespVO getTreeInspection(Long id); + + /** + * 分页查询单株树木的历史巡检记录 + * + * @param pageReqVO 分页查询请求 + * @return 巡检历史记录分页 + */ + PageResult getTreeInspectionPage(TreeInspectionPageReqVO pageReqVO); + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/treeinspection/TreeInspectionServiceImpl.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/treeinspection/TreeInspectionServiceImpl.java new file mode 100644 index 0000000..178b73b --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/treeinspection/TreeInspectionServiceImpl.java @@ -0,0 +1,212 @@ +package com.zteits.urbanops.module.garden.service.treeinspection; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; +import org.springframework.transaction.annotation.Transactional; +import jakarta.annotation.Resource; + +import com.zteits.urbanops.framework.common.pojo.PageResult; +import com.zteits.urbanops.framework.common.exception.util.ServiceExceptionUtil; +import com.zteits.urbanops.framework.security.core.util.SecurityFrameworkUtils; +import com.zteits.urbanops.module.garden.dal.dataobject.treeinspection.TreeInspectionDO; +import com.zteits.urbanops.module.garden.dal.dataobject.tree.TreeDO; +import com.zteits.urbanops.module.garden.dal.mysql.treeinspection.TreeInspectionMapper; +import com.zteits.urbanops.module.garden.dal.mysql.tree.TreeMapper; +import com.zteits.urbanops.module.garden.enums.ErrorCodeConstants; +import com.zteits.urbanops.module.garden.controller.app.treeinspection.vo.*; +import com.zteits.urbanops.module.garden.convert.treeinspection.TreeInspectionConvert; + +/** + * 行道树巡检与安全风险评估业务 Service 实现类 + * + * @author Antigravity + */ +@Service +@Validated +@Slf4j +public class TreeInspectionServiceImpl implements TreeInspectionService { + + @Resource + private TreeInspectionMapper treeInspectionMapper; + + @Resource + private TreeMapper treeMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createTreeInspection(TreeInspectionSaveReqVO createReqVO) { + // 1. 校验树木档案是否存在 + TreeDO tree = treeMapper.selectById(createReqVO.getTreeId()); + if (tree == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.TREE_NOT_EXISTS); + } + + // 2. 转换数据并组装 DO 实体 + TreeInspectionDO entity = TreeInspectionConvert.INSTANCE.convert(createReqVO); + entity.setTreenumber(tree.getTreenumber()); + + // 3. 填充用户信息与部门信息 + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + String nickname = SecurityFrameworkUtils.getLoginUserNickname(); + Long deptId = SecurityFrameworkUtils.getLoginUserDeptId(); + + entity.setInspectorId(loginUserId); + entity.setInspectorName(nickname != null ? nickname : "巡检员"); + if (entity.getDeptId() == null) { + entity.setDeptId(deptId != null ? deptId : tree.getDeptId()); + } + + // 4. 补齐各因子的权重指标(即使由前端打分,后端依然补齐数据库里各个细节因子权重以支持底层的统计与分析) + double treeSpeciesWeight = "浅根性树种".equals(entity.getTreeSpeciesType()) ? 1.1 : 1.0; + entity.setTreeSpeciesWeight(treeSpeciesWeight); + + double plantingYearsWeight = getPlantingYearsWeight(entity.getPlantingYears()); + entity.setPlantingYearsWeight(plantingYearsWeight); + + double windCorridorWeight = Boolean.TRUE.equals(entity.getIsWindCorridor()) ? 2.0 : 1.0; + entity.setWindCorridorWeight(windCorridorWeight); + + double treePoolWeight = getTreePoolWeight(entity.getTreePoolType()); + entity.setTreePoolWeight(treePoolWeight); + + double treePoolRatioWeight = getTreePoolRatioWeight(entity.getTreePoolWidthDbhRatio()); + entity.setTreePoolRatioWeight(treePoolRatioWeight); + + if (Boolean.TRUE.equals(entity.getIsEmergency())) { + double windPowerWeight = getWindPowerWeight(entity.getWindPower()); + entity.setWindPowerWeight(windPowerWeight); + } else { + entity.setWindPowerWeight(1.0); + } + + // 5. 风险打分计算(前端计算结果存储优先,后端自动补齐防空兜底) + if (entity.getDefectScore() == null + || entity.getNormalScore() == null + || entity.getNormalLevel() == null) { + + // 后端兜底自动缺陷打分 + int calculatedSum = entity.getRootDisease() + + entity.getRootAnchorage() + + entity.getRootCutting() + + entity.getCollarWoodDamage() + + entity.getCollarBarkDamage() + + entity.getCollarLoosening() + + entity.getTrunkWoodDamage() + + entity.getTrunkTilt() + + entity.getTrunkBarkDamage() + + entity.getCrownLooseBranch() + + entity.getCrownCollarAbnormal() + + entity.getCrownVentilationBalance(); + + int defectScore = calculatedSum; + // 一票否决规则 + if (entity.getCollarLoosening() == 100) { + defectScore = 100; + } else if (entity.getCollarWoodDamage() == 70 + || entity.getTrunkWoodDamage() == 70 + || entity.getTrunkTilt() == 70 + || entity.getCrownCollarAbnormal() == 70) { + defectScore = Math.max(calculatedSum, 70); + } + entity.setDefectScore(defectScore); + + // 计算常规安全得分 + double normalScore = defectScore * treeSpeciesWeight * plantingYearsWeight * windCorridorWeight * treePoolWeight * treePoolRatioWeight; + normalScore = Math.round(normalScore * 100.0) / 100.0; + entity.setNormalScore(normalScore); + entity.setNormalLevel(getRiskLevelName(normalScore)); + } + + // 6. 应急打分补齐(若前端已计算则信任前端结果,否则后端自动补齐应急兜底) + if (Boolean.TRUE.equals(entity.getIsEmergency())) { + if (entity.getEmergencyScore() == null || entity.getEmergencyLevel() == null) { + double windPowerWeight = entity.getWindPowerWeight(); + double emergencyScore = entity.getNormalScore() * windPowerWeight; + emergencyScore = Math.round(emergencyScore * 100.0) / 100.0; + entity.setEmergencyScore(emergencyScore); + entity.setEmergencyLevel(getRiskLevelName(emergencyScore)); + } + } else { + entity.setEmergencyScore(null); + entity.setEmergencyLevel(null); + } + + // 7. 保存到数据库 + treeInspectionMapper.insert(entity); + return entity.getId(); + } + + @Override + public TreeInspectionRespVO getTreeInspection(Long id) { + TreeInspectionDO entity = treeInspectionMapper.selectById(id); + if (entity == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.TREE_INSPECTION_NOT_EXISTS); + } + return TreeInspectionConvert.INSTANCE.convert(entity); + } + + @Override + public PageResult getTreeInspectionPage(TreeInspectionPageReqVO pageReqVO) { + return treeInspectionMapper.selectPage(pageReqVO); + } + + // ========== 权重因子规则辅助类 ========== + + private double getPlantingYearsWeight(String years) { + if (years == null) return 1.0; + if (years.contains("30年")) return 1.2; + if (years.contains("10-30")) return 1.1; + if (years.contains("10年")) return 1.0; + return 1.0; + } + + private double getTreePoolWeight(String type) { + if (type == null) return 1.0; + if (type.contains("硬化")) return 1.5; + if (type.contains("独立")) return 1.2; + if (type.contains("联通")) return 1.0; + return 1.0; + } + + private double getTreePoolRatioWeight(String ratio) { + if (ratio == null) return 1.0; + if (ratio.contains("3倍以下") || ratio.contains("3 倍以下") || ratio.contains("小于3")) { + return 1.3; + } + if (ratio.contains("3-5") || ratio.contains("3至5") || ratio.contains("3倍") || ratio.contains("3 倍")) { + return 1.2; + } + if (ratio.contains("5-7") || ratio.contains("5至7") || ratio.contains("5倍") || ratio.contains("5 倍")) { + return 1.1; + } + if (ratio.contains("7倍") || ratio.contains("7 倍") || ratio.contains("大于7")) { + return 1.0; + } + return 1.0; + } + + private double getWindPowerWeight(String power) { + if (power == null) return 1.0; + if (power.contains("10级以上") || power.contains("10 级以上")) return 3.0; + if (power.contains("10级") || power.contains("10 级")) return 2.0; + if (power.contains("8-9")) return 1.5; + if (power.contains("7级") || power.contains("7 级")) return 1.0; + return 1.0; + } + + private String getRiskLevelName(double score) { + if (score < 10.0) { + return "I级 (基本无风险)"; + } else if (score < 30.0) { + return "II级 (轻度风险)"; + } else if (score < 70.0) { + return "III级 (中度风险)"; + } else if (score < 100.0) { + return "IV级 (重度风险)"; + } else { + return "V级 (极度风险)"; + } + } + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/util/GisCoordinateUtil.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/util/GisCoordinateUtil.java new file mode 100644 index 0000000..d4719f5 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/util/GisCoordinateUtil.java @@ -0,0 +1,86 @@ +package com.zteits.urbanops.module.garden.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * GIS 坐标转换工具 — WGS84 ↔ GCJ02(高德坐标系) + */ +public class GisCoordinateUtil { + + private static final double PI = Math.PI; + private static final double A = 6378245.0; // 长半轴 + private static final double EE = 0.00669342162296594323; // 偏心率平方 + + /** + * WGS84 转 GCJ02(高德) + */ + public static double[] wgs84ToGcj02(double lng, double lat) { + if (outOfChina(lng, lat)) { + return new double[]{lng, lat}; + } + double dLat = transformLat(lng - 105.0, lat - 35.0); + double dLng = transformLng(lng - 105.0, lat - 35.0); + double radLat = lat / 180.0 * PI; + double magic = Math.sin(radLat); + magic = 1 - EE * magic * magic; + double sqrtMagic = Math.sqrt(magic); + dLat = (dLat * 180.0) / ((A * (1 - EE)) / (magic * sqrtMagic) * PI); + dLng = (dLng * 180.0) / (A / sqrtMagic * Math.cos(radLat) * PI); + return new double[]{lng + dLng, lat + dLat}; + } + + /** + * GCJ02 转 WGS84 + */ + public static double[] gcj02ToWgs84(double lng, double lat) { + if (outOfChina(lng, lat)) { + return new double[]{lng, lat}; + } + double[] gcj = wgs84ToGcj02(lng, lat); + return new double[]{lng * 2 - gcj[0], lat * 2 - gcj[1]}; + } + + /** 匹配 WKT 中的坐标对:数字 空格 数字 */ + private static final Pattern COORD_PAIR = Pattern.compile("(\\d+\\.?\\d*)\\s+(\\d+\\.?\\d*)"); + + /** + * 转换 WKT 字符串中的所有坐标对(WGS84 → GCJ02) + * 支持 POLYGON、MULTIPOLYGON 等任意 WKT 格式 + */ + public static String transformPolygonWkt(String wkt) { + if (wkt == null || wkt.isBlank()) { + return wkt; + } + Matcher m = COORD_PAIR.matcher(wkt); + StringBuilder sb = new StringBuilder(); + while (m.find()) { + double lng = Double.parseDouble(m.group(1)); + double lat = Double.parseDouble(m.group(2)); + double[] gcj = wgs84ToGcj02(lng, lat); + m.appendReplacement(sb, String.format("%.6f %.6f", gcj[0], gcj[1])); + } + m.appendTail(sb); + return sb.toString(); + } + + private static boolean outOfChina(double lng, double lat) { + return lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271; + } + + private static double transformLat(double x, double y) { + double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x)); + ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0; + ret += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0; + ret += (160.0 * Math.sin(y / 12.0 * PI) + 320.0 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0; + return ret; + } + + private static double transformLng(double x, double y) { + double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x)); + ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0; + ret += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0; + ret += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0; + return ret; + } +} diff --git a/urbanops-module-garden/src/main/resources/mapper/homepage/HomepageSummaryMapper.xml b/urbanops-module-garden/src/main/resources/mapper/homepage/HomepageSummaryMapper.xml index d66ea61..b42e5a6 100644 --- a/urbanops-module-garden/src/main/resources/mapper/homepage/HomepageSummaryMapper.xml +++ b/urbanops-module-garden/src/main/resources/mapper/homepage/HomepageSummaryMapper.xml @@ -587,4 +587,33 @@ INNER JOIN dept_recursive dr ON u.dept_id = dr.id GROUP BY u.id, u.nickname, u.dept_id, u.busi_line, dr.companyId, dr.companyName; + + + + + diff --git a/urbanops-module-garden/src/test/java/com/zteits/urbanops/module/garden/service/treeinspection/TreeInspectionServiceImplTest.java b/urbanops-module-garden/src/test/java/com/zteits/urbanops/module/garden/service/treeinspection/TreeInspectionServiceImplTest.java new file mode 100644 index 0000000..759ad9f --- /dev/null +++ b/urbanops-module-garden/src/test/java/com/zteits/urbanops/module/garden/service/treeinspection/TreeInspectionServiceImplTest.java @@ -0,0 +1,246 @@ +package com.zteits.urbanops.module.garden.service.treeinspection; + +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import java.time.LocalDateTime; +import java.util.Collections; + +import com.zteits.urbanops.framework.common.pojo.PageResult; +import com.zteits.urbanops.framework.test.core.ut.BaseMockitoUnitTest; +import com.zteits.urbanops.module.garden.dal.dataobject.treeinspection.TreeInspectionDO; +import com.zteits.urbanops.module.garden.dal.dataobject.tree.TreeDO; +import com.zteits.urbanops.module.garden.dal.mysql.treeinspection.TreeInspectionMapper; +import com.zteits.urbanops.module.garden.dal.mysql.tree.TreeMapper; +import com.zteits.urbanops.module.garden.controller.app.treeinspection.vo.*; +import com.zteits.urbanops.module.garden.enums.ErrorCodeConstants; + +import static com.zteits.urbanops.framework.test.core.util.AssertUtils.assertServiceException; +import static com.zteits.urbanops.framework.test.core.util.RandomUtils.randomLongId; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +public class TreeInspectionServiceImplTest extends BaseMockitoUnitTest { + + @InjectMocks + private TreeInspectionServiceImpl treeInspectionService; + + @Mock + private TreeInspectionMapper treeInspectionMapper; + + @Mock + private TreeMapper treeMapper; + + private TreeInspectionSaveReqVO createReqVOBase() { + TreeInspectionSaveReqVO req = new TreeInspectionSaveReqVO(); + req.setTreeId(1024L); + req.setInspectionTime(LocalDateTime.now()); + + // 树根 + TreeInspectionSaveReqVO.RootDTO root = new TreeInspectionSaveReqVO.RootDTO(); + root.setDisease(0); + root.setAnchorage(0); + root.setCutting(0); + req.setRoot(root); + + // 根颈 + TreeInspectionSaveReqVO.CollarDTO collar = new TreeInspectionSaveReqVO.CollarDTO(); + collar.setWoodDamage(5); // 扣 5 分 + collar.setBarkDamage(0); + collar.setLoosening(0); + req.setCollar(collar); + + // 主干 + TreeInspectionSaveReqVO.TrunkDTO trunk = new TreeInspectionSaveReqVO.TrunkDTO(); + trunk.setWoodDamage(0); + trunk.setTilt(3); // 扣 3 分 + trunk.setBarkDamage(0); + req.setTrunk(trunk); + + // 树冠 + TreeInspectionSaveReqVO.CrownDTO crown = new TreeInspectionSaveReqVO.CrownDTO(); + crown.setLooseBranch(0); + crown.setCollarAbnormal(0); + crown.setVentilationBalance(1); // 扣 1 分 + req.setCrown(crown); + + // 权重 + TreeInspectionSaveReqVO.WeightDTO weight = new TreeInspectionSaveReqVO.WeightDTO(); + weight.setTreeSpeciesType("深根性树种"); // 1.0 + weight.setPlantingYears("栽植 10-30 年"); // 1.1 + weight.setIsWindCorridor(false); // 1.0 + weight.setTreePoolType("联通树池"); // 1.0 + weight.setTreePoolWidthDbhRatio("5 倍(含)-7 倍(不含)"); // 1.1 + req.setWeight(weight); + + // 风险评估结果 (展开应急评估) + TreeInspectionSaveReqVO.ResultDTO result = new TreeInspectionSaveReqVO.ResultDTO(); + result.setIsEmergency(true); + result.setWindPower("8-9 级"); // 1.5 + req.setResult(result); + + // 现状及建议 + TreeInspectionSaveReqVO.StatusDTO status = new TreeInspectionSaveReqVO.StatusDTO(); + status.setPhotos(Collections.singletonList("http://example.com/tree.jpg")); + req.setStatus(status); + + return req; + } + + @Test + public void testCreateTreeInspection_success_normal() { + // 准备参数 + TreeInspectionSaveReqVO req = createReqVOBase(); + // 模拟一树一档案数据 + TreeDO tree = new TreeDO(); + tree.setId(1024L); + tree.setTreenumber("D0001-P1-0001"); + tree.setDeptId(200L); + when(treeMapper.selectById(eq(1024L))).thenReturn(tree); + + // 调用 + Long resultId = treeInspectionService.createTreeInspection(req); + + // 验证数据库插入 + verify(treeInspectionMapper, times(1)).insert(argThat((TreeInspectionDO entity) -> { + assertNotNull(entity); + assertEquals("D0001-P1-0001", entity.getTreenumber()); + assertEquals(1024L, entity.getTreeId()); + // 基础累加得分: collar.woodDamage (5) + trunk.tilt (3) + crown.ventilationBalance (1) = 9 分 + assertEquals(9, entity.getDefectScore()); + // 权重乘积: 9 * 1.0 (深根) * 1.1 (10-30年) * 1.0 (非风口) * 1.0 (联通树池) * 1.1 (5-7倍比) = 10.89 分 + assertEquals(10.89, entity.getNormalScore()); + assertEquals("II级 (轻度风险)", entity.getNormalLevel()); + // 应急乘积 (isEmergency = true, windPower = 8-9级 即 1.5): 10.89 * 1.5 = 16.34 分 + assertEquals(16.34, entity.getEmergencyScore()); + assertEquals("II级 (轻度风险)", entity.getEmergencyLevel()); + assertEquals(1, entity.getPhotos().size()); + assertEquals("http://example.com/tree.jpg", entity.getPhotos().get(0)); + return true; + })); + } + + @Test + public void testCreateTreeInspection_success_oneVoteVetoLoosening() { + // 根颈松动一票否决 -> 直接归为 V级极度风险 + TreeInspectionSaveReqVO req = createReqVOBase(); + req.getCollar().setLoosening(100); // 存在根颈松动 + + TreeDO tree = new TreeDO(); + tree.setId(1024L); + tree.setTreenumber("D0001-P1-0001"); + when(treeMapper.selectById(eq(1024L))).thenReturn(tree); + + treeInspectionService.createTreeInspection(req); + + verify(treeInspectionMapper, times(1)).insert(argThat((TreeInspectionDO entity) -> { + assertEquals(100, entity.getDefectScore()); + // 权重乘积: 100 * 1.0 * 1.1 * 1.0 * 1.0 * 1.1 = 121.0 + assertEquals(121.0, entity.getNormalScore()); + assertEquals("V级 (极度风险)", entity.getNormalLevel()); + return true; + })); + } + + @Test + public void testCreateTreeInspection_success_oneVoteVetoTilt() { + // 主干倾斜>=30度一票否决 -> 缺陷分基准为 70,归为 IV级重度风险及以上 + TreeInspectionSaveReqVO req = createReqVOBase(); + req.getTrunk().setTilt(70); // 主干倾斜度>=30度 + + TreeDO tree = new TreeDO(); + tree.setId(1024L); + tree.setTreenumber("D0001-P1-0001"); + when(treeMapper.selectById(eq(1024L))).thenReturn(tree); + + treeInspectionService.createTreeInspection(req); + + verify(treeInspectionMapper, times(1)).insert(argThat((TreeInspectionDO entity) -> { + // 70 (一票否决) + 5 (collar) + 1 (crown) = 76 分 + assertEquals(76, entity.getDefectScore()); + // 权重乘积: 76 * 1.0 * 1.1 * 1.0 * 1.0 * 1.1 = 91.96 + assertEquals(91.96, entity.getNormalScore()); + assertEquals("IV级 (重度风险)", entity.getNormalLevel()); + return true; + })); + } + + @Test + public void testCreateTreeInspection_treeNotExists() { + TreeInspectionSaveReqVO req = createReqVOBase(); + when(treeMapper.selectById(eq(1024L))).thenReturn(null); + + assertServiceException(() -> treeInspectionService.createTreeInspection(req), + ErrorCodeConstants.TREE_NOT_EXISTS); + } + + @Test + public void testGetTreeInspection_success() { + Long id = randomLongId(); + LocalDateTime now = LocalDateTime.now(); + TreeInspectionDO entity = new TreeInspectionDO(); + entity.setId(id); + entity.setTreeId(1024L); + entity.setTreenumber("D0001-P1-0001"); + entity.setInspectionTime(now); + entity.setRootDisease(0); + entity.setRootAnchorage(0); + entity.setRootCutting(0); + entity.setCollarWoodDamage(5); + entity.setCollarBarkDamage(0); + entity.setCollarLoosening(0); + entity.setTrunkWoodDamage(0); + entity.setTrunkTilt(3); + entity.setTrunkBarkDamage(0); + entity.setCrownLooseBranch(0); + entity.setCrownCollarAbnormal(0); + entity.setCrownVentilationBalance(1); + entity.setTreeSpeciesType("深根性树种"); + entity.setTreeSpeciesWeight(1.0); + entity.setPlantingYears("栽植 10-30 年"); + entity.setPlantingYearsWeight(1.1); + entity.setIsWindCorridor(false); + entity.setWindCorridorWeight(1.0); + entity.setTreePoolType("联通树池"); + entity.setTreePoolWeight(1.0); + entity.setTreePoolWidthDbhRatio("5 倍(含)-7 倍(不含)"); + entity.setTreePoolRatioWeight(1.1); + entity.setIsEmergency(true); + entity.setWindPower("8-9 级"); + entity.setWindPowerWeight(1.5); + entity.setDefectScore(9); + entity.setNormalScore(10.89); + entity.setNormalLevel("II级 (轻度风险)"); + entity.setEmergencyScore(16.34); + entity.setEmergencyLevel("II级 (轻度风险)"); + entity.setPhotos(Collections.singletonList("http://example.com/tree.jpg")); + + when(treeInspectionMapper.selectById(eq(id))).thenReturn(entity); + + TreeInspectionRespVO resp = treeInspectionService.getTreeInspection(id); + + assertNotNull(resp); + assertEquals(id, resp.getId()); + assertEquals("D0001-P1-0001", resp.getTreenumber()); + assertEquals(now, resp.getInspectionTime()); + assertEquals(5, resp.getCollar().getWoodDamage()); + assertEquals(3, resp.getTrunk().getTilt()); + assertEquals(1, resp.getCrown().getVentilationBalance()); + assertEquals(1.1, resp.getWeight().getPlantingYearsWeight()); + assertEquals(1.5, resp.getResult().getWindPowerWeight()); + assertEquals(10.89, resp.getResult().getNormalResult().getScore()); + assertEquals("II级 (轻度风险)", resp.getResult().getNormalResult().getLevel()); + assertEquals(1, resp.getStatus().getPhotos().size()); + } + + @Test + public void testGetTreeInspection_notExists() { + Long id = randomLongId(); + when(treeInspectionMapper.selectById(eq(id))).thenReturn(null); + + assertServiceException(() -> treeInspectionService.getTreeInspection(id), + ErrorCodeConstants.TREE_INSPECTION_NOT_EXISTS); + } + +} diff --git a/urbanops-module-report/src/main/java/com/zteits/urbanops/module/report/framework/jmreport/core/service/JmReportTokenServiceImpl.java b/urbanops-module-report/src/main/java/com/zteits/urbanops/module/report/framework/jmreport/core/service/JmReportTokenServiceImpl.java index f78a816..f5c7e5b 100644 --- a/urbanops-module-report/src/main/java/com/zteits/urbanops/module/report/framework/jmreport/core/service/JmReportTokenServiceImpl.java +++ b/urbanops-module-report/src/main/java/com/zteits/urbanops/module/report/framework/jmreport/core/service/JmReportTokenServiceImpl.java @@ -158,4 +158,33 @@ public class JmReportTokenServiceImpl implements JmReportTokenServiceI { return StrUtil.toStringOrNull(loginUser.getTenantId()); } + @Override + public String[] getPermissions(String token) { + // 设置租户上下文 + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + if (loginUser == null) { + return null; + } + TenantContextHolder.setTenantId(loginUser.getTenantId()); + + // 参见文档 https://help.jimureport.com/prodSafe/ 文档 + // 适配:如果是本系统的管理员,则返回积木报表(仪表盘/大屏设计器)的所有权限指令 + // 如果不处理,会碰到 https://t.zsxq.com/yzlkA 反馈的问题 + Long userId = SecurityFrameworkUtils.getLoginUserId(); + if (permissionApi.hasAnyRoles(userId, RoleCodeEnum.SUPER_ADMIN.getCode())) { + return new String[]{ + "drag:datasource:testConnection", // 数据库连接测试 + "drag:datasource:saveOrUpate", // 数据源保存 + "drag:datasource:delete", // 数据源删除 + "drag:analysis:sql", // SQL解析 + "drag:design:getTotalData", // 展示Online表单数据 + "drag:dataset:save", // 数据集保存 + "drag:dataset:delete", // 数据集删除 + "onl:drag:clear:recovery", // 清空回收站 + "onl:drag:page:delete" // 数据删除 + }; + } + return null; + } + } diff --git a/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/controller/admin/user/UserController.java b/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/controller/admin/user/UserController.java index 2e2481e..983ba8c 100644 --- a/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/controller/admin/user/UserController.java +++ b/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/controller/admin/user/UserController.java @@ -18,11 +18,13 @@ import com.zteits.urbanops.module.system.service.dept.DeptService; import com.zteits.urbanops.module.system.service.permission.PermissionService; import com.zteits.urbanops.module.system.service.permission.RoleService; import com.zteits.urbanops.module.system.service.user.AdminUserService; +import com.zteits.urbanops.module.system.util.dept.DeptCacheHelper; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; @@ -40,6 +42,7 @@ import java.util.stream.Collectors; import static com.zteits.urbanops.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static com.zteits.urbanops.framework.common.pojo.CommonResult.success; import static com.zteits.urbanops.framework.common.util.collection.CollectionUtils.convertList; +import static com.zteits.urbanops.framework.security.core.util.SecurityFrameworkUtils.getDeptId; @Tag(name = "管理后台 - 用户") @RestController @@ -213,11 +216,20 @@ public class UserController { public void importTemplate(HttpServletResponse response) throws IOException { // 手动创建导出 demo List list = Arrays.asList( - UserImportExcelVO.builder().username("yunai").deptId(1L).email("yunai@iocoder.cn").mobile("15601691300") - .nickname("全域").status(CommonStatusEnum.ENABLE.getStatus()).sex(SexEnum.MALE.getSex()).build(), - UserImportExcelVO.builder().username("yuanma").deptId(2L).email("yuanma@iocoder.cn").mobile("15601701300") - .nickname("源码").status(CommonStatusEnum.DISABLE.getStatus()).sex(SexEnum.FEMALE.getSex()).build() + UserImportExcelVO.builder() + .username("示例:zhangshan") + .nickname("张山") + .mobile("15601691300") + .sex(SexEnum.MALE.getSex()) + .email("yunai@iocoder.cn") + .deptId(255L) + // .status(CommonStatusEnum.ENABLE.getStatus()) + .busiLine("yl,sz") // 简洁示例 + .isInner(1) + .build() ); + //初始化部门 + DeptCacheHelper.init(deptService, getDeptId()); // 输出 ExcelUtils.write(response, "用户导入模板.xls", "用户列表", UserImportExcelVO.class, list); } diff --git a/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/controller/admin/user/vo/user/UserImportExcelVO.java b/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/controller/admin/user/vo/user/UserImportExcelVO.java index db721df..de8c4f1 100644 --- a/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/controller/admin/user/vo/user/UserImportExcelVO.java +++ b/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/controller/admin/user/vo/user/UserImportExcelVO.java @@ -2,8 +2,12 @@ package com.zteits.urbanops.module.system.controller.admin.user.vo.user; import cn.idev.excel.annotation.ExcelProperty; import com.zteits.urbanops.framework.excel.core.annotations.DictFormat; +import com.zteits.urbanops.framework.excel.core.annotations.ExcelColumnSelect; import com.zteits.urbanops.framework.excel.core.convert.DictConvert; import com.zteits.urbanops.module.system.enums.DictTypeConstants; +import com.zteits.urbanops.framework.excel.core.convert.BusinessLineConvert; +import com.zteits.urbanops.module.system.framework.excel.convert.DeptConvert; +import com.zteits.urbanops.module.system.framework.excel.core.DeptExcelColumnSelectFunction; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -18,18 +22,12 @@ import lombok.NoArgsConstructor; @NoArgsConstructor public class UserImportExcelVO { - @ExcelProperty("登录名称") + @ExcelProperty("用户账户") private String username; @ExcelProperty("用户名称") private String nickname; - @ExcelProperty("部门编号") - private Long deptId; - - @ExcelProperty("用户邮箱") - private String email; - @ExcelProperty("手机号码") private String mobile; @@ -37,11 +35,24 @@ public class UserImportExcelVO { @DictFormat(DictTypeConstants.USER_SEX) private Integer sex; - @ExcelProperty(value = "账号状态", converter = DictConvert.class) - @DictFormat(DictTypeConstants.COMMON_STATUS) - private Integer status; + @ExcelProperty("邮箱") + private String email; - @ExcelProperty("业务线") + @ExcelProperty(value = "归属部门名称", converter = DeptConvert.class) + @ExcelColumnSelect(functionName = DeptExcelColumnSelectFunction.NAME) + private Long deptId; + + //@ExcelProperty(value = "账号状态", converter = DictConvert.class) + //@DictFormat(DictTypeConstants.COMMON_STATUS) + //private Integer status; + + @ExcelProperty(value = "业务线", converter = BusinessLineConvert.class) private String busiLine; + @ExcelProperty(value = "是否是内部员工", converter = DictConvert.class) + @DictFormat(DictTypeConstants.SYSTEM_IS_INNER) + private Integer isInner; + + @ExcelProperty(value = "备注") + private String remark; } diff --git a/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/enums/DictTypeConstants.java b/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/enums/DictTypeConstants.java index 3a9b504..006c12a 100644 --- a/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/enums/DictTypeConstants.java +++ b/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/enums/DictTypeConstants.java @@ -30,6 +30,8 @@ public interface DictTypeConstants { String WORKORDER_TYPE = "work_order_type"; //工单类型 + String SYSTEM_IS_INNER = "system_is_inner";//是否是内部员工 + diff --git a/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/enums/common/CommonConstants.java b/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/enums/common/CommonConstants.java new file mode 100644 index 0000000..d6293df --- /dev/null +++ b/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/enums/common/CommonConstants.java @@ -0,0 +1,14 @@ +package com.zteits.urbanops.module.system.enums.common; + +/** + * @Classname CommonConstants + * @Description 公共静态参数 + * @Date 2026/1/3 10:15 + * @Created by wangqian + */ +public interface CommonConstants { + + /*全域督察员组长*/ + public static final String INSPECTOR_ROLE_KEY = "Inspector_global_leader"; + +} diff --git a/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/framework/excel/convert/DeptConvert.java b/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/framework/excel/convert/DeptConvert.java new file mode 100644 index 0000000..e819e88 --- /dev/null +++ b/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/framework/excel/convert/DeptConvert.java @@ -0,0 +1,106 @@ +package com.zteits.urbanops.module.system.framework.excel.convert; + +import cn.hutool.core.convert.Convert; +import cn.hutool.extra.spring.SpringUtil; +import cn.idev.excel.converters.Converter; +import cn.idev.excel.enums.CellDataTypeEnum; +import cn.idev.excel.metadata.GlobalConfiguration; +import cn.idev.excel.metadata.data.ReadCellData; +import cn.idev.excel.metadata.data.WriteCellData; +import cn.idev.excel.metadata.property.ExcelContentProperty; +import com.zteits.urbanops.module.system.dal.dataobject.dept.DeptDO; +import com.zteits.urbanops.module.system.service.dept.DeptService; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 若依框架 部门Excel转换器(官方规范版) + * 全程走缓存,绝不重复查询数据库 + * + * @author ruoyi + */ +@Slf4j +public class DeptConvert implements Converter { + + /** + * 若依 部门Service(走系统缓存) + */ + private static final DeptService deptService = SpringUtil.getBean(DeptService.class); + + /** + * 若依系统全局缓存:ID -> 部门名称 + */ + private static final Map DEPT_CACHE; + + /** + * 若依系统全局缓存:名称 -> 部门ID + */ + private static final Map DEPT_NAME_CACHE; + + // ===================== 静态初始化:只加载一次 ===================== + static { + // 从若依缓存获取所有部门(只查一次库,后面全走Redis) + List deptList = deptService.getChildDeptList(0L); + + // ID -> 名称(导出用) + DEPT_CACHE = deptList.stream() + .collect(Collectors.toMap(DeptDO::getId, DeptDO::getName)); + + // 名称 -> ID(导入用) + DEPT_NAME_CACHE = deptList.stream() + .collect(Collectors.toMap(DeptDO::getName, DeptDO::getId)); + + log.info("【若依部门Excel转换器】初始化缓存成功,共 {} 个部门", deptList.size()); + } + + @Override + public Class supportJavaTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + // ===================== 导入:Excel部门名称 → ID ===================== + @Override + public Object convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + String deptName = readCellData.getStringValue(); + + // 从缓存获取(0 查库) + Long deptId = DEPT_NAME_CACHE.get(deptName); + if (deptId == null) { + log.error("[convertToJavaData][部门名称({}) 不存在]", deptName); + return null; + } + + // 类型转换 + Class fieldClazz = contentProperty.getField().getType(); + return Convert.convert(fieldClazz, deptId); + } + + // ===================== 导出:ID → 部门名称 ===================== + @Override + public WriteCellData convertToExcelData(Object value, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + if (value == null) { + return new WriteCellData<>(""); + } + + Long deptId = Convert.toLong(value); + // 从缓存获取名称(0 查库) + String deptName = DEPT_CACHE.get(deptId); + + if (deptName == null) { + log.error("[convertToExcelData][部门ID({}) 不存在]", deptId); + return new WriteCellData<>(""); + } + + return new WriteCellData<>(deptName); + } +} diff --git a/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/framework/excel/core/DeptExcelColumnSelectFunction.java b/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/framework/excel/core/DeptExcelColumnSelectFunction.java new file mode 100644 index 0000000..5269dda --- /dev/null +++ b/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/framework/excel/core/DeptExcelColumnSelectFunction.java @@ -0,0 +1,26 @@ +package com.zteits.urbanops.module.system.framework.excel.core; + +import com.zteits.urbanops.framework.excel.core.function.ExcelColumnSelectFunction; +import com.zteits.urbanops.module.system.util.dept.DeptCacheHelper; +import org.springframework.stereotype.Service; +import java.util.List; + +@Service +public class DeptExcelColumnSelectFunction implements ExcelColumnSelectFunction { + + public static final String NAME = "deptNameList"; + + @Override + public String getName() { + return NAME; + } + + /** + * 从缓存获取部门名称列表,作为Excel下拉选项 + */ + @Override + public List getOptions() { + return DeptCacheHelper.getNameList(); // 直接走缓存! + } + +} diff --git a/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/util/dept/DeptCacheHelper.java b/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/util/dept/DeptCacheHelper.java new file mode 100644 index 0000000..04c3372 --- /dev/null +++ b/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/util/dept/DeptCacheHelper.java @@ -0,0 +1,81 @@ +package com.zteits.urbanops.module.system.util.dept; + +import cn.hutool.core.collection.CollUtil; +import com.zteits.urbanops.module.system.dal.dataobject.dept.DeptDO; +import com.zteits.urbanops.module.system.service.dept.DeptService; +import lombok.extern.slf4j.Slf4j; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 部门全局缓存工具 + * 给 Excel 导入导出、下拉框、转换器统一使用 + * 只加载一次,不重复查询 + */ +@Slf4j +public class DeptCacheHelper { + + private static List DEPT_LIST; + private static Map DEPT_ID_MAP; + private static Map DEPT_NAME_MAP; + private static List DEPT_NAME_LIST; + + /** + * 初始化部门缓存(在导入模板接口中主动调用) + */ + public static void init(DeptService deptService, Long rootDeptId) { + + //每次调用都使用传入的 deptId,每次可刷新 + if (DEPT_LIST != null) { + // 清空旧缓存 + DEPT_LIST = null; + DEPT_ID_MAP = null; + DEPT_NAME_MAP = null; + DEPT_NAME_LIST = null; + } + + try { + + // 1. 批量查询部门(只查一次) + DEPT_LIST = deptService.getLastDeptList(rootDeptId); + + if (CollUtil.isEmpty(DEPT_LIST)) { + log.warn("[DeptCacheHelper] 未查询到部门数据"); + return; + } + + // 3. 构建各种缓存 + DEPT_ID_MAP = DEPT_LIST.stream() + .collect(Collectors.toMap(DeptDO::getId, Function.identity())); + + DEPT_NAME_MAP = DEPT_LIST.stream() + .collect(Collectors.toMap(DeptDO::getName, Function.identity())); + + DEPT_NAME_LIST = DEPT_LIST.stream() + .map(DeptDO::getName) + .collect(Collectors.toList()); + + log.info("[DeptCacheHelper] 部门缓存初始化成功,共 {} 个部门", DEPT_LIST.size()); + } catch (Exception e) { + log.error("[DeptCacheHelper] 部门缓存初始化失败", e); + } + } + + // ===================== 提供给外部使用 ===================== + public static DeptDO getById(Long id) { + return DEPT_ID_MAP == null ? null : DEPT_ID_MAP.get(id); + } + + public static DeptDO getByName(String name) { + return DEPT_NAME_MAP == null ? null : DEPT_NAME_MAP.get(name); + } + + public static List getNameList() { + return DEPT_NAME_LIST == null ? Collections.emptyList() : DEPT_NAME_LIST; + } + + public static boolean isEmpty() { + return CollUtil.isEmpty(DEPT_LIST); + } +} diff --git a/urbanops-module-system/src/test/java/com/zteits/urbanops/module/system/service/user/AdminUserServiceImplTest.java b/urbanops-module-system/src/test/java/com/zteits/urbanops/module/system/service/user/AdminUserServiceImplTest.java index e45c16e..9717b40 100644 --- a/urbanops-module-system/src/test/java/com/zteits/urbanops/module/system/service/user/AdminUserServiceImplTest.java +++ b/urbanops-module-system/src/test/java/com/zteits/urbanops/module/system/service/user/AdminUserServiceImplTest.java @@ -482,7 +482,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { public void testImportUserList_02() { // 准备参数 UserImportExcelVO importUser = randomPojo(UserImportExcelVO.class, o -> { - o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + //o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 o.setSex(randomEle(SexEnum.values()).getSex()); // 保证 sex 的范围 o.setEmail(randomEmail()); o.setMobile(randomMobile()); @@ -517,7 +517,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { userMapper.insert(dbUser); // 准备参数 UserImportExcelVO importUser = randomPojo(UserImportExcelVO.class, o -> { - o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + //o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 o.setSex(randomEle(SexEnum.values()).getSex()); // 保证 sex 的范围 o.setUsername(dbUser.getUsername()); o.setEmail(randomEmail()); @@ -549,7 +549,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { userMapper.insert(dbUser); // 准备参数 UserImportExcelVO importUser = randomPojo(UserImportExcelVO.class, o -> { - o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + //o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 o.setSex(randomEle(SexEnum.values()).getSex()); // 保证 sex 的范围 o.setUsername(dbUser.getUsername()); o.setEmail(randomEmail()); diff --git a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/api/constant/BpmCommonConstant.java b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/api/constant/BpmCommonConstant.java index 5c74ed4..0d65f82 100644 --- a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/api/constant/BpmCommonConstant.java +++ b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/api/constant/BpmCommonConstant.java @@ -175,6 +175,11 @@ public class BpmCommonConstant { * 督察员 角色编码 */ public static final String INSPECTOR_GLOBAL = "Inspector_global"; + + /** + * 督察员组长 角色编码 + */ + public static final String INSPECTOR_GLOBAL_LEADER = "Inspector_global_leader"; /** * ai 工单派发人 */ diff --git a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/admin/maininfo/vo/MainInfoPageReqVO.java b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/admin/maininfo/vo/MainInfoPageReqVO.java index 6db260e..ee335d4 100644 --- a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/admin/maininfo/vo/MainInfoPageReqVO.java +++ b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/admin/maininfo/vo/MainInfoPageReqVO.java @@ -130,4 +130,5 @@ public class MainInfoPageReqVO extends PageParam { @Schema(description = "部门id") private Set deptIds; + } diff --git a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/app/garden/AppGardenWorkOrderInspectorController.java b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/app/garden/AppGardenWorkOrderInspectorController.java index 6d2dcdd..78c3c18 100644 --- a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/app/garden/AppGardenWorkOrderInspectorController.java +++ b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/app/garden/AppGardenWorkOrderInspectorController.java @@ -6,7 +6,6 @@ import com.zteits.urbanops.framework.idempotent.core.annotation.Idempotent; import com.zteits.urbanops.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; import com.zteits.urbanops.module.garden.api.road.RoadApi; import com.zteits.urbanops.module.workorder.annotation.AutoUpdateWorkerOrderInspectorBuzStatus; -import com.zteits.urbanops.module.workorder.annotation.AutoUpdateWorkerOrderUniversalBuzStatus; import com.zteits.urbanops.module.workorder.controller.app.garden.vo.common.AppUniversalApprovalReqVO; import com.zteits.urbanops.module.workorder.controller.app.garden.vo.operator.AppGardenTaskReturnExtReqVO; import com.zteits.urbanops.module.workorder.controller.app.garden.vo.operator.AppGardenTaskTeamLeaderAssignReqVO; @@ -16,12 +15,9 @@ import com.zteits.urbanops.module.workorder.controller.app.garden.vo.task.AppGar import com.zteits.urbanops.module.workorder.controller.app.garden.vo.task.AppGardenTaskRegionMgrApproveReqVO; import com.zteits.urbanops.module.workorder.dto.AppGardenWorkOrderInspectorReqVO; import com.zteits.urbanops.module.workorder.dto.AppGardenWorkOrderReqVO; -import com.zteits.urbanops.module.workorder.dto.AppGardenWorkOrderUniversalReqVO; -import com.zteits.urbanops.module.workorder.enums.InspectorOperateTypeEnum; import com.zteits.urbanops.module.workorder.enums.InspectorOperateTypeEnum; import com.zteits.urbanops.module.workorder.service.garden.BpmGardenService; import com.zteits.urbanops.module.workorder.service.garden.BpmInspectorService; -import com.zteits.urbanops.module.workorder.service.garden.BpmUniversalService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; @@ -76,6 +72,15 @@ public class AppGardenWorkOrderInspectorController { return success(inspectorService.createWorkOrder(createReqVO)); } + @PostMapping("/createBatch") + //@PreAuthorize("@ss.hasPermission('bpm:garden-workorder:create')") + @Operation(summary = "app-全域巡查员创建工单信息") + public CommonResult> createMainInfoBatch(@Valid @RequestBody List createReqVOList) { + return success(inspectorService.batchCreateWorkOrder(createReqVOList)); + } + + + @Operation(summary = "app-统一审批入口") @PostMapping("/universalApproval") @AutoUpdateWorkerOrderInspectorBuzStatus diff --git a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/app/garden/aop/InspectorBuzStatusUpdateAspect.java b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/app/garden/aop/InspectorBuzStatusUpdateAspect.java index 40fcc79..f6357b1 100644 --- a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/app/garden/aop/InspectorBuzStatusUpdateAspect.java +++ b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/app/garden/aop/InspectorBuzStatusUpdateAspect.java @@ -96,6 +96,14 @@ public class InspectorBuzStatusUpdateAspect { targeVO.setId(workerDataId); targeVO.setOrderNo(dbData.getOrderNo()); targeVO.setBusiLine(dbData.getBusiLine()); + //三级级编码 + if(StringUtil.isNotEmpty(reqVO.getOrderCode())){ + targeVO.setOrderCode(reqVO.getOrderCode()); + } + //业务类型(一级编码) + if(StringUtil.isNotEmpty(reqVO.getBusiType())){ + targeVO.setBusiType(reqVO.getBusiType()); + } //大区经理分配 if(InspectorOperateTypeEnum.SH_REGION_MANAGER_ASSIGN.getCode().equals(reqVO.getOperateType())){ RoadStreetRespDTO roadStreetRespDTO = roadApi.getRoadInfoById(reqVO.getRoadId()); @@ -163,6 +171,7 @@ public class InspectorBuzStatusUpdateAspect { busiLine = dbData.getBusiLine(); } targeVO.setBusiLine(busiLine); + targeVO.setUserId(SecurityFrameworkUtils.getLoginUserId()); targeVO.setUserName(SecurityFrameworkUtils.getLoginUserNickname()); if(ObjectUtils.isNotAllEmpty(reqVO.getExpectedFinishDate())){ diff --git a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/app/garden/vo/common/AppUniversalApprovalReqVO.java b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/app/garden/vo/common/AppUniversalApprovalReqVO.java index 33d0de7..a22885e 100644 --- a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/app/garden/vo/common/AppUniversalApprovalReqVO.java +++ b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/app/garden/vo/common/AppUniversalApprovalReqVO.java @@ -123,6 +123,12 @@ public class AppUniversalApprovalReqVO { @Schema(description = "工单名称", example = "绿地卫生") private String orderName; + @Schema(description = "三级编码") + private String orderCode; + + @Schema(description = "业务类型(一级编码)") + private String busiType; + @Schema(description = "来源ID 工单来源 1、巡查,2、游客居民,3、12345,4、网格", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") //@NotNull(message = "来源ID不能为空") private Integer sourceId; diff --git a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/app/maininfo/AppMainInfoController.java b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/app/maininfo/AppMainInfoController.java new file mode 100644 index 0000000..bb98588 --- /dev/null +++ b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/app/maininfo/AppMainInfoController.java @@ -0,0 +1,122 @@ +package com.zteits.urbanops.module.workorder.controller.app.maininfo; + +import com.zteits.urbanops.framework.apilog.core.annotation.ApiAccessLog; +import com.zteits.urbanops.framework.common.pojo.CommonResult; +import com.zteits.urbanops.framework.common.pojo.PageParam; +import com.zteits.urbanops.framework.common.pojo.PageResult; +import com.zteits.urbanops.framework.common.util.object.BeanUtils; +import com.zteits.urbanops.framework.excel.core.util.ExcelUtils; +import com.zteits.urbanops.module.system.api.user.AdminUserApi; +import com.zteits.urbanops.module.system.api.user.dto.AdminUserRespDTO; +import com.zteits.urbanops.module.system.dal.dataobject.dept.DeptDO; +import com.zteits.urbanops.module.system.service.dept.DeptService; +import com.zteits.urbanops.module.workorder.controller.admin.maininfo.vo.AdminMainInfoRespVO; +import com.zteits.urbanops.module.workorder.controller.admin.maininfo.vo.MainInfoPageReqVO; +import com.zteits.urbanops.module.workorder.controller.app.maininfo.vo.AppMainInfoPageReqVO; +import com.zteits.urbanops.module.workorder.controller.app.maininfo.vo.AppMainInfoRespVO; +import com.zteits.urbanops.module.workorder.convert.maininfo.MainInfoConvert; +import com.zteits.urbanops.module.workorder.dal.dataobject.attachment.AttachmentDO; +import com.zteits.urbanops.module.workorder.dal.dataobject.maininfo.MainInfoDO; +import com.zteits.urbanops.module.workorder.dto.MainInfoRespVO; +import com.zteits.urbanops.module.workorder.service.attachment.AttachmentService; +import com.zteits.urbanops.module.workorder.service.maininfo.MainInfoService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +import static com.zteits.urbanops.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static com.zteits.urbanops.framework.common.pojo.CommonResult.success; + +@Tag(name = "app - 工单信息") +@RestController +@RequestMapping("/workorder/main-info") +@Validated +public class AppMainInfoController { + + @Resource + private MainInfoService mainInfoService; + + @Resource + private AttachmentService attachmentService; + + @Resource + private DeptService deptService; + @Resource + private AdminUserApi adminUserApi; + + @GetMapping("/get") + @Operation(summary = "获得工单信息") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + public CommonResult getMainInfo(@RequestParam("id") Long id) { + MainInfoDO mainInfo = mainInfoService.getMainInfo(id); + // 若订单号列表非空,查询附件;否则跳过 + Map> attachmentMap = Collections.emptyMap(); + if (mainInfo != null) { + List attachmentDOList = attachmentService.getAttachmentByOrderNo(Arrays.asList(mainInfo.getOrderNo())); + if (!CollectionUtils.isEmpty(attachmentDOList)) { + attachmentMap = attachmentDOList.stream() + .collect(Collectors.groupingBy( + AttachmentDO::getBusiType, + Collectors.mapping( + AttachmentDO::getFileNames, + Collectors.toList() + ) + )); + } + } + + String companyName = ""; + if (mainInfo.getWorkerCompanyId() != null) { + DeptDO dept = deptService.getDept(mainInfo.getWorkerCompanyId()); + if (dept != null) { + companyName = dept.getName(); + } + } + + Set userIds = new HashSet<>(); + if (mainInfo.getCoHandlers() != null) { + mainInfo.getCoHandlers().stream() + .filter(Objects::nonNull) // 过滤null ID + .forEach(userIds::add); + } + userIds.add(mainInfo.getUserId()); + Map userMap = adminUserApi.getUserMap(userIds); + return success(MainInfoConvert.INSTANCE.appBuildMainInfo(mainInfo, attachmentMap, companyName, userMap)); + } + + @GetMapping("/page") + @Operation(summary = "获得工单信息分页") + public CommonResult> getMainInfoPage(@Valid AppMainInfoPageReqVO pageReqVO) { + + PageResult pageResult = mainInfoService.selectPageExcludeQuickOrder(pageReqVO); + // 6. 转换并返回结果 + return success(MainInfoConvert.INSTANCE.buildMainInfoPage(pageResult, null, null, null)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出工单信息 Excel") + @ApiAccessLog(operateType = EXPORT) + public void exportMainInfoExcel(@Valid MainInfoPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = mainInfoService.getMainInfoPage(pageReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "工单信息.xls", "数据", MainInfoRespVO.class, + BeanUtils.toBean(list, MainInfoRespVO.class)); + } + +} diff --git a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/app/maininfo/vo/AppMainInfoPageReqVO.java b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/app/maininfo/vo/AppMainInfoPageReqVO.java new file mode 100644 index 0000000..755a661 --- /dev/null +++ b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/app/maininfo/vo/AppMainInfoPageReqVO.java @@ -0,0 +1,99 @@ +package com.zteits.urbanops.module.workorder.controller.app.maininfo.vo; + +import com.zteits.urbanops.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +import static com.zteits.urbanops.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; + +@Schema(description = "APP - 工单信息分页 Request VO") +@Data +public class AppMainInfoPageReqVO extends PageParam { + + @NotEmpty + @Schema(description = "查询类型", example = "0:全部工单 1:全域工单") + public String queryType; + + @Schema(description = "搜索类型:1 位置 2 工单名称 3 情况描述 4 工单编号", example = "1") + private String type; + + @Schema(description = "搜索输入内容", example = "祭旁") + private String searchContent; + + @Schema(description = "工单号") + private String orderNo; + + @Schema(description = "工单名称", example = "张三") + private String orderName; + + @Schema(description = "审批状态", example = "2") + private Integer status; + + @Schema(description = "经纬度地址") + private String lonLatAddress; + + @Schema(description = "工单描述", example = "你说的对") + private String remark; + /** + * 时间数组:[开始时间, 结束时间] + * 由 beginTime 和 endTime 自动组装 + */ + @Schema(description = "创建时间-范围查询", hidden = true) + private LocalDateTime[] createTime; + + @Schema(description = "归属单位", example = "12") + private Long workerCompanyId; + + @Schema(description = "实施人员部门id", example = "12") + private Long companyId; + + @Schema(description = "开始日期(yyyy-MM-dd)", example = "2025-01-01") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private String beginTime; + + @Schema(description = "结束日期(yyyy-MM-dd)", example = "2025-12-10") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private String endTime; + + @Schema(description = "用户id", example = "12" , hidden = true) + private Long userId; + + /** + * 组装时间:beginTime + endTime → createTime + */ + public LocalDateTime[] getCreateTime() { + // 已经手动设置过,直接返回 + if (this.createTime != null) { + return this.createTime; + } + // 没有传时间,直接返回 null + if (beginTime == null && endTime == null) { + return null; + } + + LocalDateTime startTime = null; + LocalDateTime endTimeDate = null; + + // 开始时间:yyyy-MM-dd 00:00:00 + if (beginTime != null && !beginTime.isEmpty()) { + startTime = LocalDate.parse(beginTime).atTime(LocalTime.MIN); + } + // 结束时间:yyyy-MM-dd 23:59:59 + if (endTime != null && !endTime.isEmpty()) { + endTimeDate = LocalDate.parse(endTime).atTime(LocalTime.MAX); + } + + return new LocalDateTime[]{startTime, endTimeDate}; + } + + // 防止覆盖 + public void setCreateTime(LocalDateTime[] createTime) { + this.createTime = createTime; + } +} diff --git a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/app/maininfo/vo/AppMainInfoRespVO.java b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/app/maininfo/vo/AppMainInfoRespVO.java new file mode 100644 index 0000000..102ace1 --- /dev/null +++ b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/controller/app/maininfo/vo/AppMainInfoRespVO.java @@ -0,0 +1,184 @@ +package com.zteits.urbanops.module.workorder.controller.app.maininfo.vo; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Set; + +@Schema(description = "管理后台 - 工单信息 Response VO") +@Data +@ExcelIgnoreUnannotated +public class AppMainInfoRespVO { + + @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "7114") + @ExcelProperty("ID") + private Long id; + + @Schema(description = "业务线:yl 园林,wy 物业,sz 市政 ", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("业务线:yl 园林,wy 物业,sz 市政 ") + private String busiLine; + + @Schema(description = "工单号", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("工单号") + private String orderNo; + + @Schema(description = "工单名称", example = "张三") + @ExcelProperty("工单名称") + private String orderName; + + /** + * 工单类型 Q:快速工单,C:普通工单,O:其他工单 + */ + private String orderType; + + @Schema(description = "来源ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1828") + @ExcelProperty("来源ID 问题来源 1、巡查,2、游客居民,3、12345,4、网格 5、大区经理") + private Integer sourceId; + + @Schema(description = "来源名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @ExcelProperty("来源名称") + private String sourceName; + + @Schema(description = "道路ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "21128") + @ExcelProperty("道路ID") + private Long roadId; + + @Schema(description = "道路名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + @ExcelProperty("道路名称") + private String roadName; + + @Schema(description = "街道ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "27852") + @ExcelProperty("街道ID") + private String streetId; + + @Schema(description = "街道名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @ExcelProperty("街道名称") + private String streetName; + + @Schema(description = "养护级别ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "28278") + @ExcelProperty("养护级别ID") + private Integer curingLevelId; + + @Schema(description = "养护级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + @ExcelProperty("养护级别") + private String curingLevelName; + + @Schema(description = "提交日期", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("提交日期") + private LocalDateTime commitDate; + + @Schema(description = "希望完成时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("希望完成时间") + private LocalDateTime expectedFinishDate; + + @Schema(description = "完成时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("完成时间") + private LocalDateTime finishDate; + + @Schema(description = "紧急程度:1:特急;2:紧急;3:一般", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @ExcelProperty("紧急程度:1:特急;2:紧急;3:一般") + private Integer pressingType; + + @Schema(description = "提交用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "12757") + private Long userId; + + @Schema(description = "提交用户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + @ExcelProperty("提交用户名称") + private String userName; + + @Schema(description = "部门id", requiredMode = Schema.RequiredMode.REQUIRED, example = "26936") + private Long companyId; + + @Schema(description = "单位名称") + @ExcelProperty("单位名称") + private String companyName; + + @Schema(description = "经纬度类型: 1:国标; 2:百度;3:高德;4:腾讯", example = "2") + @ExcelProperty("经纬度类型: 1:国标; 2:百度;3:高德;4:腾讯") + private Integer latLonType; + + @Schema(description = "经度", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("经度") + private BigDecimal lat; + + @Schema(description = "维度", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("维度") + private BigDecimal lon; + + @Schema(description = "经纬度地址", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("经纬度地址") + private String lonLatAddress; + + @Schema(description = "三方工单ID") + @ExcelProperty("三方工单ID") + private String thirdWorkNo; + + @Schema(description = "三方工单结果上报状态:1:待推送;2:推送成功;3:推送失败") + @ExcelProperty("三方工单结果上报状态:1:待推送;2:推送成功;3:推送失败") + private Integer thirdPushState; + + @Schema(description = "业务状态", example = "1") + @ExcelProperty("业务状态") + private String buzStatus; + + @Schema(description = "审批状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @ExcelProperty("审批状态") + private Integer status; + + @Schema(description = "流程实例的编号", example = "9184") + @ExcelProperty("流程实例的编号") + private String processInstanceId; + + @Schema(description = "工单描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "你说的对") + @ExcelProperty("工单描述") + private String remark; + + @Schema(description = "工单完成结果描述", example = "已完成") + @ExcelProperty("工单完成结果描述") + private String handleResult; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + + @Schema(description = "文件名称") + private List fileNames; + + @Schema(description = "共同处理人") + private Set coHandlers; + @Schema(description = "共同处理人名称,多个为数组") + private List coHandlersName; + + @Schema(description = "提交人电话") + private String userMobile; + + @Schema(description = "养护员") + private String workerName; + + @Schema(description = "实施人员部门id") + private Long workerCompanyId; + + @Schema(description = "处理人") + private String assigneeName; + + @Schema(description = "审批节点") + private String approvalNode; + + @Schema(description = "问题图片") + private List problemsImgs; + @Schema(description = "开始图片") + private List startImgs; + @Schema(description = "处理中图片") + private List processingImgs; + @Schema(description = "结束图片") + private List endImgs; + @Schema(description = "人员图片") + private List personImgs; + @Schema(description = "物料图片") + private List materialImgs; +} diff --git a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/convert/maininfo/MainInfoConvert.java b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/convert/maininfo/MainInfoConvert.java index 55dd407..a66ff2e 100644 --- a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/convert/maininfo/MainInfoConvert.java +++ b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/convert/maininfo/MainInfoConvert.java @@ -6,6 +6,7 @@ import com.zteits.urbanops.framework.common.util.object.BeanUtils; import com.zteits.urbanops.module.bpm.api.task.dto.BpmUserSimpleReqDTO; import com.zteits.urbanops.module.system.api.user.dto.AdminUserRespDTO; import com.zteits.urbanops.module.workorder.controller.admin.maininfo.vo.AdminMainInfoRespVO; +import com.zteits.urbanops.module.workorder.controller.app.maininfo.vo.AppMainInfoRespVO; import com.zteits.urbanops.module.workorder.dal.dataobject.attachment.AttachmentDO; import com.zteits.urbanops.module.workorder.dal.dataobject.maininfo.MainInfoDO; import org.mapstruct.Mapper; @@ -90,4 +91,36 @@ public interface MainInfoConvert { } return mainInfoVo; } + + default AppMainInfoRespVO appBuildMainInfo(MainInfoDO mainInfoDO, Map> attachmentMap, String deptName, Map userMap) { + AppMainInfoRespVO mainInfoVo = BeanUtils.toBean(mainInfoDO, AppMainInfoRespVO.class); + if (mainInfoDO != null) { + if (attachmentMap != null && !attachmentMap.isEmpty()){ + //01 问题图片,02 开始图片,03 进行中图片,04 已完成图片,05 人员图片:06 材料图片 + mainInfoVo.setProblemsImgs(attachmentMap.getOrDefault("01", List.of())); + mainInfoVo.setStartImgs(attachmentMap.getOrDefault("02", List.of())); + mainInfoVo.setProcessingImgs(attachmentMap.getOrDefault("03", List.of())); + mainInfoVo.setEndImgs(attachmentMap.getOrDefault("04", List.of())); + mainInfoVo.setPersonImgs(attachmentMap.getOrDefault("05", List.of())); + mainInfoVo.setMaterialImgs(attachmentMap.getOrDefault("06", List.of())); + } + mainInfoVo.setCompanyName(deptName); + if (mainInfoVo.getCoHandlers() != null && !mainInfoVo.getCoHandlers().isEmpty()) { + List coHandlersNameList = Optional.ofNullable(mainInfoVo.getCoHandlers()) + .orElse(Set.of()) + .stream() + .filter(Objects::nonNull) + .map(userMap::get) + .filter(Objects::nonNull) + .map(AdminUserRespDTO::getNickname) + .filter(nickname -> nickname != null && !nickname.isEmpty()) + .collect(Collectors.toList()); + mainInfoVo.setCoHandlersName(coHandlersNameList); + }else{ + mainInfoVo.setCoHandlersName(new ArrayList<>()); + } + mainInfoVo.setUserMobile(userMap.get(mainInfoVo.getUserId()).getMobile()); + } + return mainInfoVo; + } } diff --git a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/dal/dataobject/maininfo/MainInfoDO.java b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/dal/dataobject/maininfo/MainInfoDO.java index 4eec0fc..aa2ab0c 100644 --- a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/dal/dataobject/maininfo/MainInfoDO.java +++ b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/dal/dataobject/maininfo/MainInfoDO.java @@ -1,17 +1,16 @@ package com.zteits.urbanops.module.workorder.dal.dataobject.maininfo; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; -import io.swagger.v3.oas.annotations.media.Schema; +import com.zteits.urbanops.framework.mybatis.core.dataobject.BaseDO; import lombok.*; -import java.util.*; -import java.time.LocalDateTime; -import java.math.BigDecimal; + import java.math.BigDecimal; import java.time.LocalDateTime; -import java.time.LocalDateTime; -import com.baomidou.mybatisplus.annotation.*; -import com.zteits.urbanops.framework.mybatis.core.dataobject.BaseDO; -import org.hibernate.validator.constraints.Length; +import java.util.Set; /** * 工单信息 DO @@ -46,6 +45,14 @@ public class MainInfoDO extends BaseDO { */ private String orderName; /** + * 三级编码 + */ + private String orderCode; + /** + * 业务类型 + */ + private String busiType; + /** * 工单类型 Q:快速工单,C:普通工单,O:其他工单 */ private String orderType; diff --git a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/dal/dataobject/maininfo/MainInfoExtDO.java b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/dal/dataobject/maininfo/MainInfoExtDO.java index 22ea2a1..19909ec 100644 --- a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/dal/dataobject/maininfo/MainInfoExtDO.java +++ b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/dal/dataobject/maininfo/MainInfoExtDO.java @@ -1,8 +1,6 @@ package com.zteits.urbanops.module.workorder.dal.dataobject.maininfo; -import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import com.zteits.urbanops.framework.mybatis.core.dataobject.BaseDO; import lombok.*; @@ -47,6 +45,15 @@ public class MainInfoExtDO extends BaseDO { * 工单名称 */ private String orderName; + + /** + * 三级编码 + */ + private String orderCode; + /** + * 业务类型 + */ + private String busiType; /** * 来源ID */ diff --git a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/dal/mysql/maininfo/MainInfoExtMapper.java b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/dal/mysql/maininfo/MainInfoExtMapper.java index 8bfb429..bd605f3 100644 --- a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/dal/mysql/maininfo/MainInfoExtMapper.java +++ b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/dal/mysql/maininfo/MainInfoExtMapper.java @@ -1,6 +1,5 @@ package com.zteits.urbanops.module.workorder.dal.mysql.maininfo; -import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; @@ -11,10 +10,8 @@ import com.zteits.urbanops.framework.mybatis.core.mapper.BaseMapperX; import com.zteits.urbanops.framework.mybatis.core.util.MyBatisUtils; import com.zteits.urbanops.module.system.api.user.dto.AdminUserRespDTO; import com.zteits.urbanops.module.workorder.controller.admin.maininfo.vo.MainInfoHasTaskPageReqVO; -import com.zteits.urbanops.module.workorder.controller.admin.maininfo.vo.MainInfoPageReqVO; import com.zteits.urbanops.module.workorder.dal.dataobject.maininfo.MainInfoDO; import com.zteits.urbanops.module.workorder.dal.dataobject.maininfo.MainInfoExtDO; -import jakarta.validation.constraints.NotNull; import org.apache.ibatis.annotations.*; import org.springframework.lang.NonNull; @@ -112,6 +109,8 @@ public interface MainInfoExtMapper extends BaseMapperX { @Result(column = "busi_line", property = "busiLine"), @Result(column = "order_no", property = "orderNo"), @Result(column = "order_name", property = "orderName"), + @Result(column = "order_code", property = "orderCode"), + @Result(column = "busi_type", property = "busiType"), @Result(column = "source_id", property = "sourceId"), @Result(column = "source_name", property = "sourceName"), @Result(column = "road_id", property = "roadId"), @@ -230,6 +229,8 @@ public interface MainInfoExtMapper extends BaseMapperX { @Result(column = "busi_line", property = "busiLine"), @Result(column = "order_no", property = "orderNo"), @Result(column = "order_name", property = "orderName"), + @Result(column = "order_code", property = "orderCode"), + @Result(column = "busi_type", property = "busiType"), @Result(column = "source_id", property = "sourceId"), @Result(column = "source_name", property = "sourceName"), @Result(column = "road_id", property = "roadId"), @@ -345,6 +346,8 @@ public interface MainInfoExtMapper extends BaseMapperX { @Result(column = "busi_line", property = "busiLine"), @Result(column = "order_no", property = "orderNo"), @Result(column = "order_name", property = "orderName"), + @Result(column = "order_code", property = "orderCode"), + @Result(column = "busi_type", property = "busiType"), @Result(column = "source_id", property = "sourceId"), @Result(column = "source_name", property = "sourceName"), @Result(column = "road_id", property = "roadId"), @@ -471,6 +474,8 @@ public interface MainInfoExtMapper extends BaseMapperX { @Result(column = "busi_line", property = "busiLine"), @Result(column = "order_no", property = "orderNo"), @Result(column = "order_name", property = "orderName"), + @Result(column = "order_code", property = "orderCode"), + @Result(column = "busi_type", property = "busiType"), @Result(column = "source_id", property = "sourceId"), @Result(column = "source_name", property = "sourceName"), @Result(column = "road_id", property = "roadId"), @@ -584,6 +589,8 @@ public interface MainInfoExtMapper extends BaseMapperX { @Result(column = "busi_line", property = "busiLine"), @Result(column = "order_no", property = "orderNo"), @Result(column = "order_name", property = "orderName"), + @Result(column = "order_code", property = "orderCode"), + @Result(column = "busi_type", property = "busiType"), @Result(column = "source_id", property = "sourceId"), @Result(column = "source_name", property = "sourceName"), @Result(column = "road_id", property = "roadId"), diff --git a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/dal/mysql/maininfo/MainInfoMapper.java b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/dal/mysql/maininfo/MainInfoMapper.java index 8e81494..98b74a5 100644 --- a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/dal/mysql/maininfo/MainInfoMapper.java +++ b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/dal/mysql/maininfo/MainInfoMapper.java @@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.zteits.urbanops.framework.common.pojo.PageResult; import com.zteits.urbanops.framework.mybatis.core.query.LambdaQueryWrapperX; import com.zteits.urbanops.framework.mybatis.core.mapper.BaseMapperX; +import com.zteits.urbanops.module.workorder.controller.app.maininfo.vo.AppMainInfoPageReqVO; import com.zteits.urbanops.module.workorder.dal.dataobject.maininfo.MainInfoDO; import org.apache.ibatis.annotations.Mapper; import com.zteits.urbanops.module.workorder.controller.admin.maininfo.vo.*; @@ -135,4 +136,32 @@ public interface MainInfoMapper extends BaseMapperX { return selectList(new QueryWrapper().in(field, values)); } + /** + * 获得工单信息分页 不查询快速工单 + * + * @param reqVO 分页查询 + * @return 工单信息分页 + */ + default PageResult selectPageExcludeQuickOrder(AppMainInfoPageReqVO reqVO) { + + LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX<>(); + // 不查询快速工单 → 订单类型 != "Q" + queryWrapper.ne(MainInfoDO::getOrderType, "Q"); + // 部派工单 → 工单编号以 "IWO" 开头 + if ("1".equals(reqVO.getQueryType()) ) { + queryWrapper.likeRight(MainInfoDO::getOrderNo, "IWO"); + } + queryWrapper.eqIfPresent(MainInfoDO::getWorkerCompanyId, reqVO.getWorkerCompanyId()) + .eqIfPresent(MainInfoDO::getCompanyId, reqVO.getCompanyId()) + .likeIfPresent(MainInfoDO::getOrderNo, reqVO.getOrderNo()) + .likeIfPresent(MainInfoDO::getOrderName, reqVO.getOrderName()) + .eqIfPresent(MainInfoDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(MainInfoDO::getCreateTime, reqVO.getCreateTime()) + .likeIfPresent(MainInfoDO::getLonLatAddress, reqVO.getLonLatAddress()) + .likeIfPresent(MainInfoDO::getRemark, reqVO.getRemark()) + .eqIfPresent(MainInfoDO::getUserId, reqVO.getUserId()) + .orderByDesc(MainInfoDO::getId); + return selectPage(reqVO, queryWrapper); + } + } diff --git a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/enums/ErrorCodeConstants.java b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/enums/ErrorCodeConstants.java index 86e3e6e..b8eb1e1 100644 --- a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/enums/ErrorCodeConstants.java +++ b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/enums/ErrorCodeConstants.java @@ -48,7 +48,7 @@ public interface ErrorCodeConstants { ErrorCode EVENT_MAPPING_INFO_NOT_EXISTS = new ErrorCode(1-900-004-001, "工单事件映射不存在"); - ErrorCode APP_INSPECTOR_USER_ROLE_ERROR = new ErrorCode(1-900-004-001, "该登录人不是 全域督查员,无法发起流程!"); + ErrorCode APP_INSPECTOR_USER_ROLE_ERROR = new ErrorCode(1-900-004-001, "该登录人不是 全域督查员或者组长,无法发起流程!"); ErrorCode APP_WORK_INSPECTOR_ROLE_ILLEGLE = new ErrorCode(1-900-004-003, "该登录人即是 大区经理 角色,又是 全域督察员,无法发起流程!"); diff --git a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/service/garden/BpmInspectorService.java b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/service/garden/BpmInspectorService.java index fc42531..e8ec5f7 100644 --- a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/service/garden/BpmInspectorService.java +++ b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/service/garden/BpmInspectorService.java @@ -1,12 +1,12 @@ package com.zteits.urbanops.module.workorder.service.garden; -import com.zteits.urbanops.module.workorder.controller.app.garden.vo.task.AppGardenTaskApproveReqVO; import com.zteits.urbanops.module.workorder.controller.app.garden.vo.task.AppGardenTaskInspectorApproveReqVO; import com.zteits.urbanops.module.workorder.controller.app.garden.vo.task.AppGardenTaskRegionMgrApproveReqVO; import com.zteits.urbanops.module.workorder.dto.AppGardenWorkOrderInspectorReqVO; -import com.zteits.urbanops.module.workorder.dto.AppGardenWorkOrderUniversalReqVO; import jakarta.validation.Valid; +import java.util.List; + /** * 类描述:全域督察员工单处理server * 创建人:yanhuiqing @@ -27,6 +27,14 @@ public interface BpmInspectorService { */ Long createWorkOrder(@Valid AppGardenWorkOrderInspectorReqVO createRequestVo); + + /** + * 批量创建工单 + * @param reqVOList 工单列表 + * @return 工单ID集合 + */ + List batchCreateWorkOrder(List reqVOList); + /** * 全域督察员工单分配 * @param regionPassReqVO diff --git a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/service/garden/BpmInspectorServiceImpl.java b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/service/garden/BpmInspectorServiceImpl.java index 6ad467a..ded8c15 100644 --- a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/service/garden/BpmInspectorServiceImpl.java +++ b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/service/garden/BpmInspectorServiceImpl.java @@ -11,6 +11,7 @@ import com.zteits.urbanops.module.bpm.controller.admin.task.vo.task.BpmTaskAppro import com.zteits.urbanops.module.bpm.enums.task.BpmTaskStatusEnum; import com.zteits.urbanops.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; import com.zteits.urbanops.module.bpm.service.definition.BpmProcessDefinitionService; +import com.zteits.urbanops.module.bpm.service.task.BpmProcessInstanceCopyService; import com.zteits.urbanops.module.bpm.service.task.BpmProcessInstanceService; import com.zteits.urbanops.module.bpm.service.task.BpmTaskService; import com.zteits.urbanops.module.garden.api.road.RoadApi; @@ -21,7 +22,6 @@ import com.zteits.urbanops.module.system.api.user.dto.AdminUserRespDTO; import com.zteits.urbanops.module.system.controller.admin.permission.vo.role.RoleRespVO; import com.zteits.urbanops.module.workorder.api.WorkOrderApi; import com.zteits.urbanops.module.workorder.api.constant.BpmCommonConstant; -import com.zteits.urbanops.module.workorder.controller.app.garden.vo.task.AppGardenTaskApproveReqVO; import com.zteits.urbanops.module.workorder.controller.app.garden.vo.task.AppGardenTaskInspectorApproveReqVO; import com.zteits.urbanops.module.workorder.controller.app.garden.vo.task.AppGardenTaskRegionMgrApproveReqVO; import com.zteits.urbanops.module.workorder.dal.dataobject.maininfo.MainInfoDO; @@ -29,7 +29,6 @@ import com.zteits.urbanops.module.workorder.dal.mysql.attachment.AttachmentMappe import com.zteits.urbanops.module.workorder.dal.mysql.maininfo.MainInfoExtMapper; import com.zteits.urbanops.module.workorder.dal.mysql.maininfo.MainInfoMapper; import com.zteits.urbanops.module.workorder.dto.AppGardenWorkOrderInspectorReqVO; -import com.zteits.urbanops.module.workorder.dto.AppGardenWorkOrderUniversalReqVO; import com.zteits.urbanops.module.workorder.enums.BusiLineTeamLeaderRoleCodeEnum; import com.zteits.urbanops.module.workorder.enums.EventSourceEnum; import com.zteits.urbanops.module.workorder.util.RoleListUtils; @@ -78,6 +77,8 @@ public class BpmInspectorServiceImpl implements BpmInspectorService{ private BpmProcessInstanceApi processInstanceApi; @Resource + private BpmProcessInstanceCopyService copyService; + @Resource private BpmProcessDefinitionService processDefinitionService; @Resource private WorkOrderApi workOrderApi; @@ -113,7 +114,7 @@ public class BpmInspectorServiceImpl implements BpmInspectorService{ throw exception(APP_USER_ROLE_NOT_EXISTS); } - if(!RoleListUtils.containsCode(roleList,INSPECTOR_GLOBAL)){ + if(!(RoleListUtils.containsCode(roleList,INSPECTOR_GLOBAL) || RoleListUtils.containsCode(roleList,INSPECTOR_GLOBAL_LEADER))){ throw exception(APP_INSPECTOR_USER_ROLE_ERROR); } //校验人员不能有 双重角色 @@ -213,6 +214,11 @@ public class BpmInspectorServiceImpl implements BpmInspectorService{ String processInstanceId = processInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO().setProcessDefinitionKey(BpmCommonConstant.BPM_UNIVERSE_INSPECTOR_WO) .setVariables(processInstanceVariables).setBusinessKey(String.valueOf(workOrder.getId()))); + List userIds = roleApi.getUserIdsByRoleCode("Inspector_global_leader"); + //抄送 + if(null != userIds && !userIds.isEmpty()){ + copyService.createProcessInstanceCopy(userIds,"APP巡查工单创建,自动抄送",processInstanceId,"StartEvent","发起流程",null); + } // 将工作流的编号,更新到 工单中 workOrderMapper.updateById(new MainInfoDO().setId(workOrder.getId()).setProcessInstanceId(processInstanceId)); @@ -220,6 +226,18 @@ public class BpmInspectorServiceImpl implements BpmInspectorService{ } @Override + @Transactional(rollbackFor = Exception.class) + public List batchCreateWorkOrder(List reqVOList) { + List idList = new ArrayList<>(); + for (AppGardenWorkOrderInspectorReqVO reqVO : reqVOList) { + // 调用你原来的单个创建方法 + Long id = createWorkOrder(reqVO); + idList.add(id); + } + return idList; + } + + @Override public void approveTask2TeamLeader(AppGardenTaskRegionMgrApproveReqVO reqVO) { BpmTaskApproveReqVO targetVO = BeanUtils.toBean(reqVO,BpmTaskApproveReqVO.class); Map variables = new HashMap<>(); diff --git a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/service/maininfo/MainInfoService.java b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/service/maininfo/MainInfoService.java index 1946551..904d5d2 100644 --- a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/service/maininfo/MainInfoService.java +++ b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/service/maininfo/MainInfoService.java @@ -1,6 +1,8 @@ package com.zteits.urbanops.module.workorder.service.maininfo; import java.util.*; + +import com.zteits.urbanops.module.workorder.controller.app.maininfo.vo.AppMainInfoPageReqVO; import jakarta.validation.*; import com.zteits.urbanops.module.workorder.controller.admin.maininfo.vo.*; import com.zteits.urbanops.module.workorder.dal.dataobject.maininfo.MainInfoDO; @@ -67,4 +69,12 @@ public interface MainInfoService { */ MainInfoDO getMainInfoByOrderNo(String orderNo); + /** + * 获得工单信息分页 + * + * @param pageReqVO 分页查询 + * @return 工单信息分页 + */ + PageResult selectPageExcludeQuickOrder(AppMainInfoPageReqVO pageReqVO); + } diff --git a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/service/maininfo/MainInfoServiceImpl.java b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/service/maininfo/MainInfoServiceImpl.java index acd3a1f..df91346 100644 --- a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/service/maininfo/MainInfoServiceImpl.java +++ b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/service/maininfo/MainInfoServiceImpl.java @@ -2,8 +2,12 @@ package com.zteits.urbanops.module.workorder.service.maininfo; import com.zteits.urbanops.framework.common.pojo.PageResult; import com.zteits.urbanops.framework.common.util.object.BeanUtils; +import com.zteits.urbanops.framework.common.util.object.ObjectUtils; +import com.zteits.urbanops.module.system.api.permission.RoleApi; +import com.zteits.urbanops.module.system.enums.common.CommonConstants; import com.zteits.urbanops.module.workorder.controller.admin.maininfo.vo.MainInfoPageReqVO; import com.zteits.urbanops.module.workorder.controller.admin.maininfo.vo.MainInfoSaveReqVO; +import com.zteits.urbanops.module.workorder.controller.app.maininfo.vo.AppMainInfoPageReqVO; import com.zteits.urbanops.module.workorder.dal.dataobject.maininfo.MainInfoDO; import com.zteits.urbanops.module.workorder.dal.mysql.maininfo.MainInfoMapper; import jakarta.annotation.Resource; @@ -13,6 +17,7 @@ import org.springframework.validation.annotation.Validated; import java.util.List; import static com.zteits.urbanops.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.zteits.urbanops.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; import static com.zteits.urbanops.module.workorder.enums.ErrorCodeConstants.MAIN_INFO_NOT_EXISTS; /** @@ -27,6 +32,10 @@ public class MainInfoServiceImpl implements MainInfoService { @Resource private MainInfoMapper mainInfoMapper; + @Resource + private RoleApi roleApi; + + @Override public Long createMainInfo(MainInfoSaveReqVO createReqVO) { // 插入 @@ -82,4 +91,38 @@ public class MainInfoServiceImpl implements MainInfoService { return mainInfoMapper.selectOne(MainInfoDO::getOrderNo, orderNo); } + public PageResult selectPageExcludeQuickOrder(AppMainInfoPageReqVO pageReqVO) { + String type = pageReqVO.getType(); + String content = pageReqVO.getSearchContent(); + if (ObjectUtils.isNotAllEmpty(type)){ + if("1".equals(type)){ + if(ObjectUtils.isNotAllEmpty(content)){pageReqVO.setLonLatAddress(content);} + } else if("2".equals(type)){ + if(ObjectUtils.isNotAllEmpty(content)){pageReqVO.setOrderName(content);} + } else if("3".equals(type)){ + if(ObjectUtils.isNotAllEmpty(content)){pageReqVO.setRemark(content);} + } else if("4".equals(type)){ + if(ObjectUtils.isNotAllEmpty(content)){pageReqVO.setOrderNo(content);} + } else { + if (ObjectUtils.isNotAllEmpty(content)) { + pageReqVO.setOrderName(content); + } + } + } else {//默认是工单名称 + if (ObjectUtils.isNotAllEmpty(content)) { + pageReqVO.setOrderName(content); + } + } + //部派工单 + if ("1".equals(pageReqVO.getQueryType())) { + //全域督察员查询自己发起的工单 + List userIds = roleApi.getUserIdsByRoleCode(CommonConstants.INSPECTOR_ROLE_KEY); + if (!userIds.contains(getLoginUserId())) { + pageReqVO.setUserId(getLoginUserId()); + pageReqVO.setWorkerCompanyId(null); + } + } + return mainInfoMapper.selectPageExcludeQuickOrder(pageReqVO); + } + } diff --git a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/util/RoleListUtils.java b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/util/RoleListUtils.java index d8e4b7d..531eadf 100644 --- a/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/util/RoleListUtils.java +++ b/urbanops-module-workorder/src/main/java/com/zteits/urbanops/module/workorder/util/RoleListUtils.java @@ -64,7 +64,7 @@ public class RoleListUtils { * @param code2 第二个要校验的编码(允许为 null,此时要求列表中存在 null 的 code) * @return true=同时包含code1和code2;false=不同时包含/列表为空/列表为null */ - public static boolean containsBothCodes(List roleList, String code1, String code2) { + public static boolean containsBothCodes(List roleList, String code1, String code2 ) { // 1. 列表为null/空,直接返回false if (roleList == null || roleList.isEmpty()) { return false; @@ -82,6 +82,7 @@ public class RoleListUtils { return containsCode1 && containsCode2; } + public static void teamLeaderByRoleAssign(List userList, Map processInstanceVariables){ //养护组长指派为多人 String assigneeIdStr = userList.stream() diff --git a/urbanops-server/pom.xml b/urbanops-server/pom.xml index 816a63b..27dd08a 100644 --- a/urbanops-server/pom.xml +++ b/urbanops-server/pom.xml @@ -146,6 +146,17 @@ urbanops-spring-boot-starter-protection + + + org.bouncycastle + bcprov-jdk18on + 1.80 + + + org.bouncycastle + bcutil-jdk18on + 1.80 + diff --git a/urbanops-server/src/main/resources/application-local.yaml b/urbanops-server/src/main/resources/application-local.yaml index ff41af5..231aeaf 100644 --- a/urbanops-server/src/main/resources/application-local.yaml +++ b/urbanops-server/src/main/resources/application-local.yaml @@ -186,6 +186,9 @@ urbanops: refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址 transfer-notify-url: https://yunai.natapp1.cc/admin-api/pay/notify/transfer # 支付渠道的【转账】回调地址 demo: false # 开启演示模式 + security: + mock-enable: true # 本地调试启用 Mock 认证,Token 格式: test{userId}(如 test1 表示用户ID=1) + mock-secret: test tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc justauth: diff --git a/北京市行道树安全风险评估指南 2024.11.15.pdf b/北京市行道树安全风险评估指南 2024.11.15.pdf new file mode 100644 index 0000000..c702f3e --- /dev/null +++ b/北京市行道树安全风险评估指南 2024.11.15.pdf