docs: 文档体系重构

- 合并需求文档为SYSTEM-REQUIREMENTS.md(214项需求)
- 新增概要设计OVERVIEW-DESIGN.md
- 新增7份详细设计(AUTH/SPACE/ASSET/OPS/FINANCE/FRONTEND/CROSS-DOMAIN)
- 新增进度追踪体系progress.yml
- 新增测试计划TEST-PLAN.md
- 新增API测试脚本骨架(auth/asset/ops)
- 新增E2E测试项目骨架(Playwright)
- 旧文档归档至_archive目录
This commit is contained in:
chiguyong 2026-05-18 10:49:17 +08:00
parent 5619e6943b
commit cd86a97007
116 changed files with 14620 additions and 704 deletions

View File

@ -0,0 +1,857 @@
# Ether 物业管理系统 - 系统需求规格说明书
**文档版本**: v1.0
**创建日期**: 2026-05-18
**数据来源**: FEATURE_LIST.md / REVERSE-AUTH.md / REVERSE-MDM.md / REVERSE-ASSET.md / REVERSE-OPS.md / REVERSE-FINANCE.md / 00-项目总览.md
**维护原则**: 每个需求项标注唯一ID与实现状态变更时同步更新
---
## 一、系统概述
### 1.1 系统定位与目标
Ether 物业管理平台是一个面向住宅物业和商业物业的全场景智慧管理解决方案,旨在通过数字化手段实现物业运营全流程闭环管理。
**核心目标**
- 统一物业管理数据基座,消除信息孤岛
- 实现设备全生命周期管理,从采购安装到报废退役
- 建立运维工单闭环,覆盖报修、派单、执行、验收全流程
- 构建预防性维保体系,降低设备故障率
- 实现财务收费自动化,支持多种计费方式与支付渠道
- 提供多端协同能力管理后台、员工APP、业主小程序、商办小程序
**系统组成**
| 应用 | 目录 | 端口 | 说明 |
|------|------|------|------|
| 后端服务 | ether-pms | 8080 | Spring Boot 单体应用 |
| Web 管理后台 | ether-admin | 5175 | Vue3 + TypeScript + Ant Design Vue |
| 员工 APP | ether-app-employee | 5174 | 移动端应用 |
| 业主小程序 | ether-app-owner | 5176 | 微信/支付宝小程序 |
| 商办小程序 | ether-app-commercial | 5177 | 微信/支付宝小程序 |
**后端模块划分**
| 模块 | 职责 |
|------|------|
| module-auth | 认证授权(用户/角色/权限/部门/审计日志) |
| module-mdm | 主数据管理(项目/空间节点/巡检标准/备件/能耗) |
| module-asset | 资产设备管理(设备台账/扩展表/健康评分/故障历史/归属主体) |
| module-wo | 运维工单(工单/维保计划/维保任务/巡检模板) |
| module-finance | 财务管理(待建设) |
| module-common | 公共组件与工具 |
### 1.2 用户角色定义
| 角色 | 说明 | 用户类型 | 典型操作 |
|------|------|----------|----------|
| 系统管理员 | 平台运维人员 | ENTERPRISE | 用户管理、角色配置、系统设置 |
| 项目经理 | 物业项目负责人 | ENTERPRISE | 项目管理、成员分配、工单审批 |
| 维保人员 | 设备维修保养人员 | PROJECT_STAFF | 接单、执行维保、填写报告 |
| 巡检人员 | 设备巡检执行人员 | PROJECT_STAFF | 执行巡检、签到、异常上报 |
| 业主 | 房屋产权人 | RESIDENT | 报修、缴费、查看公告 |
| 租户 | 房屋承租人 | RESIDENT | 报修、缴费 |
### 1.3 系统边界与约束
**系统边界**
- 本系统管理物业项目从入驻到退场的全周期运营
- 涵盖设备资产从采购安装到报废退役的全生命周期
- 不包含房地产开发阶段的销售管理
- 不包含物业管理公司内部的人力资源管理(仅管理项目人员配置)
**技术约束**
- 后端Spring Boot 3.x 单体架构PostgreSQL 数据库
- 前端Vue3 + TypeScript + Ant Design Vue
- 认证JWT TokenHMAC-SHA256BCrypt 密码加密
- 数据隔离基于项目维度的数据隔离X-Project-ID Header
- 移动端uni-app 跨端框架
**业务约束**
- 所有业务数据以项目为基本归属维度
- 用户可参与多个项目,需切换项目上下文
- 角色权限支持项目级隔离(系统级/项目级/部门级)
### 1.4 术语定义
| 术语 | 定义 |
|------|------|
| MDM | Master Data Management主数据管理 |
| RBAC | Role-Based Access Control基于角色的访问控制 |
| MTBF | Mean Time Between Failures平均故障间隔时间 |
| MTTR | Mean Time To Repair平均修复时间 |
| JWT | JSON Web Token用于身份认证的令牌 |
| SpaceNode | 空间节点,统一空间体系中的树形节点 |
| WorkOrder | 工单,运维服务的业务单据 |
| MaintenanceTask | 维保任务,设备维保的执行单元 |
| OwnershipEntity | 归属主体,设备的所有权/管理权归属方 |
| DataScope | 数据范围,控制用户可访问的数据边界 |
---
## 二、业务域需求
### 2.1 身份与权限域
#### 2.1.1 用户管理
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-AUTH-001 | 用户创建 | 支持创建用户用户名3-50位字母数字下划线正则`^[a-zA-Z0-9_]{3,50}$`密码BCrypt加密存储手机号格式校验正则`^1[3-9]\d{9}$`邮箱格式校验用户类型必填ENTERPRISE/PROJECT_STAFF/RESIDENT/CUSTOMER默认状态ACTIVE | 高 | 已实现 | module-auth: UserController |
| REQ-AUTH-002 | 用户查询 | 支持分页查询用户列表,可按用户名/姓名/手机号/状态/用户类型筛选 | 高 | 已实现 | module-auth: UserController |
| REQ-AUTH-003 | 用户更新 | 支持更新用户基本信息(真实姓名/手机号/邮箱/头像/状态/部门状态枚举ACTIVE/LOCKED/DISABLED | 高 | 已实现 | module-auth: UserController |
| REQ-AUTH-004 | 用户删除 | 支持删除用户,删除时记录审计日志 | 中 | 已实现 | module-auth: UserController |
| REQ-AUTH-005 | 密码修改 | 需验证原密码新密码需通过强度校验8-20位大小写+数字+特殊字符弱密码检测黑名单password/123456/admin/qwerty/letmein/welcome/monkey/dragon旧密码兼容检测非BCrypt格式强制重置 | 高 | 已实现 | module-auth: UserController |
| REQ-AUTH-006 | 多类型用户扩展 | 企业员工EnterpriseUser含员工工号/部门/职位/入职日期/员工类别项目员工ProjectStaff含所属项目/员工类型/班次/班组长/在岗状态住户Resident含身份证/住户类型/认证状态 | 高 | 已实现 | module-auth: EnterpriseUser/ProjectStaff/Resident |
| REQ-AUTH-007 | 用户角色分配 | 支持为用户分配多个角色覆盖模式通过auth_user_role中间表关联 | 高 | 已实现 | module-auth: UserController.assignRoles |
| REQ-AUTH-008 | 用户项目关联 | 支持查看用户参与的项目列表、添加用户到项目、从项目移除用户关联信息含项目角色leader/member/viewer和加入时间 | 高 | 已实现 | module-auth: UserController |
#### 2.1.2 角色与权限
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-AUTH-010 | 角色管理 | 支持创建/查询/更新/删除角色角色代码2-50位字母数字下划线唯一角色类型三级SYSTEM系统级/PROJECT项目级/DEPARTMENT部门级角色状态ENABLED/DISABLED | 高 | 已实现 | module-auth: RoleController |
| REQ-AUTH-011 | 权限管理 | 支持创建/查询/更新/删除权限,权限代码格式`module:resource:action`仅字母数字冒号下划线权限类型MENU/BUTTON/API支持资源路径和HTTP方法API类型支持父权限代码parentCode构建权限树 | 高 | 已实现 | module-auth: PermissionController |
| REQ-AUTH-012 | 角色权限分配 | 支持为角色分配多个权限覆盖模式通过auth_role_permission中间表关联支持查询角色权限列表 | 高 | 已实现 | module-auth: RoleController.assignPermissions |
| REQ-AUTH-013 | 四级数据范围 | 角色数据范围枚举ALL全部数据不过滤/PROJECT本项目数据过滤project_id/DEPARTMENT本部门数据过滤project_id+dept_id/SELF仅本人数据过滤assigned_to/creator | 高 | 已实现 | module-auth: DataScopeService |
| REQ-AUTH-014 | 双层角色分配 | 用户直接角色auth_user_role表+ 项目员工角色ProjectStaffRole表登录时收集全部角色 = 用户直接角色 U 项目员工角色 | 高 | 已实现 | module-auth: AuthService |
| REQ-AUTH-015 | 项目级角色查询 | 支持按项目ID查询该项目下的角色列表 | 中 | 已实现 | module-auth: RoleController |
| REQ-AUTH-016 | 角色关联用户查询 | 支持查询拥有指定角色的用户列表 | 中 | 已实现 | module-auth: RoleController |
| REQ-AUTH-017 | 权限树端点 | 提供权限树结构查询接口GET /permissions/tree | 中 | 未实现 | - |
| REQ-AUTH-018 | 权限校验端点 | 提供权限校验接口POST /check验证用户是否拥有指定权限 | 中 | 未实现 | - |
| REQ-AUTH-019 | 用户菜单端点 | 提供用户菜单查询接口GET /users/{id}/menus返回用户可见菜单树 | 中 | 未实现 | - |
| REQ-AUTH-020 | 用户权限查询端点 | 提供用户权限查询接口GET /users/{id}/permissions返回用户全部权限列表 | 中 | 未实现 | - |
| REQ-AUTH-021 | 角色业务属性 | 角色扩展字段businessType业务类型/terminalType终端类型/isFrontline是否一线岗位 | 低 | 未实现 | - |
| REQ-AUTH-022 | 权限菜单路由属性 | 权限扩展字段path路由路径/component组件路径/icon图标/level层级 | 低 | 未实现 | - |
#### 2.1.3 组织架构
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-AUTH-030 | 部门管理 | 支持创建/查询/更新/删除部门部门名称1-100位支持树形结构parentId自引用部门类型5种ADMIN行政管理/ENGINEERING工程部/SECURITY安保部/CS客服部/CLEANING保洁部部门状态ACTIVE/DISABLED | 高 | 已实现 | module-auth: DeptController |
| REQ-AUTH-031 | 部门树查询 | 后端返回扁平列表Controller层递归构建树形结构DeptVO含children字段 | 高 | 已实现 | module-auth: DeptController.getDeptTree |
| REQ-AUTH-032 | 部门成员查询 | 支持查询指定部门下的成员列表 | 中 | 已实现 | module-auth: DeptController.getDeptMembers |
| REQ-AUTH-033 | 按类型查询部门 | 支持按部门类型deptType筛选部门列表 | 中 | 已实现 | module-auth: DeptController.getDeptsByType |
| REQ-AUTH-034 | 部门删除约束 | 删除部门前需先删除子部门,否则拒绝操作 | 高 | 已实现 | module-auth: DeptService |
#### 2.1.4 项目成员管理
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-AUTH-040 | 项目成员列表 | 支持分页查询指定项目的成员列表 | 高 | 已实现 | module-auth: ProjectMemberController |
| REQ-AUTH-041 | 可用成员查询 | 获取可添加到项目的企业员工列表支持search参数模糊搜索用户名/姓名) | 高 | 已实现 | module-auth: ProjectMemberController |
| REQ-AUTH-042 | 添加项目成员 | 创建/更新ProjectStaff记录设定员工类型、分配状态覆盖模式更新角色先删后加员工类型6种SECURITY/CLEANING/GARDEN/MAINTENANCE/CUSTOMER_SERVICE/GENERAL | 高 | 已实现 | module-auth: ProjectMemberController |
| REQ-AUTH-043 | 移除项目成员 | 删除ProjectStaff记录级联删除ProjectStaffRole角色关联 | 高 | 已实现 | module-auth: ProjectMemberController |
| REQ-AUTH-044 | 项目员工班次 | 项目员工班次类型3种DAY白班/NIGHT夜班/ROTATION轮班在岗状态3种ASSIGNED在岗/ON_LEAVE休假/TRANSFERRED已调离 | 中 | 已实现 | module-auth: ProjectStaff |
| REQ-AUTH-045 | 项目员工角色 | 项目员工在项目中的角色分配ProjectStaffRole支持一个员工多个项目角色 | 中 | 已实现 | module-auth: ProjectStaffRole |
#### 2.1.5 住户管理
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-AUTH-050 | 住户信息管理 | 住户Resident通过共享主键一对一关联User含身份证号18位JSON忽略脱敏、住户类型3种OWNER业主/FAMILY家属/TENANT租户 | 高 | 已实现 | module-auth: Resident |
| REQ-AUTH-051 | 住户认证流程 | 认证状态4种流转UNVERIFIED未认证-> PENDING待审核-> VERIFIED已认证/REJECTED已拒绝记录认证时间和认证人 | 高 | 已实现 | module-auth: Resident |
| REQ-AUTH-052 | 住户房屋绑定 | 住户-房屋关联ResidentSpace关系类型3种OWNER/FAMILY/TENANT绑定状态4种PENDING待确认/ACTIVE生效中/EXPIRED已过期/CANCELLED已取消支持生效日期和失效日期NULL=永久) | 高 | 已实现 | module-auth: ResidentSpace |
| REQ-AUTH-053 | 房屋空间管理 | 房屋空间Space含楼栋/单元/房号/房屋类型RESIDENTIAL/COMMERCIAL/楼层/建筑面积/状态,归属项目 | 中 | 已实现 | module-auth: Space |
#### 2.1.6 认证与授权
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-AUTH-060 | 用户登录 | POST /api/auth/login请求体{username, password},流程:检查登录锁定 -> 查询用户(含角色) -> 验证密码(BCrypt) -> 检查用户状态(LOCKED/DISABLED拒绝) -> 收集全部角色 -> 生成JWT Token -> 返回{token, userId, username, realName, roles} | 高 | 已实现 | module-auth: AuthController |
| REQ-AUTH-061 | 用户登出 | POST /api/auth/logout需携带Authorization Header | 高 | 已实现 | module-auth: AuthController |
| REQ-AUTH-062 | Token刷新 | POST /api/auth/refresh需携带有效Token生成新Token返回 | 高 | 已实现 | module-auth: AuthController |
| REQ-AUTH-063 | 获取当前用户 | GET /api/auth/me需携带Authorization Header返回当前用户信息 | 高 | 已实现 | module-auth: AuthController |
| REQ-AUTH-064 | JWT Token规范 | 算法HMAC-SHA256Claims含userId(subject)/username/roles列表过期时间可配置默认86400000ms=24小时 | 高 | 已实现 | module-auth: JwtTokenProvider |
| REQ-AUTH-065 | 登录失败锁定 | 基于Redis实现Key格式`login:attempt:{username}`5次失败后锁定10分钟每次失败递增计数并设置TTL登录成功清除计数支持手动解锁 | 高 | 已实现 | module-auth: LoginAttemptService |
| REQ-AUTH-066 | 项目上下文传递 | 请求头`X-Project-ID`携带当前项目IDProjectContextInterceptor拦截器设置ThreadLocal业务层通过DataScopeService过滤数据 | 高 | 已实现 | module-auth: ProjectContextInterceptor |
#### 2.1.7 审计日志
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-AUTH-070 | 操作日志记录 | 通过`@OperationLog`注解+AOP切面自动记录操作日志记录内容操作用户/操作描述/模块/操作类型/目标类型/目标ID/操作内容/请求参数/操作结果/IP地址/用户代理/请求URL/请求方法/执行耗时/状态/错误信息 | 高 | 已实现 | module-auth: OperationLogAspect |
| REQ-AUTH-071 | 异步持久化 | 通过`auditLogExecutor`线程池异步保存审计日志,避免影响业务性能 | 高 | 已实现 | module-auth: AuditLogService |
| REQ-AUTH-072 | 审计日志查询 | 支持分页查询可按module/action/username/日期范围筛选强制限制查询范围不超过30天超过自动截断起始时间 | 高 | 已实现 | module-auth: AuditLogController |
| REQ-AUTH-073 | 模块与操作类型 | 记录模块8种USER/ROLE/PERMISSION/PROJECT/AUTH/DEPT/PROJECT_MEMBER/SYSTEM操作类型11种CREATE/UPDATE/DELETE/QUERY/VIEW/LOGIN/LOGOUT/EXPORT/IMPORT/ASSIGN/REVOKE | 中 | 已实现 | module-auth: AuditLog |
| REQ-AUTH-074 | 日志统计 | 支持获取最近30天日志统计数据提供模块列表和操作类型列表用于筛选下拉 | 中 | 已实现 | module-auth: AuditLogController |
| REQ-AUTH-075 | 日志归档 | 90天以上日志归档当前实现为直接删除TODO: 导出至对象存储) | 低 | 部分实现 | module-auth: AuditLogService |
#### 2.1.8 系统配置
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-AUTH-080 | 键值配置管理 | 支持创建/查询/更新系统配置项配置键configKey唯一配置值最大5000字符支持单个更新和批量更新 | 高 | 已实现 | module-auth: SysConfigController |
| REQ-AUTH-081 | 密码策略配置 | 密码强度规则可配置(前缀`password.*`):最小长度(默认8)/最大长度(默认20)/需大写(默认true)/需小写(默认true)/需数字(默认true)/需特殊字符(默认true) | 高 | 已实现 | module-auth: PasswordStrengthValidator |
| REQ-AUTH-082 | 数据访问授权 | 通用数据访问控制DataAccess支持按dataType/dataId/accessType/accessId/accessLevel进行细粒度授权已存在相同记录则更新accessLevel | 中 | 已实现 | module-auth: DataAccessController |
---
### 2.2 空间与项目域
#### 2.2.1 项目管理
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-MDM-001 | 项目创建 | 支持创建项目,项目编码唯一(正则`^[a-zA-Z0-9_-]+$`2-50位项目名称2-100位项目类型3种RESIDENTIAL住宅/OFFICE办公/INDUSTRIAL_PARK产业园区支持自动生成项目编码创建后自动初始化ProjectConfig默认配置 | 高 | 已实现 | module-mdm: ProjectController |
| REQ-MDM-002 | 项目查询 | 支持分页查询项目列表可按keyword/status筛选支持排序支持按ID/编码查询项目详情;支持项目选择器列表(用于下拉选择) | 高 | 已实现 | module-mdm: ProjectController |
| REQ-MDM-003 | 项目更新与删除 | 支持更新项目基本信息删除前必须调用delete-check检查关联数据存在应收未收费用时无法删除 | 高 | 已实现 | module-mdm: ProjectController |
| REQ-MDM-004 | 项目状态流转 | 4种状态PENDING待审核/ACTIVE正常/DISABLED禁用/ARCHIVED已归档流转规则PENDING->ACTIVE/DISABLED, ACTIVE->DISABLED/ARCHIVED, DISABLED->ACTIVE/ARCHIVED, ARCHIVED为终态不可流转 | 高 | 已实现 | module-mdm: ProjectController.changeStatus |
| REQ-MDM-005 | 状态变更历史 | 每次状态变更记录ProjectStatusHistoryfromStatus/toStatus/reason/operatorId/operatorName | 中 | 已实现 | module-mdm: ProjectStatusHistory |
| REQ-MDM-006 | 项目统计 | 支持查询项目统计数据ProjectStatistics快照成员数/楼栋数/单元数/房间数/业主数/租户数 | 中 | 已实现 | module-mdm: ProjectController.getStatistics |
| REQ-MDM-007 | 项目配置管理 | 项目功能开关10项预约/访客/投诉(默认开)/缴费/公告(默认开)/问卷/投票/报修(默认开)/资产/自定义配置JSON | 高 | 已实现 | module-mdm: ProjectConfigController |
| REQ-MDM-008 | 项目成员管理MDM侧 | 支持查看项目成员列表分页、添加成员从企业员工中选择支持批量、移除成员成员角色5种PROJECT_MANAGER/PROJECT_ADMIN/OPERATION_STAFF/FINANCE_STAFF/VIEWER | 高 | 已实现 | module-mdm: ProjectMemberController |
| REQ-MDM-009 | 项目删除检查 | 删除前检查关联数据,返回{canDelete, reason, statistics}存在应收未收费用时canDelete=false | 高 | 已实现 | module-mdm: ProjectController |
#### 2.2.2 空间节点管理
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-MDM-010 | 空间节点创建 | 支持创建空间节点节点名称1-100位节点大类4种NodeCategoryBUILDING建筑空间/PARKING停车空间/FACILITY设施空间/AREA区域空间节点类型15种NodeType含层级序号支持批量创建 | 高 | 已实现 | module-mdm: SpaceNodeController |
| REQ-MDM-011 | 15种节点类型 | BUILDING(楼栋,L1)/UNIT(单元,L2)/FLOOR(楼层,L3)/ROOM(房间,L4)/SHOP(商铺,L2)/GARAGE(车库,L1)/PARKING_AREA(停车区域,L2)/PARKING_SPACE(车位,L3)/EQUIPMENT_ROOM(设备房,L1)/PROPERTY_OFFICE(物业用房,L1)/SECURITY_ROOM(门岗,L1)/PUBLIC_ROOM(公共用房,L1)/PUBLIC_AREA(公共区域,L1)/GREEN_AREA(绿化区域,L1)/ROAD(道路,L1) | 高 | 已实现 | module-mdm: SpaceNode |
| REQ-MDM-012 | 树形结构维护 | 支持树形路径维护treePath物理路径id.id.id用于快速查询子孙、treePathName名称路径项目/楼栋/单元/房间用于展示、level层级深度根节点为0创建/移动节点时自动维护 | 高 | 已实现 | module-mdm: SpaceNodeService |
| REQ-MDM-013 | 空间节点查询 | 支持按项目查询节点列表、获取项目空间树、获取项目根节点、按项目+类型查询、获取子节点列表、按ID查询详情 | 高 | 已实现 | module-mdm: SpaceNodeController |
| REQ-MDM-014 | 空间节点更新与删除 | 支持更新节点信息删除前必须调用delete-check检查子节点数量普通删除有子节点时拒绝级联删除DELETE /{id}/cascade含子节点软删除isDeleted标记 | 高 | 已实现 | module-mdm: SpaceNodeController |
| REQ-MDM-015 | 面积信息管理 | 支持记录建筑面积/使用面积/公摊面积/占地面积BigDecimal(10,2) | 中 | 已实现 | module-mdm: SpaceNode |
| REQ-MDM-016 | 地理信息管理 | 支持记录经度/纬度/海拔/楼层号(正数地上,负数地下) | 中 | 已实现 | module-mdm: SpaceNode |
| REQ-MDM-017 | 设备扩展模式 | SpaceNode通过isEquipment字段标记设备节点设备节点额外使用设备扩展字段维保信息/巡检周期/特种设备证书/常用备件/能耗标准/安装环境/防护等级等20+字段) | 高 | 已实现 | module-mdm: SpaceNode |
| REQ-MDM-018 | 扩展属性JSON | 支持attributes字段String(2000) JSON格式存储类型特定属性 | 中 | 已实现 | module-mdm: SpaceNode |
| REQ-MDM-019 | 空间编码自动生成 | 自动生成空间编码规则唯一约束UNIQUE(project_id, code) | 中 | 未实现 | - |
| REQ-MDM-020 | 节点移动 | 支持移动节点到新的父节点PUT /{id}/move自动维护treePath | 中 | 未实现 | - |
| REQ-MDM-021 | 祖先/子孙查询 | 支持查询指定节点的祖先链GET /{id}/ancestors和子孙节点GET /{id}/descendants | 低 | 未实现 | - |
| REQ-MDM-022 | 空间统计分析 | 支持空间统计接口:按类型统计/按状态统计/空间总览 | 低 | 未实现 | - |
| REQ-MDM-023 | PostGIS空间查询 | 支持PostGIS空间查询范围搜索/距离计算GEOMETRY(Point)/GEOMETRY(Polygon)字段GIST空间索引 | 低 | 未实现 | - |
| REQ-MDM-024 | 地图服务 | 高德地图集成,支持标注/绘制/路径MapEditor组件地图标记/边界查询API | 低 | 未实现 | - |
| REQ-MDM-025 | 空间节点导入导出 | 支持空间节点批量导入和导出,导入模板下载 | 中 | 未实现 | - |
#### 2.2.3 巡检标准项
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-MDM-030 | 巡检标准项CRUD | 支持创建/查询/更新/删除巡检标准项按设备类型equipmentType和系统类型systemType分类每项含检查项名称/检查方法/标准值/是否必检(默认true)/排序号/状态(ACTIVE/INACTIVE) | 高 | 已实现 | module-mdm: InspectionItemController |
| REQ-MDM-031 | 标准项查询筛选 | 支持按activeOnly=true仅查询启用项equipmentType+systemType双条件过滤equipmentType单条件过滤systemType过滤 | 中 | 已实现 | module-mdm: InspectionItemService |
| REQ-MDM-032 | 巡检模板管理 | 支持创建/查询/更新/复制巡检模板,模板按项目+设备类型组织,含检查项列表(JSON格式存储),支持版本管理(version字段)模板与标准项通过equipmentType逻辑关联 | 高 | 已实现 | module-mdm: InspectionTemplateController |
| REQ-MDM-033 | 巡检记录管理 | 支持创建/查询/更新/删除/完成巡检记录,记录关联设备(equipmentId)和计划(planId)检查结果状态3种NORMAL/WARNING/ABNORMAL支持签到信息时间/位置/照片检查项结果和异常问题以JSONB存储 | 高 | 已实现 | module-mdm: InspectionRecordController |
| REQ-MDM-034 | 巡检记录查询 | 支持按设备/计划/巡检人/状态/日期范围筛选,支持按设备+日期范围组合查询 | 中 | 已实现 | module-mdm: InspectionRecordService |
---
### 2.3 设备与资产域
#### 2.3.1 设备台账
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-ASSET-001 | 设备创建 | 支持创建设备设备编码全局唯一equipmentCode设备名称必填设备类型10种ELEVATOR/HVAC/FIRE_PROTECTION/PLUMBING/ELECTRICAL/ENERGY_METER/SECURITY/LANDSCAPE/KITCHEN/OTHER归属类型4种默认PROJECTPROJECT(项目自有)/COMPANY(公司统筹)/OWNER(业主自置)/RENTAL(租赁设备)设备状态4种默认ACTIVEACTIVE(在用)/INACTIVE(停用)/MAINTENANCE(维保中)/SCRAPPED(已报废) | 高 | 已实现 | module-asset: EquipmentController |
| REQ-ASSET-002 | 设备查询 | 支持按项目查询/按空间查询/按类型查询/按归属查询设备列表支持按ID查询设备详情 | 高 | 已实现 | module-asset: EquipmentController |
| REQ-ASSET-003 | 设备更新与删除 | 支持更新设备信息支持单个删除和批量删除POST /batch-delete逻辑删除isDeleted标记 | 高 | 已实现 | module-asset: EquipmentController |
| REQ-ASSET-004 | 设备与空间关联 | 设备通过spaceNodeId关联空间节点树支持按空间节点查询设备列表 | 高 | 已实现 | module-asset: Equipment.spaceNodeId |
| REQ-ASSET-005 | 系统类型分类 | 设备系统类型9种商业地产8大系统+其他HVAC(暖通空调)/FIRE(消防)/ELEVATOR(电梯)/ELECTRICAL(电气)/PLUMBING(给排水)/BAS(弱电智能化)/KITCHEN(餐饮厨房)/LANDSCAPE(景观)/OTHER(其他) | 高 | 已实现 | module-asset: Equipment.systemType |
| REQ-ASSET-006 | 设备统计 | 支持按类型统计/按归属统计/项目设备总数查询 | 中 | 已实现 | module-asset: EquipmentController |
| REQ-ASSET-007 | 设备导入 | 支持Excel批量导入设备.xlsx/.xls文件大小最大10MB行数最大1000行文件类型校验返回{successCount, failCount, failDetails} | 高 | 已实现 | module-asset: EquipmentController.importEquipment |
| REQ-ASSET-008 | 设备导出 | 支持按项目导出设备列表为Excel文件.xlsx格式 | 中 | 已实现 | module-asset: EquipmentController.exportEquipment |
| REQ-ASSET-009 | 设备照片管理 | 支持设备照片JSONB内嵌存储照片类型4种外观/铭牌/安装位置/环境每张照片含type/url/remark | 中 | 已实现 | module-asset: Equipment.photos |
| REQ-ASSET-010 | 设备文档管理 | 支持电子文档JSONB内嵌存储文档类型4种manual/certificate/contract/other每份文档含name/url/size/type/remark | 中 | 已实现 | module-asset: Equipment.documents |
| REQ-ASSET-011 | 设备二维码 | 支持设备二维码生成、扫码查看设备详情、扫码快速报修 | 中 | 未实现 | - |
| REQ-ASSET-012 | 设备规格字段 | 设备规格型号字段specifications | 低 | 未实现 | - |
#### 2.3.2 设备扩展信息
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-ASSET-020 | 电梯扩展表 | EquipmentElevatorasset_equipment_elevator通过equipment_id UNIQUE一对一关联主表电梯类型/型号/载重(kg)/速度(m/s)/楼层数/井道尺寸/底坑深度/顶层高度/注册登记号/检验证书/下次检验日期/能耗/维保等级/应急救援预案 | 高 | 已实现 | module-asset: EquipmentElevatorService |
| REQ-ASSET-021 | 暖通扩展表 | EquipmentHvacasset_equipment_hvac暖通类型/制冷量/制热量/风量/制冷剂类型/充注量/能效比(EER)/性能系数(COP)/安装日期/保修到期/滤网更换周期/上次更换日期/风管类型/风管尺寸 | 高 | 已实现 | module-asset: EquipmentHvacService |
| REQ-ASSET-022 | 消防扩展表 | EquipmentFireasset_equipment_fire消防设备类型/安装面积/安装高度/探测范围/系统类型/分区编号/回路编号/是否启用联动/联动动作/巡检周期/上次/下次巡检日期/巡检结果/特殊要求 | 高 | 已实现 | module-asset: EquipmentFireService |
| REQ-ASSET-023 | 能源计量扩展表 | EquipmentEnergyasset_equipment_energy表计类型/能源类型/表计型号/规格/表常数/精度等级/抄表方式/上次读数/当前读数/单价/计费方式/通讯方式/通讯地址/检定周期/下次检定日期/检定证书 | 高 | 已实现 | module-asset: EquipmentEnergyService |
| REQ-ASSET-024 | 扩展表读写模式 | 扩展表通过主设备ID路径读写GET/PUT /{id}/elevator采用saveOrUpdate模式存在则更新不存在则新建 | 高 | 已实现 | module-asset: Equipment*Service |
| REQ-ASSET-025 | 无扩展表设备类型 | PLUMBING/ELECTRICAL/SECURITY/LANDSCAPE/KITCHEN/OTHER无扩展表使用主表通用字段+attributes JSON扩展 | 中 | 已实现 | module-asset: Equipment.attributes |
#### 2.3.3 设备健康评分
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-ASSET-030 | 健康评分算法 | 健康度 = 基础分(100) - 故障扣分 - 维保扣分 - 年龄扣分最低0分。故障扣分=故障次数(近30天)x5无上限维保扣分=(1-维保完成率)x20上限20分年龄扣分=设备年龄(年)x2上限10分 | 高 | 已实现[Beta] | module-asset: EquipmentHealthServiceImpl |
| REQ-ASSET-031 | 5级健康等级 | EXCELLENT(优秀,[90,100))/GOOD(良好,[75,90))/FAIR(一般,[60,75))/POOR(较差,[40,60))/CRITICAL(危急,[0,40)) | 高 | 已实现[Beta] | module-asset: HealthLevel |
| REQ-ASSET-032 | MTBF计算 | 平均故障间隔时间 = 运行时间(小时) / 故障次数计算周期默认30天可自定义仅1次故障时使用默认期间天数x24小时无故障返回0 | 中 | 已实现[Beta] | module-asset: EquipmentHealthServiceImpl |
| REQ-ASSET-033 | MTTR计算 | 平均修复时间 = 总修复时间(小时) / 修复次数仅统计已完成修复的故障记录无修复记录返回0 | 中 | 已实现[Beta] | module-asset: EquipmentHealthServiceImpl |
| REQ-ASSET-034 | 健康评分查询 | 支持获取设备最新健康度、健康度历史、手动触发计算 | 中 | 已实现[Beta] | module-asset: EquipmentHealthController |
| REQ-ASSET-035 | 维保完成率计算 | 维保完成率用于健康评分扣分当前TODO标记暂返回1.0 | 中 | 部分实现 | module-asset: EquipmentHealthServiceImpl |
#### 2.3.4 故障历史
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-ASSET-040 | 故障记录 | 支持记录设备故障故障类型4种SUDDEN(突发)/SCHEDULED(计划停机)/WARNING(预警)/OTHER(其他)故障等级4级LOW(轻微)/MEDIUM(一般)/HIGH(严重)/CRITICAL(危急)修复结果4种FIXED(已修复)/PARTIAL_FIXED(部分修复)/REPLACED(更换新设备)/PENDING(待处理) | 高 | 已实现 | module-asset: EquipmentHealthController |
| REQ-ASSET-041 | 故障自动计算 | 记录故障时自动计算:修复时长=(repairEndTime-repairStartTime)/60分钟转小时停机时长=(repairStartTime-failureTime)/60分钟转小时 | 中 | 已实现 | module-asset: EquipmentFailureHistory |
| REQ-ASSET-042 | 故障历史查询 | 支持按设备ID查询故障历史列表 | 中 | 已实现 | module-asset: EquipmentHealthController |
| REQ-ASSET-043 | 故障自动工单 | 故障记录时自动创建维修工单(事件驱动) | 中 | 未实现 | - |
#### 2.3.5 归属主体管理
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-ASSET-050 | 归属主体CRUD | 支持创建/查询/更新/删除归属主体主体类型3种COMPANY(公司)/OWNER(业主)/RENTAL_COMPANY(租赁公司),含主体名称/编码/联系人/联系电话/联系地址/营业执照/法人代表/合同编号/合同起止日期/租赁费用 | 高 | 已实现 | module-asset: OwnershipEntityController |
| REQ-ASSET-051 | 归属主体查询 | 支持按类型查询归属主体、获取全部未删除主体 | 中 | 已实现 | module-asset: OwnershipEntityController |
| REQ-ASSET-052 | 归属主体逻辑删除 | 删除操作为逻辑删除isDeleted标记查询时过滤已删除记录 | 高 | 已实现 | module-asset: OwnershipEntityController |
| REQ-ASSET-053 | 设备归属冗余 | Equipment表同时存储owningEntityId和owningEntityName避免频繁关联查询 | 中 | 已实现 | module-asset: Equipment |
#### 2.3.6 特种设备管理
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-ASSET-060 | 特种设备标记 | 设备主表含specialEquipmentType特种设备类型和specialEquipmentCert特种设备证书字段 | 中 | 已实现 | module-asset: Equipment |
| REQ-ASSET-061 | 年检周期管理 | 设备含inspectionCycle年检周期/月和nextInspectionDate下次年检日期字段 | 中 | 已实现 | module-asset: Equipment |
| REQ-ASSET-062 | 特种设备列表查询 | 支持查询项目下的特种设备列表 | 中 | 已实现 | module-mdm: SpaceNodeController.getSpecialEquipmentList |
| REQ-ASSET-063 | 即将年检预警 | 支持查询即将年检设备默认90天内按项目筛选 | 中 | 已实现 | module-mdm: SpaceNodeController.getExpiringInspectionEquipment |
| REQ-ASSET-064 | 维保到期定时提醒 | 后端定时任务检查维保合同到期/年检到期,自动发送提醒通知 | 中 | 未实现 | - |
---
### 2.4 运营与服务域
#### 2.4.1 工单管理
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-OPS-001 | 工单创建 | 支持创建工单工单编号自动生成格式WO-YYYYMMDD-XXXX4位序号按日递增工单来源6种OWNER(业主报修)/MAINTENANCE(维保计划)/INSPECTION(巡检触发)/FAULT(故障触发)/REGULATORY(法规巡检)/MANUAL(手动创建)工单类型6种REPAIR/INSPECTION/SECURITY/CLEANING/PROPERTY/CONSULTATION | 高 | 已实现 | module-wo: WorkOrderController |
| REQ-OPS-002 | 工单查询 | 支持查询工单列表可按projectId/equipmentId/source/type/status/assignedTo筛选参数互斥按优先级依次判断支持按ID查询工单详情 | 高 | 已实现 | module-wo: WorkOrderController |
| REQ-OPS-003 | 工单状态流转 | 6种状态PENDING(待分配)->ASSIGNED(已派单)->IN_PROGRESS(执行中)->COMPLETED(已完成)->VERIFIED(已验收)CANCELLED(已取消PENDING/ASSIGNED/IN_PROGRESS可转入)COMPLETED/VERIFIED不可取消 | 高 | 已实现 | module-wo: WorkOrderService |
| REQ-OPS-004 | 工单派单 | POST /{id}/assign指定负责人(assignedTo)/服务商(assignedVendor)/派单日期(assignedDate)状态PENDING->ASSIGNED | 高 | 已实现 | module-wo: WorkOrderController |
| REQ-OPS-005 | 工单执行 | POST /{id}/start记录实际开始时间(actualStart=now())状态ASSIGNED->IN_PROGRESS | 高 | 已实现 | module-wo: WorkOrderController |
| REQ-OPS-006 | 工单完成 | POST /{id}/complete记录故障原因/解决方案/处理结果/费用(人工费+备件费=总费用),自动计算实际工时(actualHours=Duration.between(actualStart,actualEnd))状态IN_PROGRESS->COMPLETED | 高 | 已实现 | module-wo: WorkOrderController |
| REQ-OPS-007 | 工单验收 | POST /{id}/verify记录验收人(verifiedBy)/评分(rating,1-5星)/备注(remark)状态COMPLETED->VERIFIED | 高 | 已实现 | module-wo: WorkOrderController |
| REQ-OPS-008 | 工单取消 | POST /{id}/cancelPENDING/ASSIGNED/IN_PROGRESS可取消COMPLETED/VERIFIED不可取消抛出异常"无法取消已完成的工单" | 中 | 已实现 | module-wo: WorkOrderController |
| REQ-OPS-009 | 工单优先级 | 4种优先级LOW/MEDIUM(默认)/HIGH/URGENT | 高 | 已实现 | module-wo: WorkOrder |
| REQ-OPS-010 | 工单明细管理 | 支持查询/批量添加工单明细WorkOrderItem明细类型3种PART(备件)/INSPECTION_ITEM(巡检项)/CHECKPOINT(检查点),含名称/数量/单位/单价/总价/是否正常/观察记录/建议 | 中 | 已实现 | module-wo: WorkOrderController |
| REQ-OPS-011 | 工单统计 | 支持工单多维度统计:总数/待分配/已派单/执行中/已完成/已验收/已取消/今日完成/今日创建/逾期/平均完成工时/平均评分/按来源分布/按类型分布/按优先级分布 | 中 | 已实现 | module-wo: WorkOrderController |
| REQ-OPS-012 | 工单挂起与恢复 | 支持工单挂起SUSPENDED状态和恢复操作任意执行中状态可挂起 | 中 | 未实现 | - |
| REQ-OPS-013 | 工单退回 | 支持已派单工单退回重分配RETURNED状态ASSIGNED状态可退回 | 中 | 未实现 | - |
| REQ-OPS-014 | 工单流转记录 | 记录工单每次状态变更的流转历史WorkOrderFlow实体 | 中 | 未实现 | - |
| REQ-OPS-015 | 工单报修人信息 | 工单含报修人信息字段reporterId/reporterName/reporterPhone/reporterAddress | 中 | 未实现 | - |
| REQ-OPS-016 | 工单分页查询 | 工单列表接口支持分页返回当前为全量List返回 | 中 | 未实现 | - |
#### 2.4.2 维保计划
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-OPS-020 | 维保计划创建 | 支持创建维保计划,计划编码自动生成(唯一),计划名称必填,关联设备(equipmentId)必填计划类型2种PREVENTIVE(预防性)/CORRECTIVE(纠正性),周期天数(cycleDays),预估工时,指定服务商 | 高 | 已实现 | module-wo: MaintenancePlan |
| REQ-OPS-021 | 维保计划状态管理 | 计划状态3种ACTIVE(启用)/INACTIVE(停用)/SUSPENDED(暂停)删除行为为逻辑删除将状态设为INACTIVE | 高 | 已实现 | module-wo: MaintenancePlan |
| REQ-OPS-022 | 维保计划周期调度 | 通过cycleDays计算下次执行日期(nextDate),上次执行日期(lastDate) | 中 | 已实现 | module-wo: MaintenancePlan |
| REQ-OPS-023 | 维保计划自动调度 | 定时任务根据维保计划周期自动生成维保任务 | 中 | 未实现 | - |
#### 2.4.3 维保任务
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-OPS-030 | 维保任务创建 | 支持创建维保任务任务编号自动生成格式EQ-YYYYMMDD-XXXX关联设备(equipmentId)必填任务类型3种PREVENTIVE(预防性)/CORRECTIVE(纠正性)/EMERGENCY(紧急维修) | 高 | 已实现 | module-wo: MaintenanceTaskController |
| REQ-OPS-031 | 维保任务状态流转 | 6种状态PENDING->ASSIGNED->IN_PROGRESS->COMPLETED->VERIFIEDCANCELLED(PENDING/ASSIGNED/IN_PROGRESS可转入),与工单状态机一致 | 高 | 已实现 | module-wo: MaintenanceTaskService |
| REQ-OPS-032 | 4种触发方式 | 触发类型4种PLAN(计划触发)/INSPECTION(巡检触发)/FAULT(故障触发)/MANUAL(手动创建) | 高 | 已实现 | module-wo: MaintenanceTask |
| REQ-OPS-033 | 自动优先级判定 | 紧急维修类型->URGENT故障触发+紧急关键词(困人/漏水/停电/火灾/爆炸/漏电/冒烟/故障停机)->URGENT故障触发无紧急关键词->HIGH巡检触发->HIGH计划触发->MEDIUM其他->MEDIUM | 高 | 已实现 | module-wo: MaintenanceTaskServiceImpl.autoDeterminePriority |
| REQ-OPS-034 | 双完成接口 | 简版完成POST /{id}/complete处理结果/实际工时/总费用/完成人详版完成POST /{id}/complete-details含故障原因/解决方案/使用备件(JSONB)/人工费/备件费 | 中 | 已实现 | module-wo: MaintenanceTaskController |
| REQ-OPS-035 | 维保任务验收 | POST /{id}/verify记录验收人/评分(1-5)/备注独立评分接口POST /{id}/rate | 中 | 已实现 | module-wo: MaintenanceTaskController |
| REQ-OPS-036 | 完成后设备联动 | 维保任务完成时自动更新设备信息:维保商=任务服务商;预防性维护更新下次巡检日期(now()+inspectionCycle默认30天);异常容错(更新设备失败不影响工单完成) | 高 | 已实现 | module-wo: MaintenanceTaskServiceImpl |
| REQ-OPS-037 | 维保任务统计 | 支持多维度统计:总数/待分配/已派单/执行中/已完成/已验收/已取消/今日完成/今日创建/逾期/平均完成工时/平均评分/按优先级分布/按触发类型分布/人工费总计/备件费总计/费用总计 | 中 | 已实现 | module-wo: MaintenanceTaskController |
| REQ-OPS-038 | 使用备件记录 | 维保任务完成时可记录使用备件列表partsUsed JSONB含备件ID/名称/数量/单价/总价 | 中 | 已实现 | module-wo: MaintenanceTask |
#### 2.4.4 巡检管理
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-OPS-040 | 巡检模板管理 | 支持创建/查询/更新/复制巡检模板模板编码自动生成唯一模板状态2种ACTIVE/INACTIVE模板含检查项列表@Transient非持久化通过templateId关联InspectionItem | 高 | 已实现 | module-wo: InspectionTemplate |
| REQ-OPS-041 | 巡检项管理 | 检查项通过templateId关联模板含检查项名称/描述/检查方法/检查标准/是否必检(默认true)/是否需要正常判定(默认true)/排序号 | 高 | 已实现 | module-wo: InspectionItem |
| REQ-OPS-042 | 巡检签到 | 巡检记录支持签到信息:签到时间(checkInTime)/签到位置(checkInLocation)/签到照片(checkInPhoto) | 中 | 已实现 | module-mdm: InspectionRecord |
| REQ-OPS-043 | 巡检异常上报 | 巡检记录支持异常问题列表problems JSONB含问题描述/照片/严重程度(LOW/MEDIUM/HIGH) | 中 | 已实现 | module-mdm: InspectionRecord |
| REQ-OPS-044 | 巡检扫码签到 | 支持扫描设备二维码进行巡检签到,记录位置信息 | 中 | 未实现 | - |
#### 2.4.5 备件管理
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-OPS-050 | 备件分类管理 | 支持创建/查询备件分类分类编码唯一支持树形分类parentId自关联含分类名称/描述/排序号 | 高 | 已实现 | module-mdm: SparePartController |
| REQ-OPS-051 | 备件台账CRUD | 支持创建/查询/更新/删除备件,备件编码唯一,含名称/分类/规格型号/计量单位/安全库存(默认0)/当前库存(默认0)/单价/供应商/供应商联系方式/存放位置/状态(ACTIVE/INACTIVE) | 高 | 已实现 | module-mdm: SparePartController |
| REQ-OPS-052 | 入库操作 | POST /in-stock增加库存记录备件ID/数量/操作人/备注更新currentStock记录SparePartRecord(类型IN含操作后余额balance) | 高 | 已实现 | module-mdm: SparePartController |
| REQ-OPS-053 | 出库操作 | POST /out-stock减少库存记录备件ID/数量/关联工单ID(可选)/操作人/备注校验库存不能为负更新currentStock记录SparePartRecord(类型OUT) | 高 | 已实现 | module-mdm: SparePartController |
| REQ-OPS-054 | 低库存预警 | 当currentStock < safeStock时触发预警GET /low-stock返回低于安全库存的备件列表 | | 已实现 | module-mdm: SparePartController |
| REQ-OPS-055 | 出入库记录查询 | 支持查询指定备件的所有出入库记录流水 | 中 | 已实现 | module-mdm: SparePartController |
| REQ-OPS-056 | 盘点与调整 | 支持盘点(CHECK)和调整(ADJUST)操作记录SparePartRecord | 中 | 已实现 | module-mdm: SparePartRecord(RecordType) |
#### 2.4.6 能耗管理
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-OPS-060 | 计量点管理 | 支持创建/查询/更新/删除计量点计量点编码自动生成EM+yyyyMMddHHmmss冲突时追加后缀能源类型6种LIGHTING(照明插座用电)/HVAC(空调用电)/POWER(动力用电)/SPECIAL(特殊用电)/WATER(给排水)/GAS(燃气),关联空间节点,含额定容量/单价(4位小数精度)/状态(ACTIVE/INACTIVE) | 高 | 已实现 | module-mdm: EnergyController |
| REQ-OPS-061 | 能耗抄表录入 | POST /consumption录入能耗数据{meterId, currentReading, recordedBy}校验仪表状态必须ACTIVE(错误码6102)/当前读数不能小于上次读数(错误码6103)/仪表必须存在(错误码6101),自动获取上次读数(首次为0),自动计算消耗量(current-previous),自动计算费用(consumption x unitPrice单价为空时为0)默认记录方式MANUAL | 高 | 已实现 | module-mdm: EnergyConsumptionService |
| REQ-OPS-062 | 能耗记录查询 | 支持按计量点查询能耗记录,可按日期范围(startDate/endDate)筛选 | 中 | 已实现 | module-mdm: EnergyController |
| REQ-OPS-063 | 按类型统计能耗 | GET /statistics/by-type按项目+月份统计各能源类型消耗量注意当前实现将总消耗全部归入LIGHTING类型存在已知缺陷 | 中 | 部分实现 | module-mdm: EnergyConsumptionService |
| REQ-OPS-064 | 单位面积能耗 | GET /statistics/unit-consumption计算总能耗/总建筑面积 | 中 | 已实现 | module-mdm: EnergyConsumptionService |
| REQ-OPS-065 | IoT自动抄表 | 支持对接IoT平台自动采集能耗数据RecordMethod.IOT | 低 | 未实现 | - |
| REQ-OPS-066 | 前后端能源类型统一 | 后端LIGHTING/HVAC/POWER/SPECIAL/WATER/GAS与前端ELECTRICITY/WATER/GAS/CENTRAL_HEATING/CENTRAL_COOLING需对齐 | 中 | 未实现 | - |
---
### 2.5 财务与收费域
#### 2.5.1 收费项目管理
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-FIN-001 | 收费项目实体 | FeeItem实体含code/name/type/billingMethod/unitPrice/unit/billDay/dueDay/overdueDay/enableLateFee/lateFeeRate/maxLateFee收费类型8种PROPERTY/PARKING/WATER/ELECTRICITY/GAS/HEATING/REPAIR_FUND/OTHER计费方式4种FIXED/AREA/USAGE/CUSTOM | 高 | 未实现 | - |
| REQ-FIN-002 | 收费项目CRUD | 支持创建/查询/更新/禁用收费项目 | 高 | 未实现 | - |
#### 2.5.2 收费账单管理
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-FIN-010 | 账单实体 | FeeBill实体含billNo/feeItemId/spaceNodeId/ownerId/billPeriod/billDate/dueDate/overdueDate/usageAmount/amount/lateFee/discount/payableAmount/paidAmount/status账单状态5种UNPAID/PARTIAL_PAID/PAID/OVERDUE/CANCELLED | 高 | 未实现 | - |
| REQ-FIN-011 | 账单自动生成 | 根据收费项目规则,按账期自动生成账单 | 高 | 未实现 | - |
| REQ-FIN-012 | 账单状态流转 | UNPAID->PARTIAL_PAID->PAID / OVERDUE->CANCELLED | 高 | 未实现 | - |
| REQ-FIN-013 | 批量账单生成 | 一次性为项目所有业主生成账单 | 中 | 未实现 | - |
| REQ-FIN-014 | 账单导出 | 导出Excel/PDF格式账单 | 中 | 未实现 | - |
#### 2.5.3 支付记录管理
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-FIN-020 | 支付记录实体 | FeePayment实体含paymentNo/amount/method/thirdPartyNo/status/failReason/paymentTime支付方式5种WECHAT/ALIPAY/CASH/BANK_TRANSFER/CARD支付状态4种PENDING/SUCCESS/FAILED/REFUNDED | 高 | 未实现 | - |
| REQ-FIN-021 | 线下收款登记 | 支持登记线下收款记录 | 高 | 未实现 | - |
| REQ-FIN-022 | 支付记录查询 | 支持查询支付记录列表 | 中 | 未实现 | - |
#### 2.5.4 退款管理
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-FIN-030 | 退款实体 | FeeRefund实体含refundNo/amount/reason/status/approverId/approveTime/thirdPartyRefundNo/refundTime退款状态4种PENDING/APPROVED/REJECTED/REFUNDED | 中 | 未实现 | - |
| REQ-FIN-031 | 退款流程 | 退款申请->审批->执行 | 中 | 未实现 | - |
#### 2.5.5 费用催缴与滞纳金
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-FIN-040 | 费用催缴定时任务 | 到期提醒3天内、逾期催缴、周汇总 | 中 | 未实现 | - |
| REQ-FIN-041 | 滞纳金自动计算 | 逾期天数 x 日利率,不超过上限 | 中 | 未实现 | - |
#### 2.5.6 能耗计费
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-FIN-050 | 能耗数据与账单对接 | 将EnergyConsumption的amount作为按用量计费的账单数据来源实现能耗抄表->费用计算->账单生成的闭环 | 高 | 未实现 | - |
| REQ-FIN-051 | 按面积计费 | 根据房产面积 x 单价计算物业费 | 中 | 未实现 | - |
| REQ-FIN-052 | 固定金额计费 | 每月固定金额的收费项目(如停车费) | 中 | 未实现 | - |
| REQ-FIN-053 | 支付网关对接 | 微信支付/支付宝SDK集成 | 中 | 未实现 | - |
| REQ-FIN-054 | 财务报表 | 收费统计/欠费分析/收入趋势 | 低 | 未实现 | - |
---
### 2.6 前端交互域
#### 2.6.1 登录与权限控制流程
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-UI-001 | 登录页面 | /auth/Login.vue输入用户名+密码调用POST /api/auth/login存储Token到本地跳转至主页 | 高 | 已实现 | ether-admin: Login.vue |
| REQ-UI-002 | 前端权限指令 | v-permission指令根据用户角色/权限控制元素显示隐藏 | 高 | 已实现 | ether-admin: v-permission |
| REQ-UI-003 | 路由权限守卫 | 前端路由守卫,根据用户权限动态渲染菜单和路由 | 高 | 已实现 | ether-admin: router |
| REQ-UI-004 | Token管理优化 | 双Token机制accessToken+refreshToken、加密存储 | 高 | 未实现 | OPT-SEC-001 |
| REQ-UI-005 | XSS防护增强 | xss库过滤、白名单配置 | 高 | 未实现 | OPT-SEC-002 |
| REQ-UI-006 | CSRF防护 | CSRF Token、SameSite Cookie | 高 | 未实现 | OPT-SEC-003 |
#### 2.6.2 项目切换与数据隔离
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-UI-010 | 项目选择 | 登录后获取用户项目列表GET /api/auth/users/{id}/projects用户选择项目后续请求携带X-Project-ID Header | 高 | 已实现 | ether-admin |
| REQ-UI-011 | 项目切换 | 支持在已参与的项目间切换,切换后刷新当前页面数据 | 高 | 已实现 | ether-admin |
| REQ-UI-012 | 菜单动态渲染 | 根据用户角色和项目配置(ProjectConfig功能开关)动态渲染菜单 | 高 | 已实现 | ether-admin |
#### 2.6.3 各模块页面交互规范
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 | 对应代码模块 |
|--------|----------|----------|--------|----------|-------------|
| REQ-UI-020 | 用户管理页面 | /system/Users.vue + /system/UserDetail.vue用户列表(分页/搜索/状态筛选)/创建用户/编辑/删除/修改密码/分配角色/查看项目 | 高 | 已实现 | ether-admin |
| REQ-UI-021 | 角色管理页面 | /system/Roles.vue角色列表(分页/搜索)/创建/编辑/删除/分配权限/查看关联用户 | 高 | 已实现 | ether-admin |
| REQ-UI-022 | 权限管理页面 | /system/Permissions.vue权限列表(分页/搜索)/创建/编辑/删除/按类型筛选/查看菜单权限 | 高 | 已实现 | ether-admin |
| REQ-UI-023 | 部门管理页面 | /system/Depts.vue部门树展示/创建/编辑/删除(需先删除子部门)/查看成员/按类型筛选 | 高 | 已实现 | ether-admin |
| REQ-UI-024 | 审计日志页面 | /system/Audit.vue日志列表(分页/模块/操作类型/用户名/日期范围筛选)/查看统计/模块和操作类型下拉 | 中 | 已实现 | ether-admin |
| REQ-UI-025 | 系统设置页面 | /system/Settings.vue查看所有配置项(键值对)/单个或批量更新配置值 | 中 | 已实现 | ether-admin |
| REQ-UI-026 | 设备列表页面 | EquipmentList.vue设备台账主列表含筛选/新增/编辑/删除/导入导出/批量删除 | 高 | 已实现 | ether-admin |
| REQ-UI-027 | 设备详情页面 | EquipmentDetail.vue设备完整信息展示(基本信息/技术参数/维保/财务/照片/文档) | 高 | 已实现 | ether-admin |
| REQ-UI-028 | 设备健康页面 | EquipmentHealth.vue健康度评分/趋势图/MTBF/MTTR/故障历史 | 中 | 已实现 | ether-admin |
| REQ-UI-029 | 工单管理页面 | WorkOrder.vue工单列表/新建/详情/派单/执行/完成/验收/取消 | 高 | 已实现 | ether-admin |
| REQ-UI-030 | 维保计划页面 | PlanList.vue维保计划列表/新建/编辑/停用 | 高 | 已实现 | ether-admin |
| REQ-UI-031 | 维保任务页面 | TaskList.vue维保任务列表/接受/开始/完成/取消 | 高 | 已实现 | ether-admin |
| REQ-UI-032 | 能耗管理页面 | MeterList.vue + ConsumptionRecord.vue + EnergyStatistics.vue计量点管理/能耗录入/能耗统计 | 中 | 已实现 | ether-admin |
| REQ-UI-033 | 列表虚拟滚动 | 后端分页、虚拟表格优化长列表性能 | 中 | 未实现 | OPT-PERF-001 |
| REQ-UI-034 | 请求优化 | 取消重复请求、请求缓存 | 中 | 未实现 | OPT-PERF-002 |
| REQ-UI-035 | WebSocket替代轮询 | 实时通信、断线重连 | 低 | 未实现 | OPT-PERF-003 |
| REQ-UI-036 | 响应式设计 | CSS媒体查询、移动端适配 | 低 | 未实现 | OPT-UX-002 |
| REQ-UI-037 | 国际化支持 | vue-i18n、中英文切换 | 低 | 未实现 | OPT-UX-005 |
---
## 三、跨域业务流程需求
### 3.1 业主报修全流程
**流程描述**:业主发起报修到工单验收评价的完整闭环。
| 步骤 | 操作 | 涉及域 | 涉及功能点 | 实现状态 |
|------|------|--------|-----------|----------|
| 1 | 业主认证身份 | 身份与权限域 | REQ-AUTH-051 住户认证流程 | 已实现 |
| 2 | 业主选择项目 | 前端交互域 | REQ-UI-010 项目选择 | 已实现 |
| 3 | 业主定位设备 | 空间与项目域 | REQ-MDM-013 空间节点查询 | 已实现 |
| 4 | 创建报修工单 | 运营与服务域 | REQ-OPS-001 工单创建(source=OWNER) | 已实现 |
| 5 | 工单派单 | 运营与服务域 | REQ-OPS-004 工单派单 | 已实现 |
| 6 | 维保人员执行 | 运营与服务域 | REQ-OPS-005 工单执行 -> REQ-OPS-006 工单完成 | 已实现 |
| 7 | 工单验收 | 运营与服务域 | REQ-OPS-007 工单验收 | 已实现 |
| 8 | 业主评价 | 运营与服务域 | REQ-OPS-007 工单验收(评分) | 已实现 |
| - | 业主端小程序 | 前端交互域 | 业主端23项特性(APP-O01~O23) | 未实现 |
### 3.2 设备全生命周期管理
**流程描述**:设备从采购安装到报废退役的全生命周期管理。
| 步骤 | 操作 | 涉及域 | 涉及功能点 | 实现状态 |
|------|------|--------|-----------|----------|
| 1 | 设备采购登记 | 设备与资产域 | REQ-ASSET-001 设备创建(purchaseDate/purchasePrice) | 已实现 |
| 2 | 设备安装就位 | 设备与资产域 | REQ-ASSET-004 设备与空间关联 + REQ-ASSET-020~023 扩展表录入 | 已实现 |
| 3 | 设备运行监控 | 设备与资产域 | REQ-ASSET-030~034 健康评分/MTBF/MTTR | 已实现[Beta] |
| 4 | 预防性维保 | 运营与服务域 | REQ-OPS-020~022 维保计划 + REQ-OPS-030~036 维保任务 | 已实现 |
| 5 | 定期巡检 | 运营与服务域 | REQ-OPS-040~043 巡检模板/签到/异常上报 | 已实现 |
| 6 | 故障处理 | 设备与资产域 | REQ-ASSET-040~042 故障记录 | 已实现 |
| 7 | 故障触发工单 | 运营与服务域 | REQ-OPS-001 工单创建(source=FAULT) | 已实现 |
| 8 | 设备报废 | 设备与资产域 | REQ-ASSET-001 设备状态变更为SCRAPPED | 已实现 |
### 3.3 预防性维保调度流程
**流程描述**:从维保计划制定到维保执行验收的完整调度闭环。
| 步骤 | 操作 | 涉及域 | 涉及功能点 | 实现状态 |
|------|------|--------|-----------|----------|
| 1 | 制定维保计划 | 运营与服务域 | REQ-OPS-020 维保计划创建(type=PREVENTIVE) | 已实现 |
| 2 | 设置周期规则 | 运营与服务域 | REQ-OPS-022 维保计划周期调度(cycleDays/nextDate) | 已实现 |
| 3 | 自动生成维保任务 | 运营与服务域 | REQ-OPS-023 维保计划自动调度 | 未实现 |
| 4 | 手动创建维保任务 | 运营与服务域 | REQ-OPS-030 维保任务创建(triggerType=PLAN) | 已实现 |
| 5 | 分配维保人员 | 运营与服务域 | REQ-OPS-031 维保任务状态流转(ASSIGNED) | 已实现 |
| 6 | 执行维保 | 运营与服务域 | REQ-OPS-034 维保任务双完成接口 | 已实现 |
| 7 | 维保验收 | 运营与服务域 | REQ-OPS-035 维保任务验收 | 已实现 |
| 8 | 更新设备记录 | 设备与资产域 | REQ-OPS-036 完成后设备联动 | 已实现 |
### 3.4 巡检异常处理流程
**流程描述**:巡检发现异常到问题解决的完整处理流程。
| 步骤 | 操作 | 涉及域 | 涉及功能点 | 实现状态 |
|------|------|--------|-----------|----------|
| 1 | 配置巡检模板 | 运营与服务域 | REQ-OPS-040 巡检模板管理 | 已实现 |
| 2 | 执行巡检签到 | 运营与服务域 | REQ-OPS-042 巡检签到 | 已实现 |
| 3 | 发现异常上报 | 运营与服务域 | REQ-OPS-043 巡检异常上报 | 已实现 |
| 4 | 创建维修工单 | 运营与服务域 | REQ-OPS-001 工单创建(source=INSPECTION) | 已实现 |
| 5 | 工单处理 | 运营与服务域 | REQ-OPS-004~007 工单派单/执行/完成/验收 | 已实现 |
| 6 | 复检确认 | 运营与服务域 | REQ-MDM-033 巡检记录管理(再次巡检) | 已实现 |
### 3.5 能耗计费流程
**流程描述**:从能耗抄表到费用收取的完整流程。
| 步骤 | 操作 | 涉及域 | 涉及功能点 | 实现状态 |
|------|------|--------|-----------|----------|
| 1 | 配置计量点 | 运营与服务域 | REQ-OPS-060 计量点管理(含单价) | 已实现 |
| 2 | 能耗抄表 | 运营与服务域 | REQ-OPS-061 能耗抄表录入 | 已实现 |
| 3 | 费用计算 | 运营与服务域 | REQ-OPS-061 自动计算费用(consumption x unitPrice) | 已实现 |
| 4 | 生成账单 | 财务与收费域 | REQ-FIN-050 能耗数据与账单对接 | 未实现 |
| 5 | 业主缴费 | 财务与收费域 | REQ-FIN-020~022 支付记录管理 | 未实现 |
| 6 | 能耗统计分析 | 运营与服务域 | REQ-OPS-063~064 能耗统计 | 部分实现 |
---
## 四、非功能需求
### 4.1 安全需求
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 |
|--------|----------|----------|--------|----------|
| REQ-NFR-001 | 密码加密 | 用户密码使用BCrypt算法加密存储不存储明文 | 高 | 已实现 |
| REQ-NFR-002 | JWT认证 | 使用HMAC-SHA256算法生成JWT TokenToken含userId/username/roles过期时间可配置默认24小时 | 高 | 已实现 |
| REQ-NFR-003 | 登录失败锁定 | 5次登录失败后锁定账户10分钟基于Redis实现 | 高 | 已实现 |
| REQ-NFR-004 | 密码强度校验 | 8-20位必须包含大小写字母+数字+特殊字符,弱密码黑名单检测 | 高 | 已实现 |
| REQ-NFR-005 | 项目数据隔离 | 基于X-Project-ID Header实现项目级数据隔离DataScopeService四级数据范围过滤 | 高 | 已实现 |
| REQ-NFR-006 | 操作审计 | AOP切面自动记录操作日志异步持久化30天查询窗口 | 高 | 已实现 |
| REQ-NFR-007 | 敏感字段脱敏 | 审计日志中敏感字段自动脱敏(如密码、身份证号) | 中 | 已实现 |
| REQ-NFR-008 | XSS防护 | 前端输入过滤、白名单配置 | 高 | 未实现 |
| REQ-NFR-009 | CSRF防护 | CSRF Token、SameSite Cookie | 高 | 未实现 |
| REQ-NFR-010 | 敏感信息保护 | 移除敏感Header、错误信息脱敏 | 中 | 未实现 |
### 4.2 性能需求
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 |
|--------|----------|----------|--------|----------|
| REQ-NFR-020 | 分页查询 | 所有列表接口支持分页查询,避免全量数据返回 | 高 | 部分实现(工单列表为全量返回) |
| REQ-NFR-021 | 异步日志 | 审计日志通过线程池异步保存,不影响业务接口响应时间 | 高 | 已实现 |
| REQ-NFR-022 | 数据库索引 | 关键查询字段建立索引(审计日志/空间节点/设备/工单/能耗记录等) | 高 | 已实现 |
| REQ-NFR-023 | 列表虚拟滚动 | 前端长列表使用虚拟滚动技术减少DOM节点 | 中 | 未实现 |
| REQ-NFR-024 | 请求去重与缓存 | 前端取消重复请求、请求结果缓存 | 中 | 未实现 |
| REQ-NFR-025 | WebSocket实时通信 | 替代轮询,实现工单状态变更实时推送 | 低 | 未实现 |
### 4.3 可用性需求
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 |
|--------|----------|----------|--------|----------|
| REQ-NFR-030 | 多项目切换 | 用户可在参与的项目间自由切换,切换后数据自动刷新 | 高 | 已实现 |
| REQ-NFR-031 | 角色权限动态菜单 | 根据用户角色和项目配置动态渲染菜单和路由 | 高 | 已实现 |
| REQ-NFR-032 | 前端权限指令 | v-permission指令控制页面元素显示隐藏 | 高 | 已实现 |
| REQ-NFR-033 | 表单分步优化 | 复杂表单使用Steps组件分步验证 | 中 | 未实现 |
| REQ-NFR-034 | 加载状态优化 | 骨架屏、防抖等加载状态优化 | 中 | 未实现 |
| REQ-NFR-035 | 列表交互增强 | 批量操作、视图切换 | 低 | 未实现 |
### 4.4 可扩展性需求
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 |
|--------|----------|----------|--------|----------|
| REQ-NFR-040 | JSONB扩展 | 设备attributes/巡检items&problems/工单photos&partsUsed等使用JSONB存储支持灵活扩展 | 高 | 已实现 |
| REQ-NFR-041 | 扩展表模式 | 设备主表+4张扩展表模式新增设备类型只需添加扩展表 | 高 | 已实现 |
| REQ-NFR-042 | 自定义配置 | 项目配置(ProjectConfig)支持功能开关和customConfig JSON | 高 | 已实现 |
| REQ-NFR-043 | 系统键值配置 | SysConfig支持运行时动态参数调整 | 中 | 已实现 |
| REQ-NFR-044 | 数据访问授权 | DataAccess通用数据访问控制支持细粒度授权扩展 | 中 | 已实现 |
### 4.5 兼容性需求
| 需求ID | 需求名称 | 需求描述 | 优先级 | 实现状态 |
|--------|----------|----------|--------|----------|
| REQ-NFR-050 | 浏览器兼容 | 管理后台支持Chrome/Firefox/Edge/Safari最新2个主版本 | 高 | 已实现 |
| REQ-NFR-051 | 移动端适配 | 员工APP和业主小程序基于uni-app跨端框架支持iOS/Android | 高 | 未实现(框架待搭建) |
| REQ-NFR-052 | 响应式设计 | 管理后台支持不同屏幕尺寸的响应式布局 | 中 | 未实现 |
---
## 五、需求追踪矩阵
### 5.1 身份与权限域
| 需求ID | 名称 | 优先级 | 实现状态 | 设计文档 | 代码模块 |
|--------|------|--------|----------|----------|----------|
| REQ-AUTH-001 | 用户创建 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: UserController |
| REQ-AUTH-002 | 用户查询 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: UserController |
| REQ-AUTH-003 | 用户更新 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: UserController |
| REQ-AUTH-004 | 用户删除 | 中 | 已实现 | REVERSE-AUTH.md | module-auth: UserController |
| REQ-AUTH-005 | 密码修改 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: UserController |
| REQ-AUTH-006 | 多类型用户扩展 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: EnterpriseUser/ProjectStaff/Resident |
| REQ-AUTH-007 | 用户角色分配 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: UserController |
| REQ-AUTH-008 | 用户项目关联 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: UserController |
| REQ-AUTH-010 | 角色管理 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: RoleController |
| REQ-AUTH-011 | 权限管理 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: PermissionController |
| REQ-AUTH-012 | 角色权限分配 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: RoleController |
| REQ-AUTH-013 | 四级数据范围 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: DataScopeService |
| REQ-AUTH-014 | 双层角色分配 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: AuthService |
| REQ-AUTH-015 | 项目级角色查询 | 中 | 已实现 | REVERSE-AUTH.md | module-auth: RoleController |
| REQ-AUTH-016 | 角色关联用户查询 | 中 | 已实现 | REVERSE-AUTH.md | module-auth: RoleController |
| REQ-AUTH-017 | 权限树端点 | 中 | 未实现 | - | - |
| REQ-AUTH-018 | 权限校验端点 | 中 | 未实现 | - | - |
| REQ-AUTH-019 | 用户菜单端点 | 中 | 未实现 | - | - |
| REQ-AUTH-020 | 用户权限查询端点 | 中 | 未实现 | - | - |
| REQ-AUTH-021 | 角色业务属性 | 低 | 未实现 | - | - |
| REQ-AUTH-022 | 权限菜单路由属性 | 低 | 未实现 | - | - |
| REQ-AUTH-030 | 部门管理 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: DeptController |
| REQ-AUTH-031 | 部门树查询 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: DeptController |
| REQ-AUTH-032 | 部门成员查询 | 中 | 已实现 | REVERSE-AUTH.md | module-auth: DeptController |
| REQ-AUTH-033 | 按类型查询部门 | 中 | 已实现 | REVERSE-AUTH.md | module-auth: DeptController |
| REQ-AUTH-034 | 部门删除约束 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: DeptService |
| REQ-AUTH-040 | 项目成员列表 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: ProjectMemberController |
| REQ-AUTH-041 | 可用成员查询 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: ProjectMemberController |
| REQ-AUTH-042 | 添加项目成员 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: ProjectMemberController |
| REQ-AUTH-043 | 移除项目成员 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: ProjectMemberController |
| REQ-AUTH-044 | 项目员工班次 | 中 | 已实现 | REVERSE-AUTH.md | module-auth: ProjectStaff |
| REQ-AUTH-045 | 项目员工角色 | 中 | 已实现 | REVERSE-AUTH.md | module-auth: ProjectStaffRole |
| REQ-AUTH-050 | 住户信息管理 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: Resident |
| REQ-AUTH-051 | 住户认证流程 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: Resident |
| REQ-AUTH-052 | 住户房屋绑定 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: ResidentSpace |
| REQ-AUTH-053 | 房屋空间管理 | 中 | 已实现 | REVERSE-AUTH.md | module-auth: Space |
| REQ-AUTH-060 | 用户登录 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: AuthController |
| REQ-AUTH-061 | 用户登出 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: AuthController |
| REQ-AUTH-062 | Token刷新 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: AuthController |
| REQ-AUTH-063 | 获取当前用户 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: AuthController |
| REQ-AUTH-064 | JWT Token规范 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: JwtTokenProvider |
| REQ-AUTH-065 | 登录失败锁定 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: LoginAttemptService |
| REQ-AUTH-066 | 项目上下文传递 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: ProjectContextInterceptor |
| REQ-AUTH-070 | 操作日志记录 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: OperationLogAspect |
| REQ-AUTH-071 | 异步持久化 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: AuditLogService |
| REQ-AUTH-072 | 审计日志查询 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: AuditLogController |
| REQ-AUTH-073 | 模块与操作类型 | 中 | 已实现 | REVERSE-AUTH.md | module-auth: AuditLog |
| REQ-AUTH-074 | 日志统计 | 中 | 已实现 | REVERSE-AUTH.md | module-auth: AuditLogController |
| REQ-AUTH-075 | 日志归档 | 低 | 部分实现 | REVERSE-AUTH.md | module-auth: AuditLogService |
| REQ-AUTH-080 | 键值配置管理 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: SysConfigController |
| REQ-AUTH-081 | 密码策略配置 | 高 | 已实现 | REVERSE-AUTH.md | module-auth: PasswordStrengthValidator |
| REQ-AUTH-082 | 数据访问授权 | 中 | 已实现 | REVERSE-AUTH.md | module-auth: DataAccessController |
### 5.2 空间与项目域
| 需求ID | 名称 | 优先级 | 实现状态 | 设计文档 | 代码模块 |
|--------|------|--------|----------|----------|----------|
| REQ-MDM-001 | 项目创建 | 高 | 已实现 | REVERSE-MDM.md | module-mdm: ProjectController |
| REQ-MDM-002 | 项目查询 | 高 | 已实现 | REVERSE-MDM.md | module-mdm: ProjectController |
| REQ-MDM-003 | 项目更新与删除 | 高 | 已实现 | REVERSE-MDM.md | module-mdm: ProjectController |
| REQ-MDM-004 | 项目状态流转 | 高 | 已实现 | REVERSE-MDM.md | module-mdm: ProjectController |
| REQ-MDM-005 | 状态变更历史 | 中 | 已实现 | REVERSE-MDM.md | module-mdm: ProjectStatusHistory |
| REQ-MDM-006 | 项目统计 | 中 | 已实现 | REVERSE-MDM.md | module-mdm: ProjectController |
| REQ-MDM-007 | 项目配置管理 | 高 | 已实现 | REVERSE-MDM.md | module-mdm: ProjectConfigController |
| REQ-MDM-008 | 项目成员管理 | 高 | 已实现 | REVERSE-MDM.md | module-mdm: ProjectMemberController |
| REQ-MDM-009 | 项目删除检查 | 高 | 已实现 | REVERSE-MDM.md | module-mdm: ProjectController |
| REQ-MDM-010 | 空间节点创建 | 高 | 已实现 | REVERSE-MDM.md | module-mdm: SpaceNodeController |
| REQ-MDM-011 | 15种节点类型 | 高 | 已实现 | REVERSE-MDM.md | module-mdm: SpaceNode |
| REQ-MDM-012 | 树形结构维护 | 高 | 已实现 | REVERSE-MDM.md | module-mdm: SpaceNodeService |
| REQ-MDM-013 | 空间节点查询 | 高 | 已实现 | REVERSE-MDM.md | module-mdm: SpaceNodeController |
| REQ-MDM-014 | 空间节点更新与删除 | 高 | 已实现 | REVERSE-MDM.md | module-mdm: SpaceNodeController |
| REQ-MDM-015 | 面积信息管理 | 中 | 已实现 | REVERSE-MDM.md | module-mdm: SpaceNode |
| REQ-MDM-016 | 地理信息管理 | 中 | 已实现 | REVERSE-MDM.md | module-mdm: SpaceNode |
| REQ-MDM-017 | 设备扩展模式 | 高 | 已实现 | REVERSE-MDM.md | module-mdm: SpaceNode |
| REQ-MDM-018 | 扩展属性JSON | 中 | 已实现 | REVERSE-MDM.md | module-mdm: SpaceNode |
| REQ-MDM-019 | 空间编码自动生成 | 中 | 未实现 | - | - |
| REQ-MDM-020 | 节点移动 | 中 | 未实现 | - | - |
| REQ-MDM-021 | 祖先/子孙查询 | 低 | 未实现 | - | - |
| REQ-MDM-022 | 空间统计分析 | 低 | 未实现 | - | - |
| REQ-MDM-023 | PostGIS空间查询 | 低 | 未实现 | - | - |
| REQ-MDM-024 | 地图服务 | 低 | 未实现 | - | - |
| REQ-MDM-025 | 空间节点导入导出 | 中 | 未实现 | - | - |
| REQ-MDM-030 | 巡检标准项CRUD | 高 | 已实现 | REVERSE-MDM.md | module-mdm: InspectionItemController |
| REQ-MDM-031 | 标准项查询筛选 | 中 | 已实现 | REVERSE-MDM.md | module-mdm: InspectionItemService |
| REQ-MDM-032 | 巡检模板管理 | 高 | 已实现 | REVERSE-MDM.md | module-mdm: InspectionTemplateController |
| REQ-MDM-033 | 巡检记录管理 | 高 | 已实现 | REVERSE-MDM.md | module-mdm: InspectionRecordController |
| REQ-MDM-034 | 巡检记录查询 | 中 | 已实现 | REVERSE-MDM.md | module-mdm: InspectionRecordService |
### 5.3 设备与资产域
| 需求ID | 名称 | 优先级 | 实现状态 | 设计文档 | 代码模块 |
|--------|------|--------|----------|----------|----------|
| REQ-ASSET-001 | 设备创建 | 高 | 已实现 | REVERSE-ASSET.md | module-asset: EquipmentController |
| REQ-ASSET-002 | 设备查询 | 高 | 已实现 | REVERSE-ASSET.md | module-asset: EquipmentController |
| REQ-ASSET-003 | 设备更新与删除 | 高 | 已实现 | REVERSE-ASSET.md | module-asset: EquipmentController |
| REQ-ASSET-004 | 设备与空间关联 | 高 | 已实现 | REVERSE-ASSET.md | module-asset: Equipment |
| REQ-ASSET-005 | 系统类型分类 | 高 | 已实现 | REVERSE-ASSET.md | module-asset: Equipment |
| REQ-ASSET-006 | 设备统计 | 中 | 已实现 | REVERSE-ASSET.md | module-asset: EquipmentController |
| REQ-ASSET-007 | 设备导入 | 高 | 已实现 | REVERSE-ASSET.md | module-asset: EquipmentController |
| REQ-ASSET-008 | 设备导出 | 中 | 已实现 | REVERSE-ASSET.md | module-asset: EquipmentController |
| REQ-ASSET-009 | 设备照片管理 | 中 | 已实现 | REVERSE-ASSET.md | module-asset: Equipment |
| REQ-ASSET-010 | 设备文档管理 | 中 | 已实现 | REVERSE-ASSET.md | module-asset: Equipment |
| REQ-ASSET-011 | 设备二维码 | 中 | 未实现 | - | - |
| REQ-ASSET-012 | 设备规格字段 | 低 | 未实现 | - | - |
| REQ-ASSET-020 | 电梯扩展表 | 高 | 已实现 | REVERSE-ASSET.md | module-asset: EquipmentElevatorService |
| REQ-ASSET-021 | 暖通扩展表 | 高 | 已实现 | REVERSE-ASSET.md | module-asset: EquipmentHvacService |
| REQ-ASSET-022 | 消防扩展表 | 高 | 已实现 | REVERSE-ASSET.md | module-asset: EquipmentFireService |
| REQ-ASSET-023 | 能源计量扩展表 | 高 | 已实现 | REVERSE-ASSET.md | module-asset: EquipmentEnergyService |
| REQ-ASSET-024 | 扩展表读写模式 | 高 | 已实现 | REVERSE-ASSET.md | module-asset: Equipment*Service |
| REQ-ASSET-025 | 无扩展表设备类型 | 中 | 已实现 | REVERSE-ASSET.md | module-asset: Equipment |
| REQ-ASSET-030 | 健康评分算法 | 高 | 已实现[Beta] | REVERSE-ASSET.md | module-asset: EquipmentHealthServiceImpl |
| REQ-ASSET-031 | 5级健康等级 | 高 | 已实现[Beta] | REVERSE-ASSET.md | module-asset: HealthLevel |
| REQ-ASSET-032 | MTBF计算 | 中 | 已实现[Beta] | REVERSE-ASSET.md | module-asset: EquipmentHealthServiceImpl |
| REQ-ASSET-033 | MTTR计算 | 中 | 已实现[Beta] | REVERSE-ASSET.md | module-asset: EquipmentHealthServiceImpl |
| REQ-ASSET-034 | 健康评分查询 | 中 | 已实现[Beta] | REVERSE-ASSET.md | module-asset: EquipmentHealthController |
| REQ-ASSET-035 | 维保完成率计算 | 中 | 部分实现 | REVERSE-ASSET.md | module-asset: EquipmentHealthServiceImpl |
| REQ-ASSET-040 | 故障记录 | 高 | 已实现 | REVERSE-ASSET.md | module-asset: EquipmentHealthController |
| REQ-ASSET-041 | 故障自动计算 | 中 | 已实现 | REVERSE-ASSET.md | module-asset: EquipmentFailureHistory |
| REQ-ASSET-042 | 故障历史查询 | 中 | 已实现 | REVERSE-ASSET.md | module-asset: EquipmentHealthController |
| REQ-ASSET-043 | 故障自动工单 | 中 | 未实现 | - | - |
| REQ-ASSET-050 | 归属主体CRUD | 高 | 已实现 | REVERSE-ASSET.md | module-asset: OwnershipEntityController |
| REQ-ASSET-051 | 归属主体查询 | 中 | 已实现 | REVERSE-ASSET.md | module-asset: OwnershipEntityController |
| REQ-ASSET-052 | 归属主体逻辑删除 | 高 | 已实现 | REVERSE-ASSET.md | module-asset: OwnershipEntityController |
| REQ-ASSET-053 | 设备归属冗余 | 中 | 已实现 | REVERSE-ASSET.md | module-asset: Equipment |
| REQ-ASSET-060 | 特种设备标记 | 中 | 已实现 | REVERSE-ASSET.md | module-asset: Equipment |
| REQ-ASSET-061 | 年检周期管理 | 中 | 已实现 | REVERSE-ASSET.md | module-asset: Equipment |
| REQ-ASSET-062 | 特种设备列表查询 | 中 | 已实现 | REVERSE-MDM.md | module-mdm: SpaceNodeController |
| REQ-ASSET-063 | 即将年检预警 | 中 | 已实现 | REVERSE-MDM.md | module-mdm: SpaceNodeController |
| REQ-ASSET-064 | 维保到期定时提醒 | 中 | 未实现 | - | - |
### 5.4 运营与服务域
| 需求ID | 名称 | 优先级 | 实现状态 | 设计文档 | 代码模块 |
|--------|------|--------|----------|----------|----------|
| REQ-OPS-001 | 工单创建 | 高 | 已实现 | REVERSE-OPS.md | module-wo: WorkOrderController |
| REQ-OPS-002 | 工单查询 | 高 | 已实现 | REVERSE-OPS.md | module-wo: WorkOrderController |
| REQ-OPS-003 | 工单状态流转 | 高 | 已实现 | REVERSE-OPS.md | module-wo: WorkOrderService |
| REQ-OPS-004 | 工单派单 | 高 | 已实现 | REVERSE-OPS.md | module-wo: WorkOrderController |
| REQ-OPS-005 | 工单执行 | 高 | 已实现 | REVERSE-OPS.md | module-wo: WorkOrderController |
| REQ-OPS-006 | 工单完成 | 高 | 已实现 | REVERSE-OPS.md | module-wo: WorkOrderController |
| REQ-OPS-007 | 工单验收 | 高 | 已实现 | REVERSE-OPS.md | module-wo: WorkOrderController |
| REQ-OPS-008 | 工单取消 | 中 | 已实现 | REVERSE-OPS.md | module-wo: WorkOrderController |
| REQ-OPS-009 | 工单优先级 | 高 | 已实现 | REVERSE-OPS.md | module-wo: WorkOrder |
| REQ-OPS-010 | 工单明细管理 | 中 | 已实现 | REVERSE-OPS.md | module-wo: WorkOrderController |
| REQ-OPS-011 | 工单统计 | 中 | 已实现 | REVERSE-OPS.md | module-wo: WorkOrderController |
| REQ-OPS-012 | 工单挂起与恢复 | 中 | 未实现 | - | - |
| REQ-OPS-013 | 工单退回 | 中 | 未实现 | - | - |
| REQ-OPS-014 | 工单流转记录 | 中 | 未实现 | - | - |
| REQ-OPS-015 | 工单报修人信息 | 中 | 未实现 | - | - |
| REQ-OPS-016 | 工单分页查询 | 中 | 未实现 | - | - |
| REQ-OPS-020 | 维保计划创建 | 高 | 已实现 | REVERSE-OPS.md | module-wo: MaintenancePlan |
| REQ-OPS-021 | 维保计划状态管理 | 高 | 已实现 | REVERSE-OPS.md | module-wo: MaintenancePlan |
| REQ-OPS-022 | 维保计划周期调度 | 中 | 已实现 | REVERSE-OPS.md | module-wo: MaintenancePlan |
| REQ-OPS-023 | 维保计划自动调度 | 中 | 未实现 | - | - |
| REQ-OPS-030 | 维保任务创建 | 高 | 已实现 | REVERSE-OPS.md | module-wo: MaintenanceTaskController |
| REQ-OPS-031 | 维保任务状态流转 | 高 | 已实现 | REVERSE-OPS.md | module-wo: MaintenanceTaskService |
| REQ-OPS-032 | 4种触发方式 | 高 | 已实现 | REVERSE-OPS.md | module-wo: MaintenanceTask |
| REQ-OPS-033 | 自动优先级判定 | 高 | 已实现 | REVERSE-OPS.md | module-wo: MaintenanceTaskServiceImpl |
| REQ-OPS-034 | 双完成接口 | 中 | 已实现 | REVERSE-OPS.md | module-wo: MaintenanceTaskController |
| REQ-OPS-035 | 维保任务验收 | 中 | 已实现 | REVERSE-OPS.md | module-wo: MaintenanceTaskController |
| REQ-OPS-036 | 完成后设备联动 | 高 | 已实现 | REVERSE-OPS.md | module-wo: MaintenanceTaskServiceImpl |
| REQ-OPS-037 | 维保任务统计 | 中 | 已实现 | REVERSE-OPS.md | module-wo: MaintenanceTaskController |
| REQ-OPS-038 | 使用备件记录 | 中 | 已实现 | REVERSE-OPS.md | module-wo: MaintenanceTask |
| REQ-OPS-040 | 巡检模板管理 | 高 | 已实现 | REVERSE-OPS.md | module-wo: InspectionTemplate |
| REQ-OPS-041 | 巡检项管理 | 高 | 已实现 | REVERSE-OPS.md | module-wo: InspectionItem |
| REQ-OPS-042 | 巡检签到 | 中 | 已实现 | REVERSE-MDM.md | module-mdm: InspectionRecord |
| REQ-OPS-043 | 巡检异常上报 | 中 | 已实现 | REVERSE-MDM.md | module-mdm: InspectionRecord |
| REQ-OPS-044 | 巡检扫码签到 | 中 | 未实现 | - | - |
| REQ-OPS-050 | 备件分类管理 | 高 | 已实现 | REVERSE-MDM.md | module-mdm: SparePartController |
| REQ-OPS-051 | 备件台账CRUD | 高 | 已实现 | REVERSE-MDM.md | module-mdm: SparePartController |
| REQ-OPS-052 | 入库操作 | 高 | 已实现 | REVERSE-MDM.md | module-mdm: SparePartController |
| REQ-OPS-053 | 出库操作 | 高 | 已实现 | REVERSE-MDM.md | module-mdm: SparePartController |
| REQ-OPS-054 | 低库存预警 | 中 | 已实现 | REVERSE-MDM.md | module-mdm: SparePartController |
| REQ-OPS-055 | 出入库记录查询 | 中 | 已实现 | REVERSE-MDM.md | module-mdm: SparePartController |
| REQ-OPS-056 | 盘点与调整 | 中 | 已实现 | REVERSE-MDM.md | module-mdm: SparePartRecord |
| REQ-OPS-060 | 计量点管理 | 高 | 已实现 | REVERSE-MDM.md | module-mdm: EnergyController |
| REQ-OPS-061 | 能耗抄表录入 | 高 | 已实现 | REVERSE-MDM.md | module-mdm: EnergyConsumptionService |
| REQ-OPS-062 | 能耗记录查询 | 中 | 已实现 | REVERSE-MDM.md | module-mdm: EnergyController |
| REQ-OPS-063 | 按类型统计能耗 | 中 | 部分实现 | REVERSE-MDM.md | module-mdm: EnergyConsumptionService |
| REQ-OPS-064 | 单位面积能耗 | 中 | 已实现 | REVERSE-MDM.md | module-mdm: EnergyConsumptionService |
| REQ-OPS-065 | IoT自动抄表 | 低 | 未实现 | - | - |
| REQ-OPS-066 | 前后端能源类型统一 | 中 | 未实现 | - | - |
### 5.5 财务与收费域
| 需求ID | 名称 | 优先级 | 实现状态 | 设计文档 | 代码模块 |
|--------|------|--------|----------|----------|----------|
| REQ-FIN-001 | 收费项目实体 | 高 | 未实现 | REVERSE-FINANCE.md | - |
| REQ-FIN-002 | 收费项目CRUD | 高 | 未实现 | REVERSE-FINANCE.md | - |
| REQ-FIN-010 | 账单实体 | 高 | 未实现 | REVERSE-FINANCE.md | - |
| REQ-FIN-011 | 账单自动生成 | 高 | 未实现 | REVERSE-FINANCE.md | - |
| REQ-FIN-012 | 账单状态流转 | 高 | 未实现 | REVERSE-FINANCE.md | - |
| REQ-FIN-013 | 批量账单生成 | 中 | 未实现 | REVERSE-FINANCE.md | - |
| REQ-FIN-014 | 账单导出 | 中 | 未实现 | REVERSE-FINANCE.md | - |
| REQ-FIN-020 | 支付记录实体 | 高 | 未实现 | REVERSE-FINANCE.md | - |
| REQ-FIN-021 | 线下收款登记 | 高 | 未实现 | REVERSE-FINANCE.md | - |
| REQ-FIN-022 | 支付记录查询 | 中 | 未实现 | REVERSE-FINANCE.md | - |
| REQ-FIN-030 | 退款实体 | 中 | 未实现 | REVERSE-FINANCE.md | - |
| REQ-FIN-031 | 退款流程 | 中 | 未实现 | REVERSE-FINANCE.md | - |
| REQ-FIN-040 | 费用催缴定时任务 | 中 | 未实现 | REVERSE-FINANCE.md | - |
| REQ-FIN-041 | 滞纳金自动计算 | 中 | 未实现 | REVERSE-FINANCE.md | - |
| REQ-FIN-050 | 能耗数据与账单对接 | 高 | 未实现 | REVERSE-FINANCE.md | - |
| REQ-FIN-051 | 按面积计费 | 中 | 未实现 | REVERSE-FINANCE.md | - |
| REQ-FIN-052 | 固定金额计费 | 中 | 未实现 | REVERSE-FINANCE.md | - |
| REQ-FIN-053 | 支付网关对接 | 中 | 未实现 | REVERSE-FINANCE.md | - |
| REQ-FIN-054 | 财务报表 | 低 | 未实现 | REVERSE-FINANCE.md | - |
### 5.6 需求统计汇总
| 业务域 | 已实现 | 部分实现 | 未实现 | 合计 |
|--------|--------|----------|--------|------|
| 身份与权限域 | 37 | 1 | 6 | 44 |
| 空间与项目域 | 23 | 0 | 7 | 30 |
| 设备与资产域 | 28 | 1 | 4 | 33 |
| 运营与服务域 | 36 | 1 | 9 | 46 |
| 财务与收费域 | 0 | 0 | 19 | 19 |
| 前端交互域 | 14 | 0 | 7 | 21 |
| 非功能需求 | 12 | 1 | 8 | 21 |
| **合计** | **150** | **4** | **60** | **214** |
**实现率**:已实现 70.1% | 部分实现 1.9% | 未实现 28.0%
---
> **文档说明**: 本文档基于7份源文档整合生成所有需求项均标注唯一ID和实现状态。已实现功能以反推文档REVERSE-*.md中的实际代码为准未实现功能从原需求文档和特性清单中提取。需求描述力求具体到可验证程度。

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,578 @@
# 跨域业务流程 - 详细设计
**文档类型**: 详细设计文档
**生成日期**: 2026-05-18
**数据来源**: REVERSE-FINANCE.md + REVERSE-AUTH.md + 路由/API层源码 + 后端实体分析
---
## 一、业主报修全流程
### 1.1 流程概述
业主报修是Ether系统中最核心的跨域业务流程涉及从业主发起报修到最终评价的完整生命周期
**业主提交报修 -> 系统创建工单 -> 调度派单 -> 维保人员执行 -> 业主验收 -> 评价反馈**
### 1.2 涉及域和功能点
| 步骤 | 涉及域 | 功能点 | API端点 |
|------|--------|--------|---------|
| 1. 业主身份识别 | 认证域 | 业主登录、Token验证 | `POST /api/auth/login` |
| 2. 项目/空间定位 | 空间域 | 获取业主关联房产 | `GET /api/mdm/space-nodes/project/{projectId}/tree` |
| 3. 设备关联(可选) | 设备域 | 按空间查询设备 | `GET /api/asset/equipment/by-space/{spaceNodeId}` |
| 4. 创建工单 | 运营域 | 创建报修工单 | `POST /api/wo/work-orders` |
| 5. 工单派发 | 运营域 | 指派维保人员 | `POST /api/wo/work-orders/{id}/assign` |
| 6. 开始执行 | 运营域 | 维保人员接单 | `POST /api/wo/work-orders/{id}/start` |
| 7. 完成工单 | 运营域 | 填写处理结果 | `POST /api/wo/work-orders/{id}/complete` |
| 8. 业主验收 | 运营域 | 确认维修结果 | `POST /api/wo/work-orders/{id}/verify` |
| 9. 评价反馈 | 运营域 | 评分评价 | `POST /api/wo/work-orders/{id}/verify` (含rating) |
| 10. 备件消耗 | 备件域 | 出库记录 | `POST /api/ops/spare-parts/out-stock` |
| 11. 审计记录 | 认证域 | 操作日志 | 自动记录AOP |
### 1.3 数据流图
```mermaid
flowchart TD
A[业主登录] -->|POST /api/auth/login| B[获取Token+角色]
B --> C[选择项目/房产]
C -->|GET /api/mdm/space-nodes/project/id/tree| D[加载空间树]
D --> E{是否关联设备?}
E -->|是| F[选择设备]
F -->|GET /api/asset/equipment/by-space/spaceNodeId| G[设备列表]
E -->|否| H[直接描述问题]
G --> I[填写报修信息]
H --> I
I -->|POST /api/wo/work-orders| J[工单创建 PENDING]
J -->|POST /api/wo/work-orders/id/assign| K[派发 ASSIGNED]
K -->|POST /api/wo/work-orders/id/start| L[执行中 IN_PROGRESS]
L --> M{是否需要备件?}
M -->|是| N[备件出库]
N -->|POST /api/ops/spare-parts/out-stock| O[更新库存]
M -->|否| P[填写处理结果]
O --> P
P -->|POST /api/wo/work-orders/id/complete| Q[已完成 COMPLETED]
Q -->|POST /api/wo/work-orders/id/verify| R[已验收 VERIFIED]
R --> S[评价评分]
style A fill:#e1f5fe
style J fill:#fff3e0
style K fill:#e3f2fd
style L fill:#fff8e1
style Q fill:#e8f5e9
style R fill:#e0f7fa
```
### 1.4 状态流转图
```mermaid
stateDiagram-v2
[*] --> PENDING: 业主提交报修
PENDING --> ASSIGNED: 管理员派单
PENDING --> CANCELLED: 业主取消
ASSIGNED --> IN_PROGRESS: 维保人员开始执行
ASSIGNED --> PENDING: 退回重派
ASSIGNED --> CANCELLED: 管理员取消
IN_PROGRESS --> SUSPENDED: 挂起(缺件/等审批)
IN_PROGRESS --> RETURNED: 退回(无法处理)
IN_PROGRESS --> COMPLETED: 填写处理结果
SUSPENDED --> IN_PROGRESS: 恢复执行
RETURNED --> ASSIGNED: 重新派单
COMPLETED --> VERIFIED: 业主验收通过
COMPLETED --> IN_PROGRESS: 验收不通过,返工
VERIFIED --> [*]: 流程结束
CANCELLED --> [*]: 流程结束
```
### 1.5 异常场景处理
| 异常场景 | 触发条件 | 处理策略 | 涉及API |
|---------|---------|---------|---------|
| 设备不存在 | 业主选择的设备ID在数据库中不存在 | 返回404错误提示设备不存在允许不关联设备提交 | `GET /api/asset/equipment/{id}` |
| 派单失败 | 指定的维保人员不存在或不可用 | 返回错误提示,管理员重新选择人员 | `POST /api/wo/work-orders/{id}/assign` |
| 执行超时 | 工单超过SLA时间未完成 | 系统自动标记超时,通知项目经理 | 定时任务检查 |
| 验收不通过 | 业主对维修结果不满意 | 工单回退到IN_PROGRESS维保人员返工 | `POST /api/wo/work-orders/{id}/complete` -> 重新执行 |
| 备件缺货 | 维修所需备件库存不足 | 工单挂起SUSPENDED等待备件入库 | `GET /api/ops/spare-parts/low-stock` |
| 业主取消 | 业主在派单前取消 | 工单状态变为CANCELLED | `POST /api/wo/work-orders/{id}/cancel` |
| 重复提交 | 业主短时间内重复提交相同报修 | 前端防抖 + 后端幂等校验 | `POST /api/wo/work-orders` |
| 维保人员退回 | 维保人员无法处理该工单 | 工单退回RETURNED管理员重新派单 | `POST /api/wo/work-orders/{id}/return` |
| 工单挂起 | 执行中遇到阻塞(等审批/等配件) | 工单挂起SUSPENDED解除后恢复 | `POST /api/wo/work-orders/{id}/suspend` |
| 业主未验收 | 完成后业主长时间未验收 | 超过7天自动验收通过 | 定时任务检查 |
### 1.6 权限控制
| 角色 | 可执行操作 | 数据范围 |
|------|-----------|---------|
| 业主(OWNER) | 创建工单、查看自己的工单、验收、评价 | 仅自己创建的工单SELF |
| 项目管理员(PROJECT_ADMIN) | 查看项目所有工单、派单、取消 | 本项目工单PROJECT |
| 维保人员(MAINTENANCE) | 查看分配给自己的工单、开始、完成、退回 | 分配给自己的工单SELF |
| 系统管理员(SYS_ADMIN) | 所有操作 | 全部数据ALL |
**权限校验点**:
```
创建工单: 业主只能为关联房产创建工单
派单: 仅项目管理员/系统管理员可派单
执行: 仅被指派的维保人员可开始/完成
验收: 仅工单创建者(业主)可验收
评价: 仅工单创建者(业主)可评价
```
---
## 二、设备全生命周期管理
### 2.1 流程概述
设备全生命周期管理覆盖设备从采购入库到最终报废的完整过程:
**采购登记 -> 安装调试 -> 正常运行 -> 定期维保 -> 巡检监控 -> 故障维修 -> 报废处置**
### 2.2 涉及域和功能点
| 步骤 | 涉及域 | 功能点 | API端点 |
|------|--------|--------|---------|
| 1. 设备采购登记 | 设备域 | 创建设备记录 | `POST /api/asset/equipment` |
| 2. 安装位置关联 | 空间域 | 关联空间节点 | `PUT /api/asset/equipment/{id}` (spaceNodeId) |
| 3. 扩展信息录入 | 设备域 | 电梯/暖通/消防等扩展 | `PUT /api/asset/equipment/{id}/elevator` 等 |
| 4. 照片/文档上传 | 设备域 | 照片和文档管理 | PhotoManager / DocumentManager |
| 5. 归属主体关联 | 设备域 | 设置归属类型和主体 | `GET /api/asset/ownership-entity` |
| 6. 正常运行监控 | 设备域 | 健康度计算 | `POST /api/asset/equipment-health/calculate` |
| 7. 定期维保 | 维保域 | 创建维保计划 | `POST /api/mdm/maintenance-plans` |
| 8. 维保执行 | 维保域 | 维保工单流转 | `POST /api/ops/maintenance-tasks/{id}/start` 等 |
| 9. 巡检监控 | 巡检域 | 巡检记录 | `POST /api/mdm/inspection-records` |
| 10. 故障维修 | 运营域 | 创建维修工单 | `POST /api/wo/work-orders` |
| 11. 健康度评估 | 设备域 | MTBF/MTTR分析 | `GET /api/asset/equipment-health/mtbf/{id}` |
| 12. 设备报废 | 设备域 | 更新设备状态 | `PUT /api/asset/equipment/{id}` (status=SCRAPPED) |
### 2.3 数据流图
```mermaid
flowchart TD
A[设备采购] -->|POST /api/asset/equipment| B[设备登记]
B --> C[安装调试]
C -->|PUT /api/asset/equipment/id + spaceNodeId| D[关联空间位置]
D -->|PUT /api/asset/equipment/id/elevator| E[录入扩展信息]
E --> F[正常运行]
F -->|POST /api/asset/equipment-health/calculate| G[健康度评估]
G --> H{设备状态判断}
H -->|健康| I[定期维保]
H -->|预警| J[加强巡检]
H -->|异常| K[故障维修]
I -->|POST /api/mdm/maintenance-plans| L[创建维保计划]
L -->|定时任务| M[生成维保工单]
M -->|POST /api/ops/maintenance-tasks/id/start| N[执行维保]
N -->|POST /api/ops/maintenance-tasks/id/complete| O[维保完成]
O --> F
J -->|POST /api/mdm/inspection-records| P[巡检记录]
P --> Q{发现异常?}
Q -->|是| K
Q -->|否| F
K -->|POST /api/wo/work-orders| R[创建维修工单]
R -->|POST /api/wo/work-orders/id/complete| S[维修完成]
S -->|POST /api/asset/equipment-health/calculate| T[重新评估健康度]
T --> H
F --> U{达到报废条件?}
U -->|是| V[设备报废]
U -->|否| I
V -->|PUT /api/asset/equipment/id status=SCRAPPED| W[报废处置]
style F fill:#e8f5e9
style K fill:#ffebee
style V fill:#f3e5f5
```
### 2.4 状态流转图
```mermaid
stateDiagram-v2
[*] --> PURCHASED: 采购登记
PURCHASED --> INSTALLING: 安装调试
INSTALLING --> COMMISSIONING: 调试验收
COMMISSIONING --> RUNNING: 验收通过
COMMISSIONING --> INSTALLING: 验收不通过
RUNNING --> MAINTENANCE: 定期维保
RUNNING --> INSPECTION: 巡检监控
RUNNING --> FAULT: 故障发生
MAINTENANCE --> RUNNING: 维保完成
INSPECTION --> RUNNING: 巡检正常
INSPECTION --> FAULT: 巡检发现异常
FAULT --> REPAIRING: 维修中
REPAIRING --> RUNNING: 维修完成
REPAIRING --> SCRAPPED: 无法修复
RUNNING --> SCRAPPED: 达到使用年限
RUNNING --> SCRAPPED: 经济性报废
SCRAPPED --> [*]
```
### 2.5 异常场景处理
| 异常场景 | 触发条件 | 处理策略 |
|---------|---------|---------|
| 设备重复登记 | 相同序列号/资产编码已存在 | 后端校验唯一性返回409冲突 |
| 安装位置冲突 | 同一空间节点已有同类型设备 | 允许同空间多设备,但给出提示 |
| 维保计划冲突 | 同一设备存在重叠的维保计划 | 校验计划周期不重叠 |
| 健康度计算失败 | 设备缺少历史数据 | 返回默认健康度,标记为"数据不足" |
| 维修无法修复 | 多次维修后仍故障 | 建议报废,升级审批流程 |
| 备件不兼容 | 维修使用的备件与设备不匹配 | 备件选择时按设备类型筛选 |
| 巡检数据异常 | 巡检结果与设备状态不符 | 标记为"需复核",触发二次巡检 |
| 报废审批 | 高价值设备报废 | 需项目经理审批确认 |
### 2.6 权限控制
| 角色 | 可执行操作 | 数据范围 |
|------|-----------|---------|
| 系统管理员(SYS_ADMIN) | 设备全生命周期操作 | ALL |
| 项目管理员(PROJECT_ADMIN) | 本项目设备管理 | PROJECT |
| 工程人员(ENGINEERING) | 设备登记、维保、巡检 | PROJECT + DEPARTMENT |
| 维保人员(MAINTENANCE) | 执行维保工单 | SELF分配给自己的 |
| 业主(OWNER) | 查看关联设备信息 | SELF关联房产的设备 |
---
## 三、预防性维保调度流程
### 3.1 流程概述
预防性维保调度是基于维保计划自动生成维保工单的定时调度流程:
**创建维保计划 -> 定时调度检查 -> 自动生成维保工单 -> 派单执行 -> 验收 -> 更新设备记录**
### 3.2 涉及域和功能点
| 步骤 | 涉及域 | 功能点 | API端点 |
|------|--------|--------|---------|
| 1. 创建维保计划 | 维保域 | 设置设备、周期、维保内容 | `POST /api/mdm/maintenance-plans` |
| 2. 定时调度检查 | 维保域 | MaintenanceScheduler检查到期计划 | 定时任务Cron |
| 3. 自动生成工单 | 运营域 | 根据计划创建维保工单 | `POST /api/ops/maintenance-tasks` |
| 4. 自动派单 | 运营域 | 按计划指定维保人员/供应商 | `POST /api/ops/maintenance-tasks/{id}/assign` |
| 5. 执行维保 | 运营域 | 维保人员执行 | `POST /api/ops/maintenance-tasks/{id}/start` |
| 6. 记录备件消耗 | 备件域 | 出库记录 | `POST /api/ops/spare-parts/out-stock` |
| 7. 完成维保 | 运营域 | 填写维保结果 | `POST /api/ops/maintenance-tasks/{id}/complete-details` |
| 8. 验收 | 运营域 | 验收确认 | `POST /api/ops/maintenance-tasks/{id}/verify` |
| 9. 更新计划 | 维保域 | 更新lastDate和nextDate | `PUT /api/mdm/maintenance-plans/{id}` |
| 10. 更新设备 | 设备域 | 更新设备维保信息 | `PUT /api/asset/equipment/{id}` |
### 3.3 数据流图
```mermaid
flowchart TD
A[管理员创建维保计划] -->|POST /api/mdm/maintenance-plans| B[计划存储 ACTIVE]
B --> C{MaintenanceScheduler}
C -->|每天凌晨检查| D{nextDate <= 今天?}
D -->|是| E{计划状态=ACTIVE?}
D -->|否| C
E -->|是| F[自动生成维保工单]
E -->|否| C
F -->|POST /api/ops/maintenance-tasks| G[工单 PENDING]
G -->|POST /api/ops/maintenance-tasks/id/assign| H[派发 ASSIGNED]
H -->|POST /api/ops/maintenance-tasks/id/start| I[执行中 IN_PROGRESS]
I --> J{需要备件?}
J -->|是| K[备件出库]
K -->|POST /api/ops/spare-parts/out-stock| L[更新库存]
J -->|否| M[填写维保结果]
L --> M
M -->|POST /api/ops/maintenance-tasks/id/complete-details| N[已完成 COMPLETED]
N -->|POST /api/ops/maintenance-tasks/id/verify| O[已验收 VERIFIED]
O -->|PUT /api/mdm/maintenance-plans/id| P[更新计划lastDate/nextDate]
P -->|PUT /api/asset/equipment/id| Q[更新设备维保信息]
Q --> C
style C fill:#fff3e0
style F fill:#e3f2fd
style O fill:#e0f7fa
```
### 3.4 定时调度设计MaintenanceScheduler
**调度器配置**:
| 配置项 | 值 | 说明 |
|--------|-----|------|
| Cron表达式 | `0 0 6 * * ?` | 每天凌晨6点执行 |
| 检查范围 | 所有ACTIVE状态的维保计划 | nextDate <= 当天 |
| 生成策略 | 每个到期计划生成一个维保工单 | triggerType = PLAN |
| 防重复 | 检查同一计划+同一天是否已生成工单 | 避免重复生成 |
| 通知 | 生成工单后通知项目管理员 | 待实现 |
**调度逻辑伪代码**:
```
MaintenanceScheduler.execute():
1. 查询所有 ACTIVE 状态的维保计划
2. 过滤 nextDate <= 今天 的计划
3. 对每个到期计划:
a. 检查是否已生成工单planId + 当天日期)
b. 若未生成,创建维保工单:
- taskType = PREVENTIVE
- triggerType = PLAN
- equipmentId = plan.equipmentId
- title = "预防性维护: " + plan.planName
- assignedVendor = plan.assignedVendor
c. 更新计划 lastDate = 今天, nextDate = 今天 + plan.cycleDays
4. 发送通知给相关项目管理员
```
**nextDate计算规则**:
```
nextDate = lastDate + cycleDays
首次: nextDate = 创建日期 + cycleDays
完成后: nextDate = 实际完成日期 + cycleDays
```
### 3.5 异常场景处理
| 异常场景 | 触发条件 | 处理策略 |
|---------|---------|---------|
| 计划已暂停 | 计划状态为SUSPENDED | 跳过该计划,不生成工单 |
| 计划已停用 | 计划状态为INACTIVE | 跳过该计划,不生成工单 |
| 设备已报废 | 关联设备状态为SCRAPPED | 自动暂停计划,通知管理员 |
| 重复生成 | 同一计划同一天已生成工单 | 跳过,幂等处理 |
| 供应商不可用 | assignedVendor对应的供应商已失效 | 生成工单但不自动派单,通知管理员手动派单 |
| 备件不足 | 维保所需备件库存不足 | 工单挂起SUSPENDED等待备件入库 |
| 维保超时 | 工单超过预估时间未完成 | 通知项目经理,标记超时 |
| 调度服务宕机 | 定时任务未执行 | 启动时补偿执行,检查遗漏的计划 |
### 3.6 权限控制
| 角色 | 可执行操作 | 数据范围 |
|------|-----------|---------|
| 系统管理员(SYS_ADMIN) | 创建/修改/删除维保计划 | ALL |
| 项目管理员(PROJECT_ADMIN) | 创建/修改本项目维保计划 | PROJECT |
| 工程人员(ENGINEERING) | 查看维保计划 | PROJECT |
| 维保人员(MAINTENANCE) | 执行分配的维保工单 | SELF |
| 外部供应商 | 执行指派的维保工单 | SELF通过assignedVendor |
---
## 四、巡检异常处理流程
### 4.1 流程概述
巡检异常处理流程描述巡检过程中发现异常后的上报和处理机制:
**巡检执行 -> 发现异常 -> 上报异常 -> 创建工单 -> 处理异常 -> 复检确认**
### 4.2 涉及域和功能点
| 步骤 | 涉及域 | 功能点 | API端点 |
|------|--------|--------|---------|
| 1. 加载巡检模板 | 巡检域 | 获取点检模板 | `GET /api/ops/inspection-templates?projectId=` |
| 2. 执行巡检 | 巡检域 | 创建巡检记录 | `POST /api/mdm/inspection-records` |
| 3. 记录巡检项 | 巡检域 | 逐项检查并记录 | InspectionRecordItem[] |
| 4. 发现异常 | 巡检域 | 标记异常项 + 问题描述 | problems[] + status=ABNORMAL |
| 5. 完成巡检 | 巡检域 | 提交巡检结果 | `POST /api/mdm/inspection-records/{id}/complete` |
| 6. 自动创建工单 | 运营域 | 根据异常自动创建工单 | `POST /api/wo/work-orders` |
| 7. 派单处理 | 运营域 | 指派维修人员 | `POST /api/wo/work-orders/{id}/assign` |
| 8. 维修执行 | 运营域 | 执行维修 | `POST /api/wo/work-orders/{id}/start` -> `complete` |
| 9. 复检 | 巡检域 | 对异常项进行复检 | `POST /api/mdm/inspection-records` (复检记录) |
| 10. 更新设备状态 | 设备域 | 更新设备运行状态 | `PUT /api/asset/equipment/{id}` |
### 4.3 数据流图
```mermaid
flowchart TD
A[巡检人员选择设备] -->|GET /api/ops/inspection-templates| B[加载点检模板]
B --> C[逐项检查]
C --> D{检查结果}
D -->|全部正常| E[标记NORMAL]
D -->|部分预警| F[标记WARNING]
D -->|发现异常| G[标记ABNORMAL]
E -->|POST /api/mdm/inspection-records/id/complete| H[巡检完成]
F --> H
G --> I[记录异常详情]
I -->|problems数组| J[提交巡检记录]
J -->|POST /api/mdm/inspection-records/id/complete| K[巡检完成 ABNORMAL]
K --> L{异常严重度}
L -->|HIGH 严重| M[自动创建紧急工单]
L -->|MEDIUM 中等| N[自动创建普通工单]
L -->|LOW 轻微| O[记录待处理]
M -->|POST /api/wo/work-orders priority=URGENT| P[工单 PENDING]
N -->|POST /api/wo/work-orders priority=MEDIUM| P
O --> Q[定期汇总处理]
P -->|POST /api/wo/work-orders/id/assign| R[派发 ASSIGNED]
R -->|POST /api/wo/work-orders/id/start| S[执行中 IN_PROGRESS]
S -->|POST /api/wo/work-orders/id/complete| T[已完成 COMPLETED]
T -->|POST /api/wo/work-orders/id/verify| U[已验收 VERIFIED]
U --> V[复检]
V -->|POST /api/mdm/inspection-records 复检记录| W{复检结果}
W -->|通过| X[更新设备状态正常]
W -->|不通过| Y[重新创建工单]
X -->|PUT /api/asset/equipment/id| Z[流程结束]
Y --> P
style G fill:#ffebee
style K fill:#fff3e0
style M fill:#ffcdd2
style X fill:#e8f5e9
```
### 4.4 异常场景处理
| 异常场景 | 触发条件 | 处理策略 |
|---------|---------|---------|
| 巡检模板缺失 | 设备类型无对应模板 | 允许自由录入巡检项,提示管理员创建模板 |
| 巡检中断 | 巡检过程中网络断开 | 本地暂存巡检数据,恢复后提交 |
| 异常误报 | 巡检人员误判为异常 | 复检时纠正,工单可取消 |
| 工单创建失败 | 自动创建工单时后端异常 | 巡检记录仍保存,手动创建工单 |
| 维修后复检不通过 | 异常未完全修复 | 重新创建工单,升级优先级 |
| 多次复检不通过 | 同一异常反复出现 | 标记为"顽固故障",建议设备报废评估 |
| 巡检人员不足 | 无可用巡检人员 | 延迟巡检,通知项目经理 |
| 设备无法巡检 | 设备位置不可达或停机 | 记录原因,标记为"待巡检" |
### 4.5 权限控制
| 角色 | 可执行操作 | 数据范围 |
|------|-----------|---------|
| 巡检人员(INSPECTOR) | 执行巡检、记录异常 | SELF分配给自己的巡检任务 |
| 项目管理员(PROJECT_ADMIN) | 查看巡检结果、派单处理 | PROJECT |
| 维保人员(MAINTENANCE) | 处理异常工单 | SELF分配给自己的工单 |
| 系统管理员(SYS_ADMIN) | 管理巡检模板 | ALL |
---
## 五、能耗计费流程
### 5.1 流程概述
能耗计费流程覆盖从抄表到最终支付的完整计费链路,部分环节已实现:
**抄表录入 -> 消耗量计算 -> 费用计算 -> 账单生成 -> 催缴提醒 -> 支付 -> 滞纳金计算 [部分实现]**
### 5.2 涉及域和功能点
| 步骤 | 涉及域 | 功能点 | 实现状态 | API端点 |
|------|--------|--------|---------|---------|
| 1. 计量点管理 | MDM域 | 创建/管理计量点 | 已实现 | `POST/GET/PUT/DELETE /api/ops/energy/meters` |
| 2. 抄表录入 | MDM域 | 录入能耗读数 | 已实现 | `POST /api/ops/energy/consumption` |
| 3. 消耗量计算 | MDM域 | currentReading - previousReading | 已实现 | 自动计算 |
| 4. 费用计算 | MDM域 | consumption * unitPrice | 已实现 | 自动计算 |
| 5. 按类型统计 | MDM域 | 按能源类型汇总 | 已实现(有缺陷) | `GET /api/ops/energy/statistics/by-type` |
| 6. 单方能耗 | MDM域 | 项目总消耗/总面积 | 已实现 | `GET /api/ops/energy/statistics/unit-consumption` |
| 7. 收费项目配置 | 财务域 | 创建按用量计费的收费项目 | 未实现 | `POST /api/finance/fee-items` |
| 8. 账单生成 | 财务域 | 根据能耗数据生成账单 | 未实现 | `POST /api/finance/bills/generate` |
| 9. 催缴提醒 | 财务域 | 到期/逾期提醒 | 未实现 | 定时任务 |
| 10. 滞纳金计算 | 财务域 | 逾期天数 * 日利率 | 未实现 | 定时任务 |
| 11. 线下收款 | 财务域 | 登记支付 | 未实现 | `POST /api/finance/payments` |
| 12. 线上支付 | 财务域 | 微信/支付宝 | 未实现 | `POST /api/finance/payments/online/prepare` |
| 13. 退款 | 财务域 | 退款申请/审批/执行 | 未实现 | `POST /api/finance/refunds` |
### 5.3 数据流图
```mermaid
flowchart TD
subgraph 已实现
A[创建计量点] -->|POST /api/ops/energy/meters| B[EnergyMeter]
B --> C[抄表录入]
C -->|POST /api/ops/energy/consumption| D[EnergyConsumption]
D -->|自动计算| E[consumption = current - previous]
E -->|自动计算| F[amount = consumption * unitPrice]
F --> G[能耗统计]
G -->|GET /api/ops/energy/statistics/by-type| H[按类型汇总]
G -->|GET /api/ops/energy/statistics/unit-consumption| I[单方能耗]
end
subgraph 未实现
F --> J[收费项目配置]
J -->|POST /api/finance/fee-items type=ELECTRICITY/WATER/GAS billingMethod=USAGE| K[FeeItem]
K --> L[账单自动生成]
L -->|POST /api/finance/bills/generate sourceType=AUTO sourceId=EnergyConsumption.id| M[FeeBill UNPAID]
M --> N{到期提醒}
N -->|到期前3天| O[发送提醒]
N -->|逾期| P[计算滞纳金]
P -->|lateFee = 逾期天数 * payableAmount * lateFeeRate| Q[更新滞纳金]
M --> R[支付]
R -->|线下| S[登记收款 POST /api/finance/payments]
R -->|线上| T[发起支付 POST /api/finance/payments/online/prepare]
S --> U[FeePayment SUCCESS]
T --> V[等待回调]
V --> U
U --> W[更新账单状态 PAID]
U --> X{需要退款?}
X -->|是| Y[申请退款 POST /api/finance/refunds]
Y --> Z[审批]
Z --> AA[执行退款]
X -->|否| AB[流程结束]
end
style A fill:#e8f5e9
style B fill:#e8f5e9
style C fill:#e8f5e9
style D fill:#e8f5e9
style E fill:#e8f5e9
style F fill:#e8f5e9
style G fill:#e8f5e9
style H fill:#e8f5e9
style I fill:#e8f5e9
style J fill:#fff3e0
style K fill:#fff3e0
style L fill:#fff3e0
style M fill:#fff3e0
style N fill:#fff3e0
style O fill:#fff3e0
style P fill:#fff3e0
style Q fill:#fff3e0
style R fill:#fff3e0
style S fill:#fff3e0
style T fill:#fff3e0
style U fill:#fff3e0
style V fill:#fff3e0
style W fill:#fff3e0
style X fill:#fff3e0
style Y fill:#fff3e0
style Z fill:#fff3e0
style AA fill:#fff3e0
style AB fill:#fff3e0
```
### 5.4 异常场景处理
| 异常场景 | 触发条件 | 处理策略 | 实现状态 |
|---------|---------|---------|---------|
| 读数递减 | 当前读数小于上次读数 | 拒绝录入返回错误码6103 | 已实现 |
| 仪表不存在 | meterId在数据库中不存在 | 拒绝录入返回错误码6101 | 已实现 |
| 仪表已停用 | 仪表状态为INACTIVE | 拒绝录入返回错误码6102 | 已实现 |
| 单价未设置 | meter.unitPrice为null | amount设为0 | 已实现 |
| 按类型统计错误 | 全部归入LIGHTING | 修复为按meter.energyType分项汇总 | 待修复 |
| 前后端枚举不一致 | 后端6种 vs 前端5种 | 统一枚举定义 | 待修复 |
| 能耗数据缺失 | 账期内无抄表记录 | 生成0元账单标记"无抄表数据" | 未实现 |
| 业主无关联房产 | 生成账单时业主无房产 | 跳过该业主,记录日志 | 未实现 |
| 重复生成账单 | 同一收费项目+业主+账期 | 幂等校验,跳过已存在的账单 | 未实现 |
| 支付超时 | 线上支付未收到回调 | 对账任务检查,人工介入 | 未实现 |
| 滞纳金溢出 | 计算结果超过maxLateFee | 截断为maxLateFee上限 | 未实现 |
| 部分支付后退款 | 业主部分支付后申请退款 | 按支付记录逐笔退款 | 未实现 |
### 5.5 权限控制
| 角色 | 可执行操作 | 数据范围 |
|------|-----------|---------|
| 系统管理员(SYS_ADMIN) | 管理计量点、收费项目、账单 | ALL |
| 项目管理员(PROJECT_ADMIN) | 查看本项目能耗和账单 | PROJECT |
| 财务人员(FINANCE_STAFF) | 收费登记、账单管理 | PROJECT |
| 财务主管(FINANCE_SUPERVISOR) | 退款审批、财务报表 | PROJECT |
| 业主(OWNER) | 查看自己的账单、在线缴费 | SELF |
| 巡检/维保人员 | 抄表录入 | SELF分配给自己的 |
**已实现的权限控制**:
- 计量点管理:无特定角色限制(仅要求已登录)
- 能耗录入:无特定角色限制(仅要求已登录)
- 能耗统计:无特定角色限制(仅要求已登录)
**待实现的权限控制**:
- 收费项目管理finance:fee-item:* 权限
- 账单管理finance:bill:* 权限
- 支付管理finance:payment:* 权限
- 退款管理finance:refund:* 权限
- 业主端:仅查看自己的账单和支付记录

View File

@ -0,0 +1,810 @@
# 财务与收费域 - 详细设计
**文档类型**: 详细设计文档
**生成日期**: 2026-05-18
**数据来源**: REVERSE-FINANCE.md反推文档+ 04-FINANCE.md原设计文档+ 实际代码分析
---
## 一、功能点清单
| 编号 | 功能模块 | 功能点 | 实现状态 | 说明 |
|------|---------|--------|---------|------|
| FIN-001 | 能耗计量管理 | 计量点CRUD | 已实现 | module-mdm 中 EnergyMeter |
| FIN-002 | 能耗计量管理 | 计量点编码自动生成 | 已实现 | EM + yyyyMMddHHmmss |
| FIN-003 | 能耗计量管理 | 计量点软删除 | 已实现 | 状态设为 INACTIVE |
| FIN-004 | 能耗录入 | 能耗数据录入 | 已实现 | 手动录入 + IoT预留 |
| FIN-005 | 能耗录入 | 读数递增校验 | 已实现 | 当前读数 >= 上次读数 |
| FIN-006 | 能耗录入 | 自动计算消耗量与费用 | 已实现 | consumption = current - previous; amount = consumption * unitPrice |
| FIN-007 | 能耗统计 | 按类型统计消耗 | 已实现(有缺陷) | 当前全部归入LIGHTING需修复 |
| FIN-008 | 能耗统计 | 单方能耗 | 已实现 | 遍历项目ACTIVE计量点累加 |
| FIN-009 | 收费项目管理 | 收费项目CRUD | 未实现 | FeeItem 实体不存在 |
| FIN-010 | 收费项目管理 | 周期性收费配置 | 未实现 | 按月/季/年收费 |
| FIN-011 | 收费项目管理 | 一次性收费配置 | 未实现 | 临时收费项目 |
| FIN-012 | 收费项目管理 | 按面积计费配置 | 未实现 | 面积 * 单价 |
| FIN-013 | 收费项目管理 | 按用量计费配置 | 未实现 | 用量 * 单价(对接能耗) |
| FIN-014 | 收费项目管理 | 固定金额计费配置 | 未实现 | 每月固定金额 |
| FIN-015 | 账单管理 | 账单自动生成 | 未实现 | 根据收费项目规则按账期生成 |
| FIN-016 | 账单管理 | 账单手动生成 | 未实现 | 人工创建单笔账单 |
| FIN-017 | 账单管理 | 批量账单生成 | 未实现 | 一次性为项目所有业主生成 |
| FIN-018 | 账单管理 | 账单状态流转 | 未实现 | UNPAID/PARTIAL_PAID/PAID/OVERDUE/CANCELLED |
| FIN-019 | 账单管理 | 催缴提醒 | 未实现 | 到期前3天提醒 + 逾期催缴 |
| FIN-020 | 账单管理 | 账单导出 | 未实现 | Excel/PDF 格式 |
| FIN-021 | 支付管理 | 线下收款登记 | 未实现 | 现金/银行转账/刷卡 |
| FIN-022 | 支付管理 | 线上支付对接 | 未实现 | 微信/支付宝 |
| FIN-023 | 支付管理 | 支付确认 | 未实现 | 支付状态确认 |
| FIN-024 | 支付管理 | 支付记录查询 | 未实现 | 按账单/业主/时间查询 |
| FIN-025 | 退款管理 | 退款申请 | 未实现 | 业主发起退款 |
| FIN-026 | 退款管理 | 退款审核 | 未实现 | 审批流程 |
| FIN-027 | 退款管理 | 退款执行 | 未实现 | 原路退回/线下退款 |
| FIN-028 | 滞纳金 | 滞纳金自动计算 | 未实现 | 逾期天数 * 日利率 |
| FIN-029 | 滞纳金 | 滞纳金上限控制 | 未实现 | 不超过 maxLateFee |
| FIN-030 | 能耗对接 | 能耗数据与账单对接 | 未实现 | EnergyConsumption -> FeeBill |
| FIN-031 | 能耗修复 | 修复按类型统计 | 未实现 | 按 meter.energyType 真正分项汇总 |
| FIN-032 | 能耗修复 | 统一前后端能源类型枚举 | 未实现 | 后端6种 vs 前端5种 |
| FIN-033 | 财务报表 | 收费统计 | 未实现 | 按项目/类型/时间维度 |
| FIN-034 | 财务报表 | 欠费分析 | 未实现 | 逾期/欠费金额统计 |
| FIN-035 | 财务报表 | 收入趋势 | 未实现 | 月度/季度趋势图 |
---
## 二、数据结构设计
### 2.1 已实现实体
#### 2.1.1 EnergyMeter能源计量点
**所在模块**: module-mdm
**数据库表**: `ops_energy_meter`
**源码**: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/entity/EnergyMeter.java`
| 字段名 | 类型 | 数据库列名 | 约束 | 说明 |
|--------|------|-----------|------|------|
| id | UUID | id | PK | 主键 |
| projectId | UUID | project_id | NOT NULL | 所属项目 |
| meterCode | String | meter_code | NOT NULL, UNIQUE | 计量点编码EM + yyyyMMddHHmmss |
| meterName | String | meter_name | NOT NULL | 计量点名称 |
| energyType | EnergyType | energy_type | NOT NULL | 能源类型枚举 |
| spaceNodeId | UUID | space_node_id | -- | 关联空间节点 |
| installationLocation | String | installation_location | -- | 安装位置 |
| ratedCapacity | BigDecimal(10,2) | rated_capacity | -- | 额定容量 |
| unitPrice | BigDecimal(10,4) | unit_price | -- | 单价4位小数精度 |
| status | Status | status | NOT NULL, 默认ACTIVE | 状态枚举 |
| createdAt | LocalDateTime | created_at | -- | 创建时间 |
| updatedAt | LocalDateTime | updated_at | -- | 更新时间 |
**枚举定义**:
```java
public enum EnergyType {
LIGHTING("照明插座用电"),
HVAC("空调用电"),
POWER("动力用电"),
SPECIAL("特殊用电"),
WATER("给排水"),
GAS("燃气");
}
public enum Status {
ACTIVE, INACTIVE
}
```
#### 2.1.2 EnergyConsumption能耗记录
**所在模块**: module-mdm
**数据库表**: `ops_energy_consumption`
**源码**: `ether-pms/module-mdm/src/main/java/com/ether/pms/mdm/entity/EnergyConsumption.java`
| 字段名 | 类型 | 数据库列名 | 约束 | 说明 |
|--------|------|-----------|------|------|
| id | UUID | id | PK | 主键 |
| projectId | UUID | project_id | NOT NULL | 所属项目 |
| meterId | UUID | meter_id | NOT NULL | 关联计量点 |
| consumptionDate | LocalDate | consumption_date | NOT NULL | 记录日期 |
| previousReading | BigDecimal(12,2) | previous_reading | -- | 上次读数 |
| currentReading | BigDecimal(12,2) | current_reading | -- | 当前读数 |
| consumption | BigDecimal(12,2) | consumption | NOT NULL | 消耗量 = current - previous |
| amount | BigDecimal(10,2) | amount | -- | 费用 = consumption * unitPrice |
| recordedBy | UUID | recorded_by | -- | 记录人 |
| recordMethod | RecordMethod | record_method | -- | 记录方式默认MANUAL |
| remarks | String(1000) | remarks | -- | 备注 |
| createdAt | LocalDateTime | created_at | -- | 创建时间 |
**数据库索引**:
```sql
idx_ec_meter_date -- (meter_id, consumption_date)
idx_ec_project_date -- (project_id, consumption_date)
```
**枚举定义**:
```java
public enum RecordMethod {
MANUAL, // 手动录入
IOT // IoT自动采集
}
```
---
### 2.2 待实现实体
#### 2.2.1 FeeItem收费项目
**目标模块**: module-finance待创建
**目标表**: `fin_fee_item`
| 字段名 | 类型 | 数据库列名 | 约束 | 说明 |
|--------|------|-----------|------|------|
| id | UUID | id | PK | 主键 |
| projectId | UUID | project_id | NOT NULL | 所属项目 |
| code | VARCHAR(50) | code | NOT NULL, UNIQUE | 收费项目编码FI + yyyyMMddHHmmss |
| name | VARCHAR(100) | name | NOT NULL | 收费项目名称 |
| type | VARCHAR(20) | type | NOT NULL | 收费类型枚举 |
| billingMethod | VARCHAR(20) | billing_method | NOT NULL | 计费方式枚举 |
| unitPrice | DECIMAL(12,4) | unit_price | -- | 单价(按用量/面积计费时使用) |
| unit | VARCHAR(20) | unit | -- | 计量单位kWh/吨/平方米/月/次) |
| fixedAmount | DECIMAL(12,2) | fixed_amount | -- | 固定金额FIXED计费方式 |
| billDay | INT | bill_day | -- | 出账日每月几号出账1-28 |
| dueDay | INT | due_day | -- | 到期日出账后第N天到期 |
| overdueDay | INT | overdue_day | -- | 逾期日到期后第N天开始计滞纳金 |
| enableLateFee | BOOLEAN | enable_late_fee | NOT NULL, 默认false | 是否启用滞纳金 |
| lateFeeRate | DECIMAL(8,6) | late_fee_rate | -- | 日滞纳金利率如0.0005 = 万分之五/天) |
| maxLateFee | DECIMAL(12,2) | max_late_fee | -- | 滞纳金上限金额 |
| description | VARCHAR(500) | description | -- | 收费项目说明 |
| status | VARCHAR(20) | status | NOT NULL, 默认ENABLED | 状态ENABLED/DISABLED |
| createdAt | TIMESTAMP | created_at | NOT NULL | 创建时间 |
| updatedAt | TIMESTAMP | updated_at | NOT NULL | 更新时间 |
| createdBy | UUID | created_by | -- | 创建人 |
**枚举定义**:
```java
public enum FeeType {
PROPERTY("物业费"),
PARKING("停车费"),
WATER("水费"),
ELECTRICITY("电费"),
GAS("燃气费"),
HEATING("供暖费"),
REPAIR_FUND("维修基金"),
OTHER("其他");
}
public enum BillingMethod {
FIXED("固定金额"), // 每月固定金额
AREA("按面积计费"), // 面积 * 单价
USAGE("按用量计费"), // 用量 * 单价(对接能耗)
CUSTOM("自定义"); // 自定义计费规则
}
```
**数据库索引**:
```sql
idx_fi_project_status -- (project_id, status)
idx_fi_code -- (code) -- UNIQUE约束自带索引
idx_fi_type -- (type, status)
```
**外键关系**:
```sql
fk_fi_project -- project_id REFERENCES mdm_project(id)
```
---
#### 2.2.2 FeeBill收费账单
**目标模块**: module-finance待创建
**目标表**: `fin_fee_bill`
| 字段名 | 类型 | 数据库列名 | 约束 | 说明 |
|--------|------|-----------|------|------|
| id | UUID | id | PK | 主键 |
| billNo | VARCHAR(30) | bill_no | NOT NULL, UNIQUE | 账单编号BL + yyyyMMdd + 4位序号 |
| feeItemId | UUID | fee_item_id | NOT NULL | 关联收费项目 |
| projectId | UUID | project_id | NOT NULL | 所属项目 |
| spaceNodeId | UUID | space_node_id | -- | 关联空间节点(房产) |
| ownerId | UUID | owner_id | -- | 关联业主ID |
| billPeriod | VARCHAR(20) | bill_period | NOT NULL | 账期如2026-05 |
| billDate | DATE | bill_date | NOT NULL | 出账日期 |
| dueDate | DATE | due_date | NOT NULL | 到期日期 |
| overdueDate | DATE | overdue_date | -- | 逾期日期 |
| usageAmount | DECIMAL(12,2) | usage_amount | -- | 用量(按用量计费时) |
| usageUnit | VARCHAR(20) | usage_unit | -- | 用量单位 |
| area | DECIMAL(12,2) | area | -- | 面积(按面积计费时) |
| amount | DECIMAL(12,2) | amount | NOT NULL | 应收金额 |
| lateFee | DECIMAL(12,2) | late_fee | 默认0 | 滞纳金 |
| discount | DECIMAL(12,2) | discount | 默认0 | 优惠金额 |
| payableAmount | DECIMAL(12,2) | payable_amount | NOT NULL | 应付金额 = amount + lateFee - discount |
| paidAmount | DECIMAL(12,2) | paid_amount | 默认0 | 已付金额 |
| status | VARCHAR(20) | status | NOT NULL, 默认UNPAID | 账单状态枚举 |
| sourceType | VARCHAR(20) | source_type | -- | 来源类型AUTO/MANUAL/IMPORT |
| sourceId | UUID | source_id | -- | 来源ID如能耗记录ID |
| remark | VARCHAR(500) | remark | -- | 备注 |
| createdAt | TIMESTAMP | created_at | NOT NULL | 创建时间 |
| updatedAt | TIMESTAMP | updated_at | NOT NULL | 更新时间 |
| createdBy | UUID | created_by | -- | 创建人 |
**枚举定义**:
```java
public enum BillStatus {
UNPAID("未支付"),
PARTIAL_PAID("部分支付"),
PAID("已支付"),
OVERDUE("已逾期"),
CANCELLED("已取消");
}
public enum BillSourceType {
AUTO("自动生成"),
MANUAL("手动创建"),
IMPORT("导入");
}
```
**数据库索引**:
```sql
idx_fb_bill_no -- (bill_no) -- UNIQUE约束自带索引
idx_fb_project_period -- (project_id, bill_period)
idx_fb_owner_status -- (owner_id, status)
idx_fb_fee_item -- (fee_item_id, bill_period)
idx_fb_space_node -- (space_node_id, bill_period)
idx_fb_due_date -- (due_date, status) -- 用于催缴查询
idx_fb_overdue -- (status, overdue_date) -- 用于滞纳金计算
```
**外键关系**:
```sql
fk_fb_fee_item -- fee_item_id REFERENCES fin_fee_item(id)
fk_fb_project -- project_id REFERENCES mdm_project(id)
fk_fb_space_node -- space_node_id REFERENCES mdm_space_node(id)
```
---
#### 2.2.3 FeePayment支付记录
**目标模块**: module-finance待创建
**目标表**: `fin_fee_payment`
| 字段名 | 类型 | 数据库列名 | 约束 | 说明 |
|--------|------|-----------|------|------|
| id | UUID | id | PK | 主键 |
| paymentNo | VARCHAR(30) | payment_no | NOT NULL, UNIQUE | 支付编号PY + yyyyMMddHHmmss |
| billId | UUID | bill_id | NOT NULL | 关联账单 |
| amount | DECIMAL(12,2) | amount | NOT NULL | 支付金额 |
| method | VARCHAR(20) | method | NOT NULL | 支付方式枚举 |
| thirdPartyNo | VARCHAR(100) | third_party_no | -- | 第三方交易号(微信/支付宝) |
| status | VARCHAR(20) | status | NOT NULL, 默认PENDING | 支付状态枚举 |
| failReason | VARCHAR(500) | fail_reason | -- | 失败原因 |
| paymentTime | TIMESTAMP | payment_time | -- | 实际支付时间 |
| operatorId | UUID | operator_id | -- | 操作人(线下收款时为收费员) |
| receiptNo | VARCHAR(30) | receipt_no | -- | 收据编号 |
| remark | VARCHAR(500) | remark | -- | 备注 |
| createdAt | TIMESTAMP | created_at | NOT NULL | 创建时间 |
| updatedAt | TIMESTAMP | updated_at | NOT NULL | 更新时间 |
**枚举定义**:
```java
public enum PaymentMethod {
WECHAT("微信支付"),
ALIPAY("支付宝"),
CASH("现金"),
BANK_TRANSFER("银行转账"),
CARD("刷卡");
}
public enum PaymentStatus {
PENDING("待支付"),
SUCCESS("支付成功"),
FAILED("支付失败"),
REFUNDED("已退款");
}
```
**数据库索引**:
```sql
idx_fp_payment_no -- (payment_no) -- UNIQUE约束自带索引
idx_fp_bill_id -- (bill_id)
idx_fp_status -- (status, payment_time)
idx_fp_method -- (method, payment_time)
idx_fp_operator -- (operator_id, payment_time)
```
**外键关系**:
```sql
fk_fp_bill -- bill_id REFERENCES fin_fee_bill(id)
```
---
#### 2.2.4 FeeRefund退款记录
**目标模块**: module-finance待创建
**目标表**: `fin_fee_refund`
| 字段名 | 类型 | 数据库列名 | 约束 | 说明 |
|--------|------|-----------|------|------|
| id | UUID | id | PK | 主键 |
| refundNo | VARCHAR(30) | refund_no | NOT NULL, UNIQUE | 退款编号RF + yyyyMMddHHmmss |
| paymentId | UUID | payment_id | NOT NULL | 关联支付记录 |
| billId | UUID | bill_id | NOT NULL | 关联账单 |
| amount | DECIMAL(12,2) | amount | NOT NULL | 退款金额 |
| reason | VARCHAR(500) | reason | NOT NULL | 退款原因 |
| status | VARCHAR(20) | status | NOT NULL, 默认PENDING | 退款状态枚举 |
| applicantId | UUID | applicant_id | NOT NULL | 申请人ID |
| approverId | UUID | approver_id | -- | 审批人ID |
| approveTime | TIMESTAMP | approve_time | -- | 审批时间 |
| approveRemark | VARCHAR(500) | approve_remark | -- | 审批备注 |
| thirdPartyRefundNo | VARCHAR(100) | third_party_refund_no | -- | 第三方退款单号 |
| refundTime | TIMESTAMP | refund_time | -- | 实际退款时间 |
| refundMethod | VARCHAR(20) | refund_method | -- | 退款方式ORIGINAL/OFFLINE |
| remark | VARCHAR(500) | remark | -- | 备注 |
| createdAt | TIMESTAMP | created_at | NOT NULL | 创建时间 |
| updatedAt | TIMESTAMP | updated_at | NOT NULL | 更新时间 |
**枚举定义**:
```java
public enum RefundStatus {
PENDING("待审批"),
APPROVED("已审批"),
REJECTED("已拒绝"),
REFUNDED("已退款"),
FAILED("退款失败");
}
public enum RefundMethod {
ORIGINAL("原路退回"),
OFFLINE("线下退款");
}
```
**数据库索引**:
```sql
idx_fr_refund_no -- (refund_no) -- UNIQUE约束自带索引
idx_fr_payment -- (payment_id)
idx_fr_bill -- (bill_id)
idx_fr_status -- (status)
idx_fr_applicant -- (applicant_id, status)
```
**外键关系**:
```sql
fk_fr_payment -- payment_id REFERENCES fin_fee_payment(id)
fk_fr_bill -- bill_id REFERENCES fin_fee_bill(id)
```
---
### 2.3 实体关系图
```
EnergyMeter (module-mdm) FeeItem (module-finance)
| |
v v
EnergyConsumption ---费用计算---> FeeBill (自动/手动生成)
(抄表数据) | |
| +---> FeePayment (支付记录)
| |
| v
| FeeRefund (退款记录)
|
+-- sourceType=AUTO, sourceId=EnergyConsumption.id
```
**跨模块引用关系**:
- FeeBill.sourceId 可指向 EnergyConsumption.id按用量计费时
- FeeBill.spaceNodeId 引用 mdm_space_node.id
- FeeBill.ownerId 引用 auth_user.id业主
- FeePayment.operatorId 引用 auth_user.id收费员
- FeeRefund.applicantId / approverId 引用 auth_user.id
---
## 三、API设计
### 3.1 已实现API能耗管理相关端点
**基础路径**: `/api/ops/energy`
**控制器**: `EnergyController`
#### 3.1.1 计量点管理
| 方法 | 路径 | 说明 | 请求参数 | 响应类型 |
|------|------|------|---------|---------|
| POST | `/meters` | 创建计量点 | EnergyMeter JSON | `ApiResponse<EnergyMeter>` |
| GET | `/meters` | 查询计量点列表 | projectId(必填), energyType(可选) | `ApiResponse<List<EnergyMeter>>` |
| GET | `/meters/{id}` | 获取计量点详情 | path: id | `ApiResponse<EnergyMeter>` |
| PUT | `/meters/{id}` | 更新计量点 | path: id, EnergyMeter JSON | `ApiResponse<EnergyMeter>` |
| DELETE | `/meters/{id}` | 删除计量点(软删除) | path: id | `ApiResponse<Void>` |
#### 3.1.2 能耗记录
| 方法 | 路径 | 说明 | 请求参数 | 响应类型 |
|------|------|------|---------|---------|
| POST | `/consumption` | 录入能耗数据 | `{meterId, currentReading, recordedBy}` | `ApiResponse<EnergyConsumption>` |
| GET | `/consumption/{meterId}` | 查询能耗记录 | path: meterId, startDate(可选), endDate(可选) | `ApiResponse<List<EnergyConsumption>>` |
#### 3.1.3 能耗统计
| 方法 | 路径 | 说明 | 请求参数 | 响应类型 |
|------|------|------|---------|---------|
| GET | `/statistics/by-type` | 按类型统计消耗 | projectId, month(yyyy-MM-dd) | `ApiResponse<Map<EnergyType, BigDecimal>>` |
| GET | `/statistics/unit-consumption` | 单方能耗 | projectId, month(yyyy-MM-dd) | `ApiResponse<BigDecimal>` |
---
### 3.2 待实现API收费项目/账单/支付/退款相关端点
#### 3.2.1 FeeItemController -- 收费项目管理
**基础路径**: `/api/finance/fee-items`
| 方法 | 路径 | 说明 | 请求体/参数 | 响应类型 |
|------|------|------|------------|---------|
| POST | `/` | 创建收费项目 | FeeItemForm JSON | `ApiResponse<FeeItem>` |
| GET | `/` | 查询收费项目列表 | projectId(必填), type(可选), status(可选), page, size | `ApiResponse<PageResponse<FeeItem>>` |
| GET | `/{id}` | 获取收费项目详情 | path: id | `ApiResponse<FeeItem>` |
| PUT | `/{id}` | 更新收费项目 | path: id, FeeItemForm JSON | `ApiResponse<FeeItem>` |
| PUT | `/{id}/status` | 启用/禁用收费项目 | `{status: "ENABLED"/"DISABLED"}` | `ApiResponse<Void>` |
| DELETE | `/{id}` | 删除收费项目 | path: id | `ApiResponse<Void>` |
**FeeItemForm**:
```json
{
"projectId": "UUID",
"name": "物业费",
"type": "PROPERTY",
"billingMethod": "AREA",
"unitPrice": 3.50,
"unit": "平方米/月",
"fixedAmount": null,
"billDay": 1,
"dueDay": 15,
"overdueDay": 5,
"enableLateFee": true,
"lateFeeRate": 0.0005,
"maxLateFee": 500.00,
"description": "住宅物业费"
}
```
#### 3.2.2 FeeBillController -- 账单管理
**基础路径**: `/api/finance/bills`
| 方法 | 路径 | 说明 | 请求体/参数 | 响应类型 |
|------|------|------|------------|---------|
| POST | `/` | 手动创建账单 | FeeBillForm JSON | `ApiResponse<FeeBill>` |
| POST | `/generate` | 按收费项目自动生成账单 | `{projectId, feeItemId, billPeriod}` | `ApiResponse<List<FeeBill>>` |
| POST | `/generate-batch` | 批量生成账单 | `{projectId, billPeriod, feeItemIds[]}` | `ApiResponse<BatchResult>` |
| GET | `/` | 查询账单列表 | projectId, ownerId, status, billPeriod, page, size | `ApiResponse<PageResponse<FeeBill>>` |
| GET | `/{id}` | 获取账单详情 | path: id | `ApiResponse<FeeBill>` |
| PUT | `/{id}` | 更新账单 | path: id, FeeBillForm JSON | `ApiResponse<FeeBill>` |
| POST | `/{id}/cancel` | 取消账单 | `{reason}` | `ApiResponse<Void>` |
| GET | `/overdue` | 查询逾期账单 | projectId, page, size | `ApiResponse<PageResponse<FeeBill>>` |
| GET | `/statistics` | 账单统计 | projectId, billPeriod | `ApiResponse<BillStatistics>` |
| GET | `/export` | 导出账单 | projectId, billPeriod, format | `Blob` |
**FeeBillForm**:
```json
{
"feeItemId": "UUID",
"projectId": "UUID",
"spaceNodeId": "UUID",
"ownerId": "UUID",
"billPeriod": "2026-05",
"usageAmount": 150.50,
"usageUnit": "kWh",
"area": 120.00,
"amount": 525.00,
"remark": ""
}
```
**BillStatistics**:
```json
{
"totalAmount": 150000.00,
"paidAmount": 120000.00,
"unpaidAmount": 30000.00,
"overdueAmount": 15000.00,
"lateFeeAmount": 500.00,
"totalCount": 200,
"paidCount": 160,
"unpaidCount": 30,
"overdueCount": 10
}
```
#### 3.2.3 FeePaymentController -- 支付管理
**基础路径**: `/api/finance/payments`
| 方法 | 路径 | 说明 | 请求体/参数 | 响应类型 |
|------|------|------|------------|---------|
| POST | `/` | 创建支付记录(线下收款) | PaymentForm JSON | `ApiResponse<FeePayment>` |
| GET | `/` | 查询支付记录 | billId, ownerId, method, status, startDate, endDate, page, size | `ApiResponse<PageResponse<FeePayment>>` |
| GET | `/{id}` | 获取支付详情 | path: id | `ApiResponse<FeePayment>` |
| POST | `/{id}/confirm` | 确认支付 | `{status: "SUCCESS"/"FAILED", failReason}` | `ApiResponse<FeePayment>` |
| POST | `/online/prepare` | 发起线上支付 | `{billId, method}` | `ApiResponse<PaymentPrepareResult>` |
| POST | `/online/callback` | 支付回调 | 第三方回调数据 | `ApiResponse<Void>` |
**PaymentForm**:
```json
{
"billId": "UUID",
"amount": 525.00,
"method": "CASH",
"receiptNo": "RCP20260518001",
"remark": ""
}
```
#### 3.2.4 FeeRefundController -- 退款管理
**基础路径**: `/api/finance/refunds`
| 方法 | 路径 | 说明 | 请求体/参数 | 响应类型 |
|------|------|------|------------|---------|
| POST | `/` | 申请退款 | RefundForm JSON | `ApiResponse<FeeRefund>` |
| GET | `/` | 查询退款记录 | billId, status, startDate, endDate, page, size | `ApiResponse<PageResponse<FeeRefund>>` |
| GET | `/{id}` | 获取退款详情 | path: id | `ApiResponse<FeeRefund>` |
| POST | `/{id}/approve` | 审批通过 | `{approveRemark}` | `ApiResponse<FeeRefund>` |
| POST | `/{id}/reject` | 审批拒绝 | `{approveRemark}` | `ApiResponse<FeeRefund>` |
| POST | `/{id}/execute` | 执行退款 | `{thirdPartyRefundNo, refundMethod}` | `ApiResponse<FeeRefund>` |
**RefundForm**:
```json
{
"paymentId": "UUID",
"billId": "UUID",
"amount": 525.00,
"reason": "重复缴费",
"refundMethod": "ORIGINAL"
}
```
#### 3.2.5 FeeReminderJob -- 催缴定时任务
| 任务 | Cron表达式 | 说明 |
|------|-----------|------|
| 到期提醒 | `0 0 9 * * ?` | 每天上午9点检查3天内到期账单发送提醒 |
| 逾期催缴 | `0 0 10 * * ?` | 每天上午10点检查逾期账单发送催缴通知 |
| 滞纳金计算 | `0 0 2 * * ?` | 每天凌晨2点计算逾期账单滞纳金 |
| 周汇总 | `0 0 18 ? * MON` | 每周一18点汇总逾期账单通知项目经理 |
---
## 四、业务规则
### 4.1 能耗计费规则(已实现 + 待完善)
#### 4.1.1 已实现规则
| 规则编号 | 规则名称 | 规则描述 | 实现位置 |
|---------|---------|---------|---------|
| E-001 | 仪表状态校验 | 只能对ACTIVE状态的仪表进行抄表 | EnergyConsumptionServiceImpl |
| E-002 | 读数递增校验 | 当前读数不能小于上次读数 | EnergyConsumptionServiceImpl |
| E-003 | 自动获取上次读数 | 从最近一条记录获取previousReading首次为0 | EnergyConsumptionServiceImpl |
| E-004 | 自动计算消耗量 | consumption = currentReading - previousReading | EnergyConsumptionServiceImpl |
| E-005 | 自动计算费用 | amount = consumption * meter.unitPrice单价为空时为0 | EnergyConsumptionServiceImpl |
| E-006 | 按月统计 | 根据month参数计算月份起止日期 | EnergyConsumptionServiceImpl |
| E-007 | 单方能耗 | 遍历项目所有ACTIVE计量点累加月度消耗量 | EnergyConsumptionServiceImpl |
#### 4.1.2 待修复缺陷
| 缺陷编号 | 描述 | 严重度 | 修复方案 |
|---------|------|--------|---------|
| E-BUG-001 | getConsumptionByType()将总消耗全部归入LIGHTING | 高 | 按 meter.energyType 真正分项汇总 |
| E-BUG-002 | 前后端能源类型枚举不一致 | 中 | 统一为后端6种枚举前端对齐 |
---
### 4.2 收费项目规则(待设计)
| 规则编号 | 规则名称 | 规则描述 | 适用计费方式 |
|---------|---------|---------|------------|
| FI-001 | 周期性收费 | 按月/季/年定期出账billDay指定出账日 | FIXED / AREA / USAGE |
| FI-002 | 一次性收费 | 临时创建,不参与自动出账 | CUSTOM |
| FI-003 | 临时收费 | 针对特定业主的临时性收费 | CUSTOM |
| FI-004 | 固定金额计费 | 每期收取固定金额如停车费300元/月) | FIXED |
| FI-005 | 按面积计费 | 面积 * 单价如物业费3.5元/平方米/月) | AREA |
| FI-006 | 按用量计费 | 用量 * 单价如电费0.85元/kWh对接能耗数据 | USAGE |
| FI-007 | 编码自动生成 | 格式FI + yyyyMMddHHmmss冲突时追加后缀 | 全部 |
| FI-008 | 禁用不删除 | 收费项目禁用后不再参与自动出账,已有账单不受影响 | 全部 |
| FI-009 | 项目隔离 | 收费项目属于特定项目,跨项目不可见 | 全部 |
---
### 4.3 账单生成规则(待设计)
| 规则编号 | 规则名称 | 规则描述 |
|---------|---------|---------|
| BL-001 | 自动生成 | 根据收费项目的billDay定时任务自动为项目下所有业主生成账单 |
| BL-002 | 手动生成 | 管理员手动为指定业主创建账单 |
| BL-003 | 批量生成 | 一次性为项目所有业主生成某类收费项目的账单 |
| BL-004 | 账单编号生成 | 格式BL + yyyyMMdd + 4位序号序号按天递增 |
| BL-005 | 到期日计算 | dueDate = billDate + feeItem.dueDay天 |
| BL-006 | 逾期日计算 | overdueDate = dueDate + feeItem.overdueDay天 |
| BL-007 | 按用量计费账单 | 从EnergyConsumption获取当期用量计算金额 |
| BL-008 | 按面积计费账单 | 从SpaceNode获取面积计算金额 = 面积 * 单价 |
| BL-009 | 固定金额账单 | 金额 = feeItem.fixedAmount |
| BL-010 | 防重复生成 | 同一收费项目 + 同一业主 + 同一账期不可重复生成 |
| BL-011 | 催缴提醒 | 到期前3天发送提醒逾期后发送催缴通知 |
| BL-012 | 账单取消 | 已支付/部分支付的账单不可取消,需先退款 |
---
### 4.4 支付流程(待设计)
| 规则编号 | 规则名称 | 规则描述 |
|---------|---------|---------|
| PY-001 | 线下收款 | 管理员登记现金/转账/刷卡收款,直接确认成功 |
| PY-002 | 线上支付 | 调用微信/支付宝SDK生成预支付单等待回调确认 |
| PY-003 | 支付确认 | 线上支付由回调确认,线下支付由操作员确认 |
| PY-004 | 部分支付 | 支持部分支付账单状态变为PARTIAL_PAID |
| PY-005 | 超额支付 | 支付金额不可超过应付金额payableAmount - paidAmount |
| PY-006 | 支付编号生成 | 格式PY + yyyyMMddHHmmss |
| PY-007 | 账单状态联动 | 支付成功后更新账单paidAmount和status |
| PY-008 | 收据编号 | 线下收款时生成收据编号 |
**支付状态流转**:
```
PENDING --(支付成功)--> SUCCESS
PENDING --(支付失败)--> FAILED
SUCCESS --(发起退款)--> REFUNDED
```
**账单状态联动**:
```
UNPAID --(部分支付)--> PARTIAL_PAID
UNPAID/PARTIAL_PAID --(全额支付)--> PAID
UNPAID --(超过逾期日)--> OVERDUE
任意状态 --(取消)--> CANCELLED需先退款
```
---
### 4.5 退款流程(待设计)
| 规则编号 | 规则名称 | 规则描述 |
|---------|---------|---------|
| RF-001 | 退款申请 | 业主或管理员发起退款申请,需指定退款原因 |
| RF-002 | 退款金额限制 | 退款金额不可超过原支付金额 |
| RF-003 | 退款审批 | 退款金额 > 1000元需审批否则自动通过 |
| RF-004 | 审批通过 | 审批人确认后退款状态变为APPROVED |
| RF-005 | 审批拒绝 | 审批人可拒绝退款,需填写拒绝原因 |
| RF-006 | 原路退回 | 线上支付的退款原路退回至支付账户 |
| RF-007 | 线下退款 | 现金/转账支付的退款通过线下处理 |
| RF-008 | 退款执行 | 记录第三方退款单号和实际退款时间 |
| RF-009 | 账单金额回退 | 退款成功后账单paidAmount减少状态可能回退 |
| RF-010 | 退款编号生成 | 格式RF + yyyyMMddHHmmss |
**退款状态流转**:
```
PENDING --(审批通过)--> APPROVED
PENDING --(审批拒绝)--> REJECTED
APPROVED --(退款成功)--> REFUNDED
APPROVED --(退款失败)--> FAILED
```
---
### 4.6 滞纳金计算(待设计)
| 规则编号 | 规则名称 | 规则描述 |
|---------|---------|---------|
| LF-001 | 计算触发 | 每天凌晨2点定时任务检查逾期账单 |
| LF-002 | 计算公式 | lateFee = 逾期天数 * payableAmount * lateFeeRate |
| LF-003 | 逾期天数 | 从overdueDate开始计算到当前日期 |
| LF-004 | 上限控制 | lateFee不超过feeItem.maxLateFee |
| LF-005 | 累加计算 | 每天累加,不覆盖之前的滞纳金 |
| LF-006 | 前提条件 | 仅当feeItem.enableLateFee = true时计算 |
| LF-007 | 已支付账单 | 已全额支付的账单不再计算滞纳金 |
| LF-008 | 已取消账单 | 已取消的账单不再计算滞纳金 |
| LF-009 | 部分支付 | 滞纳金基于剩余应付金额计算 |
**滞纳金计算示例**:
```
假设:
payableAmount = 1000元
lateFeeRate = 0.0005(万分之五/天)
maxLateFee = 500元
overdueDate = 2026-05-10
当前日期 = 2026-05-18
计算:
逾期天数 = 8天
每日滞纳金 = 1000 * 0.0005 = 0.5元
累计滞纳金 = 8 * 0.5 = 4元未超过上限500元
```
---
## 五、执行约束
| 约束编号 | 约束名称 | 约束描述 |
|---------|---------|---------|
| CON-001 | 金额精度 | 所有金额字段使用DECIMAL(12,2)单价使用DECIMAL(12,4)利率使用DECIMAL(8,6) |
| CON-002 | 事务一致性 | 账单生成、支付确认、退款执行必须在同一事务中完成账单状态更新 |
| CON-003 | 并发控制 | 支付操作使用乐观锁version字段或状态校验防止重复支付 |
| CON-004 | 审计日志 | 所有收费/支付/退款操作必须记录审计日志 |
| CON-005 | 项目隔离 | 所有查询必须按projectId过滤确保项目间数据隔离 |
| CON-006 | 软删除 | 收费项目使用启用/禁用替代物理删除,账单/支付/退款不允许删除 |
| CON-007 | 编码唯一性 | billNo/paymentNo/refundNo全局唯一生成时需处理冲突 |
| CON-008 | 定时任务幂等 | 催缴/滞纳金计算任务必须幂等,重复执行不产生副作用 |
---
## 六、权限控制
| 权限编码 | 权限名称 | 适用角色 | 说明 |
|---------|---------|---------|------|
| finance:fee-item:list | 查看收费项目 | 项目管理员/财务人员 | 查看项目下收费项目列表 |
| finance:fee-item:create | 创建收费项目 | 系统管理员 | 创建新的收费项目 |
| finance:fee-item:update | 修改收费项目 | 系统管理员 | 修改收费项目配置 |
| finance:fee-item:delete | 删除收费项目 | 系统管理员 | 删除/禁用收费项目 |
| finance:bill:list | 查看账单 | 项目管理员/财务人员 | 查看项目下账单列表 |
| finance:bill:create | 创建账单 | 财务人员 | 手动创建账单 |
| finance:bill:generate | 生成账单 | 财务人员 | 自动/批量生成账单 |
| finance:bill:cancel | 取消账单 | 财务主管 | 取消账单(需审批) |
| finance:bill:export | 导出账单 | 财务人员 | 导出账单Excel/PDF |
| finance:payment:create | 登记收款 | 财务人员 | 线下收款登记 |
| finance:payment:confirm | 确认支付 | 财务人员 | 确认支付状态 |
| finance:payment:list | 查看支付记录 | 财务人员 | 查看支付记录列表 |
| finance:refund:apply | 申请退款 | 财务人员/业主 | 发起退款申请 |
| finance:refund:approve | 审批退款 | 财务主管 | 审批退款申请 |
| finance:refund:execute | 执行退款 | 财务人员 | 执行退款操作 |
| finance:refund:list | 查看退款记录 | 财务人员 | 查看退款记录列表 |
| finance:statistics:view | 查看财务统计 | 项目管理员/财务主管 | 查看财务报表 |
| finance:late-fee:config | 配置滞纳金 | 系统管理员 | 配置滞纳金规则 |
**数据级权限**:
- 财务人员PROJECT数据范围只能查看/操作所属项目的财务数据
- 财务主管ALL数据范围可查看所有项目的财务数据
- 业主SELF数据范围只能查看自己的账单和支付记录
---
## 七、例外情况处理
| 例外编号 | 例外场景 | 处理策略 | 错误码 |
|---------|---------|---------|--------|
| EX-001 | 重复生成账单 | 检查feeItemId + ownerId + billPeriod唯一性已存在则跳过 | 7001 |
| EX-002 | 支付金额超过应付金额 | 校验支付金额 <= payableAmount - paidAmount | 7002 |
| EX-003 | 已支付账单取消 | 拒绝取消,提示需先退款 | 7003 |
| EX-004 | 退款金额超过支付金额 | 校验退款金额 <= 原支付金额 | 7004 |
| EX-005 | 重复支付 | 状态校验,已支付/已退款的支付记录不可再次确认 | 7005 |
| EX-006 | 能耗数据缺失 | 按用量计费时无对应能耗记录生成0元账单并标记异常 | 7006 |
| EX-007 | 业主无关联房产 | 生成账单时业主无房产关联,跳过并记录日志 | 7007 |
| EX-008 | 线上支付超时 | 支付状态保持PENDING由对账任务处理 | 7008 |
| EX-009 | 线上支付回调异常 | 记录原始回调数据,人工介入处理 | 7009 |
| EX-010 | 滞纳金计算溢出 | lateFee不超过maxLateFee上限 | 7010 |
| EX-011 | 收费项目被禁用后自动出账 | 跳过已禁用的收费项目 | 7011 |
| EX-012 | 批量生成部分失败 | 记录成功/失败数量和失败原因返回BatchResult | 7012 |

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,370 +0,0 @@
# 设施设备领域技术方案
**领域编号**: 4.3
**微服务**: ether-asset
**最后更新**: 2026-02-14
---
## 一、领域概述
### 1.1 领域职责
设施设备领域负责管理物业资产全生命周期:
- 设备台账管理(电梯、空调、消防、给排水、供配电等)
- 设备维保计划与记录
- 设备故障管理
- 设备与工单联动
### 1.2 核心概念
| 概念 | 说明 | 对应实体 |
| ------------ | ---------------------------- | ----------------- |
| **设备台账** | 设备基础信息和运行状态 | Equipment |
| **维保计划** | 定期保养计划 | MaintenancePlan |
| **维保记录** | 保养执行记录 | MaintenanceRecord |
| **设备类型** | 设备分类(电梯/空调/消防等) | EquipmentType |
---
## 二、领域模型
### 2.1 聚合根设计
#### Equipment设备台账
```java
@Entity
@Table(name = "asset_equipment")
@Data
public class Equipment {
@Id
private UUID id;
private UUID projectId;
private String code; // 设备编码
private String name; // 设备名称
// 分类
private EquipmentType type; // ELEVATOR/AC/FIRE/PLUMBING/POWER
private EquipmentStatus status; // NORMAL/FAULT/MAINTAINING/SCRAPPED
// 品牌型号
private String brand;
private String model;
private String specifications;
private String serialNumber;
// 厂商信息
private String manufacturer;
private String supplier;
private String supplierPhone;
// 位置
private UUID spaceNodeId; // 关联空间节点
private String locationDesc; // 位置描述
// 时间
private LocalDate purchaseDate;
private LocalDate installDate;
private LocalDate warrantyDate;
private Integer lifespanYears; // 设计寿命(年)
// 维保
private Integer maintenanceCycle; // 保养周期(天)
private LocalDate lastMaintenanceDate;
private LocalDate nextMaintenanceDate;
// 负责人
private UUID managerId;
private String managerName;
private String contactPhone;
// 二维码
private String qrCode;
// 扩展属性(JSONB)
private String attributes;
// 审计字段
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
```
**设备类型枚举**:
```java
public enum EquipmentType {
ELEVATOR("电梯"),
AIR_CONDITIONER("空调"),
FIRE_FIGHTING("消防设备"),
PLUMBING("给排水"),
POWER_SUPPLY("供配电"),
SECURITY("安防设备"),
PARKING("停车设备"),
OTHER("其他");
}
```
#### MaintenancePlan维保计划
```java
@Entity
@Table(name = "asset_maintenance_plan")
@Data
public class MaintenancePlan {
@Id
private UUID id;
private UUID projectId;
private UUID equipmentId; // 关联设备
private String name;
private MaintenanceType type; // DAILY/WEEKLY/MONTHLY/QUARTERLY/YEARLY
// 周期
private Integer cycleDays; // 周期天数
private String cronExpression; // Cron表达式
// 内容
private String content; // 保养内容描述
private String checkItems; // 检查项清单(JSON)
// 负责人
private UUID maintainerId;
private String maintainerName;
// 提醒
private Integer remindDays; // 提前提醒天数
// 状态
private Boolean enabled;
// 审计字段
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
```
#### MaintenanceRecord维保记录
```java
@Entity
@Table(name = "asset_maintenance_record")
@Data
public class MaintenanceRecord {
@Id
private UUID id;
private UUID equipmentId;
private UUID planId; // 关联计划
// 维保信息
private LocalDate maintenanceDate;
private MaintenanceType type;
private String content;
// 执行人
private UUID maintainerId;
private String maintainerName;
// 结果
private MaintenanceResult result; // NORMAL/ABNORMAL
private String remark;
private String images;
// 检查项结果
private String checkResults; // [{"item":"检查A","result":"PASS"}]
// 关联工单
private UUID workOrderId; // 异常时关联的维修工单
// 下次维保
private LocalDate nextMaintenanceDate;
// 审计字段
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
```
---
## 三、设备与工单联动
### 3.1 故障自动报修
```java
@Component
public class EquipmentFaultHandler {
@Autowired
private WorkOrderService workOrderService;
@EventListener
public void onEquipmentFault(EquipmentFaultEvent event) {
Equipment equipment = event.getEquipment();
// 创建设备维修工单
WorkOrder workOrder = new WorkOrder();
workOrder.setOrderType(WorkOrderType.REPAIR);
workOrder.setTitle("设备故障: " + equipment.getName());
workOrder.setDescription(event.getFaultDescription());
workOrder.setEquipmentId(equipment.getId());
workOrder.setSpaceNodeId(equipment.getSpaceNodeId());
workOrder.setPriority(WorkOrderPriority.HIGH);
workOrder.setSource(WorkOrderSource.IOT);
workOrderService.create(workOrder);
// 更新设备状态
equipment.setStatus(EquipmentStatus.FAULT);
equipmentRepository.save(equipment);
}
}
```
### 3.2 维保到期提醒
```java
@Component
public class MaintenanceReminderJob {
@Scheduled(cron = "0 0 9 * * ?") // 每天9点执行
public void remindUpcomingMaintenance() {
// 查询7天内到期的维保计划
LocalDate targetDate = LocalDate.now().plusDays(7);
List<Equipment> equipments = equipmentRepository
.findByNextMaintenanceDateBeforeAndStatus(targetDate, EquipmentStatus.NORMAL);
for (Equipment equipment : equipments) {
notificationService.sendMaintenanceReminder(equipment);
}
}
}
```
---
## 四、实现状态与差异
### 4.1 实现状态
| 功能模块 | 实现状态 | 备注 |
| ----------------- | ----------- | ---------------------- |
| Equipment | 🟢 已实现 | 基础CRUD、二维码生成 |
| MaintenancePlan | 🟢 已实现 | 已迁移至 ether-asset |
| MaintenanceRecord | 🟢 已实现 | 已迁移至 ether-asset |
| 设备二维码 | 🟢 已实现 | 支持扫码查看设备信息 |
| 故障自动工单 | 🟡 部分实现 | 设备状态变更可关联工单 |
| IoT集成 | 🔴 未实现 | 待开发 |
### 4.2 与设计方案的差异
| 设计项 | 设计方案 | 现有实现 | 差异分析 |
| ------------ | ------------ | ----------- | --------------------------------------- |
| **维保归属** | ether-asset | ether-asset | ✅ 已修正:维保实体已统一在 ether-asset |
| **设备管理** | ether-asset | ether-asset | ✅ 已修正:设备实体已统一在 ether-asset |
| **设备联动** | 故障自动工单 | 部分实现 | 🟡 功能完善中 |
### 4.3 改进计划
| 优先级 | 改进项 | 说明 |
| ------ | ------------------ | ---------------------------------------------------- |
| ~~P1~~ | ~~迁移维保实体~~ | ~~将MaintenancePlan/Record迁移到ether-asset~~ 已完成 |
| P2 | 完善故障自动工单 | 设备状态变更时自动创建维修工单 |
| P2 | 完善设备二维码功能 | 支持扫码报修 |
| P3 | IoT传感器接入 | 对接传感器数据 |
---
## 五、数据库表结构
```sql
-- 设备台账表
CREATE TABLE asset_equipment (
id UUID PRIMARY KEY,
project_id UUID NOT NULL,
code VARCHAR(50) NOT NULL,
name VARCHAR(100) NOT NULL,
type VARCHAR(20) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'NORMAL',
brand VARCHAR(100),
model VARCHAR(100),
specifications VARCHAR(500),
serial_number VARCHAR(100),
manufacturer VARCHAR(100),
supplier VARCHAR(100),
supplier_phone VARCHAR(20),
space_node_id UUID,
location_desc VARCHAR(255),
purchase_date DATE,
install_date DATE,
warranty_date DATE,
lifespan_years INTEGER,
maintenance_cycle INTEGER,
last_maintenance_date DATE,
next_maintenance_date DATE,
manager_id UUID,
manager_name VARCHAR(100),
contact_phone VARCHAR(20),
qr_code VARCHAR(255),
attributes JSONB,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(project_id, code)
);
-- 维保计划表
CREATE TABLE asset_maintenance_plan (
id UUID PRIMARY KEY,
project_id UUID NOT NULL,
equipment_id UUID NOT NULL,
name VARCHAR(100) NOT NULL,
type VARCHAR(20) NOT NULL,
cycle_days INTEGER,
cron_expression VARCHAR(50),
content TEXT,
check_items JSONB,
maintainer_id UUID,
maintainer_name VARCHAR(100),
remind_days INTEGER DEFAULT 3,
enabled BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 维保记录表
CREATE TABLE asset_maintenance_record (
id UUID PRIMARY KEY,
equipment_id UUID NOT NULL,
plan_id UUID,
maintenance_date DATE NOT NULL,
type VARCHAR(20) NOT NULL,
content TEXT,
maintainer_id UUID,
maintainer_name VARCHAR(100),
result VARCHAR(20),
remark VARCHAR(500),
images TEXT,
check_results JSONB,
work_order_id UUID,
next_maintenance_date DATE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 创建索引
CREATE INDEX idx_equipment_project ON asset_equipment(project_id);
CREATE INDEX idx_equipment_type ON asset_equipment(type);
CREATE INDEX idx_equipment_status ON asset_equipment(status);
CREATE INDEX idx_equipment_space ON asset_equipment(space_node_id);
CREATE INDEX idx_equipment_next_maintenance ON asset_equipment(next_maintenance_date);
CREATE INDEX idx_maintenance_plan_equipment ON asset_maintenance_plan(equipment_id);
CREATE INDEX idx_maintenance_record_equipment ON asset_maintenance_record(equipment_id);
```
---
**文档维护**: 本领域技术方案由 ether-asset 服务负责人维护

File diff suppressed because it is too large Load Diff

1407
03-PROGRESS/progress.yml Normal file

File diff suppressed because it is too large Load Diff

486
04-TESTING/TEST-PLAN.md Normal file
View File

@ -0,0 +1,486 @@
# Ether 物业管理系统 - 测试计划
**文档版本**: v1.0
**创建日期**: 2026-05-18
**适用范围**: Ether PMS 全系统测试
---
## 一、测试策略
### 1.1 测试目标
- 确保所有已实现功能的API接口正确性API测试通过率 >= 95%
- 确保核心业务流程端到端可用E2E测试通过率 >= 90%
- 建立TDD开发规范新功能必须先写测试再实现
- 为6大业务域建立完整的测试用例体系
- 覆盖5条跨域业务流程的端到端验证
### 1.2 测试范围
**6大业务域**
| 业务域 | 模块 | 核心功能 | 已实现需求数 |
|--------|------|---------|-------------|
| 身份与权限域 | module-auth | 用户/角色/权限/部门/认证/审计 | 37 |
| 空间与项目域 | module-mdm | 项目/空间节点/巡检标准 | 23 |
| 设备与资产域 | module-asset | 设备台账/扩展表/健康评分/故障/归属 | 28 |
| 运营与服务域 | module-wo + module-mdm | 工单/维保/巡检/备件/能耗 | 36 |
| 财务与收费域 | module-finance(待建) | 收费项目/账单/支付/退款 | 0 |
| 前端交互域 | ether-admin | 登录/权限/项目切换/各模块页面 | 14 |
**5条跨域业务流程**
1. 业主报修全流程(认证 -> 空间定位 -> 工单创建 -> 派单 -> 执行 -> 验收)
2. 设备全生命周期管理(采购 -> 安装 -> 运行监控 -> 维保 -> 报废)
3. 预防性维保调度流程(计划 -> 周期调度 -> 任务生成 -> 执行 -> 验收 -> 设备联动)
4. 巡检异常处理流程(模板 -> 签到 -> 异常上报 -> 工单 -> 处理 -> 复检)
5. 能耗计费流程(计量点 -> 抄表 -> 费用计算 -> 账单 -> 缴费)
### 1.3 测试层级
| 测试层级 | 工具 | 目标 | 执行频率 |
|---------|------|------|---------|
| 单元测试 | JUnit 5 + Mockito | Service层业务逻辑、算法验证 | 每次提交 |
| API接口测试 | Shell curl脚本 | REST API正确性、状态码、响应格式 | 每日构建 |
| E2E端到端测试 | Playwright + TypeScript | 用户操作流程、跨域业务闭环 | 每日构建 |
### 1.4 TDD开发规范
**强制要求**所有新功能开发必须遵循TDD流程。
```
1. 红(Red) - 先编写失败的测试用例
2. 绿(Green) - 编写最小代码使测试通过
3. 重构(Refactor) - 优化代码结构,保持测试通过
```
**TDD准入规则**
- 新增功能必须先编写测试用例无测试用例的PR不予合并
- Bug修复必须先编写复现测试验证修复后测试通过
- 重构必须确保既有测试全部通过
---
## 二、测试环境
### 2.1 后端测试环境
| 项目 | 配置 |
|------|------|
| 框架 | Spring Boot 3.x Test |
| 单元测试 | JUnit 5 + Mockito |
| 集成测试 | Spring Boot Test + @WebMvcTest / @SpringBootTest |
| 数据库 | H2内存数据库(单元测试) / PostgreSQL Testcontainers(集成测试) |
| 后端地址 | http://localhost:8080 |
| 认证方式 | JWT Token (Authorization: Bearer {token}) |
### 2.2 前端测试环境
| 项目 | 配置 |
|------|------|
| 单元测试 | Vitest |
| E2E测试 | Playwright |
| 管理后台地址 | http://localhost:5175 |
| 浏览器 | Chromium (CI) / Chrome (本地) |
### 2.3 测试数据管理
**测试数据准备**
- 每个测试脚本开头通过API创建所需测试数据
- 使用固定的测试账号admin / admin123
- 测试项目使用固定编码前缀TEST-
- 测试数据命名规范:[TEST]-[模块]-[功能]-[序号]
**测试数据清理**
- 每个测试脚本结尾删除创建的测试数据
- 使用唯一标识避免数据冲突
- API测试使用独立项目隔离数据
---
## 三、API接口测试
### 3.1 测试框架选型
| 方案 | 选定 | 说明 |
|------|------|------|
| Shell curl脚本 | 是 | 轻量级无额外依赖适合CI环境 |
| Postman Collection | 否 | 需要GUI不适合自动化CI |
| Newman | 备选 | 可从Postman Collection导出运行 |
### 3.2 测试脚本组织
```
04-TESTING/api/
auth/ - 认证授权域API测试
test-auth.sh
space/ - 空间与项目域API测试
test-space.sh
asset/ - 设备与资产域API测试
test-asset.sh
ops/ - 运营与服务域API测试
test-ops.sh
finance/ - 财务与收费域API测试
test-finance.sh
run-all.sh - 执行全部API测试
run-module.sh - 执行指定模块API测试
```
### 3.3 测试脚本规范
每个API测试脚本必须包含
1. **前置条件** - 登录获取Token、创建依赖数据
2. **请求** - curl发送HTTP请求携带正确的Header
3. **断言** - 检查HTTP状态码、响应JSON字段
4. **清理** - 删除测试创建的数据
**脚本结构**
```bash
#!/bin/bash
BASE_URL="http://localhost:8080/api"
TOKEN=""
login() { ... }
test_XXX() {
local response=$(curl -s -w "\n%{http_code}" ...)
local body=$(echo "$response" | head -n -1)
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "test_XXX"
}
cleanup() { ... }
login
test_XXX
cleanup
print_summary
```
### 3.4 测试脚本目录结构
```
04-TESTING/api/
auth/test-auth.sh - 认证授权域API测试
space/test-space.sh - 空间与项目域API测试
asset/test-asset.sh - 设备与资产域API测试
ops/test-ops.sh - 运营与服务域API测试
finance/test-finance.sh - 财务与收费域API测试
run-all.sh - 执行全部API测试
run-module.sh - 执行指定模块API测试
```
---
## 四、E2E端到端测试
### 4.1 测试框架选型
| 项目 | 配置 |
|------|------|
| 框架 | Playwright |
| 语言 | TypeScript |
| 运行器 | Playwright Test |
| 浏览器 | Chromium |
| 设计模式 | Page Object Model |
### 4.2 测试场景设计
**5条跨域业务流程**
| 编号 | 流程名称 | 涉及域 | 关键步骤 |
|------|---------|--------|---------|
| E2E-CROSS-001 | 业主报修全流程 | auth+space+ops | 登录->选项目->定位设备->创建工单->派单->执行->完成->验收 |
| E2E-CROSS-002 | 设备全生命周期 | asset+ops | 创建设备->关联空间->录入扩展表->健康评分->创建维保->完成->报废 |
| E2E-CROSS-003 | 预防性维保调度 | ops+asset | 创建维保计划->创建维保任务->分配->执行->完成->验收->设备联动 |
| E2E-CROSS-004 | 巡检异常处理 | ops+asset | 创建巡检模板->执行巡检->上报异常->创建工单->处理->复检 |
| E2E-CROSS-005 | 能耗计费流程 | ops+finance | 创建计量点->抄表录入->费用计算->生成账单->缴费 |
**各域核心操作**
| 域 | 测试场景 |
|----|---------|
| auth | 登录/登出/Token刷新/密码修改/角色分配 |
| space | 项目创建/空间树构建/节点CRUD |
| asset | 设备创建/扩展表管理/健康评分查询 |
| ops | 工单全流程/维保任务/备件出入库 |
| finance | 收费项目/账单生成/支付登记 |
### 4.3 Page Object模式设计
```
pages/
LoginPage.ts - 登录页:用户名/密码输入、登录按钮、错误提示
DashboardPage.ts - 仪表盘:菜单导航、项目选择器、数据概览
WorkOrderPage.ts - 工单页:列表/创建/详情/派单/执行/完成/验收
EquipmentPage.ts - 设备页:列表/创建/详情/健康评分
SpacePage.ts - 空间页:空间树/节点管理
MaintenancePage.ts - 维保页:计划列表/任务列表
```
### 4.4 测试脚本目录结构
```
04-TESTING/e2e/
specs/
auth/
login.spec.ts - 登录流程E2E测试
space/
project.spec.ts - 项目管理E2E测试
asset/
equipment.spec.ts - 设备管理E2E测试
ops/
work-order.spec.ts - 工单管理E2E测试
finance/
billing.spec.ts - 财务管理E2E测试
cross-domain/
owner-repair.spec.ts - 业主报修全流程E2E测试
equipment-lifecycle.spec.ts - 设备全生命周期E2E测试
maintenance-schedule.spec.ts - 预防性维保调度E2E测试
fixtures/
test-data.json - 测试数据
pages/
LoginPage.ts - 登录页Page Object
DashboardPage.ts - 仪表盘Page Object
playwright.config.ts - Playwright配置
run-all.sh - 执行全部E2E测试
run-module.sh - 执行指定模块E2E测试
```
---
## 五、测试用例清单
### 5.1 身份与权限域
| 用例ID | 名称 | 前置条件 | 测试步骤 | 预期结果 | 优先级 |
|--------|------|---------|---------|---------|--------|
| TC-AUTH-001 | 用户登录-正常 | 用户已存在 | POST /api/auth/login {username, password} | 200, 返回token/userId/roles | P0 |
| TC-AUTH-002 | 用户登录-密码错误 | 用户已存在 | POST /api/auth/login {username, wrong_pwd} | 401, 返回错误信息 | P0 |
| TC-AUTH-003 | 用户登录-锁定 | 连续5次失败 | 连续5次错误密码登录 | 账户锁定10分钟 | P0 |
| TC-AUTH-004 | Token刷新 | 已登录 | POST /api/auth/refresh | 200, 返回新Token | P0 |
| TC-AUTH-005 | 获取当前用户 | 已登录 | GET /api/auth/me | 200, 返回用户信息 | P0 |
| TC-AUTH-006 | 用户登出 | 已登录 | POST /api/auth/logout | 200 | P0 |
| TC-AUTH-007 | 创建用户 | 管理员已登录 | POST /api/auth/users {username, password, userType} | 201, 返回用户ID | P0 |
| TC-AUTH-008 | 查询用户列表 | 管理员已登录 | GET /api/auth/users?page=0&size=10 | 200, 返回分页数据 | P0 |
| TC-AUTH-009 | 更新用户 | 用户已存在 | PUT /api/auth/users/{id} {realName, phone} | 200, 返回更新后数据 | P0 |
| TC-AUTH-010 | 删除用户 | 用户已存在 | DELETE /api/auth/users/{id} | 200 | P1 |
| TC-AUTH-011 | 修改密码 | 用户已登录 | PUT /api/auth/users/{id}/password | 200 | P0 |
| TC-AUTH-012 | 弱密码拒绝 | 管理员已登录 | POST创建用户 password=123456 | 400, 拒绝弱密码 | P0 |
| TC-AUTH-013 | 创建角色 | 管理员已登录 | POST /api/auth/roles {code, name, type} | 201 | P0 |
| TC-AUTH-014 | 角色权限分配 | 角色/权限已存在 | POST /api/auth/roles/{id}/permissions | 200 | P0 |
| TC-AUTH-015 | 创建权限 | 管理员已登录 | POST /api/auth/permissions {code, name, type} | 201 | P0 |
| TC-AUTH-016 | 部门树查询 | 部门已存在 | GET /api/auth/depts/tree | 200, 返回树形结构 | P1 |
| TC-AUTH-017 | 部门删除约束 | 部门有子部门 | DELETE /api/auth/depts/{id} | 400, 拒绝删除 | P0 |
| TC-AUTH-018 | 项目成员管理 | 项目/用户已存在 | 添加/查询/移除项目成员 | 各操作返回正确结果 | P0 |
| TC-AUTH-019 | 住户认证流程 | 住户已存在 | UNVERIFIED->PENDING->VERIFIED | 状态正确流转 | P1 |
| TC-AUTH-020 | 审计日志查询 | 操作已产生日志 | GET /api/auth/audit-logs | 200, 返回日志列表 | P1 |
### 5.2 空间与项目域
| 用例ID | 名称 | 前置条件 | 测试步骤 | 预期结果 | 优先级 |
|--------|------|---------|---------|---------|--------|
| TC-SPACE-001 | 创建项目 | 管理员已登录 | POST /api/mdm/projects {code, name, projectType} | 201, 返回项目ID | P0 |
| TC-SPACE-002 | 查询项目列表 | 项目已存在 | GET /api/mdm/projects | 200, 返回分页数据 | P0 |
| TC-SPACE-003 | 项目状态流转 | 项目状态PENDING | PUT /api/mdm/projects/{id}/status {status: ACTIVE} | 200 | P0 |
| TC-SPACE-004 | 项目删除检查 | 项目有应收费用 | GET /api/mdm/projects/{id}/delete-check | canDelete=false | P0 |
| TC-SPACE-005 | 项目配置管理 | 项目已存在 | GET/PUT /api/mdm/projects/{id}/config | 配置正确读写 | P1 |
| TC-SPACE-006 | 创建空间节点 | 项目已存在 | POST /api/mdm/space-nodes {name, nodeType} | 201 | P0 |
| TC-SPACE-007 | 获取空间树 | 节点已创建 | GET /api/mdm/space-nodes/project/{id}/tree | 200, 返回树形结构 | P0 |
| TC-SPACE-008 | 空间节点删除检查 | 节点有子节点 | GET /api/mdm/space-nodes/{id}/delete-check | 返回子节点数 | P0 |
| TC-SPACE-009 | 空间节点级联删除 | 节点有子节点 | DELETE /api/mdm/space-nodes/{id}/cascade | 200, 子节点全部删除 | P0 |
| TC-SPACE-010 | 巡检标准项CRUD | 管理员已登录 | 创建/查询/更新/删除巡检标准项 | 各操作返回正确结果 | P0 |
| TC-SPACE-011 | 巡检模板管理 | 标准项已存在 | 创建/查询/更新/复制巡检模板 | 各操作返回正确结果 | P1 |
### 5.3 设备与资产域
| 用例ID | 名称 | 前置条件 | 测试步骤 | 预期结果 | 优先级 |
|--------|------|---------|---------|---------|--------|
| TC-ASSET-001 | 创建设备 | 项目/空间已存在 | POST /api/asset/equipment {equipmentCode, name, type} | 201 | P0 |
| TC-ASSET-002 | 按项目查询设备 | 设备已存在 | GET /api/asset/equipment/by-project/{projectId} | 200, 返回设备列表 | P0 |
| TC-ASSET-003 | 按空间查询设备 | 设备已关联空间 | GET /api/asset/equipment/by-space/{spaceNodeId} | 200, 返回设备列表 | P0 |
| TC-ASSET-004 | 设备批量删除 | 多个设备已存在 | POST /api/asset/equipment/batch-delete {ids} | 200 | P1 |
| TC-ASSET-005 | 电梯扩展表管理 | 电梯设备已存在 | GET/PUT /api/asset/equipment/{id}/elevator | 扩展表正确读写 | P0 |
| TC-ASSET-006 | 暖通扩展表管理 | 暖通设备已存在 | GET/PUT /api/asset/equipment/{id}/hvac | 扩展表正确读写 | P0 |
| TC-ASSET-007 | 消防扩展表管理 | 消防设备已存在 | GET/PUT /api/asset/equipment/{id}/fire | 扩展表正确读写 | P0 |
| TC-ASSET-008 | 能源计量扩展表管理 | 能源设备已存在 | GET/PUT /api/asset/equipment/{id}/energy | 扩展表正确读写 | P0 |
| TC-ASSET-009 | 设备Excel导入 | 准备xlsx文件 | POST /api/asset/equipment/import | 返回successCount/failCount | P1 |
| TC-ASSET-010 | 设备Excel导出 | 设备已存在 | GET /api/asset/equipment/export?projectId={id} | 返回xlsx文件 | P1 |
| TC-ASSET-011 | 健康评分查询 | 设备已存在 | GET /api/asset/equipment/{id}/health | 200, 返回健康度/等级 | P1 |
| TC-ASSET-012 | 故障记录 | 设备已存在 | POST /api/asset/equipment/{id}/failures | 201, 自动计算时长 | P0 |
| TC-ASSET-013 | 归属主体CRUD | 管理员已登录 | 创建/查询/更新/删除归属主体 | 各操作返回正确结果 | P1 |
| TC-ASSET-014 | 设备统计 | 设备已存在 | GET /api/asset/equipment/statistics/by-type | 200, 返回统计数据 | P1 |
### 5.4 运营与服务域
| 用例ID | 名称 | 前置条件 | 测试步骤 | 预期结果 | 优先级 |
|--------|------|---------|---------|---------|--------|
| TC-OPS-001 | 创建工单 | 项目/设备已存在 | POST /api/wo/work-orders {title, source, type} | 201, workNo自动生成 | P0 |
| TC-OPS-002 | 工单派单 | 工单状态PENDING | POST /api/wo/work-orders/{id}/assign | 200, 状态变ASSIGNED | P0 |
| TC-OPS-003 | 工单开始执行 | 工单状态ASSIGNED | POST /api/wo/work-orders/{id}/start | 200, 状态变IN_PROGRESS | P0 |
| TC-OPS-004 | 工单完成 | 工单状态IN_PROGRESS | POST /api/wo/work-orders/{id}/complete | 200, 状态变COMPLETED | P0 |
| TC-OPS-005 | 工单验收 | 工单状态COMPLETED | POST /api/wo/work-orders/{id}/verify {rating} | 200, 状态变VERIFIED | P0 |
| TC-OPS-006 | 工单取消-待分配 | 工单状态PENDING | POST /api/wo/work-orders/{id}/cancel | 200, 状态变CANCELLED | P1 |
| TC-OPS-007 | 工单取消-已完成 | 工单状态COMPLETED | POST /api/wo/work-orders/{id}/cancel | 400, 拒绝取消 | P0 |
| TC-OPS-008 | 工单统计 | 工单已存在 | GET /api/wo/work-orders/statistics | 200, 返回统计数据 | P1 |
| TC-OPS-009 | 创建维保任务 | 设备已存在 | POST /api/wo/maintenance-tasks {equipmentId, type} | 201 | P0 |
| TC-OPS-010 | 维保任务状态流转 | 任务状态PENDING | assign->start->complete->verify | 状态正确流转 | P0 |
| TC-OPS-011 | 维保任务自动优先级 | 创建紧急维修任务 | POST创建type=EMERGENCY | priority自动为URGENT | P0 |
| TC-OPS-012 | 维保任务完成后设备联动 | 任务关联设备 | 完成维保任务 | 设备维保商/下次巡检日期更新 | P0 |
| TC-OPS-013 | 备件入库 | 备件已存在 | POST /api/ops/spare-parts/in-stock | 200, currentStock增加 | P0 |
| TC-OPS-014 | 备件出库 | 备件库存充足 | POST /api/ops/spare-parts/out-stock | 200, currentStock减少 | P0 |
| TC-OPS-015 | 备件出库-库存不足 | 备件库存不足 | POST出库数量>库存 | 400, 拒绝出库 | P0 |
| TC-OPS-016 | 低库存预警 | 备件currentStock<safeStock | GET /api/ops/spare-parts/low-stock | 返回低库存备件列表 | P1 |
| TC-OPS-017 | 能耗抄表录入 | 计量点已存在 | POST /api/ops/energy/consumption | 201, 自动计算消耗量和费用 | P0 |
| TC-OPS-018 | 能耗抄表-读数递减 | 上次读数100 | POST currentReading=50 | 400, 拒绝递减读数 | P0 |
| TC-OPS-019 | 维保计划管理 | 管理员已登录 | 创建/查询/更新维保计划 | 各操作返回正确结果 | P1 |
| TC-OPS-020 | 巡检模板管理 | 管理员已登录 | 创建/查询/更新/复制巡检模板 | 各操作返回正确结果 | P1 |
### 5.5 财务与收费域
| 用例ID | 名称 | 前置条件 | 测试步骤 | 预期结果 | 优先级 |
|--------|------|---------|---------|---------|--------|
| TC-FIN-001 | 创建收费项目 | 模块已实现 | POST /api/finance/fee-items | 201 | P0 |
| TC-FIN-002 | 查询收费项目 | 收费项目已存在 | GET /api/finance/fee-items | 200 | P0 |
| TC-FIN-003 | 生成账单 | 收费项目/业主已存在 | POST /api/finance/fee-bills/generate | 201 | P0 |
| TC-FIN-004 | 账单状态流转 | 账单已存在 | UNPAID->PARTIAL_PAID->PAID | 状态正确流转 | P0 |
| TC-FIN-005 | 线下收款登记 | 账单已存在 | POST /api/finance/fee-payments | 201 | P0 |
| TC-FIN-006 | 退款申请 | 支付已完成 | POST /api/finance/fee-refunds | 201 | P1 |
| TC-FIN-007 | 滞纳金计算 | 账单逾期 | 自动计算滞纳金 | 金额正确 | P1 |
### 5.6 前端交互域
| 用例ID | 名称 | 前置条件 | 测试步骤 | 预期结果 | 优先级 |
|--------|------|---------|---------|---------|--------|
| TC-FE-001 | 登录页面-正常登录 | 系统可访问 | 输入用户名密码->点击登录 | 跳转至仪表盘 | P0 |
| TC-FE-002 | 登录页面-错误密码 | 系统可访问 | 输入错误密码->点击登录 | 显示错误提示 | P0 |
| TC-FE-003 | 项目选择 | 已登录 | 点击项目选择器->选择项目 | 页面数据刷新 | P0 |
| TC-FE-004 | 权限控制 | 不同角色用户 | 访问无权限页面/按钮 | 隐藏或禁用 | P0 |
| TC-FE-005 | 用户管理页面 | 管理员已登录 | 列表/创建/编辑/删除用户 | 各操作正常 | P0 |
| TC-FE-006 | 设备管理页面 | 管理员已登录 | 列表/创建/详情/导入 | 各操作正常 | P0 |
| TC-FE-007 | 工单管理页面 | 管理员已登录 | 列表/创建/派单/执行/验收 | 各操作正常 | P0 |
### 5.7 跨域业务流程
| 用例ID | 名称 | 前置条件 | 测试步骤 | 预期结果 | 优先级 |
|--------|------|---------|---------|---------|--------|
| TC-CROSS-001 | 业主报修全流程 | 业主/设备/维保人员已存在 | 登录->选项目->创建工单->派单->执行->完成->验收 | 工单状态PENDING->VERIFIED | P0 |
| TC-CROSS-002 | 设备全生命周期 | 项目/空间已存在 | 创建设备->关联空间->录入扩展表->健康评分->维保->报废 | 设备状态ACTIVE->SCRAPPED | P0 |
| TC-CROSS-003 | 预防性维保调度 | 设备/维保计划已存在 | 创建计划->创建任务->分配->执行->完成->验收 | 任务完成+设备信息更新 | P0 |
| TC-CROSS-004 | 巡检异常处理 | 巡检模板/设备已存在 | 创建模板->执行巡检->上报异常->创建工单->处理 | 异常问题解决 | P0 |
| TC-CROSS-005 | 能耗计费流程 | 计量点/收费项目已存在 | 创建计量点->抄表->费用计算->生成账单->缴费 | 账单状态PAID | P0 |
---
## 六、测试执行与报告
### 6.1 单模块测试执行
```bash
# API测试
./04-TESTING/api/run-module.sh auth
./04-TESTING/api/run-module.sh space
./04-TESTING/api/run-module.sh asset
./04-TESTING/api/run-module.sh ops
./04-TESTING/api/run-module.sh finance
# E2E测试
./04-TESTING/e2e/run-module.sh auth
./04-TESTING/e2e/run-module.sh space
./04-TESTING/e2e/run-module.sh asset
./04-TESTING/e2e/run-module.sh ops
./04-TESTING/e2e/run-module.sh finance
./04-TESTING/e2e/run-module.sh cross-domain
```
### 6.2 全量测试执行
```bash
# API全量测试
./04-TESTING/api/run-all.sh
# E2E全量测试
./04-TESTING/e2e/run-all.sh
```
### 6.3 串联测试执行
跨域流程测试需要按顺序执行,确保数据依赖正确:
```bash
# 业主报修全流程
./04-TESTING/e2e/run-module.sh cross-domain
```
### 6.4 测试报告生成
**API测试报告**
- 脚本执行后自动输出测试摘要PASS/FAIL/总计/通过率)
- 详细日志输出到 04-TESTING/reports/api-{module}-{date}.log
**E2E测试报告**
- Playwright HTML报告04-TESTING/reports/e2e-report/
- 执行命令:`npx playwright test --reporter=html`
- 报告内容:通过率、失败截图、执行耗时
**报告指标**
- 总用例数 / 通过数 / 失败数 / 跳过数
- 通过率 = 通过数 / 总用例数 * 100%
- 失败分析:按失败原因分类(断言失败/超时/环境问题)
### 6.5 测试报告输出目录
```
04-TESTING/reports/
api-auth-YYYYMMDD.log
api-space-YYYYMMDD.log
api-asset-YYYYMMDD.log
api-ops-YYYYMMDD.log
api-finance-YYYYMMDD.log
e2e-report/
index.html
trace/
screenshots/
```
---
## 七、测试准入准出标准
### 7.1 准入标准
代码提交测试必须满足以下条件:
| 准入项 | 要求 |
|--------|------|
| 代码审查 | PR必须经过至少1人审查通过 |
| 单元测试 | 新增/修改代码必须有对应单元测试 |
| 编译通过 | mvn clean install -DskipTests 无错误 |
| 类型检查 | npx vue-tsc --noEmit 无错误 |
| API文档 | 新增/修改接口必须更新API文档 |
### 7.2 准出标准
版本发布必须满足以下条件:
| 准出项 | 要求 |
|--------|------|
| API测试通过率 | >= 95% |
| E2E测试通过率 | >= 90% |
| P0缺陷 | 0个 |
| P1缺陷 | 0个 |
| P2缺陷 | 不超过5个需有修复计划 |
| 单元测试覆盖率 | 核心Service层 >= 80% |
| 性能测试 | 关键接口响应时间 < 500ms |
**缺陷优先级定义**
| 级别 | 定义 | 修复时限 |
|------|------|---------|
| P0 | 系统崩溃/数据丢失/安全漏洞 | 4小时内 |
| P1 | 核心功能不可用/业务流程阻断 | 24小时内 |
| P2 | 功能异常但有规避方案 | 3个工作日内 |
| P3 | UI显示问题/体验优化 | 下个迭代 |

View File

@ -0,0 +1,321 @@
#!/bin/bash
BASE_URL="http://localhost:8080/api"
TOKEN=""
PROJECT_ID=""
PASS_COUNT=0
FAIL_COUNT=0
TOTAL_COUNT=0
assert_eq() {
local actual="$1"
local expected="$2"
local name="$3"
TOTAL_COUNT=$((TOTAL_COUNT + 1))
if [ "$actual" = "$expected" ]; then
echo " PASS: $name"
PASS_COUNT=$((PASS_COUNT + 1))
else
echo " FAIL: $name (expected=$expected, actual=$actual)"
FAIL_COUNT=$((FAIL_COUNT + 1))
fi
}
assert_contains() {
local haystack="$1"
local needle="$2"
local name="$3"
TOTAL_COUNT=$((TOTAL_COUNT + 1))
if echo "$haystack" | grep -q "$needle"; then
echo " PASS: $name"
PASS_COUNT=$((PASS_COUNT + 1))
else
echo " FAIL: $name (expected to contain: $needle)"
FAIL_COUNT=$((FAIL_COUNT + 1))
fi
}
login() {
echo "=== 登录获取Token ==="
local response=$(curl -s -X POST "$BASE_URL/auth/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}')
TOKEN=$(echo "$response" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
if [ -z "$TOKEN" ]; then
echo "FATAL: 登录失败无法获取Token"
exit 1
fi
echo "Token获取成功"
}
setup_project() {
echo "=== 创建测试项目 ==="
local response=$(curl -s -X POST "$BASE_URL/mdm/projects" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: " \
-d "{\"code\":\"TEST-ASSET-$$\",\"name\":\"ASSET测试项目\",\"projectType\":\"RESIDENTIAL\"}")
PROJECT_ID=$(echo "$response" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -z "$PROJECT_ID" ]; then
echo "FATAL: 创建测试项目失败"
exit 1
fi
echo "测试项目创建成功: $PROJECT_ID"
}
test_create_equipment() {
echo "--- TC-ASSET-001: 创建设备 ---"
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/asset/equipment" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d "{\"equipmentCode\":\"TEST-EQ-$$_$RANDOM\",\"equipmentName\":\"测试电梯设备\",\"equipmentType\":\"ELEVATOR\",\"systemType\":\"ELEVATOR\",\"ownershipType\":\"PROJECT\"}")
local status=$(echo "$response" | tail -n 1)
local body=$(echo "$response" | head -n -1)
assert_eq "$status" "201" "TC-ASSET-001: 创建设备返回201"
EQUIPMENT_ID=$(echo "$body" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
}
test_query_equipment_by_project() {
echo "--- TC-ASSET-002: 按项目查询设备 ---"
local response=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/asset/equipment/by-project/$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-ASSET-002: 按项目查询设备返回200"
}
test_query_equipment_by_space() {
echo "--- TC-ASSET-003: 按空间查询设备 ---"
local response=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/asset/equipment/by-space/00000000-0000-0000-0000-000000000000" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-ASSET-003: 按空间查询设备返回200"
}
test_batch_delete_equipment() {
echo "--- TC-ASSET-004: 设备批量删除 ---"
local eq1_response=$(curl -s -X POST "$BASE_URL/asset/equipment" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d "{\"equipmentCode\":\"BATCH-DEL-1-$$_$RANDOM\",\"equipmentName\":\"批量删除测试1\",\"equipmentType\":\"HVAC\",\"systemType\":\"HVAC\",\"ownershipType\":\"PROJECT\"}")
local eq1_id=$(echo "$eq1_response" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
local eq2_response=$(curl -s -X POST "$BASE_URL/asset/equipment" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d "{\"equipmentCode\":\"BATCH-DEL-2-$$_$RANDOM\",\"equipmentName\":\"批量删除测试2\",\"equipmentType\":\"HVAC\",\"systemType\":\"HVAC\",\"ownershipType\":\"PROJECT\"}")
local eq2_id=$(echo "$eq2_response" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -n "$eq1_id" ] && [ -n "$eq2_id" ]; then
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/asset/equipment/batch-delete" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d "{\"ids\":[\"$eq1_id\",\"$eq2_id\"]}")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-ASSET-004: 批量删除返回200"
else
echo " SKIP: TC-ASSET-004 (无法创建测试设备)"
fi
}
test_elevator_extension() {
echo "--- TC-ASSET-005: 电梯扩展表管理 ---"
if [ -z "$EQUIPMENT_ID" ]; then
echo " SKIP: TC-ASSET-005 (无设备ID)"
return
fi
local put_response=$(curl -s -w "\n%{http_code}" -X PUT "$BASE_URL/asset/equipment/$EQUIPMENT_ID/elevator" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d '{"elevatorType":"PASSENGER","model":"TEST-001","loadCapacity":1000,"speed":2.5,"floorCount":20}')
local put_status=$(echo "$put_response" | tail -n 1)
assert_eq "$put_status" "200" "TC-ASSET-005: 更新电梯扩展表返回200"
local get_response=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/asset/equipment/$EQUIPMENT_ID/elevator" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID")
local get_status=$(echo "$get_response" | tail -n 1)
assert_eq "$get_status" "200" "TC-ASSET-005: 查询电梯扩展表返回200"
}
test_hvac_extension() {
echo "--- TC-ASSET-006: 暖通扩展表管理 ---"
local hvac_response=$(curl -s -X POST "$BASE_URL/asset/equipment" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d "{\"equipmentCode\":\"HVAC-TEST-$$_$RANDOM\",\"equipmentName\":\"测试暖通设备\",\"equipmentType\":\"HVAC\",\"systemType\":\"HVAC\",\"ownershipType\":\"PROJECT\"}")
local hvac_id=$(echo "$hvac_response" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -z "$hvac_id" ]; then
echo " SKIP: TC-ASSET-006 (无法创建暖通设备)"
return
fi
local put_response=$(curl -s -w "\n%{http_code}" -X PUT "$BASE_URL/asset/equipment/$hvac_id/hvac" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d '{"hvacType":"CHILLER","coolingCapacity":500,"heatingCapacity":400}')
local put_status=$(echo "$put_response" | tail -n 1)
assert_eq "$put_status" "200" "TC-ASSET-006: 更新暖通扩展表返回200"
HVAC_EQ_ID="$hvac_id"
}
test_fire_extension() {
echo "--- TC-ASSET-007: 消防扩展表管理 ---"
local fire_response=$(curl -s -X POST "$BASE_URL/asset/equipment" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d "{\"equipmentCode\":\"FIRE-TEST-$$_$RANDOM\",\"equipmentName\":\"测试消防设备\",\"equipmentType\":\"FIRE_PROTECTION\",\"systemType\":\"FIRE\",\"ownershipType\":\"PROJECT\"}")
local fire_id=$(echo "$fire_response" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -z "$fire_id" ]; then
echo " SKIP: TC-ASSET-007 (无法创建消防设备)"
return
fi
local put_response=$(curl -s -w "\n%{http_code}" -X PUT "$BASE_URL/asset/equipment/$fire_id/fire" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d '{"fireEquipmentType":"SPRINKLER","installationArea":200}')
local put_status=$(echo "$put_response" | tail -n 1)
assert_eq "$put_status" "200" "TC-ASSET-007: 更新消防扩展表返回200"
FIRE_EQ_ID="$fire_id"
}
test_energy_extension() {
echo "--- TC-ASSET-008: 能源计量扩展表管理 ---"
local energy_response=$(curl -s -X POST "$BASE_URL/asset/equipment" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d "{\"equipmentCode\":\"ENERGY-TEST-$$_$RANDOM\",\"equipmentName\":\"测试能源计量设备\",\"equipmentType\":\"ENERGY_METER\",\"systemType\":\"ELECTRICAL\",\"ownershipType\":\"PROJECT\"}")
local energy_id=$(echo "$energy_response" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -z "$energy_id" ]; then
echo " SKIP: TC-ASSET-008 (无法创建能源计量设备)"
return
fi
local put_response=$(curl -s -w "\n%{http_code}" -X PUT "$BASE_URL/asset/equipment/$energy_id/energy" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d '{"meterType":"ELECTRICITY","energyType":"LIGHTING","accuracyClass":"1.0"}')
local put_status=$(echo "$put_response" | tail -n 1)
assert_eq "$put_status" "200" "TC-ASSET-008: 更新能源计量扩展表返回200"
ENERGY_EQ_ID="$energy_id"
}
test_equipment_statistics() {
echo "--- TC-ASSET-009: 设备统计 ---"
local response=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/asset/equipment/statistics/by-type?projectId=$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-ASSET-009: 设备统计返回200"
}
test_health_score() {
echo "--- TC-ASSET-010: 健康评分查询 ---"
if [ -z "$EQUIPMENT_ID" ]; then
echo " SKIP: TC-ASSET-010 (无设备ID)"
return
fi
local response=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/asset/equipment/$EQUIPMENT_ID/health" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-ASSET-010: 健康评分查询返回200"
}
test_failure_record() {
echo "--- TC-ASSET-011: 故障记录 ---"
if [ -z "$EQUIPMENT_ID" ]; then
echo " SKIP: TC-ASSET-011 (无设备ID)"
return
fi
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/asset/equipment/$EQUIPMENT_ID/failures" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d '{"failureType":"SUDDEN","failureLevel":"MEDIUM","description":"测试故障记录","failureTime":"2026-05-18T10:00:00"}')
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "201" "TC-ASSET-011: 故障记录返回201"
}
test_ownership_entity_crud() {
echo "--- TC-ASSET-012: 归属主体CRUD ---"
local create_response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/asset/ownership-entities" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d "{\"entityName\":\"测试归属主体-$$_$RANDOM\",\"entityType\":\"COMPANY\",\"contactPerson\":\"张三\",\"contactPhone\":\"13800138000\"}")
local create_status=$(echo "$create_response" | tail -n 1)
local create_body=$(echo "$create_response" | head -n -1)
assert_eq "$create_status" "201" "TC-ASSET-012: 创建归属主体返回201"
OWNERSHIP_ID=$(echo "$create_body" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -n "$OWNERSHIP_ID" ]; then
local query_response=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/asset/ownership-entities/$OWNERSHIP_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID")
local query_status=$(echo "$query_response" | tail -n 1)
assert_eq "$query_status" "200" "TC-ASSET-012: 查询归属主体返回200"
fi
}
cleanup() {
echo "=== 清理测试数据 ==="
if [ -n "$PROJECT_ID" ]; then
curl -s -X DELETE "$BASE_URL/mdm/projects/$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" > /dev/null 2>&1
fi
}
print_summary() {
echo ""
echo "=========================================="
echo " Asset Domain API Test Summary"
echo "=========================================="
echo " Total: $TOTAL_COUNT"
echo " Passed: $PASS_COUNT"
echo " Failed: $FAIL_COUNT"
if [ "$TOTAL_COUNT" -gt 0 ]; then
local rate=$(echo "scale=1; $PASS_COUNT * 100 / $TOTAL_COUNT" | bc)
echo " Rate: ${rate}%"
fi
echo "=========================================="
if [ "$FAIL_COUNT" -gt 0 ]; then
return 1
fi
return 0
}
EQUIPMENT_ID=""
HVAC_EQ_ID=""
FIRE_EQ_ID=""
ENERGY_EQ_ID=""
OWNERSHIP_ID=""
echo "=========================================="
echo " Ether PMS - Asset Domain API Tests"
echo "=========================================="
echo ""
login
setup_project
test_create_equipment
test_query_equipment_by_project
test_query_equipment_by_space
test_batch_delete_equipment
test_elevator_extension
test_hvac_extension
test_fire_extension
test_energy_extension
test_equipment_statistics
test_health_score
test_failure_record
test_ownership_entity_crud
cleanup
print_summary

297
04-TESTING/api/auth/test-auth.sh Executable file
View File

@ -0,0 +1,297 @@
#!/bin/bash
BASE_URL="http://localhost:8080/api"
TOKEN=""
PASS_COUNT=0
FAIL_COUNT=0
TOTAL_COUNT=0
assert_eq() {
local actual="$1"
local expected="$2"
local name="$3"
TOTAL_COUNT=$((TOTAL_COUNT + 1))
if [ "$actual" = "$expected" ]; then
echo " PASS: $name"
PASS_COUNT=$((PASS_COUNT + 1))
else
echo " FAIL: $name (expected=$expected, actual=$actual)"
FAIL_COUNT=$((FAIL_COUNT + 1))
fi
}
assert_contains() {
local haystack="$1"
local needle="$2"
local name="$3"
TOTAL_COUNT=$((TOTAL_COUNT + 1))
if echo "$haystack" | grep -q "$needle"; then
echo " PASS: $name"
PASS_COUNT=$((PASS_COUNT + 1))
else
echo " FAIL: $name (expected to contain: $needle)"
FAIL_COUNT=$((FAIL_COUNT + 1))
fi
}
assert_not_contains() {
local haystack="$1"
local needle="$2"
local name="$3"
TOTAL_COUNT=$((TOTAL_COUNT + 1))
if echo "$haystack" | grep -q "$needle"; then
echo " FAIL: $name (expected NOT to contain: $needle)"
FAIL_COUNT=$((FAIL_COUNT + 1))
else
echo " PASS: $name"
PASS_COUNT=$((PASS_COUNT + 1))
fi
}
login() {
echo "=== 登录获取Token ==="
local response=$(curl -s -X POST "$BASE_URL/auth/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}')
TOKEN=$(echo "$response" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
if [ -z "$TOKEN" ]; then
echo "FATAL: 登录失败无法获取Token"
echo "Response: $response"
exit 1
fi
echo "Token获取成功"
}
test_login_success() {
echo "--- TC-AUTH-001: 用户登录-正常 ---"
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/auth/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}')
local status=$(echo "$response" | tail -n 1)
local body=$(echo "$response" | head -n -1)
assert_eq "$status" "200" "TC-AUTH-001: 登录返回200"
assert_contains "$body" '"token"' "TC-AUTH-001: 响应包含token"
assert_contains "$body" '"userId"' "TC-AUTH-001: 响应包含userId"
assert_contains "$body" '"roles"' "TC-AUTH-001: 响应包含roles"
}
test_login_wrong_password() {
echo "--- TC-AUTH-002: 用户登录-密码错误 ---"
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/auth/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"wrongpassword"}')
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "401" "TC-AUTH-002: 错误密码返回401"
}
test_login_lockout() {
echo "--- TC-AUTH-003: 用户登录-锁定机制 ---"
local test_user="lockout_test_$$"
curl -s -X POST "$BASE_URL/auth/users" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d "{\"username\":\"$test_user\",\"password\":\"Test@1234\",\"userType\":\"ENTERPRISE\"}" > /dev/null 2>&1
for i in $(seq 1 5); do
curl -s -X POST "$BASE_URL/auth/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$test_user\",\"password\":\"wrong$i\"}" > /dev/null 2>&1
done
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/auth/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$test_user\",\"password\":\"Test@1234\"}")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "401" "TC-AUTH-003: 5次失败后账户锁定"
}
test_get_current_user() {
echo "--- TC-AUTH-005: 获取当前用户 ---"
local response=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/auth/me" \
-H "Authorization: Bearer $TOKEN")
local status=$(echo "$response" | tail -n 1)
local body=$(echo "$response" | head -n -1)
assert_eq "$status" "200" "TC-AUTH-005: 获取当前用户返回200"
assert_contains "$body" '"username"' "TC-AUTH-005: 响应包含username"
}
test_refresh_token() {
echo "--- TC-AUTH-004: Token刷新 ---"
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/auth/refresh" \
-H "Authorization: Bearer $TOKEN")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-AUTH-004: Token刷新返回200"
}
test_logout() {
echo "--- TC-AUTH-006: 用户登出 ---"
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/auth/logout" \
-H "Authorization: Bearer $TOKEN")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-AUTH-006: 登出返回200"
}
test_create_user() {
echo "--- TC-AUTH-007: 创建用户 ---"
local username="testuser_$$"
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/auth/users" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d "{\"username\":\"$username\",\"password\":\"Test@1234\",\"userType\":\"ENTERPRISE\",\"realName\":\"Test User\"}")
local status=$(echo "$response" | tail -n 1)
local body=$(echo "$response" | head -n -1)
assert_eq "$status" "201" "TC-AUTH-007: 创建用户返回201"
CREATED_USER_ID=$(echo "$body" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
}
test_list_users() {
echo "--- TC-AUTH-008: 查询用户列表 ---"
local response=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/auth/users?page=0&size=10" \
-H "Authorization: Bearer $TOKEN")
local status=$(echo "$response" | tail -n 1)
local body=$(echo "$response" | head -n -1)
assert_eq "$status" "200" "TC-AUTH-008: 查询用户列表返回200"
assert_contains "$body" '"content"' "TC-AUTH-008: 响应包含content"
}
test_update_user() {
echo "--- TC-AUTH-009: 更新用户 ---"
if [ -z "$CREATED_USER_ID" ]; then
echo " SKIP: TC-AUTH-009 (无用户ID)"
return
fi
local response=$(curl -s -w "\n%{http_code}" -X PUT "$BASE_URL/auth/users/$CREATED_USER_ID" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"realName":"Updated Name","phone":"13800138000"}')
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-AUTH-009: 更新用户返回200"
}
test_delete_user() {
echo "--- TC-AUTH-010: 删除用户 ---"
if [ -z "$CREATED_USER_ID" ]; then
echo " SKIP: TC-AUTH-010 (无用户ID)"
return
fi
local response=$(curl -s -w "\n%{http_code}" -X DELETE "$BASE_URL/auth/users/$CREATED_USER_ID" \
-H "Authorization: Bearer $TOKEN")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-AUTH-010: 删除用户返回200"
}
test_weak_password_rejected() {
echo "--- TC-AUTH-012: 弱密码拒绝 ---"
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/auth/users" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"username":"weakpwd_$$_test","password":"123456","userType":"ENTERPRISE"}')
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "400" "TC-AUTH-012: 弱密码返回400"
}
test_create_role() {
echo "--- TC-AUTH-013: 创建角色 ---"
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/auth/roles" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d "{\"code\":\"TEST_ROLE_$$\",\"name\":\"Test Role\",\"type\":\"SYSTEM\"}")
local status=$(echo "$response" | tail -n 1)
local body=$(echo "$response" | head -n -1)
assert_eq "$status" "201" "TC-AUTH-013: 创建角色返回201"
CREATED_ROLE_ID=$(echo "$body" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
}
test_assign_role_permissions() {
echo "--- TC-AUTH-014: 角色权限分配 ---"
if [ -z "$CREATED_ROLE_ID" ]; then
echo " SKIP: TC-AUTH-014 (无角色ID)"
return
fi
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/auth/roles/$CREATED_ROLE_ID/permissions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"permissionIds":[]}')
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-AUTH-014: 角色权限分配返回200"
}
test_create_permission() {
echo "--- TC-AUTH-015: 创建权限 ---"
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/auth/permissions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d "{\"code\":\"test:resource:action_$$\",\"name\":\"Test Permission\",\"type\":\"API\"}")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "201" "TC-AUTH-015: 创建权限返回201"
}
test_dept_tree() {
echo "--- TC-AUTH-016: 部门树查询 ---"
local response=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/auth/depts/tree" \
-H "Authorization: Bearer $TOKEN")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-AUTH-016: 部门树查询返回200"
}
test_audit_log_query() {
echo "--- TC-AUTH-020: 审计日志查询 ---"
local response=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/auth/audit-logs?page=0&size=10" \
-H "Authorization: Bearer $TOKEN")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-AUTH-020: 审计日志查询返回200"
}
cleanup() {
echo "=== 清理测试数据 ==="
if [ -n "$CREATED_ROLE_ID" ]; then
curl -s -X DELETE "$BASE_URL/auth/roles/$CREATED_ROLE_ID" \
-H "Authorization: Bearer $TOKEN" > /dev/null 2>&1
fi
}
print_summary() {
echo ""
echo "=========================================="
echo " Auth Domain API Test Summary"
echo "=========================================="
echo " Total: $TOTAL_COUNT"
echo " Passed: $PASS_COUNT"
echo " Failed: $FAIL_COUNT"
if [ "$TOTAL_COUNT" -gt 0 ]; then
local rate=$(echo "scale=1; $PASS_COUNT * 100 / $TOTAL_COUNT" | bc)
echo " Rate: ${rate}%"
fi
echo "=========================================="
if [ "$FAIL_COUNT" -gt 0 ]; then
return 1
fi
return 0
}
CREATED_USER_ID=""
CREATED_ROLE_ID=""
echo "=========================================="
echo " Ether PMS - Auth Domain API Tests"
echo "=========================================="
echo ""
login
test_login_success
test_login_wrong_password
test_login_lockout
test_get_current_user
test_refresh_token
test_create_user
test_list_users
test_update_user
test_weak_password_rejected
test_create_role
test_assign_role_permissions
test_create_permission
test_dept_tree
test_delete_user
test_audit_log_query
test_logout
cleanup
print_summary

357
04-TESTING/api/ops/test-ops.sh Executable file
View File

@ -0,0 +1,357 @@
#!/bin/bash
BASE_URL="http://localhost:8080/api"
TOKEN=""
PROJECT_ID=""
PASS_COUNT=0
FAIL_COUNT=0
TOTAL_COUNT=0
assert_eq() {
local actual="$1"
local expected="$2"
local name="$3"
TOTAL_COUNT=$((TOTAL_COUNT + 1))
if [ "$actual" = "$expected" ]; then
echo " PASS: $name"
PASS_COUNT=$((PASS_COUNT + 1))
else
echo " FAIL: $name (expected=$expected, actual=$actual)"
FAIL_COUNT=$((FAIL_COUNT + 1))
fi
}
assert_contains() {
local haystack="$1"
local needle="$2"
local name="$3"
TOTAL_COUNT=$((TOTAL_COUNT + 1))
if echo "$haystack" | grep -q "$needle"; then
echo " PASS: $name"
PASS_COUNT=$((PASS_COUNT + 1))
else
echo " FAIL: $name (expected to contain: $needle)"
FAIL_COUNT=$((FAIL_COUNT + 1))
fi
}
login() {
echo "=== 登录获取Token ==="
local response=$(curl -s -X POST "$BASE_URL/auth/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}')
TOKEN=$(echo "$response" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
if [ -z "$TOKEN" ]; then
echo "FATAL: 登录失败无法获取Token"
exit 1
fi
echo "Token获取成功"
}
setup_project() {
echo "=== 创建测试项目 ==="
local response=$(curl -s -X POST "$BASE_URL/mdm/projects" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: " \
-d "{\"code\":\"TEST-OPS-$$\",\"name\":\"OPS测试项目\",\"projectType\":\"RESIDENTIAL\"}")
PROJECT_ID=$(echo "$response" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -z "$PROJECT_ID" ]; then
echo "FATAL: 创建测试项目失败"
exit 1
fi
echo "测试项目创建成功: $PROJECT_ID"
}
test_create_work_order() {
echo "--- TC-OPS-001: 创建工单 ---"
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/wo/work-orders" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d "{\"title\":\"测试工单-$$\",\"source\":\"MANUAL\",\"type\":\"REPAIR\",\"priority\":\"MEDIUM\",\"description\":\"API测试工单\"}")
local status=$(echo "$response" | tail -n 1)
local body=$(echo "$response" | head -n -1)
assert_eq "$status" "201" "TC-OPS-001: 创建工单返回201"
assert_contains "$body" '"workNo"' "TC-OPS-001: 响应包含workNo"
WORK_ORDER_ID=$(echo "$body" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
}
test_query_work_orders() {
echo "--- TC-OPS-002: 查询工单列表 ---"
local response=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/wo/work-orders?projectId=$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-OPS-002: 查询工单列表返回200"
}
test_assign_work_order() {
echo "--- TC-OPS-003: 工单派单 ---"
if [ -z "$WORK_ORDER_ID" ]; then
echo " SKIP: TC-OPS-003 (无工单ID)"
return
fi
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/wo/work-orders/$WORK_ORDER_ID/assign" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d '{"assignedTo":"test_maintainer","assignedDate":"2026-05-18"}')
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-OPS-003: 工单派单返回200"
}
test_start_work_order() {
echo "--- TC-OPS-004: 工单开始执行 ---"
if [ -z "$WORK_ORDER_ID" ]; then
echo " SKIP: TC-OPS-004 (无工单ID)"
return
fi
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/wo/work-orders/$WORK_ORDER_ID/start" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-OPS-004: 工单开始执行返回200"
}
test_complete_work_order() {
echo "--- TC-OPS-005: 工单完成 ---"
if [ -z "$WORK_ORDER_ID" ]; then
echo " SKIP: TC-OPS-005 (无工单ID)"
return
fi
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/wo/work-orders/$WORK_ORDER_ID/complete" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d '{"faultCause":"测试故障原因","solution":"测试解决方案","result":"已修复","laborCost":100,"partsCost":50}')
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-OPS-005: 工单完成返回200"
}
test_verify_work_order() {
echo "--- TC-OPS-006: 工单验收 ---"
if [ -z "$WORK_ORDER_ID" ]; then
echo " SKIP: TC-OPS-006 (无工单ID)"
return
fi
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/wo/work-orders/$WORK_ORDER_ID/verify" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d '{"verifiedBy":"admin","rating":5,"remark":"验收通过"}')
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-OPS-006: 工单验收返回200"
}
test_cancel_completed_work_order() {
echo "--- TC-OPS-007: 工单取消-已完成拒绝 ---"
if [ -z "$WORK_ORDER_ID" ]; then
echo " SKIP: TC-OPS-007 (无工单ID)"
return
fi
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/wo/work-orders/$WORK_ORDER_ID/cancel" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "400" "TC-OPS-007: 已完成工单取消返回400"
}
test_work_order_statistics() {
echo "--- TC-OPS-008: 工单统计 ---"
local response=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/wo/work-orders/statistics?projectId=$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-OPS-008: 工单统计返回200"
}
test_create_maintenance_task() {
echo "--- TC-OPS-009: 创建维保任务 ---"
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/wo/maintenance-tasks" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d "{\"equipmentId\":\"00000000-0000-0000-0000-000000000000\",\"taskType\":\"PREVENTIVE\",\"triggerType\":\"MANUAL\",\"title\":\"测试维保任务-$$\"}")
local status=$(echo "$response" | tail -n 1)
local body=$(echo "$response" | head -n -1)
if [ "$status" = "201" ] || [ "$status" = "200" ]; then
assert_eq "$status" "$status" "TC-OPS-009: 创建维保任务成功"
MAINT_TASK_ID=$(echo "$body" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
else
assert_contains "$body" "equipment" "TC-OPS-009: 创建维保任务(设备不存在时返回错误)"
fi
}
test_spare_part_crud() {
echo "--- TC-OPS-010: 备件CRUD ---"
local create_response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/ops/spare-parts" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d "{\"partCode\":\"TEST-SP-$$_$RANDOM\",\"partName\":\"测试备件\",\"unit\":\"个\",\"safeStock\":10,\"currentStock\":100,\"unitPrice\":25.00}")
local create_status=$(echo "$create_response" | tail -n 1)
local create_body=$(echo "$create_response" | head -n -1)
assert_eq "$create_status" "201" "TC-OPS-010: 创建备件返回201"
SPARE_PART_ID=$(echo "$create_body" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
}
test_spare_part_in_stock() {
echo "--- TC-OPS-011: 备件入库 ---"
if [ -z "$SPARE_PART_ID" ]; then
echo " SKIP: TC-OPS-011 (无备件ID)"
return
fi
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/ops/spare-parts/in-stock" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d "{\"sparePartId\":\"$SPARE_PART_ID\",\"quantity\":50,\"operator\":\"admin\",\"remark\":\"API测试入库\"}")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-OPS-011: 备件入库返回200"
}
test_spare_part_out_stock() {
echo "--- TC-OPS-012: 备件出库 ---"
if [ -z "$SPARE_PART_ID" ]; then
echo " SKIP: TC-OPS-012 (无备件ID)"
return
fi
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/ops/spare-parts/out-stock" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d "{\"sparePartId\":\"$SPARE_PART_ID\",\"quantity\":10,\"operator\":\"admin\",\"remark\":\"API测试出库\"}")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-OPS-012: 备件出库返回200"
}
test_spare_part_out_stock_insufficient() {
echo "--- TC-OPS-013: 备件出库-库存不足 ---"
if [ -z "$SPARE_PART_ID" ]; then
echo " SKIP: TC-OPS-013 (无备件ID)"
return
fi
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/ops/spare-parts/out-stock" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d "{\"sparePartId\":\"$SPARE_PART_ID\",\"quantity\":99999,\"operator\":\"admin\",\"remark\":\"库存不足测试\"}")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "400" "TC-OPS-013: 库存不足出库返回400"
}
test_low_stock_warning() {
echo "--- TC-OPS-014: 低库存预警 ---"
local response=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/ops/spare-parts/low-stock?projectId=$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-OPS-014: 低库存预警返回200"
}
test_energy_consumption() {
echo "--- TC-OPS-015: 能耗抄表录入 ---"
local meter_response=$(curl -s -X POST "$BASE_URL/ops/energy/meters" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d "{\"meterName\":\"测试计量点-$$\",\"energyType\":\"LIGHTING\",\"unitPrice\":0.85}")
local meter_id=$(echo "$meter_response" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -z "$meter_id" ]; then
echo " SKIP: TC-OPS-015 (无法创建计量点)"
return
fi
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/ops/energy/consumption" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d "{\"meterId\":\"$meter_id\",\"currentReading\":100,\"recordedBy\":\"admin\"}")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "201" "TC-OPS-015: 能耗抄表录入返回201"
ENERGY_METER_ID="$meter_id"
}
test_energy_decreasing_reading() {
echo "--- TC-OPS-016: 能耗抄表-读数递减 ---"
if [ -z "$ENERGY_METER_ID" ]; then
echo " SKIP: TC-OPS-016 (无计量点ID)"
return
fi
local response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/ops/energy/consumption" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID" \
-d "{\"meterId\":\"$ENERGY_METER_ID\",\"currentReading\":50,\"recordedBy\":\"admin\"}")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "400" "TC-OPS-016: 读数递减返回400"
}
test_maintenance_plan() {
echo "--- TC-OPS-017: 维保计划管理 ---"
local response=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/wo/maintenance-plans?projectId=$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Project-ID: $PROJECT_ID")
local status=$(echo "$response" | tail -n 1)
assert_eq "$status" "200" "TC-OPS-017: 查询维保计划返回200"
}
cleanup() {
echo "=== 清理测试数据 ==="
if [ -n "$PROJECT_ID" ]; then
curl -s -X DELETE "$BASE_URL/mdm/projects/$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" > /dev/null 2>&1
fi
}
print_summary() {
echo ""
echo "=========================================="
echo " Ops Domain API Test Summary"
echo "=========================================="
echo " Total: $TOTAL_COUNT"
echo " Passed: $PASS_COUNT"
echo " Failed: $FAIL_COUNT"
if [ "$TOTAL_COUNT" -gt 0 ]; then
local rate=$(echo "scale=1; $PASS_COUNT * 100 / $TOTAL_COUNT" | bc)
echo " Rate: ${rate}%"
fi
echo "=========================================="
if [ "$FAIL_COUNT" -gt 0 ]; then
return 1
fi
return 0
}
WORK_ORDER_ID=""
MAINT_TASK_ID=""
SPARE_PART_ID=""
ENERGY_METER_ID=""
echo "=========================================="
echo " Ether PMS - Ops Domain API Tests"
echo "=========================================="
echo ""
login
setup_project
test_create_work_order
test_query_work_orders
test_assign_work_order
test_start_work_order
test_complete_work_order
test_verify_work_order
test_cancel_completed_work_order
test_work_order_statistics
test_create_maintenance_task
test_spare_part_crud
test_spare_part_in_stock
test_spare_part_out_stock
test_spare_part_out_stock_insufficient
test_low_stock_warning
test_energy_consumption
test_energy_decreasing_reading
test_maintenance_plan
cleanup
print_summary

29
04-TESTING/api/run-all.sh Executable file
View File

@ -0,0 +1,29 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
MODULES="auth space asset ops finance"
TOTAL_PASS=0
TOTAL_FAIL=0
TOTAL_COUNT=0
echo "=========================================="
echo " Ether PMS - All API Tests"
echo "=========================================="
echo ""
for module in $MODULES; do
script="$SCRIPT_DIR/$module/test-$module.sh"
if [ -f "$script" ]; then
echo ">>> Running $module API tests..."
echo "----------------------------------------"
bash "$script"
exit_code=$?
echo ""
else
echo ">>> SKIP: $module (test script not found: $script)"
echo ""
fi
done
echo "=========================================="
echo " All API Tests Complete"
echo "=========================================="

27
04-TESTING/api/run-module.sh Executable file
View File

@ -0,0 +1,27 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [ -z "$1" ]; then
echo "Usage: ./run-module.sh <module>"
echo ""
echo "Available modules:"
echo " auth - 身份与权限域"
echo " space - 空间与项目域"
echo " asset - 设备与资产域"
echo " ops - 运营与服务域"
echo " finance - 财务与收费域"
exit 1
fi
MODULE="$1"
SCRIPT="$SCRIPT_DIR/$MODULE/test-$MODULE.sh"
if [ ! -f "$SCRIPT" ]; then
echo "ERROR: Test script not found: $SCRIPT"
exit 1
fi
echo ">>> Running $MODULE API tests..."
echo "----------------------------------------"
bash "$SCRIPT"
exit $?

View File

@ -0,0 +1,39 @@
import { Page, Locator } from '@playwright/test';
export class DashboardPage {
readonly page: Page;
readonly projectSelector: Locator;
readonly sidebarMenu: Locator;
readonly logoutButton: Locator;
constructor(page: Page) {
this.page = page;
this.projectSelector = page.locator('.project-selector, [data-testid="project-selector"]');
this.sidebarMenu = page.locator('.ant-menu, .sidebar-menu');
this.logoutButton = page.locator('button:has-text("退出"), [data-testid="logout"]');
}
async goto() {
await this.page.goto('/');
await this.page.waitForLoadState('networkidle');
}
async selectProject(projectName: string) {
await this.projectSelector.click();
await this.page.locator(`.ant-select-item:has-text("${projectName}")`).click();
await this.page.waitForLoadState('networkidle');
}
async navigateTo(menuItem: string) {
await this.sidebarMenu.locator(`text=${menuItem}`).click();
await this.page.waitForLoadState('networkidle');
}
async logout() {
await this.logoutButton.click();
}
async isDashboardVisible(): Promise<boolean> {
return await this.page.locator('.dashboard, [data-testid="dashboard"]').isVisible();
}
}

View File

@ -0,0 +1,37 @@
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.usernameInput = page.locator('input[placeholder*="用户名"], input[id="username"], input[name="username"]');
this.passwordInput = page.locator('input[placeholder*="密码"], input[id="password"], input[name="password"]');
this.loginButton = page.locator('button[type="submit"], button:has-text("登录")');
this.errorMessage = page.locator('.ant-message-error, .ant-alert-error, .error-message');
}
async goto() {
await this.page.goto('/auth/login');
await this.page.waitForLoadState('networkidle');
}
async login(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
async loginAsAdmin() {
await this.login('admin', 'admin123');
}
async getErrorMessage(): Promise<string> {
await this.errorMessage.waitFor({ state: 'visible', timeout: 5000 });
return await this.errorMessage.textContent() || '';
}
}

View File

@ -0,0 +1,61 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './specs',
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: 1,
reporter: [
['html', { outputFolder: '../reports/e2e-report' }],
['list'],
],
timeout: 30000,
expect: {
timeout: 10000,
},
use: {
baseURL: 'http://localhost:5175',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{
name: 'auth',
testDir: './specs/auth',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'space',
testDir: './specs/space',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'asset',
testDir: './specs/asset',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'ops',
testDir: './specs/ops',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'finance',
testDir: './specs/finance',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'cross-domain',
testDir: './specs/cross-domain',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
command: 'cd ../../../ether-admin && npm run dev',
url: 'http://localhost:5175',
reuseExistingServer: !process.env.CI,
timeout: 60000,
},
});

32
04-TESTING/e2e/run-all.sh Executable file
View File

@ -0,0 +1,32 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "=========================================="
echo " Ether PMS - All E2E Tests"
echo "=========================================="
echo ""
cd "$SCRIPT_DIR"
if [ ! -f "playwright.config.ts" ]; then
echo "ERROR: playwright.config.ts not found in $SCRIPT_DIR"
exit 1
fi
if [ ! -d "node_modules" ]; then
echo "Installing dependencies..."
npm install
npx playwright install chromium
fi
echo "Running all E2E tests..."
npx playwright test --reporter=html,list
EXIT_CODE=$?
echo ""
echo "=========================================="
echo " E2E Tests Complete (exit code: $EXIT_CODE)"
echo "=========================================="
exit $EXIT_CODE

42
04-TESTING/e2e/run-module.sh Executable file
View File

@ -0,0 +1,42 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [ -z "$1" ]; then
echo "Usage: ./run-module.sh <module>"
echo ""
echo "Available modules:"
echo " auth - 认证授权域"
echo " space - 空间与项目域"
echo " asset - 设备与资产域"
echo " ops - 运营与服务域"
echo " finance - 财务与收费域"
echo " cross-domain - 跨域流程"
exit 1
fi
MODULE="$1"
cd "$SCRIPT_DIR"
if [ ! -f "playwright.config.ts" ]; then
echo "ERROR: playwright.config.ts not found in $SCRIPT_DIR"
exit 1
fi
if [ ! -d "node_modules" ]; then
echo "Installing dependencies..."
npm install
npx playwright install chromium
fi
echo "Running $MODULE E2E tests..."
npx playwright test --project="$MODULE" --reporter=list
EXIT_CODE=$?
echo ""
echo "=========================================="
echo " $MODULE E2E Tests Complete (exit code: $EXIT_CODE)"
echo "=========================================="
exit $EXIT_CODE

View File

@ -0,0 +1,48 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../../pages/LoginPage';
import { DashboardPage } from '../../pages/DashboardPage';
test.describe('登录流程', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
await loginPage.goto();
});
test('TC-FE-001: 正常登录成功跳转至仪表盘', async ({ page }) => {
await loginPage.loginAsAdmin();
await page.waitForURL('**/dashboard**', { timeout: 10000 });
await expect(dashboardPage.isDashboardVisible()).resolves.toBe(true);
});
test('TC-FE-002: 错误密码显示错误提示', async ({ page }) => {
await loginPage.login('admin', 'wrongpassword');
const errorMsg = await loginPage.getErrorMessage();
expect(errorMsg.length).toBeGreaterThan(0);
});
test('TC-FE-003: 空用户名无法提交', async ({ page }) => {
await loginPage.passwordInput.fill('admin123');
await loginPage.loginButton.click();
await expect(page.locator('.ant-form-item-explain-error')).toBeVisible();
});
test('TC-FE-004: 登出后返回登录页', async ({ page }) => {
await loginPage.loginAsAdmin();
await page.waitForURL('**/dashboard**', { timeout: 10000 });
await dashboardPage.logout();
await page.waitForURL('**/login**', { timeout: 10000 });
await expect(loginPage.usernameInput).toBeVisible();
});
test('TC-FE-005: Token过期后自动跳转登录页', async ({ page }) => {
await loginPage.loginAsAdmin();
await page.waitForURL('**/dashboard**', { timeout: 10000 });
await page.evaluate(() => localStorage.removeItem('token'));
await page.reload();
await page.waitForURL('**/login**', { timeout: 10000 });
});
});

View File

@ -0,0 +1,116 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../../pages/LoginPage';
import { DashboardPage } from '../../pages/DashboardPage';
test.describe('业主报修全流程', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
await loginPage.goto();
await loginPage.loginAsAdmin();
await page.waitForURL('**/dashboard**', { timeout: 10000 });
});
test('TC-CROSS-001: 业主报修完整流程 - 从创建工单到验收', async ({ page }) => {
await dashboardPage.navigateTo('工单管理');
await page.waitForLoadState('networkidle');
const createButton = page.locator('button:has-text("新建"), button:has-text("创建")');
if (await createButton.isVisible()) {
await createButton.click();
await page.waitForLoadState('networkidle');
const titleInput = page.locator('input[id="title"], input[placeholder*="标题"]');
if (await titleInput.isVisible()) {
await titleInput.fill('E2E测试-业主报修工单');
}
const descInput = page.locator('textarea[id="description"], textarea[placeholder*="描述"]');
if (await descInput.isVisible()) {
await descInput.fill('E2E自动化测试业主报修全流程验证');
}
const submitButton = page.locator('button:has-text("确定"), button:has-text("提交")');
if (await submitButton.isVisible()) {
await submitButton.click();
await page.waitForLoadState('networkidle');
}
}
const workOrderRow = page.locator('tr:has-text("E2E测试-业主报修工单"), .ant-card:has-text("E2E测试")');
if (await workOrderRow.isVisible()) {
await expect(workOrderRow).toBeVisible();
}
});
test('TC-CROSS-001-派单: 工单派单流程', async ({ page }) => {
await dashboardPage.navigateTo('工单管理');
await page.waitForLoadState('networkidle');
const pendingOrder = page.locator('tr:has-text("待分配"), .ant-tag:has-text("待分配")').first();
if (await pendingOrder.isVisible()) {
await pendingOrder.click();
await page.waitForLoadState('networkidle');
const assignButton = page.locator('button:has-text("派单")');
if (await assignButton.isVisible()) {
await assignButton.click();
await page.waitForLoadState('networkidle');
}
}
});
test('TC-CROSS-001-执行完成: 工单执行与完成', async ({ page }) => {
await dashboardPage.navigateTo('工单管理');
await page.waitForLoadState('networkidle');
const assignedOrder = page.locator('tr:has-text("已派单"), .ant-tag:has-text("已派单")').first();
if (await assignedOrder.isVisible()) {
await assignedOrder.click();
await page.waitForLoadState('networkidle');
const startButton = page.locator('button:has-text("开始执行")');
if (await startButton.isVisible()) {
await startButton.click();
await page.waitForLoadState('networkidle');
}
const completeButton = page.locator('button:has-text("完成")');
if (await completeButton.isVisible()) {
await completeButton.click();
await page.waitForLoadState('networkidle');
}
}
});
test('TC-CROSS-001-验收: 工单验收流程', async ({ page }) => {
await dashboardPage.navigateTo('工单管理');
await page.waitForLoadState('networkidle');
const completedOrder = page.locator('tr:has-text("已完成"), .ant-tag:has-text("已完成")').first();
if (await completedOrder.isVisible()) {
await completedOrder.click();
await page.waitForLoadState('networkidle');
const verifyButton = page.locator('button:has-text("验收")');
if (await verifyButton.isVisible()) {
await verifyButton.click();
await page.waitForLoadState('networkidle');
const ratingSelect = page.locator('.ant-rate, [data-testid="rating"]');
if (await ratingSelect.isVisible()) {
await ratingSelect.locator('.ant-rate-star').nth(4).click();
}
const confirmButton = page.locator('button:has-text("确认"), button:has-text("提交")');
if (await confirmButton.isVisible()) {
await confirmButton.click();
await page.waitForLoadState('networkidle');
}
}
}
});
});

View File

@ -2,7 +2,10 @@
**领域编号**: 4.2
**微服务**: ether-ops
**最后更新**: 2026-02-10
**最后更新**: 2026-04-26
> **更新记录**:
> - 2026-04-26: 反向同步实际代码实现到文档。主要变更工单状态机更新为PENDING→ASSIGNED→IN_PROGRESS→COMPLETED→VERIFIED/CANCELLED标注TODO: 增加SUSPENDED/RETURNED工单类型枚举更新REPAIR/INSPECTION/SECURITY/CLEANING/PROPERTY/CONSULTATION工单来源枚举更新OWNER/MAINTENANCE/INSPECTION/FAULT/REGULATORY/MANUAL新增实体WorkOrderItem/MaintenancePlan/MaintenanceTask/InspectionTemplate/InspectionItem新增字段planId/triggerType/assignedVendor/laborCost/partsCost/totalCost/signature字段拆分resultDescription→result+faultCause+solution新增业务规则优先级自动判定/设备联动更新/维保任务双完成接口API路径从/api/v1/ops改为/api/wo标注TODO: 工单列表需改为分页查询。
---
@ -21,7 +24,12 @@
| 概念 | 说明 | 对应实体 |
|------|------|----------|
| **工单** | 综合业务单据,支持多种类型 | WorkOrder |
| **工单明细** | 工单下的处理项目清单 | WorkOrderItem |
| **工单流转** | 工单状态变更记录 | WorkOrderFlow |
| **维保计划** | 定期保养计划 | MaintenancePlan |
| **维保任务** | 维保执行工单 | MaintenanceTask |
| **巡检模板** | 巡检检查项模板 | InspectionTemplate |
| **巡检项目** | 巡检模板中的检查项 | InspectionItem |
| **通知渠道** | 消息发送通道 | NotificationChannel |
| **通知模板** | 消息内容模板 | NotificationTemplate |
| **通知规则** | 触发条件和发送策略 | NotificationRule |
@ -47,10 +55,10 @@ public class WorkOrder {
private String orderNo; // 工单编号: WO2024021000001
// 工单类型
private WorkOrderType orderType; // REPAIR/COMPLAINT/CLEANING/SECURITY/OTHER
private WorkOrderType orderType; // REPAIR/INSPECTION/SECURITY/CLEANING/PROPERTY/CONSULTATION
private WorkOrderStatus status; // 状态机
private WorkOrderPriority priority; // URGENT/HIGH/MEDIUM/LOW
private WorkOrderSource source; // APP/PHONE/INSPECTION/IOT/SYSTEM
private WorkOrderSource source; // OWNER/MAINTENANCE/INSPECTION/FAULT/REGULATORY/MANUAL
// 基本信息
private String title;
@ -66,25 +74,35 @@ public class WorkOrder {
private UUID spaceNodeId; // 关联空间
private UUID equipmentId; // 关联设备
// 维保关联(新增)
private UUID planId; // 关联维保计划ID
private String triggerType; // 触发类型: MANUAL/AUTO/SCHEDULED
// 处理人信息
private UUID assigneeId;
private String assigneeName;
private UUID assignedVendor; // 指派供应商ID新增
private LocalDateTime assignedAt;
private LocalDateTime acceptedAt;
private LocalDateTime startedAt;
private LocalDateTime completedAt;
private LocalDateTime closedAt;
// 费用
private BigDecimal actualCost;
private BigDecimal materialCost;
private BigDecimal laborCost;
// 费用(拆分细化)
private BigDecimal laborCost; // 人工费(新增)
private BigDecimal partsCost; // 材料费(新增)
private BigDecimal totalCost; // 总费用新增替代原actualCost
// 结果
private String resultDescription;
// 结果拆分原resultDescription → result + faultCause + solution
private String result; // 处理结果
private String faultCause; // 故障原因
private String solution; // 解决方案
private Integer satisfactionScore;
private String satisfactionComment;
// 签名确认(新增)
private String signature; // 签名图片URL
// 附件
private String images;
private String attachments;
@ -99,10 +117,38 @@ public class WorkOrder {
}
```
**工单类型枚举(更新)**:
```java
public enum WorkOrderType {
REPAIR("报修"),
INSPECTION("巡检"),
SECURITY("安全"),
CLEANING("保洁"),
PROPERTY("物业"),
CONSULTATION("咨询");
}
```
**工单来源枚举(更新)**:
```java
public enum WorkOrderSource {
OWNER("业主报修"),
MAINTENANCE("维保触发"),
INSPECTION("巡检发现"),
FAULT("设备故障"),
REGULATORY("监管要求"),
MANUAL("手动创建");
}
```
**业务规则**:
- orderNo 自动生成,格式: WO + yyyyMMdd + 5位序号
- 状态流转必须通过合法的业务操作
- 关闭工单时必须填写处理结果
- **优先级自动判定**新增根据工单类型和来源自动设置优先级如安全类默认HIGH设备故障默认HIGH
- **设备联动更新**(新增):工单完成时自动更新关联设备状态
#### WorkOrderFlow工单流转记录
@ -118,7 +164,7 @@ public class WorkOrderFlow {
// 流转信息
private WorkOrderStatus fromStatus;
private WorkOrderStatus toStatus;
private String action; // 操作: ASSIGN/ACCEPT/START/COMPLETE/CLOSE
private String action; // 操作: ASSIGN/START/COMPLETE/VERIFY/CANCEL
// 操作人
private UUID operatorId;
@ -133,24 +179,164 @@ public class WorkOrderFlow {
}
```
#### WorkOrderItem工单明细新增
```java
@Entity
@Table(name = "ops_work_order_item")
@Data
public class WorkOrderItem {
@Id
private UUID id;
private UUID workOrderId;
private UUID projectId;
private String itemName; // 明细名称
private String itemType; // 明细类型
private String description; // 明细描述
private Integer quantity; // 数量
private BigDecimal unitPrice; // 单价
private BigDecimal amount; // 金额
private String remark; // 备注
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
```
#### MaintenancePlan维保计划新增
```java
@Entity
@Table(name = "ops_maintenance_plan")
@Data
public class MaintenancePlan {
@Id
private UUID id;
private UUID projectId;
private UUID equipmentId;
private String name;
private MaintenanceType type; // DAILY/WEEKLY/MONTHLY/QUARTERLY/YEARLY
private Integer cycleDays;
private String cronExpression;
private String content;
private String checkItems; // JSON
private UUID maintainerId;
private String maintainerName;
private Integer remindDays;
private Boolean enabled;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
```
#### MaintenanceTask维保任务新增
```java
@Entity
@Table(name = "ops_maintenance_task")
@Data
public class MaintenanceTask {
@Id
private UUID id;
private UUID projectId;
private UUID equipmentId;
private UUID planId;
private String taskCode;
private MaintenanceTaskStatus status; // PENDING/IN_PROGRESS/COMPLETED/CANCELLED
private LocalDate maintenanceDate;
private MaintenanceType type;
private String content;
private UUID maintainerId;
private String maintainerName;
private MaintenanceResult result;
private String remark;
private String images;
private String checkResults; // JSON
private BigDecimal laborCost;
private BigDecimal partsCost;
private BigDecimal totalCost;
private String signature;
private LocalDate nextMaintenanceDate;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
public enum MaintenanceTaskStatus {
PENDING, IN_PROGRESS, COMPLETED, CANCELLED;
}
```
> **维保任务双完成接口**(新增业务规则): 维保任务提供两个完成接口:
> 1. `POST /api/wo/maintenance-tasks/{id}/complete` - 维保人员完成
> 2. `POST /api/wo/maintenance-tasks/{id}/verify` - 主管验证确认
#### InspectionTemplate巡检模板新增
```java
@Entity
@Table(name = "ops_inspection_template")
@Data
public class InspectionTemplate {
@Id
private UUID id;
private UUID projectId;
private String name;
private String description;
private String category; // 模板分类
private Boolean enabled;
@OneToMany(mappedBy = "templateId", fetch = FetchType.LAZY)
private List<InspectionItem> items;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
```
#### InspectionItem巡检项目新增
```java
@Entity
@Table(name = "ops_inspection_item")
@Data
public class InspectionItem {
@Id
private UUID id;
private UUID templateId;
private String itemName; // 检查项名称
private String checkStandard; // 检查标准
private String checkMethod; // 检查方法
private Integer sortOrder; // 排序
private Boolean required; // 是否必检
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
```
### 2.2 状态机设计
```
┌─────────────┐
│ CREATED │ ← 创建工单
│ (已创建) │
PENDING │ ← 创建工单
│ (待处理) │
└──────┬──────┘
│ 分配
┌─────────────┐
│ ASSIGNED │ ← 分配给处理人
│ (已分配) │
└──────┬──────┘
│ 接单
┌─────────────┐
│ ACCEPTED │ ← 处理人接单
│ (已接单) │
└──────┬──────┘
│ 开始处理
@ -164,30 +350,30 @@ public class WorkOrderFlow {
│ COMPLETED │ ← 处理完成
│ (已完成) │
└──────┬──────┘
关闭
验证
┌─────────────┐
CLOSED │ ← 工单关闭
│ (已关闭) │
VERIFIED │ ← 验证通过
│ (已验证) │
└─────────────┘
特殊状态:
- SUSPENDED (已挂起): 任意状态可转入,可恢复
- RETURNED (已退回): ASSIGNED状态可转入需重新分配
- CANCELLED (已取消): PENDING/ASSIGNED状态可转入
```
> **TODO**: 当前状态机缺少 SUSPENDED(已挂起) 和 RETURNED(已退回) 状态,需后续补充:
> - SUSPENDED: 任意状态可转入,可恢复到原状态
> - RETURNED: ASSIGNED状态可转入需重新分配
**状态流转规则**:
| 当前状态 | 允许操作 | 下一状态 | 权限 |
|---------|---------|---------|------|
| CREATED | 分配 | ASSIGNED | 管理员/调度员 |
| ASSIGNED | 接单 | ACCEPTED | 被指派人 |
| ASSIGNED | 退回 | RETURNED | 被指派人 |
| ACCEPTED | 开始 | IN_PROGRESS | 被指派人 |
| PENDING | 分配 | ASSIGNED | 管理员/调度员 |
| ASSIGNED | 开始 | IN_PROGRESS | 被指派人 |
| IN_PROGRESS | 完成 | COMPLETED | 被指派人 |
| COMPLETED | 关闭 | CLOSED | 管理员/创建人 |
| * | 挂起 | SUSPENDED | 管理员 |
| SUSPENDED | 恢复 | 原状态 | 管理员 |
| COMPLETED | 验证 | VERIFIED | 管理员/创建人 |
| PENDING/ASSIGNED | 取消 | CANCELLED | 管理员 |
---
@ -443,9 +629,11 @@ public class WorkOrderStatisticsService {
### 5.1 WorkOrder API
> **注意**: API路径已从 `/api/v1/ops` 改为 `/api/wo`
```java
@RestController
@RequestMapping("/api/v1/ops/work-orders")
@RequestMapping("/api/wo/work-orders")
@Tag(name = "工单管理")
public class WorkOrderController {
@ -458,8 +646,8 @@ public class WorkOrderController {
public Result<WorkOrderVO> getById(@PathVariable UUID id);
@GetMapping
@Operation(summary = "分页查询工单")
public Result<Page<WorkOrderVO>> page(WorkOrderQueryRequest request);
@Operation(summary = "查询工单列表")
public Result<List<WorkOrderVO>> list(WorkOrderQueryRequest request);
@PutMapping("/{id}")
@Operation(summary = "更新工单")
@ -476,10 +664,6 @@ public class WorkOrderController {
public Result<WorkOrderVO> assign(@PathVariable UUID id,
@RequestBody @Valid WorkOrderAssignRequest request);
@PostMapping("/{id}/accept")
@Operation(summary = "接单")
public Result<WorkOrderVO> accept(@PathVariable UUID id);
@PostMapping("/{id}/start")
@Operation(summary = "开始处理")
public Result<WorkOrderVO> start(@PathVariable UUID id);
@ -489,19 +673,15 @@ public class WorkOrderController {
public Result<WorkOrderVO> complete(@PathVariable UUID id,
@RequestBody @Valid WorkOrderCompleteRequest request);
@PostMapping("/{id}/close")
@Operation(summary = "关闭工单")
public Result<WorkOrderVO> close(@PathVariable UUID id,
@RequestBody @Valid WorkOrderCloseRequest request);
@PostMapping("/{id}/verify")
@Operation(summary = "验证工单")
public Result<WorkOrderVO> verify(@PathVariable UUID id,
@RequestBody @Valid WorkOrderVerifyRequest request);
@PostMapping("/{id}/suspend")
@Operation(summary = "挂起工单")
public Result<WorkOrderVO> suspend(@PathVariable UUID id,
@RequestBody WorkOrderSuspendRequest request);
@PostMapping("/{id}/resume")
@Operation(summary = "恢复工单")
public Result<WorkOrderVO> resume(@PathVariable UUID id);
@PostMapping("/{id}/cancel")
@Operation(summary = "取消工单")
public Result<WorkOrderVO> cancel(@PathVariable UUID id,
@RequestBody WorkOrderCancelRequest request);
// 流转记录
@GetMapping("/{id}/flows")
@ -510,11 +690,13 @@ public class WorkOrderController {
}
```
> **TODO**: 工单列表接口GET /api/wo/work-orders当前返回 List需改为分页查询返回 Page<WorkOrderVO>)。
### 5.2 Notification API
```java
@RestController
@RequestMapping("/api/v1/ops/notifications")
@RequestMapping("/api/wo/notifications")
@Tag(name = "消息通知")
public class NotificationController {
@ -573,7 +755,7 @@ public class NotificationController {
```java
@RestController
@RequestMapping("/api/v1/ops/statistics")
@RequestMapping("/api/wo/statistics")
@Tag(name = "工单统计")
public class WorkOrderStatisticsController {
@ -612,34 +794,48 @@ public class WorkOrderStatisticsController {
| 功能模块 | 实现状态 | 备注 |
|---------|---------|------|
| WorkOrder | 🟢 已实现 | 完整状态机、CRUD |
| WorkOrder | 🟢 已实现 | 完整状态机(PENDING→ASSIGNED→IN_PROGRESS→COMPLETED→VERIFIED/CANCELLED) |
| WorkOrderItem | 🟢 已实现 | 工单明细 |
| WorkOrderFlow | 🟢 已实现 | 自动记录流转 |
| MaintenancePlan | 🟢 已实现 | 维保计划 |
| MaintenanceTask | 🟢 已实现 | 维保任务,双完成接口 |
| InspectionTemplate | 🟢 已实现 | 巡检模板 |
| InspectionItem | 🟢 已实现 | 巡检项目 |
| NotificationChannel | 🟢 已实现 | 基础CRUD |
| NotificationTemplate | 🟢 已实现 | 基础CRUD |
| NotificationRule | 🟢 已实现 | 基础CRUD |
| NotificationHistory | 🟢 已实现 | 基础CRUD |
| 工单与通知集成 | 🟢 已实现 | 事件驱动 |
| 工单统计 | 🟢 已实现 | 多维度统计 |
| 优先级自动判定 | 🟢 已实现 | 根据类型/来源自动设置 |
| 设备联动更新 | 🟢 已实现 | 工单完成时更新设备状态 |
| SLA监控 | 🔴 未实现 | 超时预警 |
| 智能派单 | 🔴 未实现 | 自动分配 |
| SUSPENDED/RETURNED状态 | 🔴 未实现 | TODO: 需增加挂起和退回状态 |
| 工单分页查询 | 🔴 未实现 | TODO: 列表接口需改为分页查询 |
### 6.2 与设计方案的差异
| 设计项 | 设计方案 | 现有实现 | 差异分析 |
|--------|----------|----------|----------|
| **工单状态机** | 完整状态流转 | 已实现所有状态 | ✅ 符合设计 |
| **通知渠道** | 多渠道支持 | 仅站内信实现 | ⚠️ 其他渠道待扩展 |
| **工单统计** | 多维度分析 | 基本实现 | 🟡 可扩展更多维度 |
| **巡检归属** | ether-ops | ether-mdm | 🟡 领域边界模糊 |
| **工单状态机** | CREATED→ASSIGNED→ACCEPTED→IN_PROGRESS→COMPLETED→CLOSED | PENDING→ASSIGNED→IN_PROGRESS→COMPLETED→VERIFIED/CANCELLED | 已更新:简化流程,增加验证环节 |
| **工单类型** | REPAIR/COMPLAINT/CLEANING/SECURITY/OTHER | REPAIR/INSPECTION/SECURITY/CLEANING/PROPERTY/CONSULTATION | 已更新:更贴合物业场景 |
| **工单来源** | APP/PHONE/INSPECTION/IOT/SYSTEM | OWNER/MAINTENANCE/INSPECTION/FAULT/REGULATORY/MANUAL | 已更新:按业务角色区分 |
| **结果字段** | resultDescription单字段 | result+faultCause+solution三字段 | 已拆分:更细粒度的结果记录 |
| **费用字段** | actualCost/materialCost/laborCost | laborCost/partsCost/totalCost | 已更新:更清晰的费用分类 |
| **通知渠道** | 多渠道支持 | 仅站内信实现 | 其他渠道待扩展 |
| **API路径** | /api/v1/ops | /api/wo | 已更新:路径简化 |
| **巡检归属** | ether-ops | ether-mdm | 领域边界模糊 |
### 6.3 待改进项
| 优先级 | 改进项 | 说明 |
|--------|--------|------|
| P2 | 实现SLA监控 | 工单超时预警和自动升级 |
| P0 | 工单分页查询 | 列表接口需改为分页查询 |
| P0 | 增加SUSPENDED/RETURNED状态 | 挂起和退回是常见业务需求 |
| P1 | 实现SLA监控 | 工单超时预警和自动升级 |
| P2 | 扩展通知渠道 | 短信、邮件、推送渠道 |
| P3 | 智能派单算法 | 基于负载、技能、位置的自动分配 |
| P3 | 添加工单满意度 | 业主评价机制 |
---
@ -652,7 +848,7 @@ CREATE TABLE ops_work_order (
project_id UUID NOT NULL,
order_no VARCHAR(32) NOT NULL,
order_type VARCHAR(20) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'CREATED',
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
priority VARCHAR(20) NOT NULL DEFAULT 'MEDIUM',
source VARCHAR(20) NOT NULL,
title VARCHAR(200) NOT NULL,
@ -663,19 +859,25 @@ CREATE TABLE ops_work_order (
reporter_address VARCHAR(255),
space_node_id UUID,
equipment_id UUID,
plan_id UUID,
trigger_type VARCHAR(20),
assignee_id UUID,
assignee_name VARCHAR(100),
assigned_vendor UUID,
assigned_at TIMESTAMP,
accepted_at TIMESTAMP,
started_at TIMESTAMP,
completed_at TIMESTAMP,
closed_at TIMESTAMP,
actual_cost NUMERIC(12,2),
material_cost NUMERIC(12,2),
labor_cost NUMERIC(12,2),
result_description TEXT,
parts_cost NUMERIC(12,2),
total_cost NUMERIC(12,2),
result TEXT,
fault_cause TEXT,
solution TEXT,
satisfaction_score INTEGER,
satisfaction_comment VARCHAR(500),
signature VARCHAR(255),
images TEXT,
attachments TEXT,
attributes JSONB,
@ -686,6 +888,22 @@ CREATE TABLE ops_work_order (
UNIQUE(project_id, order_no)
);
-- 工单明细表(新增)
CREATE TABLE ops_work_order_item (
id UUID PRIMARY KEY,
work_order_id UUID NOT NULL REFERENCES ops_work_order(id),
project_id UUID NOT NULL,
item_name VARCHAR(200) NOT NULL,
item_type VARCHAR(50),
description TEXT,
quantity INTEGER,
unit_price NUMERIC(12,2),
amount NUMERIC(12,2),
remark VARCHAR(500),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 工单流转表
CREATE TABLE ops_work_order_flow (
id UUID PRIMARY KEY,
@ -700,6 +918,76 @@ CREATE TABLE ops_work_order_flow (
images TEXT
);
-- 维保计划表(新增)
CREATE TABLE ops_maintenance_plan (
id UUID PRIMARY KEY,
project_id UUID NOT NULL,
equipment_id UUID NOT NULL,
name VARCHAR(100) NOT NULL,
type VARCHAR(20) NOT NULL,
cycle_days INTEGER,
cron_expression VARCHAR(50),
content TEXT,
check_items JSONB,
maintainer_id UUID,
maintainer_name VARCHAR(100),
remind_days INTEGER DEFAULT 3,
enabled BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 维保任务表(新增)
CREATE TABLE ops_maintenance_task (
id UUID PRIMARY KEY,
project_id UUID NOT NULL,
equipment_id UUID NOT NULL,
plan_id UUID,
task_code VARCHAR(50),
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
maintenance_date DATE NOT NULL,
type VARCHAR(20) NOT NULL,
content TEXT,
maintainer_id UUID,
maintainer_name VARCHAR(100),
result VARCHAR(20),
remark VARCHAR(500),
images TEXT,
check_results JSONB,
labor_cost NUMERIC(12,2),
parts_cost NUMERIC(12,2),
total_cost NUMERIC(12,2),
signature VARCHAR(255),
next_maintenance_date DATE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 巡检模板表(新增)
CREATE TABLE ops_inspection_template (
id UUID PRIMARY KEY,
project_id UUID NOT NULL,
name VARCHAR(100) NOT NULL,
description TEXT,
category VARCHAR(50),
enabled BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 巡检项目表(新增)
CREATE TABLE ops_inspection_item (
id UUID PRIMARY KEY,
template_id UUID NOT NULL REFERENCES ops_inspection_template(id),
item_name VARCHAR(200) NOT NULL,
check_standard TEXT,
check_method VARCHAR(100),
sort_order INTEGER DEFAULT 0,
required BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 通知渠道表
CREATE TABLE ops_notification_channel (
id UUID PRIMARY KEY,
@ -775,7 +1063,13 @@ CREATE INDEX idx_work_order_project ON ops_work_order(project_id);
CREATE INDEX idx_work_order_status ON ops_work_order(status);
CREATE INDEX idx_work_order_assignee ON ops_work_order(assignee_id);
CREATE INDEX idx_work_order_created ON ops_work_order(created_at);
CREATE INDEX idx_work_order_type ON ops_work_order(order_type);
CREATE INDEX idx_work_order_flow_order ON ops_work_order_flow(work_order_id);
CREATE INDEX idx_work_order_item_order ON ops_work_order_item(work_order_id);
CREATE INDEX idx_maintenance_plan_equipment ON ops_maintenance_plan(equipment_id);
CREATE INDEX idx_maintenance_task_equipment ON ops_maintenance_task(equipment_id);
CREATE INDEX idx_maintenance_task_status ON ops_maintenance_task(status);
CREATE INDEX idx_inspection_item_template ON ops_inspection_item(template_id);
CREATE INDEX idx_notification_history_receiver ON ops_notification_history(receiver_id);
CREATE INDEX idx_notification_history_status ON ops_notification_history(status);
```

View File

@ -1,8 +1,12 @@
# Ether 空间节点管理架构设计方案
**版本**: v2.0
**版本**: v2.1
**设计日期**: 2026-02-16
**设计目标**: 重构空间节点管理,统一空间体系,支持地图服务对接
**最后更新**: 2026-04-26
> **更新记录**:
> - 2026-04-26: 反向同步实际代码实现到文档。主要变更树形查询改为按项目维度类型查询统一为按类型查询新增删除前检查和级联删除新增楼层信息查询新增软删除isDeleted字段NodeType增加PUBLIC_ROOM标注TODO: SpaceNode需增加code字段和唯一约束标注TODO: 设备扩展字段应统一到Equipment独立表API路径去掉v1版本号。
---
@ -155,7 +159,7 @@ CREATE TABLE mdm_space_node (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by UUID,
updated_by UUID,
is_deleted BOOLEAN DEFAULT FALSE,
is_deleted BOOLEAN DEFAULT FALSE, -- 软删除标记(实际代码使用 isDeleted 字段)
-- 约束
CONSTRAINT uk_space_node_project_code UNIQUE (project_id, code)
@ -193,6 +197,7 @@ public enum SpaceNodeType {
UNIT("单元", SpaceNodeCategory.BUILDING, 2),
FLOOR("楼层", SpaceNodeCategory.BUILDING, 3),
ROOM("房间", SpaceNodeCategory.BUILDING, 4),
PUBLIC_ROOM("公共房间", SpaceNodeCategory.BUILDING, 4),
SHOP("商铺", SpaceNodeCategory.BUILDING, 2),
// 停车空间
@ -311,57 +316,65 @@ GROUP BY p.id, p.code, p.name, p.updated_at;
### 3.1 RESTful API 规范
```
基础路径: /api/v1/mdm/space-nodes
基础路径: /api/mdm/space-nodes
┌─────────────────────────────────────────────────────────────────────────────┐
│ 空间节点 API 接口 │
└─────────────────────────────────────────────────────────────────────────────┘
1. 基础 CRUD
POST /api/v1/mdm/space-nodes 创建空间节点
PUT /api/v1/mdm/space-nodes/{id} 更新空间节点
DELETE /api/v1/mdm/space-nodes/{id} 删除空间节点
GET /api/v1/mdm/space-nodes/{id} 查询空间节点详情
POST /api/mdm/space-nodes 创建空间节点
PUT /api/mdm/space-nodes/{id} 更新空间节点
DELETE /api/mdm/space-nodes/{id} 删除空间节点(含删除前检查)
GET /api/mdm/space-nodes/{id} 查询空间节点详情
2. 树形结构
GET /api/v1/mdm/space-nodes/tree 获取完整树形结构
GET /api/v1/mdm/space-nodes/roots 获取根节点列表
GET /api/v1/mdm/space-nodes/{id}/children 获取子节点列表
GET /api/v1/mdm/space-nodes/{id}/ancestors 获取祖先节点链
GET /api/v1/mdm/space-nodes/{id}/descendants 获取所有子孙节点
PUT /api/v1/mdm/space-nodes/{id}/move 移动节点到新父节点
2. 树形结构(按项目维度查询)
GET /api/mdm/space-nodes/tree?projectId={id} 获取项目空间树
GET /api/mdm/space-nodes/roots?projectId={id} 获取项目根节点列表
GET /api/mdm/space-nodes/{id}/children 获取子节点列表
GET /api/mdm/space-nodes/{id}/ancestors 获取祖先节点链
GET /api/mdm/space-nodes/{id}/descendants 获取所有子孙节点
PUT /api/mdm/space-nodes/{id}/move 移动节点到新父节点
3. 类型查询
GET /api/v1/mdm/space-nodes/buildings 获取楼栋列表
GET /api/v1/mdm/space-nodes/rooms 获取房间列表
GET /api/v1/mdm/space-nodes/parking-spaces 获取车位列表
GET /api/v1/mdm/space-nodes/shops 获取商铺列表
3. 类型查询(统一为按类型查询)
GET /api/mdm/space-nodes?type={nodeType} 按类型查询空间节点
4. 批量操作
POST /api/v1/mdm/space-nodes/batch 批量创建
PUT /api/v1/mdm/space-nodes/batch 批量更新
DELETE /api/v1/mdm/space-nodes/batch 批量删除
POST /api/v1/mdm/space-nodes/import Excel导入
GET /api/v1/mdm/space-nodes/export/template 下载导入模板
GET /api/v1/mdm/space-nodes/export 导出数据
4. 楼层信息查询(新增)
GET /api/mdm/space-nodes/{id}/floors 获取楼栋下的楼层列表
5. 地图相关
GET /api/v1/mdm/space-nodes/map/markers 获取地图标注点
GET /api/v1/mdm/space-nodes/map/boundaries 获取区域边界
PUT /api/v1/mdm/space-nodes/{id}/location 更新位置信息
PUT /api/v1/mdm/space-nodes/{id}/boundary 更新区域边界
5. 批量操作
POST /api/mdm/space-nodes/batch 批量创建
PUT /api/mdm/space-nodes/batch 批量更新
DELETE /api/mdm/space-nodes/batch 批量删除
POST /api/mdm/space-nodes/import Excel导入
GET /api/mdm/space-nodes/export/template 下载导入模板
GET /api/mdm/space-nodes/export 导出数据
6. 统计分析
GET /api/v1/mdm/space-nodes/statistics 空间统计数据
GET /api/v1/mdm/space-nodes/statistics/by-type 按类型统计
GET /api/v1/mdm/space-nodes/statistics/by-status 按状态统计
6. 地图相关
GET /api/mdm/space-nodes/map/markers 获取地图标注点
GET /api/mdm/space-nodes/map/boundaries 获取区域边界
PUT /api/mdm/space-nodes/{id}/location 更新位置信息
PUT /api/mdm/space-nodes/{id}/boundary 更新区域边界
7. 统计分析
GET /api/mdm/space-nodes/statistics 空间统计数据
GET /api/mdm/space-nodes/statistics/by-type 按类型统计
GET /api/mdm/space-nodes/statistics/by-status 按状态统计
```
> **变更说明**:
> - API路径已去掉v1版本号统一为 `/api/mdm/space-nodes`
> - 树形查询改为按项目维度(`tree?projectId={id}`),不再返回全量树
> - 类型查询统一为 `?type={nodeType}` 参数化查询,替代原有的 `/buildings`、`/rooms` 等独立端点
> - 新增删除前检查:删除节点前检查是否有子节点和关联业务数据
> - 新增级联删除:支持级联删除子节点
> - 新增楼层信息查询:`GET /{id}/floors`
### 3.2 请求/响应示例
**创建楼栋**:
```json
// POST /api/v1/mdm/space-nodes
// POST /api/mdm/space-nodes
{
"projectId": "uuid",
"code": "B001",
@ -401,7 +414,7 @@ GROUP BY p.id, p.code, p.name, p.updated_at;
**批量创建房间**:
```json
// POST /api/v1/mdm/space-nodes/batch
// POST /api/mdm/space-nodes/batch
{
"parentId": "unit-uuid",
"nodeType": "ROOM",
@ -790,17 +803,23 @@ function saveLocation() {
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 获取空间树 | GET | /api/v1/mdm/space-nodes/tree | 项目空间树 |
| 获取根节点 | GET | /api/v1/mdm/space-nodes/roots | 项目下根节点 |
| 获取节点详情 | GET | /api/v1/mdm/space-nodes/{id} | 节点详情 |
| 创建节点 | POST | /api/v1/mdm/space-nodes | 创建空间节点 |
| 更新节点 | PUT | /api/v1/mdm/space-nodes/{id} | 更新节点 |
| 删除节点 | DELETE | /api/v1/mdm/space-nodes/{id} | 删除节点 |
| 获取子节点 | GET | /api/v1/mdm/space-nodes/{id}/children | 子节点列表 |
| 批量创建 | POST | /api/v1/mdm/space-nodes/batch | 批量创建 |
| 批量导入 | POST | /api/v1/mdm/space-nodes/import | Excel导入 |
| 获取空间树 | GET | /api/mdm/space-nodes/tree | 项目空间树(按项目维度) |
| 获取根节点 | GET | /api/mdm/space-nodes/roots | 项目下根节点 |
| 获取节点详情 | GET | /api/mdm/space-nodes/{id} | 节点详情 |
| 创建节点 | POST | /api/mdm/space-nodes | 创建空间节点 |
| 更新节点 | PUT | /api/mdm/space-nodes/{id} | 更新节点 |
| 删除节点 | DELETE | /api/mdm/space-nodes/{id} | 删除节点(含删除前检查和级联删除) |
| 获取子节点 | GET | /api/mdm/space-nodes/{id}/children | 子节点列表 |
| 获取楼层列表 | GET | /api/mdm/space-nodes/{id}/floors | 楼栋下楼层列表(新增) |
| 按类型查询 | GET | /api/mdm/space-nodes?type={type} | 按类型查询(统一接口) |
| 批量创建 | POST | /api/mdm/space-nodes/batch | 批量创建 |
| 批量导入 | POST | /api/mdm/space-nodes/import | Excel导入 |
---
**文档版本**: v2.0
**最后更新**: 2026-03-23
**文档版本**: v2.1
**最后更新**: 2026-04-26
> **TODO 项**:
> - SpaceNode 需增加 code 字段和唯一约束(当前代码中 code 字段可能缺失或不唯一)
> - 设备扩展字段attributes JSONB 中的设备信息)应统一到 Equipment 独立表管理,避免数据冗余

View File

@ -0,0 +1,815 @@
# 设施设备领域技术方案
**领域编号**: 4.3
**微服务**: ether-asset
**最后更新**: 2026-04-26
> **更新记录**:
> - 2026-04-26: 反向同步实际代码实现到文档。主要变更设备类型枚举更新为10种新增ENERGY_METER/LANDSCAPE/KITCHENAC→HVACFIRE_FIGHTING→FIRE_PROTECTIONPOWER_SUPPLY→ELECTRICAL删除PARKING设备状态枚举更新NORMAL→ACTIVEFAULT→INACTIVEMAINTAINING→MAINTENANCE新增4张专业扩展表Elevator/Hvac/Fire/Energy新增系统类型SystemType 9种新增归属管理OwnershipType+OwnershipEntity新增健康评分算法MTBF/MTTR标注为Beta新增故障历史EquipmentFailureHistory新增设备照片/文档JSONB内嵌新增Excel导入导出新增特种设备管理新增能耗标准字段字段名变更code→equipmentCode, name→equipmentName, locationDesc→installationLocation维保计划迁移至独立API维保记录升级为维保工单标注TODO: 维保完成率计算暂返回1.0。
---
## 一、领域概述
### 1.1 领域职责
设施设备领域负责管理物业资产全生命周期:
- 设备台账管理(电梯、空调、消防、给排水、供配电等)
- 设备维保计划与记录
- 设备故障管理
- 设备与工单联动
### 1.2 核心概念
| 概念 | 说明 | 对应实体 |
|------|------|----------|
| **设备台账** | 设备基础信息和运行状态 | Equipment |
| **电梯扩展** | 电梯专业参数(载重/速度/注册号等) | EquipmentElevator |
| **暖通扩展** | 暖通空调专业参数(制冷量/制热量/能效比等) | EquipmentHvac |
| **消防扩展** | 消防设备专业参数(探测范围/联动/分区等) | EquipmentFire |
| **能源计量扩展** | 能源表计专业参数(表常数/精度/通讯等) | EquipmentEnergy |
| **健康评分** | 设备健康度评估结果 | EquipmentHealthScore |
| **故障历史** | 设备故障与维修记录 | EquipmentFailureHistory |
| **归属主体** | 设备归属方信息 | OwnershipEntity |
| **维保计划** | 定期保养计划已迁移至独立API | MaintenancePlan |
| **维保工单** | 维保执行工单(原维保记录升级) | MaintenanceTask |
---
## 二、领域模型
### 2.1 聚合根设计
#### Equipment设备台账
```java
@Entity
@Table(name = "asset_equipment")
@Data
public class Equipment {
@Id
private UUID id;
private UUID projectId;
private String equipmentCode; // 设备编码原code
private String equipmentName; // 设备名称原name
// 分类
private EquipmentType type; // 10种设备类型
private EquipmentStatus status; // ACTIVE/INACTIVE/MAINTENANCE/SCRAPPED
// 系统类型(新增)
private SystemType systemType; // 所属系统类型9种
// 品牌型号
private String brand;
private String model;
private String specifications;
private String serialNumber;
// 厂商信息
private String manufacturer;
private String supplier;
private String supplierPhone;
// 位置
private UUID spaceNodeId; // 关联空间节点
private String installationLocation; // 安装位置原locationDesc
// 时间
private LocalDate purchaseDate;
private LocalDate installDate;
private LocalDate warrantyDate;
private Integer lifespanYears; // 设计寿命(年)
// 维保
private Integer maintenanceCycle; // 保养周期(天)
private LocalDate lastMaintenanceDate;
private LocalDate nextMaintenanceDate;
// 负责人
private UUID managerId;
private String managerName;
private String contactPhone;
// 归属管理(新增)
private OwnershipType ownershipType; // SELF(自持)/DEVELOPER(开发商)/OWNER(业主)/VENDOR(厂商)
private UUID ownershipEntityId; // 归属主体ID
// 特种设备管理(新增)
private Boolean isSpecialEquipment; // 是否特种设备
private String specialEquipmentCode; // 特种设备使用登记证编号
private String specialEquipmentType; // 特种设备类型
private LocalDate specialInspectionDate; // 特种设备检验日期
private LocalDate nextSpecialInspectionDate; // 下次检验日期
// 能耗标准(新增)
private String energyStandard; // 能效标准
private BigDecimal ratedPower; // 额定功率(kW)
private BigDecimal energyEfficiencyRating; // 能效等级
// 二维码
private String qrCode;
// 设备照片/文档新增JSONB内嵌
@JdbcTypeCode(SqlTypes.JSON)
@Column(columnDefinition = "jsonb")
private List<AttachmentDTO> photos; // 设备照片列表
@JdbcTypeCode(SqlTypes.JSON)
@Column(columnDefinition = "jsonb")
private List<AttachmentDTO> documents; // 设备文档列表
// 扩展属性(JSONB)
private String attributes;
// 审计字段
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
```
**系统类型枚举(新增)**:
```java
public enum SystemType {
ELEVATOR_SYSTEM("电梯系统"),
HVAC_SYSTEM("暖通系统"),
FIRE_SYSTEM("消防系统"),
PLUMBING_SYSTEM("给排水系统"),
ELECTRICAL_SYSTEM("供配电系统"),
SECURITY_SYSTEM("安防系统"),
LANDSCAPE_SYSTEM("园林系统"),
DRAINAGE_SYSTEM("排污系统"),
OTHER("其他");
}
```
**归属类型枚举(新增)**:
```java
public enum OwnershipType {
SELF("自持"),
DEVELOPER("开发商"),
OWNER("业主"),
VENDOR("厂商");
}
```
**附件DTO**:
```java
@Data
public class AttachmentDTO {
private String name; // 文件名
private String url; // 文件URL
private String type; // 文件类型
private Long size; // 文件大小
private LocalDateTime uploadedAt; // 上传时间
}
```
**设备类型枚举**:
```java
// 设备类型枚举10种
public enum EquipmentType {
ELEVATOR("电梯"),
HVAC("暖通空调"),
FIRE_PROTECTION("消防设备"),
PLUMBING("给排水"),
ELECTRICAL("供配电"),
ENERGY_METER("能源计量"),
SECURITY("安防设备"),
LANDSCAPE("园林景观"),
KITCHEN("厨房设备"),
OTHER("其他设备");
}
```
#### EquipmentElevator电梯扩展表新增
```java
@Entity
@Table(name = "asset_equipment_elevator")
@Data
public class EquipmentElevator {
@Id
private UUID id;
private UUID equipmentId; // 关联设备ID
private UUID projectId;
private Integer loadCapacity; // 载重(kg)
private BigDecimal speed; // 速度(m/s)
private Integer floors; // 楼层数
private Integer stops; // 停站数
private String doorType; // 门类型
private String driveType; // 驱动方式
private String registrationCode; // 使用登记证编号
private LocalDate inspectionExpiryDate; // 检验有效期
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
```
#### EquipmentHvac暖通扩展表新增
```java
@Entity
@Table(name = "asset_equipment_hvac")
@Data
public class EquipmentHvac {
@Id
private UUID id;
private UUID equipmentId;
private UUID projectId;
private BigDecimal coolingCapacity; // 制冷量(kW)
private BigDecimal heatingCapacity; // 制热量(kW)
private BigDecimal cop; // 能效比
private String refrigerant; // 制冷剂
private BigDecimal airflow; // 风量(m³/h)
private String pipeType; // 管道类型
private BigDecimal pipeLength; // 管道长度(m)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
```
#### EquipmentFire消防扩展表新增
```java
@Entity
@Table(name = "asset_equipment_fire")
@Data
public class EquipmentFire {
@Id
private UUID id;
private UUID equipmentId;
private UUID projectId;
private String detectionRange; // 探测范围
private Boolean hasLinkage; // 是否联动
private String fireZone; // 防火分区
private String extinguishingAgent; // 灭火剂类型
private BigDecimal agentCapacity; // 灭火剂容量
private LocalDate lastTestDate; // 上次测试日期
private LocalDate nextTestDate; // 下次测试日期
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
```
#### EquipmentEnergy能源计量扩展表新增
```java
@Entity
@Table(name = "asset_equipment_energy")
@Data
public class EquipmentEnergy {
@Id
private UUID id;
private UUID equipmentId;
private UUID projectId;
private String meterConstant; // 表常数
private String accuracyClass; // 精度等级
private String communicationProtocol; // 通讯协议
private String meterCategory; // 表计类别
private BigDecimal ctRatio; // CT变比
private BigDecimal ptRatio; // PT变比
private LocalDate calibrationDate; // 校验日期
private LocalDate nextCalibrationDate; // 下次校验日期
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
```
#### EquipmentHealthScore健康评分新增Beta
```java
@Entity
@Table(name = "asset_equipment_health_score")
@Data
public class EquipmentHealthScore {
@Id
private UUID id;
private UUID equipmentId;
private UUID projectId;
private BigDecimal score; // 健康评分(0-100)
private String level; // 健康等级: EXCELLENT/GOOD/FAIR/POOR
private BigDecimal mtbf; // 平均故障间隔时间(MTBF, 小时)
private BigDecimal mttr; // 平均修复时间(MTTR, 小时)
private Integer totalFailures; // 总故障次数
private Integer totalMaintenance; // 总维保次数
private BigDecimal availability; // 可用率(%)
private LocalDateTime calculatedAt; // 计算时间
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
```
> **Beta标注**: 健康评分算法基于MTBF/MTTR计算当前为Beta版本评分模型和权重参数需根据实际运行数据持续优化。
#### EquipmentFailureHistory故障历史新增
```java
@Entity
@Table(name = "asset_equipment_failure_history")
@Data
public class EquipmentFailureHistory {
@Id
private UUID id;
private UUID equipmentId;
private UUID projectId;
private LocalDateTime failureTime; // 故障时间
private String failureType; // 故障类型
private String failureDescription; // 故障描述
private String faultCause; // 故障原因
private String solution; // 解决方案
private UUID repairWorkOrderId; // 关联维修工单
private LocalDateTime resolvedTime; // 修复时间
private Long downtimeMinutes; // 停机时长(分钟)
private String reportedBy; // 报告人
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
```
#### OwnershipEntity归属主体新增
```java
@Entity
@Table(name = "asset_ownership_entity")
@Data
public class OwnershipEntity {
@Id
private UUID id;
private UUID projectId;
private OwnershipType type; // SELF/DEVELOPER/OWNER/VENDOR
private String name; // 归属方名称
private String contactPerson; // 联系人
private String contactPhone; // 联系电话
private String address; // 地址
private String description; // 描述
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
```
#### MaintenancePlan维保计划
> **注意**: 维保计划已迁移至独立API`/api/asset/maintenance-plans`),不再嵌套在设备管理接口下。
```java
@Entity
@Table(name = "asset_maintenance_plan")
@Data
public class MaintenancePlan {
@Id
private UUID id;
private UUID projectId;
private UUID equipmentId; // 关联设备
private String name;
private MaintenanceType type; // DAILY/WEEKLY/MONTHLY/QUARTERLY/YEARLY
// 周期
private Integer cycleDays; // 周期天数
private String cronExpression; // Cron表达式
// 内容
private String content; // 保养内容描述
private String checkItems; // 检查项清单(JSON)
// 负责人
private UUID maintainerId;
private String maintainerName;
// 提醒
private Integer remindDays; // 提前提醒天数
// 状态
private Boolean enabled;
// 审计字段
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
```
#### MaintenanceTask维保工单原MaintenanceRecord升级
> **注意**: 原MaintenanceRecord已升级为MaintenanceTask维保工单增加了工单状态流转、费用记录、签名确认等字段。
```java
@Entity
@Table(name = "asset_maintenance_task")
@Data
public class MaintenanceTask {
@Id
private UUID id;
private UUID equipmentId;
private UUID planId; // 关联计划
private UUID projectId;
// 工单信息
private String taskCode; // 工单编号
private MaintenanceTaskStatus status; // PENDING/IN_PROGRESS/COMPLETED/CANCELLED
// 维保信息
private LocalDate maintenanceDate;
private MaintenanceType type;
private String content;
// 执行人
private UUID maintainerId;
private String maintainerName;
// 结果
private MaintenanceResult result; // NORMAL/ABNORMAL
private String remark;
private String images;
// 检查项结果
private String checkResults; // [{"item":"检查A","result":"PASS"}]
// 费用(新增)
private BigDecimal laborCost; // 人工费
private BigDecimal partsCost; // 材料费
private BigDecimal totalCost; // 总费用
// 签名确认(新增)
private String signature; // 签名图片URL
// 关联工单
private UUID workOrderId; // 异常时关联的维修工单
// 下次维保
private LocalDate nextMaintenanceDate;
// 审计字段
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
public enum MaintenanceTaskStatus {
PENDING("待执行"),
IN_PROGRESS("执行中"),
COMPLETED("已完成"),
CANCELLED("已取消");
}
```
---
## 三、设备与工单联动
### 3.1 故障自动报修
```java
@Component
public class EquipmentFaultHandler {
@Autowired
private WorkOrderService workOrderService;
@EventListener
public void onEquipmentFault(EquipmentFaultEvent event) {
Equipment equipment = event.getEquipment();
// 创建设备维修工单
WorkOrder workOrder = new WorkOrder();
workOrder.setOrderType(WorkOrderType.REPAIR);
workOrder.setTitle("设备故障: " + equipment.getEquipmentName());
workOrder.setDescription(event.getFaultDescription());
workOrder.setEquipmentId(equipment.getId());
workOrder.setSpaceNodeId(equipment.getSpaceNodeId());
workOrder.setPriority(WorkOrderPriority.HIGH);
workOrder.setSource(WorkOrderSource.FAULT);
workOrderService.create(workOrder);
// 更新设备状态
equipment.setStatus(EquipmentStatus.INACTIVE);
equipmentRepository.save(equipment);
}
}
```
### 3.2 维保到期提醒
```java
@Component
public class MaintenanceReminderJob {
@Scheduled(cron = "0 0 9 * * ?") // 每天9点执行
public void remindUpcomingMaintenance() {
// 查询7天内到期的维保计划
LocalDate targetDate = LocalDate.now().plusDays(7);
List<Equipment> equipments = equipmentRepository
.findByNextMaintenanceDateBeforeAndStatus(targetDate, EquipmentStatus.ACTIVE);
for (Equipment equipment : equipments) {
notificationService.sendMaintenanceReminder(equipment);
}
}
}
```
---
## 四、实现状态与差异
### 4.1 实现状态
| 功能模块 | 实现状态 | 备注 |
|---------|---------|------|
| Equipment | 🟢 已实现 | 基础CRUD字段名已更新(equipmentCode/equipmentName/installationLocation) |
| EquipmentElevator | 🟢 已实现 | 电梯专业扩展表 |
| EquipmentHvac | 🟢 已实现 | 暖通专业扩展表 |
| EquipmentFire | 🟢 已实现 | 消防专业扩展表 |
| EquipmentEnergy | 🟢 已实现 | 能源计量扩展表 |
| EquipmentHealthScore | 🟢 已实现(Beta) | 健康评分MTBF/MTTR算法Beta版本 |
| EquipmentFailureHistory | 🟢 已实现 | 故障历史记录 |
| OwnershipEntity | 🟢 已实现 | 归属主体管理 |
| MaintenancePlan | 🟢 已实现 | 已迁移至独立API |
| MaintenanceTask | 🟢 已实现 | 原MaintenanceRecord升级为维保工单 |
| 设备二维码 | 🟢 已实现 | 支持扫码查看设备信息 |
| Excel导入导出 | 🟢 已实现 | 设备台账Excel导入导出 |
| 特种设备管理 | 🟢 已实现 | 特种设备登记证/检验日期 |
| 能耗标准字段 | 🟢 已实现 | 能效标准/额定功率/能效等级 |
| 设备照片/文档 | 🟢 已实现 | JSONB内嵌存储 |
| 故障自动工单 | 🟡 部分实现 | 设备状态变更可关联工单 |
| 维保完成率计算 | 🟡 部分实现 | TODO: 当前暂返回1.0,需实现真实计算 |
| IoT集成 | 🔴 未实现 | 待开发 |
### 4.2 与设计方案的差异
| 设计项 | 设计方案 | 现有实现 | 差异分析 |
|--------|----------|----------|----------|
| **维保归属** | ether-asset | ether-asset | 已修正:维保实体已统一在 ether-asset |
| **设备管理** | ether-asset | ether-asset | 已修正:设备实体已统一在 ether-asset |
| **设备联动** | 故障自动工单 | 部分实现 | 功能完善中 |
| **设备类型** | 8种枚举 | 10种枚举 | 已扩展新增ENERGY_METER/LANDSCAPE/KITCHEN |
| **设备状态** | NORMAL/FAULT | ACTIVE/INACTIVE | 已修正:语义更清晰 |
| **专业扩展** | 无 | 4张扩展表 | 已新增Elevator/Hvac/Fire/Energy |
| **健康评分** | 无 | Beta实现 | 已新增基于MTBF/MTTR |
| **归属管理** | 无 | OwnershipEntity | 已新增4种归属类型 |
| **维保记录** | MaintenanceRecord | MaintenanceTask | 已升级:增加工单状态/费用/签名 |
| **字段命名** | code/name/locationDesc | equipmentCode/equipmentName/installationLocation | 已修正:命名更规范 |
### 4.3 改进计划
| 优先级 | 改进项 | 说明 |
|--------|--------|------|
| P0 | 维保完成率计算 | 当前暂返回1.0,需实现真实计算逻辑 |
| P1 | 健康评分优化 | Beta版评分模型需根据实际运行数据持续优化 |
| P1 | 完善故障自动工单 | 设备状态变更时自动创建维修工单 |
| P2 | 完善设备二维码功能 | 支持扫码报修 |
| P3 | IoT传感器接入 | 对接传感器数据 |
---
## 五、数据库表结构
```sql
-- 设备台账表
CREATE TABLE asset_equipment (
id UUID PRIMARY KEY,
project_id UUID NOT NULL,
equipment_code VARCHAR(50) NOT NULL,
equipment_name VARCHAR(100) NOT NULL,
type VARCHAR(20) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
system_type VARCHAR(30),
brand VARCHAR(100),
model VARCHAR(100),
specifications VARCHAR(500),
serial_number VARCHAR(100),
manufacturer VARCHAR(100),
supplier VARCHAR(100),
supplier_phone VARCHAR(20),
space_node_id UUID,
installation_location VARCHAR(255),
purchase_date DATE,
install_date DATE,
warranty_date DATE,
lifespan_years INTEGER,
maintenance_cycle INTEGER,
last_maintenance_date DATE,
next_maintenance_date DATE,
manager_id UUID,
manager_name VARCHAR(100),
contact_phone VARCHAR(20),
ownership_type VARCHAR(20),
ownership_entity_id UUID,
is_special_equipment BOOLEAN DEFAULT FALSE,
special_equipment_code VARCHAR(100),
special_equipment_type VARCHAR(50),
special_inspection_date DATE,
next_special_inspection_date DATE,
energy_standard VARCHAR(100),
rated_power DECIMAL(10,2),
energy_efficiency_rating DECIMAL(3,1),
qr_code VARCHAR(255),
photos JSONB,
documents JSONB,
attributes JSONB,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(project_id, equipment_code)
);
-- 电梯扩展表
CREATE TABLE asset_equipment_elevator (
id UUID PRIMARY KEY,
equipment_id UUID NOT NULL REFERENCES asset_equipment(id),
project_id UUID NOT NULL,
load_capacity INTEGER,
speed DECIMAL(5,2),
floors INTEGER,
stops INTEGER,
door_type VARCHAR(50),
drive_type VARCHAR(50),
registration_code VARCHAR(100),
inspection_expiry_date DATE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 暖通扩展表
CREATE TABLE asset_equipment_hvac (
id UUID PRIMARY KEY,
equipment_id UUID NOT NULL REFERENCES asset_equipment(id),
project_id UUID NOT NULL,
cooling_capacity DECIMAL(10,2),
heating_capacity DECIMAL(10,2),
cop DECIMAL(5,2),
refrigerant VARCHAR(50),
airflow DECIMAL(10,2),
pipe_type VARCHAR(50),
pipe_length DECIMAL(10,2),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 消防扩展表
CREATE TABLE asset_equipment_fire (
id UUID PRIMARY KEY,
equipment_id UUID NOT NULL REFERENCES asset_equipment(id),
project_id UUID NOT NULL,
detection_range VARCHAR(100),
has_linkage BOOLEAN,
fire_zone VARCHAR(100),
extinguishing_agent VARCHAR(50),
agent_capacity DECIMAL(10,2),
last_test_date DATE,
next_test_date DATE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 能源计量扩展表
CREATE TABLE asset_equipment_energy (
id UUID PRIMARY KEY,
equipment_id UUID NOT NULL REFERENCES asset_equipment(id),
project_id UUID NOT NULL,
meter_constant VARCHAR(50),
accuracy_class VARCHAR(20),
communication_protocol VARCHAR(50),
meter_category VARCHAR(50),
ct_ratio DECIMAL(10,2),
pt_ratio DECIMAL(10,2),
calibration_date DATE,
next_calibration_date DATE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 健康评分表
CREATE TABLE asset_equipment_health_score (
id UUID PRIMARY KEY,
equipment_id UUID NOT NULL REFERENCES asset_equipment(id),
project_id UUID NOT NULL,
score DECIMAL(5,2),
level VARCHAR(20),
mtbf DECIMAL(12,2),
mttr DECIMAL(12,2),
total_failures INTEGER DEFAULT 0,
total_maintenance INTEGER DEFAULT 0,
availability DECIMAL(5,2),
calculated_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 故障历史表
CREATE TABLE asset_equipment_failure_history (
id UUID PRIMARY KEY,
equipment_id UUID NOT NULL REFERENCES asset_equipment(id),
project_id UUID NOT NULL,
failure_time TIMESTAMP NOT NULL,
failure_type VARCHAR(50),
failure_description TEXT,
fault_cause TEXT,
solution TEXT,
repair_work_order_id UUID,
resolved_time TIMESTAMP,
downtime_minutes BIGINT,
reported_by VARCHAR(100),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 归属主体表
CREATE TABLE asset_ownership_entity (
id UUID PRIMARY KEY,
project_id UUID NOT NULL,
type VARCHAR(20) NOT NULL,
name VARCHAR(100) NOT NULL,
contact_person VARCHAR(100),
contact_phone VARCHAR(20),
address VARCHAR(255),
description TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 维保计划表
CREATE TABLE asset_maintenance_plan (
id UUID PRIMARY KEY,
project_id UUID NOT NULL,
equipment_id UUID NOT NULL,
name VARCHAR(100) NOT NULL,
type VARCHAR(20) NOT NULL,
cycle_days INTEGER,
cron_expression VARCHAR(50),
content TEXT,
check_items JSONB,
maintainer_id UUID,
maintainer_name VARCHAR(100),
remind_days INTEGER DEFAULT 3,
enabled BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 维保工单表(原维保记录表升级)
CREATE TABLE asset_maintenance_task (
id UUID PRIMARY KEY,
equipment_id UUID NOT NULL,
plan_id UUID,
project_id UUID NOT NULL,
task_code VARCHAR(50),
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
maintenance_date DATE NOT NULL,
type VARCHAR(20) NOT NULL,
content TEXT,
maintainer_id UUID,
maintainer_name VARCHAR(100),
result VARCHAR(20),
remark VARCHAR(500),
images TEXT,
check_results JSONB,
labor_cost DECIMAL(10,2),
parts_cost DECIMAL(10,2),
total_cost DECIMAL(10,2),
signature VARCHAR(255),
work_order_id UUID,
next_maintenance_date DATE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 创建索引
CREATE INDEX idx_equipment_project ON asset_equipment(project_id);
CREATE INDEX idx_equipment_type ON asset_equipment(type);
CREATE INDEX idx_equipment_status ON asset_equipment(status);
CREATE INDEX idx_equipment_space ON asset_equipment(space_node_id);
CREATE INDEX idx_equipment_next_maintenance ON asset_equipment(next_maintenance_date);
CREATE INDEX idx_equipment_system_type ON asset_equipment(system_type);
CREATE INDEX idx_equipment_ownership ON asset_equipment(ownership_entity_id);
CREATE INDEX idx_equipment_special ON asset_equipment(is_special_equipment) WHERE is_special_equipment = TRUE;
CREATE INDEX idx_maintenance_plan_equipment ON asset_maintenance_plan(equipment_id);
CREATE INDEX idx_maintenance_task_equipment ON asset_maintenance_task(equipment_id);
CREATE INDEX idx_maintenance_task_status ON asset_maintenance_task(status);
CREATE INDEX idx_failure_history_equipment ON asset_equipment_failure_history(equipment_id);
CREATE INDEX idx_health_score_equipment ON asset_equipment_health_score(equipment_id);
```
---
**文档维护**: 本领域技术方案由 ether-asset 服务负责人维护

View File

@ -2,7 +2,10 @@
**领域编号**: 4.4
**微服务**: ether-finance
**最后更新**: 2026-02-10
**最后更新**: 2026-04-26
> **更新记录**:
> - 2026-04-26: 反向同步实际代码实现到文档。主要变更:修正 FeeItem/FeeBill/FeePayment 的实现状态标注为"未实现"(原标注为"已实现"与实际代码不符);修正费用催缴/滞纳金的实现状态标注为"未实现"新增能耗计量管理章节EnergyMeter/EnergyConsumption当前在 module-mdm 中实现新增能源类型枚举EnergyType新增记录方式枚举RecordMethod明确能耗管理与财务计费的边界。
---
@ -19,12 +22,14 @@
### 1.2 核心概念
| 概念 | 说明 | 对应实体 |
|------|------|----------|
| **收费项目** | 可收费的项目定义 | FeeItem |
| **账单** | 应收费用单据 | FeeBill |
| **支付记录** | 支付流水记录 | FeePayment |
| **退款** | 退款申请与处理 | FeeRefund |
| 概念 | 说明 | 对应实体 | 实现状态 |
|------|------|----------|----------|
| **收费项目** | 可收费的项目定义 | FeeItem | 未实现 |
| **账单** | 应收费用单据 | FeeBill | 未实现 |
| **支付记录** | 支付流水记录 | FeePayment | 未实现 |
| **退款** | 退款申请与处理 | FeeRefund | 未实现 |
| **能源计量点** | 能耗数据采集点 | EnergyMeter | 已实现(module-mdm) |
| **能耗记录** | 抄表数据与费用计算 | EnergyConsumption | 已实现(module-mdm) |
---
@ -308,41 +313,193 @@ public class FeeReminderJob {
---
## 四、实现状态与差异
## 四、能耗计量管理(已实现,当前在 module-mdm 中)
### 4.1 实现状态
> **边界说明**: 能耗计量管理当前在 `module-mdm` 中实现属于基础数据管理MDM范畴。能耗数据采集和简单费用计算是已实现功能但正式的账单生成、支付处理等财务功能属于本领域module-finance的待实现功能。两者通过 EnergyConsumption.amount 对接:能耗模块提供用量和计算值,财务模块将其作为按用量计费的账单数据来源。
### 4.1 EnergyMeter能源计量点
```java
@Entity
@Table(name = "ops_energy_meter")
@Data
public class EnergyMeter {
@Id
private UUID id;
private UUID projectId;
private String meterCode; // 计量点编码自动生成EM + yyyyMMddHHmmss
private String meterName; // 计量点名称
private EnergyType energyType; // 能源类型枚举
private UUID spaceNodeId; // 关联空间节点
private String installationLocation;// 安装位置
private BigDecimal ratedCapacity; // 额定容量
private BigDecimal unitPrice; // 单价精度4位小数用于费用计算
private Status status; // ACTIVE/INACTIVE
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
```
**能源类型枚举**:
```java
public enum EnergyType {
LIGHTING("照明插座用电"),
HVAC("空调用电"),
POWER("动力用电"),
SPECIAL("特殊用电"),
WATER("给排水"),
GAS("燃气");
}
```
> **已知问题**: 前后端能源类型枚举不一致。后端为 LIGHTING/HVAC/POWER/SPECIAL/WATER/GAS前端为 ELECTRICITY/WATER/GAS/CENTRAL_HEATING/CENTRAL_COOLING需对齐。
### 4.2 EnergyConsumption能耗记录
```java
@Entity
@Table(name = "ops_energy_consumption")
@Data
public class EnergyConsumption {
@Id
private UUID id;
private UUID projectId;
private UUID meterId; // 关联计量点
private LocalDate consumptionDate; // 记录日期
private BigDecimal previousReading; // 上次读数
private BigDecimal currentReading; // 当前读数
private BigDecimal consumption; // 消耗量 = current - previous
private BigDecimal amount; // 费用 = consumption x unitPrice
private UUID recordedBy; // 记录人
private RecordMethod recordMethod; // 记录方式
private String remarks; // 备注
private LocalDateTime createdAt;
}
```
**记录方式枚举**:
```java
public enum RecordMethod {
MANUAL("手动录入"),
IOT("IoT自动采集");
}
```
### 4.3 能耗业务规则
| 规则 | 说明 |
|------|------|
| 编码自动生成 | 格式EM + yyyyMMddHHmmss冲突时追加后缀 |
| 仪表状态校验 | 只能对 ACTIVE 状态的仪表进行抄表 |
| 读数递增校验 | 当前读数不能小于上次读数 |
| 自动获取上次读数 | 从最近一条记录获取 previousReading首次为 0 |
| 自动计算消耗量 | consumption = currentReading - previousReading |
| 自动计算费用 | amount = consumption x meter.unitPrice单价为空时为 0 |
| 默认手动录入 | recordMethod 默认设为 MANUAL |
| 软删除 | 删除操作将状态设为 INACTIVE |
### 4.4 能耗 API 接口
```
基础路径: /api/ops/energy
计量点管理:
POST /meters 创建计量点
GET /meters 查询计量点列表projectId必填energyType可选
GET /meters/{id} 获取计量点详情
PUT /meters/{id} 更新计量点
DELETE /meters/{id} 删除计量点(软删除)
能耗记录:
POST /consumption 录入能耗数据
GET /consumption/{meterId} 查询能耗记录startDate/endDate可选
能耗统计:
GET /statistics/by-type 按类型统计消耗
GET /statistics/unit-consumption 单方能耗
```
> **已知缺陷**: `getConsumptionByType()` 当前将项目总消耗全部归入 LIGHTING 类型,未按 meter.energyType 真正分项汇总,需修复。
### 4.5 能耗与财务的边界
```
EnergyMeter (module-mdm) FeeItem (module-finance待实现)
| |
v v
EnergyConsumption --费用计算--> FeeBill (自动/手动生成,待实现)
(抄表数据) |
v
FeePayment (支付记录,待实现)
|
v
FeeRefund (退款记录,待实现)
```
- **能耗模块module-mdm职责**: 计量点管理、抄表录入、消耗量计算、简单费用计算consumption x unitPrice
- **财务模块module-finance待实现职责**: 收费项目管理、账单生成、支付处理、催缴通知、滞纳金计算、退款处理
---
## 五、实现状态与差异
### 5.1 实现状态
| 功能模块 | 实现状态 | 备注 |
|---------|---------|------|
| FeeItem | 🟢 已实现 | 基础CRUD |
| FeeBill | 🟢 已实现 | 基础CRUD |
| FeePayment | 🟢 已实现 | 基础CRUD |
| 费用催缴 | 🟢 已实现 | 定时任务 |
| 滞纳金计算 | 🟢 已实现 | 自动计算 |
| FeeItem | 🔴 未实现 | 无 module-finance 模块,无 fin_* 表 |
| FeeBill | 🔴 未实现 | 无 FeeBill 实体/服务/控制器 |
| FeePayment | 🔴 未实现 | 无 FeePayment 实体/服务/控制器 |
| FeeRefund | 🔴 未实现 | 原设计即标注未实现 |
| 费用催缴 | 🔴 未实现 | 无 FeeReminderJob 定时任务 |
| 滞纳金计算 | 🔴 未实现 | 无计算逻辑 |
| EnergyMeter | 🟢 已实现 | 在 module-mdm 中实现 |
| EnergyConsumption | 🟢 已实现 | 在 module-mdm 中实现,含简单费用计算 |
| 能耗统计分析 | 🟢 已实现(有缺陷) | 按类型统计未真正分项汇总 |
| 退款功能 | 🔴 未实现 | 待开发 |
| 财务报表 | 🔴 未实现 | 待开发 |
| 支付网关 | 🔴 未实现 | 待对接 |
### 4.2 与设计方案的差异
### 5.2 与设计方案的差异
| 设计项 | 设计方案 | 现有实现 | 差异分析 |
|--------|----------|----------|----------|
| **实体归属** | ether-finance | ether-finance + ether-mdm重复 | ❌ 存在重复实体 |
| **支付网关** | 对接微信/支付宝 | 未对接 | 🔴 功能缺失 |
| **退款** | 退款流程 | 未实现 | 🔴 功能缺失 |
| **FeeItem** | 完整实体定义 | 不存在 | 完全缺失,需新建 module-finance |
| **FeeBill** | 完整实体定义 | 不存在 | 完全缺失 |
| **FeePayment** | 完整实体定义 | 不存在 | 完全缺失 |
| **FeeRefund** | 完整实体定义 | 不存在 | 完全缺失 |
| **费用催缴** | FeeReminderJob | 不存在 | 完全缺失 |
| **滞纳金** | 自动计算逻辑 | 不存在 | 完全缺失 |
| **能耗管理** | 未在设计文档中 | module-mdm中已实现 | 设计文档遗漏,需补充 |
| **能源类型** | FeeType(8种) | EnergyType(6种) | 实际更细分(按用电分项),但缺少供暖/物业费 |
| **前后端枚举** | 统一 | 不一致 | 后端LIGHTING/HVAC/POWER/SPECIAL/WATER/GAS vs 前端ELECTRICITY/WATER/GAS/CENTRAL_HEATING/CENTRAL_COOLING |
### 4.3 改进计划
### 5.3 改进计划
| 优先级 | 改进项 | 说明 |
|--------|--------|------|
| P1 | 清理重复实体 | 删除ether-mdm中的Fee相关实体 |
| P2 | 实现退款功能 | 退款申请、审批、执行流程 |
| P2 | 对接支付网关 | 集成微信支付、支付宝SDK |
| P0 | 创建 module-finance 模块 | 独立财务模块,包含 entity/service/controller/repository |
| P0 | 实现 FeeItem 收费项目 | 含完整字段和 CRUD API |
| P0 | 实现 FeeBill 账单 | 含账单自动生成和状态流转 |
| P0 | 实现 FeePayment 支付记录 | 线下收款登记、支付记录查询 |
| P0 | 能耗数据与账单对接 | 将 EnergyConsumption 的 amount 作为按用量计费的账单数据来源 |
| P1 | 修复能耗按类型统计 | getConsumptionByType() 应按 meter.energyType 真正分项汇总 |
| P1 | 统一前后端能源类型枚举 | 对齐后端和前端的 EnergyType 定义 |
| P1 | 实现费用催缴定时任务 | 到期提醒、逾期催缴、周汇总 |
| P1 | 实现滞纳金自动计算 | 逾期天数 x 日利率,不超过上限 |
| P1 | 实现退款功能 | 退款申请、审批、执行流程 |
| P2 | 对接支付网关 | 集成微信支付、支付宝 SDK |
| P2 | 财务报表 | 收费统计、欠费分析、收入趋势 |
---
## 五、数据库表结构
## 、数据库表结构
```sql
-- 收费项目表
@ -436,6 +593,49 @@ CREATE INDEX idx_fee_bill_status ON fin_fee_bill(status);
CREATE INDEX idx_fee_bill_owner ON fin_fee_bill(owner_id);
CREATE INDEX idx_fee_bill_due_date ON fin_fee_bill(due_date);
CREATE INDEX idx_fee_payment_bill ON fin_fee_payment(bill_id);
-- ============================================================
-- 以下为已实现的能耗计量相关表(在 module-mdm 中)
-- ============================================================
-- 能源计量点表(已实现)
CREATE TABLE ops_energy_meter (
id UUID PRIMARY KEY,
project_id UUID NOT NULL,
meter_code VARCHAR(50) NOT NULL,
meter_name VARCHAR(100) NOT NULL,
energy_type VARCHAR(20) NOT NULL,
space_node_id UUID,
installation_location VARCHAR(255),
rated_capacity NUMERIC(10,2),
unit_price NUMERIC(10,4),
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(project_id, meter_code)
);
-- 能耗记录表(已实现)
CREATE TABLE ops_energy_consumption (
id UUID PRIMARY KEY,
project_id UUID NOT NULL,
meter_id UUID NOT NULL REFERENCES ops_energy_meter(id),
consumption_date DATE NOT NULL,
previous_reading NUMERIC(12,2),
current_reading NUMERIC(12,2),
consumption NUMERIC(12,2) NOT NULL,
amount NUMERIC(10,2),
recorded_by UUID,
record_method VARCHAR(20) DEFAULT 'MANUAL',
remarks VARCHAR(1000),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 能耗表索引
CREATE INDEX idx_energy_meter_project ON ops_energy_meter(project_id);
CREATE INDEX idx_energy_meter_status ON ops_energy_meter(status);
CREATE INDEX idx_ec_meter_date ON ops_energy_consumption(meter_id, consumption_date);
CREATE INDEX idx_ec_project_date ON ops_energy_consumption(project_id, consumption_date);
```
---

View File

@ -2,7 +2,10 @@
**领域编号**: 4.5
**微服务**: ether-auth
**最后更新**: 2026-02-27
**最后更新**: 2026-04-26
> **更新记录**:
> - 2026-04-26: 反向同步实际代码实现到文档。主要变更Role.type 改为 SYSTEM/PROJECT/DEPARTMENT 三级分类Role.status 改为 RoleStatus 枚举Permission 结构简化parentCode 替代 parentIdUserRole 简化为 M2M 中间表OperationLog 更名为 AuditLog表名和字段结构变更新增实体EnterpriseUser/ProjectStaff/ProjectStaffRole/Resident/ResidentSpace/Space/UserProject/DataAccess/Dept/SysConfig新增 API部门管理/项目成员管理/数据访问授权/审计日志/系统配置/企业员工/用户项目关联);新增业务规则(登录失败锁定/密码强度校验/审计日志增强/项目成员角色/部门类型/住户认证角色体系改为数据库动态管理API路径去掉v1版本号。标注 TODO 项Permission 需增加菜单路由属性path/component/icon
---
@ -23,11 +26,20 @@
| 概念 | 说明 | 对应实体 |
|------|------|----------|
| **用户** | 系统登录账户 | User |
| **角色** | 权限集合 | Role |
| **权限** | 功能访问控制点 | Permission |
| **项目** | 多租户隔离单位 | Project |
| **访客凭证** | 临时访问授权 | VisitorCredential |
| **用户** | 系统登录账户,支持四种用户类型 | User |
| **企业员工** | 企业类型用户的扩展信息 | EnterpriseUser |
| **项目员工** | 项目类型员工的扩展信息,含班次、岗位状态 | ProjectStaff |
| **住户** | 业主/家属/租户,含认证流程 | Resident |
| **角色** | 权限集合,支持系统级/项目级/部门级 | Role |
| **权限** | 功能访问控制点,支持菜单/按钮/API三种类型 | Permission |
| **部门** | 组织架构节点,支持树形结构 | Dept |
| **用户-项目关联** | 用户参与项目的多对多关系 | UserProject |
| **项目员工角色** | 项目员工在项目中的角色分配 | ProjectStaffRole |
| **数据访问授权** | 细粒度数据访问控制记录(接口预留) | DataAccess |
| **审计日志** | 操作审计记录 | AuditLog |
| **房屋空间** | 项目下的房屋/空间信息 | Space |
| **住户-房屋关联** | 住户与房屋的绑定关系 | ResidentSpace |
| **系统配置** | 键值对形式的系统参数 | SysConfig |
---
@ -119,8 +131,8 @@ public class User {
private UUID id;
private String username; // 登录账号
private String password; // 加密密码
private String salt; // 密码盐值
private String password; // BCrypt加密密码
private String salt; // 密码盐值BCrypt模式下冗余
private String realName; // 真实姓名
private String phone;
@ -128,6 +140,9 @@ public class User {
private String avatar; // 头像URL
private UserStatus status; // ACTIVE/LOCKED/DISABLED
private UserType userType; // ENTERPRISE/PROJECT_STAFF/RESIDENT/CUSTOMER
private UUID deptId; // 所属部门ID
private LocalDateTime lastLoginTime;
private String lastLoginIp;
@ -149,6 +164,13 @@ public enum UserStatus {
LOCKED("锁定"),
DISABLED("禁用");
}
public enum UserType {
ENTERPRISE("企业员工"),
PROJECT_STAFF("项目员工"),
RESIDENT("住户"),
CUSTOMER("客户");
}
```
#### Role角色
@ -166,15 +188,13 @@ public class Role {
private String code; // 角色编码
private String description; // 角色描述
private RoleType type; // SYSTEM(系统预设)/CUSTOM(自定义)
private RoleType type; // SYSTEM(系统级)/PROJECT(项目级)/DEPARTMENT(部门级)
// 数据权限范围(四级)
private DataScope dataScope; // ALL/PROJECT/DEPARTMENT/SELF
// 新增:业务属性
private BusinessType businessType; // 业务类型
private TerminalType terminalType; // 终端类型限制
private Boolean isFrontline; // 是否一线执行人员
// 角色状态(枚举替代布尔值)
private RoleStatus status; // ENABLED(启用)/DISABLED(禁用)
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
@ -184,14 +204,19 @@ public class Role {
)
private List<Permission> permissions;
private Boolean enabled;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
public enum RoleType {
SYSTEM("系统预设"),
CUSTOM("自定义");
SYSTEM("系统级"),
PROJECT("项目级"),
DEPARTMENT("部门级");
}
public enum RoleStatus {
ENABLED("启用"),
DISABLED("禁用");
}
// 数据权限范围枚举(四级)
@ -201,26 +226,10 @@ public enum DataScope {
DEPARTMENT("本部门数据"),
SELF("仅本人数据");
}
// 业务类型枚举
public enum BusinessType {
MANAGEMENT("管理类"),
ENGINEERING("工程类"),
SECURITY("安保类"),
CLEANING("保洁类"),
CUSTOMER_SERVICE("客服类"),
FINANCE("财务类");
}
// 终端类型枚举
public enum TerminalType {
ALL("全终端"),
ADMIN_ONLY("仅管理后台"),
MOBILE_ONLY("仅移动端"),
OWNER_APP("业主端");
}
```
> **注意**: 实际代码中角色通过数据库动态管理,无硬编码预定义角色列表。原设计中的 businessType/terminalType/isFrontline 业务属性字段未实现,如需使用需后续补充。
#### Permission权限
```java
@ -230,62 +239,40 @@ public enum TerminalType {
public class Permission {
@Id
private UUID id;
private UUID projectId; // 项目隔离NULL表示系统级
private String name; // 权限名称
private String code; // 权限编码: module:resource:action
private PermissionType type; // MENU/BUTTON/API
private String type; // 权限类型: MENU/BUTTON/APIString类型
// 模块和资源信息
private String module; // 模块标识: system/mdm/ops/finance
private String resource; // 资源类型: user/role/work_order
private String action; // 操作类型: view/create/edit/delete/assign
// 资源信息
private String resource; // 资源路径API类型
private String method; // HTTP方法API类型
// 菜单属性
private String path; // 路由路径
private String component; // 组件路径
private String icon; // 图标
private Integer sortOrder; // 排序
private String description; // 权限描述
// 树形结构
private UUID parentId; // 父权限ID
private Integer level; // 层级
// 树形结构简化parentCode替代parentId
private String parentCode; // 父权限代码(替代原 parentId UUID引用
private Integer sortOrder; // 排序序号
// API属性
private String apiMethod; // GET/POST/PUT/DELETE
private String apiPath; // API路径
@ManyToMany(fetch = FetchType.LAZY, mappedBy = "permissions")
private List<Role> roles;
private Boolean enabled;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
public enum PermissionType {
MENU("菜单"),
BUTTON("按钮"),
API("接口");
}
```
> **TODO**: Permission 当前实现缺少菜单路由属性path/component/icon以及模块/资源/操作拆分字段module/resource/action需后续补充以支持前端菜单渲染和权限树展示。
### 3.2 关联实体
#### UserRole用户角色关联
> **注意**: 实际代码中 UserRole 已简化为 JPA 自动管理的 M2M 中间表 `auth_user_role`,仅包含 user_id 和 role_id 两个外键列,去掉了原设计中的 projectId 和 isDefault 字段。
```java
@Entity
@Table(name = "auth_user_role")
@Data
public class UserRole {
@Id
private UUID id;
private UUID userId;
private UUID roleId;
private UUID projectId; // 项目隔离
private Boolean isDefault; // 是否默认角色
private LocalDateTime createdAt;
}
// auth_user_role 关联表JPA自动管理无独立实体
// 字段: user_id (FK → auth_user), role_id (FK → auth_role)
```
#### RolePermission角色权限关联
@ -788,117 +775,89 @@ public class VisitorCredentialService {
---
## 九、操作日志审计
## 九、审计日志
### 9.1 操作日志设计
### 9.1 审计日志设计
> **注意**: 实际代码中操作日志已更名为审计日志AuditLog表名从 `auth_operation_log` 改为 `sys_audit_log`字段结构也有较大变更增加了目标追踪targetType/targetId、操作内容content、操作结果result、租户IDtenantId等字段并采用异步持久化。
```java
@Entity
@Table(name = "auth_operation_log")
@Table(name = "sys_audit_log")
@Data
public class OperationLog {
public class AuditLog {
@Id
private UUID id;
private UUID projectId;
private UUID userId;
private String username;
private String realName;
private String username; // 操作用户名
private String operation; // 操作描述
private String module; // 模块标识
private ActionType action; // 操作类型枚举
private String module;
private String operation;
private String description;
// 目标追踪(新增)
private String targetType; // 目标类型
private String targetId; // 目标ID
private String requestMethod;
private String requestUrl;
private String requestParams;
private String requestBody;
// 操作详情(替代原 requestBody/responseBody
private String content; // 操作内容
private String params; // 请求参数
private String result; // 操作结果
private Integer responseStatus;
private String responseBody;
private Long executionTime;
private String ipAddress;
private String userAgent;
private String requestUrl;
private String requestMethod;
private Integer executionTimeMs; // 执行耗时(ms)
private Boolean hasError;
private String errorMessage;
private AuditStatus status; // SUCCESS/FAIL
private String errorMsg; // 错误信息
private UUID tenantId; // 租户ID新增
private LocalDateTime createdAt;
}
public enum ActionType {
CREATE, UPDATE, DELETE, QUERY, VIEW,
LOGIN, LOGOUT, EXPORT, IMPORT, ASSIGN, REVOKE;
}
public enum AuditStatus {
SUCCESS("成功"),
FAIL("失败");
}
```
### 9.2 日志记录切面
### 9.2 日志记录注解
```java
@Aspect
@Component
@Slf4j
public class OperationLogAspect {
@Autowired
private OperationLogRepository operationLogRepository;
@Around("@annotation(operationLog)")
public Object around(ProceedingJoinPoint point, LogOperation operationLog) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
UUID userId = SecurityUtils.getCurrentUserId();
String username = SecurityUtils.getCurrentUsername();
OperationLog logEntry = new OperationLog();
logEntry.setId(UUID.randomUUID());
logEntry.setProjectId(ProjectContextHolder.getCurrentProjectId());
logEntry.setUserId(userId);
logEntry.setUsername(username);
logEntry.setModule(operationLog.module());
logEntry.setOperation(operationLog.operation());
logEntry.setDescription(operationLog.description());
logEntry.setRequestMethod(request.getMethod());
logEntry.setRequestUrl(request.getRequestURI());
logEntry.setIpAddress(getClientIp(request));
logEntry.setUserAgent(request.getHeader("User-Agent"));
logEntry.setCreatedAt(LocalDateTime.now());
long startTime = System.currentTimeMillis();
try {
Object result = point.proceed();
logEntry.setResponseStatus(200);
logEntry.setHasError(false);
return result;
} catch (Exception e) {
logEntry.setHasError(true);
logEntry.setErrorMessage(e.getMessage());
throw e;
} finally {
logEntry.setExecutionTime(System.currentTimeMillis() - startTime);
operationLogRepository.save(logEntry);
}
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation {
String module();
public @interface OperationLog {
String operation();
String description();
String businessType() default "";
String module();
AuditLog.ActionType action();
}
```
> **业务规则**:
> - 审计日志通过 `@OperationLog` 注解 + AOP 切面自动记录
> - 采用异步持久化(`auditLogExecutor` 线程池),避免影响业务性能
> - 查询窗口强制限制30天超过30天的查询自动截断起始时间
> - 归档策略90天以上日志归档当前实现为直接删除TODO: 导出至对象存储)
> - 记录模块USER / ROLE / PERMISSION / PROJECT / AUTH / DEPT / PROJECT_MEMBER / SYSTEM
---
## 十、API 接口
> **注意**: 实际代码中 API 路径已去掉 v1 版本号,统一为 `/api/auth` 前缀。
### 10.1 认证接口
```java
@RestController
@RequestMapping("/api/v1/auth")
@RequestMapping("/api/auth")
@Tag(name = "认证管理")
public class AuthController {
@ -910,29 +869,30 @@ public class AuthController {
@Operation(summary = "用户登出")
public Result<Void> logout();
@PostMapping("/refresh-token")
@PostMapping("/refresh")
@Operation(summary = "刷新Token")
public Result<TokenResponse> refreshToken(@RequestBody RefreshTokenRequest request);
public Result<TokenResponse> refreshToken();
@GetMapping("/current-user")
@GetMapping("/me")
@Operation(summary = "获取当前用户信息")
public Result<UserVO> getCurrentUser();
@PostMapping("/change-password")
@Operation(summary = "修改密码")
public Result<Void> changePassword(@RequestBody @Valid ChangePasswordRequest request);
@GetMapping("/user-permissions")
@Operation(summary = "获取用户权限信息")
public Result<UserPermissionsResponse> getUserPermissions();
}
```
> **登录流程增强**(实际代码新增):
> 1. 检查登录失败锁定Redis5次失败锁定10分钟
> 2. 查询用户(含角色)
> 3. 验证密码BCrypt
> 4. 检查用户状态LOCKED/DISABLED拒绝
> 5. 收集所有角色(用户直接角色 + 项目员工角色)
> 6. 生成JWT Token含userId、username、roles claims
> 7. 返回Token和用户基本信息
### 10.2 用户管理接口
```java
@RestController
@RequestMapping("/api/v1/auth/users")
@RequestMapping("/api/auth/users")
@Tag(name = "用户管理")
public class UserController {
@ -956,63 +916,264 @@ public class UserController {
@Operation(summary = "删除用户")
public Result<Void> delete(@PathVariable UUID id);
@PutMapping("/{id}/password")
@Operation(summary = "修改密码")
public Result<Void> changePassword(@PathVariable UUID id, @RequestBody @Valid ChangePasswordRequest request);
@PostMapping("/{id}/roles")
@Operation(summary = "分配角色")
public Result<Void> assignRoles(@PathVariable UUID id, @RequestBody UserRoleAssignRequest request);
@PostMapping("/{id}/status")
@Operation(summary = "修改用户状态")
public Result<Void> changeStatus(@PathVariable UUID id, @RequestBody UserStatusChangeRequest request);
@GetMapping("/{id}/projects")
@Operation(summary = "获取用户项目列表")
public Result<List<UserProjectVO>> getUserProjects(@PathVariable UUID id);
@PostMapping("/{id}/projects")
@Operation(summary = "添加用户到项目")
public Result<Void> addUserToProject(@PathVariable UUID id, @RequestBody UserProjectRequest request);
@DeleteMapping("/{id}/projects/{projectId}")
@Operation(summary = "从项目移除用户")
public Result<Void> removeUserFromProject(@PathVariable UUID id, @PathVariable UUID projectId);
@GetMapping("/enterprise")
@Operation(summary = "获取企业员工列表")
public Result<List<EnterpriseUserVO>> getEnterpriseUsers();
}
```
> **密码修改规则**(实际代码新增):
> - 需验证原密码
> - 新密码需通过强度校验8-20位大小写+数字+特殊字符)
> - 弱密码检测黑名单password、123456、admin等
### 10.3 角色权限接口
```java
@RestController
@RequestMapping("/api/v1/auth")
@Tag(name = "角色权限管理")
public class RolePermissionController {
@RequestMapping("/api/auth/roles")
@Tag(name = "角色管理")
public class RoleController {
@PostMapping("/roles")
@PostMapping
@Operation(summary = "创建角色")
public Result<RoleVO> createRole(@RequestBody @Valid RoleCreateRequest request);
@GetMapping("/roles")
@Operation(summary = "查询角色列表")
public Result<List<RoleVO>> listRoles();
@GetMapping
@Operation(summary = "分页查询角色列表")
public Result<Page<RoleVO>> listRoles();
@PutMapping("/roles/{id}")
@GetMapping("/{id}")
@Operation(summary = "获取角色详情")
public Result<RoleVO> getRoleById(@PathVariable UUID id);
@GetMapping("/project/{projectId}")
@Operation(summary = "根据项目ID查询角色")
public Result<List<RoleVO>> getRolesByProject(@PathVariable UUID projectId);
@PutMapping("/{id}")
@Operation(summary = "更新角色")
public Result<RoleVO> updateRole(@PathVariable UUID id, @RequestBody @Valid RoleUpdateRequest request);
@DeleteMapping("/roles/{id}")
@DeleteMapping("/{id}")
@Operation(summary = "删除角色")
public Result<Void> deleteRole(@PathVariable UUID id);
@PostMapping("/roles/{id}/permissions")
@PostMapping("/{id}/permissions")
@Operation(summary = "分配权限")
public Result<Void> assignPermissions(@PathVariable UUID id, @RequestBody RolePermissionAssignRequest request);
@GetMapping("/permissions")
@Operation(summary = "查询权限列表")
public Result<List<PermissionVO>> listPermissions();
@GetMapping("/{id}/permissions")
@Operation(summary = "获取角色权限列表")
public Result<List<PermissionVO>> getRolePermissions(@PathVariable UUID id);
@GetMapping("/permissions/tree")
@Operation(summary = "获取权限树")
public Result<List<PermissionTreeVO>> getPermissionTree();
@GetMapping("/{id}/users")
@Operation(summary = "获取拥有某角色的用户")
public Result<List<UserVO>> getRoleUsers(@PathVariable UUID id);
}
```
@GetMapping("/users/{id}/permissions")
@Operation(summary = "获取用户权限")
public Result<List<PermissionVO>> getUserPermissions(@PathVariable UUID id);
### 10.4 权限管理接口
@GetMapping("/users/{id}/menus")
@Operation(summary = "获取用户菜单")
public Result<List<MenuVO>> getUserMenus(@PathVariable UUID id);
```java
@RestController
@RequestMapping("/api/auth/permissions")
@Tag(name = "权限管理")
public class PermissionController {
@PostMapping("/check")
@Operation(summary = "校验权限")
public Result<Boolean> checkPermission(@RequestBody PermissionCheckRequest request);
@PostMapping
@Operation(summary = "创建权限")
public Result<PermissionVO> createPermission(@RequestBody @Valid PermissionCreateRequest request);
@GetMapping
@Operation(summary = "分页查询权限列表")
public Result<Page<PermissionVO>> listPermissions();
@GetMapping("/{id}")
@Operation(summary = "获取权限详情")
public Result<PermissionVO> getPermissionById(@PathVariable UUID id);
@GetMapping("/type/{type}")
@Operation(summary = "根据类型查询权限")
public Result<List<PermissionVO>> getPermissionsByType(@PathVariable String type);
@GetMapping("/menus")
@Operation(summary = "查询所有菜单权限")
public Result<List<PermissionVO>> listMenuPermissions();
@PutMapping("/{id}")
@Operation(summary = "更新权限")
public Result<PermissionVO> updatePermission(@PathVariable UUID id, @RequestBody @Valid PermissionUpdateRequest request);
@DeleteMapping("/{id}")
@Operation(summary = "删除权限")
public Result<Void> deletePermission(@PathVariable UUID id);
}
```
### 10.5 部门管理接口(新增)
```java
@RestController
@RequestMapping("/api/auth/depts")
@Tag(name = "部门管理")
public class DeptController {
@GetMapping("/tree")
@Operation(summary = "获取部门树")
public Result<List<DeptTreeVO>> getDeptTree();
@GetMapping
@Operation(summary = "获取所有启用部门")
public Result<List<DeptVO>> listActiveDepts();
@GetMapping("/{id}")
@Operation(summary = "获取部门详情")
public Result<DeptVO> getById(@PathVariable UUID id);
@PostMapping
@Operation(summary = "创建部门")
public Result<DeptVO> create(@RequestBody @Valid DeptCreateRequest request);
@PutMapping("/{id}")
@Operation(summary = "更新部门")
public Result<DeptVO> update(@PathVariable UUID id, @RequestBody @Valid DeptUpdateRequest request);
@DeleteMapping("/{id}")
@Operation(summary = "删除部门")
public Result<Void> delete(@PathVariable UUID id);
@GetMapping("/{deptId}/members")
@Operation(summary = "获取部门成员")
public Result<List<UserVO>> getDeptMembers(@PathVariable UUID deptId);
@GetMapping("/by-type/{deptType}")
@Operation(summary = "根据类型查询部门")
public Result<List<DeptVO>> getDeptsByType(@PathVariable String deptType);
}
```
> **部门类型**: ADMIN(行政管理) / ENGINEERING(工程部) / SECURITY(安保部) / CS(客服部) / CLEANING(保洁部)
### 10.6 项目成员管理接口(新增)
```java
@RestController
@RequestMapping("/api/auth/projects")
@Tag(name = "项目成员管理")
public class ProjectMemberController {
@GetMapping("/{projectId}/members")
@Operation(summary = "查询项目成员列表(分页)")
public Result<Page<ProjectStaffVO>> listMembers(@PathVariable UUID projectId);
@GetMapping("/{projectId}/available-members")
@Operation(summary = "获取可添加成员(企业员工)")
public Result<List<EnterpriseUserVO>> getAvailableMembers(@PathVariable UUID projectId);
@PostMapping("/{projectId}/members")
@Operation(summary = "添加项目成员")
public Result<Void> addMember(@PathVariable UUID projectId, @RequestBody AddMemberRequest request);
@DeleteMapping("/{projectId}/members/{userId}")
@Operation(summary = "移除项目成员")
public Result<Void> removeMember(@PathVariable UUID projectId, @PathVariable UUID userId);
}
```
### 10.7 数据访问授权接口(新增,接口预留)
```java
@RestController
@RequestMapping("/api/data-access")
@Tag(name = "数据访问授权")
public class DataAccessController {
@PostMapping
@Operation(summary = "授予数据访问权限")
public Result<DataAccessVO> grantAccess(@RequestBody @Valid DataAccessGrantRequest request);
@DeleteMapping("/{id}")
@Operation(summary = "撤销数据访问权限")
public Result<Void> revokeAccess(@PathVariable UUID id);
@GetMapping
@Operation(summary = "查询数据访问记录")
public Result<List<DataAccessVO>> queryAccess(@RequestParam String dataType, @RequestParam UUID dataId);
}
```
### 10.8 审计日志接口(新增)
```java
@RestController
@RequestMapping("/api/audit-logs")
@Tag(name = "审计日志")
public class AuditLogController {
@GetMapping
@Operation(summary = "分页查询审计日志")
public Result<Page<AuditLogVO>> page(AuditLogQueryRequest request);
@GetMapping("/modules")
@Operation(summary = "获取模块列表")
public Result<List<String>> getModules();
@GetMapping("/actions")
@Operation(summary = "获取操作类型列表")
public Result<List<String>> getActions();
@GetMapping("/stats")
@Operation(summary = "获取最近30天日志统计")
public Result<AuditStatsVO> getStats();
}
```
> **查询约束**: 强制限制查询范围不超过30天防止大量数据查询。
### 10.9 系统配置接口(新增)
```java
@RestController
@RequestMapping("/api/config")
@Tag(name = "系统配置")
public class SysConfigController {
@GetMapping
@Operation(summary = "获取所有配置项")
public Result<List<SysConfigVO>> listConfigs();
@GetMapping("/{configKey}")
@Operation(summary = "根据键获取配置")
public Result<SysConfigVO> getConfig(@PathVariable String configKey);
@PutMapping("/{configKey}")
@Operation(summary = "更新单个配置")
public Result<Void> updateConfig(@PathVariable String configKey, @RequestBody String configValue);
@PutMapping
@Operation(summary = "批量更新配置")
public Result<Void> batchUpdateConfigs(@RequestBody List<SysConfigUpdateRequest> requests);
}
```
@ -1024,28 +1185,48 @@ public class RolePermissionController {
| 功能模块 | 实现状态 | 备注 |
|---------|---------|------|
| User | 🟢 已实现 | 基础CRUD |
| Role | 🟢 已实现 | 基础CRUD |
| Permission | 🟢 已实现 | 基础CRUD |
| JWT认证 | 🟢 已实现 | Token生成/验证 |
| User | 🟢 已实现 | 基础CRUD,含 userType/deptId 扩展字段 |
| Role | 🟢 已实现 | 基础CRUD,三级分类(SYSTEM/PROJECT/DEPARTMENT)RoleStatus枚举 |
| Permission | 🟢 已实现 | 基础CRUD,简化结构(parentCode替代parentId) |
| JWT认证 | 🟢 已实现 | Token生成/验证,含登录失败锁定(Redis) |
| 项目上下文 | 🟢 已实现 | 项目隔离 |
| 访客凭证 | 🟢 已实现 | 二维码生成/验证 |
| 数据权限 | 🟡 部分实现 | 框架已搭建需补充PROJECT级别 |
| 操作日志 | 🟢 已实现 | AOP切面记录 |
| 审计日志 | 🟢 已实现 | AuditLog(原OperationLog)异步持久化30天查询窗口 |
| 部门管理 | 🟢 已实现 | 树形结构5种部门类型 |
| 项目成员管理 | 🟢 已实现 | ProjectStaff + ProjectStaffRole |
| 企业员工 | 🟢 已实现 | EnterpriseUser扩展实体 |
| 住户管理 | 🟢 已实现 | Resident + ResidentSpace + Space |
| 用户项目关联 | 🟢 已实现 | UserProject多对多 |
| 系统配置 | 🟢 已实现 | SysConfig键值对 |
| 数据访问授权 | 🟢 已实现 | DataAccess接口预留 |
| 登录失败锁定 | 🟢 已实现 | Redis实现5次失败锁10分钟 |
| 密码强度校验 | 🟢 已实现 | 8-20位+大小写数字特殊字符+弱密码检测 |
| 数据权限 | 🟡 部分实现 | DataScopeService提供判断方法未实现SQL自动注入 |
| 按钮级权限 | ⏳ 待实现 | 细粒度操作权限 |
| 状态驱动权限 | ⏳ 待实现 | 根据业务状态控制操作 |
| 字段脱敏 | ⏳ 待实现 | 敏感字段脱敏 |
| 访客凭证 | 🔴 未实现 | 原设计有但未在认证模块实现 |
| 角色业务属性 | 🔴 未实现 | businessType/terminalType/isFrontline未实现 |
| 权限树端点 | 🔴 未实现 | GET /permissions/tree |
| 权限校验端点 | 🔴 未实现 | POST /check |
| 用户菜单端点 | 🔴 未实现 | GET /users/{id}/menus |
| 用户权限查询端点 | 🔴 未实现 | GET /users/{id}/permissions |
| Permission菜单路由属性 | 🔴 未实现 | path/component/icon字段缺失 |
### 11.2 改进计划
| 优先级 | 改进项 | 说明 |
|--------|--------|------|
| P0 | 按钮级权限控制 | 新增操作按钮权限编码 |
| P0 | 状态驱动权限 | 工单状态与操作联动 |
| P1 | 数据权限完善 | 补充PROJECT级别 |
| P1 | 扫码强制机制 | 巡检扫码策略配置 |
| P0 | Permission菜单路由属性 | 增加 path/component/icon/module/action 字段,支持前端菜单渲染 |
| P0 | 权限树端点 | 实现 GET /permissions/tree |
| P0 | 用户菜单端点 | 实现 GET /users/{id}/menus |
| P0 | 权限校验端点 | 实现 POST /check |
| P1 | 数据权限完善 | 实现SQL自动注入补充PROJECT级别 |
| P1 | 角色业务属性 | 新增 businessType/terminalType/isFrontline 字段 |
| P1 | 按钮级权限控制 | 新增操作按钮权限编码 |
| P1 | 状态驱动权限 | 工单状态与操作联动 |
| P2 | 字段脱敏 | 敏感字段脱敏 |
| P2 | 角色业务属性 | 新增业务类型、终端类型字段 |
| P2 | 访客凭证 | 在认证模块实现访客凭证管理 |
| P2 | 审计日志归档 | 导出至对象存储替代直接删除 |
---
@ -1227,6 +1408,7 @@ CREATE INDEX idx_operation_log_created ON auth_operation_log(created_at);
| 2026-02-27 | 新增数据权限四级范围 | - |
| 2026-02-27 | 新增状态驱动权限设计 | - |
| 2026-02-27 | 新增角色业务属性字段 | - |
| 2026-04-26 | 反向同步实际代码实现Role.type改为SYSTEM/PROJECT/DEPARTMENT三级分类Role.status改为RoleStatus枚举Permission结构简化(parentCode替代parentId)UserRole简化为M2M中间表OperationLog更名为AuditLog新增实体(EnterpriseUser/ProjectStaff/ProjectStaffRole/Resident/ResidentSpace/Space/UserProject/DataAccess/Dept/SysConfig)新增API(部门管理/项目成员管理/数据访问授权/审计日志/系统配置/企业员工/用户项目关联);新增业务规则(登录失败锁定/密码强度校验/审计日志增强/项目成员角色/部门类型/住户认证)角色体系改为数据库动态管理API路径去掉v1版本号标注TODO: Permission需增加菜单路由属性 | - |
---

Some files were not shown because too many files have changed in this diff Show More