feat: 医院后勤管理系统初始提交 - 后端代码 + 完整文档

master
jiang 3 weeks ago
commit da8b02b492

@ -0,0 +1,93 @@
---
name: docs-merge-refactor
overview: 将docs目录下04-08文档合并精简为3个文件开发与测试规范、接口规范、项目技术要求删除核心数据模型和TAPD需求拆解两个延后文档
todos:
- id: create-04-dev-test-spec
content: 新建 04-开发与测试规范.md含Git规范、代码风格、测试、前后端协作、CI/CD五大板块
status: pending
- id: merge-05-06-api-spec
content: 合并 05-API接口规范.md + 06-扩展接口规范.md 为 05-接口规范.md
status: pending
- id: rename-08-to-06
content: 将 08-项目技术要求.md 改为 06-项目技术要求.md移除已拆分章节并更新引用路径
status: pending
dependencies:
- create-04-dev-test-spec
- id: delete-old-files
content: 删除旧文件04-核心数据模型.md、05-API接口规范.md、06-扩展接口规范.md、07-TAPD需求拆解.md、08-项目技术要求.md
status: pending
dependencies:
- merge-05-06-api-spec
- rename-08-to-06
---
## 产品概述
将医院后勤管理项目的文档体系从5个文件(04-08)精简为3个文件优先整理开发规范和技术要求延后数据模型设计。
## 核心功能
- 删除 `04-核心数据模型.md``07-TAPD需求拆解.md`(延后处理)
- 新建 `04-开发与测试规范.md`包含Git分支/提交规范、代码风格规范、测试规范、前后端协作规范、CI/CD流程规范
- 合并 `05-API接口规范.md` + `06-扩展接口规范.md``05-接口规范.md`
- 将 `08-项目技术要求.md` 重命名为 `06-项目技术要求.md`
## 最终文档结构
```
docs/
├── 01-模块划分.md
├── 02-功能清单-超级管理员.md
├── 02-功能清单-物业公司.md
├── 02-功能清单-医院.md
├── 02-功能清单-小程序端.md
├── 03-业务流转逻辑-超级管理员.md
├── 03-业务流转逻辑-物业公司.md
├── 03-业务流转逻辑-医院.md
├── 03-业务流转逻辑-小程序端.md
├── 04-开发与测试规范.md ← 新建
├── 05-接口规范.md ← 原05+06合并
└── 06-项目技术要求.md ← 原08重命名
```
## 技术栈
纯文档整理任务无代码技术栈依赖。文档内容基于现有项目技术栈Java 17 + Spring Boot 3 + Vue 3 + uni-app
## 实现方案
### 1. 新建 04-开发与测试规范.md
从08-项目技术要求.md第七章"开发规范"和第九章"测试要求"中提取相关内容并大幅扩展为独立文档包含5大板块
- **Git分支与提交规范**分支策略已有框架、Commit Message规范Conventional Commits、分支保护规则、Merge Request流程
- **代码风格规范**后端Java规范基于08第七章7.3命名规范扩展、前端Vue/TypeScript规范、小程序uni-app规范、ESLint/Prettier配置
- **测试规范**单元测试JUnit5+Mockito、集成测试、E2E测试、测试覆盖率要求从08第九章提取并细化
- **前后端协作规范**API联调流程、接口文档管理Swagger、Mock数据规范、联调排期约定
- **CI/CD流程规范**Jenkins/GitLab CI流水线配置、构建-测试-部署各阶段门禁、环境管理策略从08第十章提取
### 2. 合并 05+06 为 05-接口规范.md
- 05-API接口规范.md 作为主体,保持原有结构不变
- 06-扩展接口规范.md 作为新章节追加到主体末尾,章节号顺延
- 合并后结构:一~十七原05内容+ 十八~二十七原06内容章节号重编
- 删除原06中与05重复的全局约定内容
### 3. 重命名 08 → 06-项目技术要求.md
- 内容基本不变但需移除已拆分到04的开发规范和测试规范章节改为引用04
- 更新文档内引用路径(如"详见06-扩展接口规范.md"改为"详见05-接口规范.md"
### 4. 删除旧文件
- 删除 04-核心数据模型.md
- 删除 05-API接口规范.md
- 删除 06-扩展接口规范.md
- 删除 07-TAPD需求拆解.md
- 删除 08-项目技术要求.md
## 实施注意事项
- 04-开发与测试规范.md 的内容应与08-项目技术要求.md 中的开发/测试章节保持一致04独立后08中对应章节改为引用
- 合并接口规范时保持原有API路径、请求体、响应体格式不变仅做章节编号调整
- 08重命名为06后内部引用路径需同步更新

@ -0,0 +1,162 @@
---
name: docs-merge-v2
overview: 将04-08文档精简为3个规范类文档接口规范只写约定不写具体字段删除数据模型和TAPD拆解
todos:
- id: create-04-dev-test-spec
content: 新建 04-开发与测试规范.md提取08第七/九章内容并扩展前端/小程序/Commit/协作规范
status: completed
- id: create-05-api-spec
content: 新建 05-接口规范.md提取05全局约定+认证规范+06模块化架构规范删除具体接口字段
status: completed
- id: rename-08-to-06
content: 将08改写为06-项目技术要求.md移除已拆分章节改为引用更新内部引用路径
status: completed
dependencies:
- create-04-dev-test-spec
- create-05-api-spec
- id: delete-old-files
content: 删除旧文件04-核心数据模型.md、05-API接口规范.md、06-扩展接口规范.md、07-TAPD需求拆解.md、08-项目技术要求.md
status: completed
dependencies:
- rename-08-to-06
---
## 产品概述
将医院后勤管理项目的04-08号文档精简合并只保留规范和约定层面内容去掉具体数据模型表结构、具体接口请求/响应字段定义、TAPD需求拆解等延后处理的内容。
## 核心功能
- 删除 `04-核心数据模型.md`(表结构延后到开发阶段)
- 删除 `07-TAPD需求拆解.md`(需求拆解延后处理)
- 新建 `04-开发与测试规范.md`从08中提取开发规范(第七章)和测试规范(第九章),并适当扩展
- 新建 `05-接口规范.md`从05中提取全局约定(第一章)+认证规范(第二章概要)从06中提取模块化架构规范(IModulePlugin接口定义、模块注册流程、事件通信规范、模块开发规范、前端加载机制、版本升级、安全规范),删除所有具体接口的请求/响应字段定义
- 将 `08-项目技术要求.md` 重命名为 `06-项目技术要求.md`移除已拆分到04的开发规范和测试规范章节改为引用04
## 最终文档结构
```
docs/
├── 01-模块划分.md
├── 02-功能清单-超级管理员.md
├── 02-功能清单-物业公司.md
├── 02-功能清单-医院.md
├── 02-功能清单-小程序端.md
├── 03-业务流转逻辑-超级管理员.md
├── 03-业务流转逻辑-物业公司.md
├── 03-业务流转逻辑-医院.md
├── 03-业务流转逻辑-小程序端.md
├── 04-开发与测试规范.md ← 新建提取自08第七/九章)
├── 05-接口规范.md ← 新建提取05全局约定+06架构规范删除具体接口
└── 06-项目技术要求.md ← 原08重命名移除已拆分章节
```
## 技术栈
纯文档整理任务无代码技术栈依赖。文档内容基于现有项目技术栈Java 17 + Spring Boot 3 + Vue 3 + uni-app + MariaDB + Redis
## 实现方案
### 1. 新建 04-开发与测试规范.md
从08-项目技术要求.md第七章"开发规范"和第九章"测试要求"中提取并重组:
**开发规范部分来自08第七章**
- API规范引用05-接口规范.md保留核心约定如RESTful风格、统一响应格式、错误码分段、分页参数
- 代码分层规范Controller→Service→Repository三层分离DTO/VO/Entity严格分离
- 命名规范(包名、类名、方法名、常量、数据库表名/字段名、枚举值、API路径
- 数据库规范tenant_id必含、雪花主键、逻辑删除、枚举VARCHAR、JSON字段限制、索引规范
- 模块开发规范引用05-接口规范.md IModulePlugin规范模块目录结构模块间通信规则
- 审计日志规范(@AuditLog注解、AOP切面、记录内容、变更快照
- Git分支规范main/develop/feature/hotfix/release
- 代码审查要求(审查清单)
**测试规范部分来自08第九章**
- 单元测试覆盖率要求、框架JUnit5+Mockito、命名规范、必须测试场景
- 测试前置流程修改后必先测试、CI门禁、JaCoCo覆盖率报告
- 集成测试(多租户隔离、读写分离、权限实时生效、蓝牙策略)
- 蓝牙场景测试(信号/距离/离线/断开等场景)
- 性能测试JMeter/wrk、并发场景、慢SQL阈值
- 安全测试SQL注入、越权访问、敏感数据、接口鉴权
**扩展内容**
- 前端开发规范Vue 3组件规范、TypeScript规范、状态管理规范—— 从08中的前端技术栈推导
- 小程序开发规范uni-app规范、蓝牙调用规范—— 从08中的小程序技术栈和蓝牙要求推导
- Commit Message规范Conventional Commits格式
- 前后端协作规范API联调流程、接口文档管理、Mock数据规范
### 2. 新建 05-接口规范.md
**从05-API接口规范.md提取仅规范/约定,不提取具体接口字段)**
- 一、全局约定(完整保留):请求头、统一响应格式、错误码规范、通用错误码、查询参数约定
- 二、认证模块仅保留规范概要认证方式说明Web端用户名密码→JWT、小程序微信openid→JWT、首次绑定手机号、JWT载荷标准字段删除每个接口的具体请求/响应字段
- 十七、接口权限汇总(保留,作为规范参考)
**删除的内容(具体接口定义,开发时再细化)**
- 三~十六章所有具体API端点、请求体、响应体示例
**从06-扩展接口规范.md提取架构规范层面**
- 一、设计目标(模块即插即用、热注册、隔离、租户级控制)
- 二、核心接口定义IModulePlugin接口及所有数据结构ModuleInfo/MenuDefinition/PermissionDefinition/RouteDefinition/TableDefinition/ConfigDefinition/ModuleContext
- 三、模块注册中心ModuleRegistry接口、注册流程
- 四、模块启用/禁用管理仅保留规范说明删除具体API请求/响应字段)
- 五、模块间事件通信EventBus、预定义事件表、事件订阅示例
- 六、模块配置管理仅保留规范说明删除具体API请求/响应字段)
- 七、扩展模块实现规范目录结构、module.yml、IModulePlugin实现示例——这些是开发规范必须保留
- 八、前端模块加载机制
- 九、模块版本升级
- 十、模块安全规范
### 3. 修改 08 → 06-项目技术要求.md
**移除的章节**已拆分到04和05
- 7.1 API规范 → 改为引用05
- 7.2 代码分层规范 → 移至04
- 7.3 命名规范 → 移至04
- 7.4 数据库规范 → 移至04
- 7.5 模块开发规范 → 改为引用05
- 7.6 审计日志规范 → 移至04
- 7.7 Git分支规范 → 移至04
- 7.8 代码审查要求 → 移至04
- 9.1~9.6 测试要求 → 全部移至04
- 附录A 蓝牙策略配置数据模型 → 删除(表结构延后)
**保留的章节**(技术架构和运维层面):
- 一、技术栈选型与版本要求
- 二、架构设计要求
- 三、去中间件方案
- 四、安全要求4.1认证方案、4.2四级权限体系、4.3数据权限、4.4蓝牙策略配置、4.5审计日志、4.6通用安全要求)
- 五、性能要求
- 六、数据库备份与恢复
- 八、部署与运维要求
- 十、环境配置管理
**新增引用**
- 开发规范 → "详见 04-开发与测试规范.md"
- 测试规范 → "详见 04-开发与测试规范.md"
- 模块化架构 → "详见 05-接口规范.md"
- 更新文档内引用路径("06-扩展接口规范.md"→"05-接口规范.md""04-核心数据模型.md"→删除引用)
### 4. 删除旧文件
- 04-核心数据模型.md
- 05-API接口规范.md
- 06-扩展接口规范.md
- 07-TAPD需求拆解.md
- 08-项目技术要求.md
## 实施注意事项
- 05-接口规范.md中IModulePlugin相关代码示例保留这些是架构规范不是业务接口字段
- 06中第四章模块启用/禁用API和第六章模块配置API保留路径表格删除请求/响应体示例
- 08移入04的内容保持原意不变仅做格式调整和引用更新
- 文档编号连续01→02→03→04→05→06无跳号

@ -0,0 +1,339 @@
---
name: 医院物业SaaS管理后台-概要设计
overview: 设计面向医院物业公司的SaaS模式管理员后台系统概要设计涵盖在线报修、巡检、保洁、服务评价等核心模块以及医废管理、陪检管理、合同管理、分段招标管理等扩展模块接口预留。
design:
architecture:
framework: react
component: tdesign
styleKeywords:
- 企业级SaaS
- 专业高效
- 数据驱动
- 清爽蓝白
- 卡片式布局
fontSystem:
fontFamily: PingFang SC
heading:
size: 24px
weight: 600
subheading:
size: 16px
weight: 500
body:
size: 14px
weight: 400
colorSystem:
primary:
- "#0052D9"
- "#266FE8"
- "#4787F0"
background:
- "#F3F4F6"
- "#FFFFFF"
- "#F9FAFB"
text:
- "#1F2937"
- "#4B5563"
- "#9CA3AF"
functional:
- "#00A870"
- "#E34D59"
- "#ED7B2F"
- "#0052D9"
todos:
- id: design-overview
content: 编写概要设计文档,涵盖系统架构、模块划分、数据模型、接口规范与扩展机制
status: completed
- id: data-model
content: 设计核心数据模型(租户、班组、工单、巡检计划、保洁任务、评价、蓝牙设备)
status: completed
dependencies:
- design-overview
- id: api-spec
content: 设计各模块RESTful API接口规范含请求/响应结构与权限要求
status: completed
dependencies:
- design-overview
- id: extension-interface
content: 设计IModulePlugin扩展接口规范定义模块注册、菜单注入、权限声明机制
status: completed
dependencies:
- design-overview
- id: tapd-import
content: 使用[skill:TAPD]将需求拆解录入TAPD创建Epic与Story树
status: completed
dependencies:
- design-overview
- data-model
- api-spec
---
## 产品概述
面向医院物业公司的SaaS模式管理员后台系统提供报修、巡检、保洁及服务评价四大核心功能模块支持多租户隔离与自定义班组人员分配并预留医废管理、陪检管理、合同管理、分段招标管理等扩展模块接口。
## 核心功能
### 在线报修模块
- 微信扫码注册:扫描二维码进入小程序/公众号,自动注册并绑定身份信息
- 上传照片:报修时支持拍摄/上传故障照片最多9张含水印时间定位
- 工单生成与流转:自动生成工单编号,按预设规则流转(待分配→处理中→待验收→已完成/已关闭),支持催单、退单、转单
- 延期及协助维修:维修人员可申请延期,需主管审批;可请求其他班组协助,生成协助子工单
### 巡检管理模块
- 主管制定计划:按区域/设备/频次创建巡检计划,自动生成巡检任务排期
- 蓝牙卡定位打卡通过蓝牙Beacon定位确认巡检人员到达指定位置防止代签
- 异常记录:巡检发现异常可拍照上传、标记严重等级、自动触发报修工单
### 保洁管理模块
- 多级管理:项目→区域→楼栋→楼层→区域责任人五级保洁管理架构
- 蓝牙卡定位确认完成:保洁人员到达指定区域蓝牙打卡确认完成,主管可抽查
- 保洁任务看板:日/周/月任务视图,超时未完成自动预警
### 服务评价模块
- 五分制评分:对报修/巡检/保洁服务进行1-5分打分
- 留言评价:支持文字留言及图片上传
- 评价看板:汇总统计各班组/人员评分,生成绩效报表
### 系统管理
- 自定义班组:创建/编辑班组,设置班组长,支持多班组协作
- 人员分配逻辑:按区域/技能/排班等维度分配人员到班组,支持一人多班组
- 多租户隔离SaaS模式下各物业公司数据完全隔离
- 角色权限:超管→租户管理员→项目经理→主管→员工五级角色体系
### 扩展模块预留
- 医废管理:医废收集、称重、转运、销毁全流程追踪
- 陪检管理:陪检任务分配、签到确认、耗时统计
- 合同管理:合同台账、到期预警、续签审批
- 分段招标管理:招标计划、供应商管理、评标定标
## 技术栈
- 前端React 18 + TypeScript + Ant Design Pro + Tailwind CSS
- 后端Node.js + NestJSTypeScript
- 数据库PostgreSQL多租户共享数据库隔离schema方案+ Redis缓存/会话)
- 消息队列RabbitMQ工单流转、异步通知
- 对象存储腾讯云COS图片/附件)
- 微信集成微信小程序API + 微信公众号API
## 实现方案
### 架构策略
采用领域驱动设计DDD分层架构按业务领域拆分微服务模块NestJS Module级别预留标准扩展接口。SaaS多租户采用共享数据库、Schema隔离方案通过TenantContext中间件自动切换。
### 核心技术决策
1. **多租户方案**:共享数据库+行级隔离tenant_id字段兼顾成本与隔离性通过NestJS Guard统一注入租户上下文
2. **工单流转引擎**:基于状态机模式实现,状态转移规则可配置,支持自定义流转路径
3. **蓝牙定位**前端小程序对接蓝牙Beacon协议后端校验打卡位置合法性
4. **扩展模块接口**定义IModulePlugin接口新模块通过实现标准接口注册到系统自动获得菜单、权限、路由注入能力
### 数据流
```
微信扫码/小程序 → API网关(NestJS) → 租户识别中间件 → 业务模块 → PostgreSQL/Redis
→ 消息队列(异步通知/工单流转)
→ 腾讯云COS(图片存储)
```
## 架构设计
```mermaid
graph TB
subgraph 前端层
A[管理员后台 React]
B[微信小程序]
end
subgraph API网关层
C[NestJS API Gateway]
C1[租户识别中间件]
C2[JWT认证守卫]
C3[权限守卫]
end
subgraph 业务模块层
D1[报修模块 RepairModule]
D2[巡检模块 InspectionModule]
D3[保洁模块 CleaningModule]
D4[评价模块 EvaluationModule]
D5[组织架构模块 OrgModule]
D6[系统管理模块 SystemModule]
D7[扩展模块接口 IModulePlugin]
end
subgraph 基础设施层
E1[PostgreSQL]
E2[Redis]
E3[RabbitMQ]
E4[腾讯云COS]
E5[微信API]
end
A --> C
B --> C
C --> C1 --> C2 --> C3
C3 --> D1 & D2 & D3 & D4 & D5 & D6 & D7
D1 & D2 & D3 & D4 & D5 & D6 & D7 --> E1 & E2 & E3 & E4
D1 & D2 & D3 --> E5
```
### 模块插件化扩展设计
```mermaid
graph LR
subgraph 核心框架
M[ModuleRegistry 注册中心]
R[路由自动注入]
P[权限自动注册]
N[菜单自动生成]
end
subgraph 扩展模块
E1[医废管理 MedicalWasteModule]
E2[陪检管理 EscortModule]
E3[合同管理 ContractModule]
E4[分段招标 BiddingModule]
end
E1 & E2 & E3 & E4 -->|实现IModulePlugin| M
M --> R & P & N
```
## 目录结构
```
hospital-facility-management/
├── packages/
│ ├── server/ # 后端服务
│ │ ├── src/
│ │ │ ├── main.ts # 入口文件NestJS bootstrap
│ │ │ ├── app.module.ts # 根模块,注册所有业务模块
│ │ │ ├── common/ # 公共层
│ │ │ │ ├── guards/ # 租户守卫、JWT守卫、权限守卫
│ │ │ │ ├── interceptors/ # 响应格式化、日志拦截器
│ │ │ │ ├── filters/ # 异常过滤器
│ │ │ │ ├── decorators/ # 自定义装饰器(@Tenant, @Permissions
│ │ │ │ ├── pipes/ # 验证管道
│ │ │ │ └── interfaces/ # IModulePlugin, ITenantContext等公共接口
│ │ │ ├── config/ # 配置管理数据库、Redis、COS、微信等
│ │ │ ├── modules/
│ │ │ │ ├── auth/ # 认证模块微信登录、JWT、角色管理
│ │ │ │ ├── tenant/ # 租户管理模块
│ │ │ │ ├── org/ # 组织架构模块(班组、人员分配)
│ │ │ │ ├── repair/ # 报修模块
│ │ │ │ │ ├── repair.module.ts
│ │ │ │ │ ├── controllers/
│ │ │ │ │ ├── services/
│ │ │ │ │ ├── entities/
│ │ │ │ │ ├── dto/
│ │ │ │ │ └── workflow/ # 工单状态机引擎
│ │ │ │ ├── inspection/ # 巡检模块
│ │ │ │ ├── cleaning/ # 保洁模块
│ │ │ │ ├── evaluation/ # 评价模块
│ │ │ │ └── _plugins/ # 扩展模块目录
│ │ │ │ ├── medical-waste/ # 医废管理(预留)
│ │ │ │ ├── escort/ # 陪检管理(预留)
│ │ │ │ ├── contract/ # 合同管理(预留)
│ │ │ │ └── bidding/ # 分段招标(预留)
│ │ │ └── shared/ # 共享服务(通知、文件上传、蓝牙校验)
│ │ └── test/
│ └── admin-web/ # 管理员后台前端
│ ├── src/
│ │ ├── layouts/ # 布局组件SiderMenu + Header + Content
│ │ ├── pages/
│ │ │ ├── dashboard/ # 仪表盘首页
│ │ │ ├── repair/ # 报修管理页面
│ │ │ ├── inspection/ # 巡检管理页面
│ │ │ ├── cleaning/ # 保洁管理页面
│ │ │ ├── evaluation/ # 评价管理页面
│ │ │ ├── org/ # 组织架构管理页面
│ │ │ └── system/ # 系统设置页面
│ │ ├── components/ # 公共组件
│ │ ├── services/ # API调用层
│ │ ├── stores/ # 状态管理Zustand
│ │ ├── hooks/ # 自定义Hooks
│ │ └── utils/ # 工具函数
│ └── public/
└── package.json # Monorepo 根配置
```
## 实现要点
- **工单流转引擎**采用有限状态机XState理念状态转移规则存储于数据库支持租户自定义流转路径
- **蓝牙定位校验**小程序端扫描Beacon信号强度(RSSI)换算距离,后端校验打卡坐标与任务区域匹配度
- **多租户隔离**所有业务表携带tenant_idTypeORM通过全局scope自动注入查询条件防止越权
- **扩展模块注册**IModulePlugin接口定义register()、getMenus()、getPermissions()方法,新模块实现后自动注入
- **消息通知**工单状态变更、巡检超时、保洁预警等事件通过RabbitMQ发布消费端推送微信模板消息
## 设计风格
采用现代企业级SaaS管理后台风格以TDesign组件库为基础搭配Tailwind CSS自定义样式。整体视觉追求专业、高效、清晰符合医院物业管理行业特征。
## 页面规划
### 1. 仪表盘首页Dashboard
- **顶部统计卡片区**:今日报修数/进行中工单/待巡检数/保洁完成率,大字号数字+环比趋势箭头
- **工单状态分布图**:环形图展示各状态工单占比
- **最新工单列表**展示最近10条工单含状态标签、紧急程度、处理人
- **班组工作量排行**:横向柱状图展示各班组本周完成量
- **评分趋势图**折线图展示近30天平均评分走势
### 2. 报修管理页
- **工单列表区**:表格展示,支持状态/日期/区域/班组多维度筛选,含快捷操作按钮
- **工单详情抽屉**:右侧抽屉展示工单完整信息、流转记录时间轴、照片附件、评价信息
- **工单创建弹窗**:表单录入报修信息,支持图片上传预览
### 3. 巡检管理页
- **巡检计划看板**:日历视图+列表视图切换,按颜色区分正常/异常/未执行
- **巡检记录详情**:展示打卡时间、定位信息、异常记录及关联工单
- **计划创建表单**:区域选择、频次设置、人员分配、蓝牙点位绑定
### 4. 保洁管理页
- **保洁任务看板**:看板视图(待执行/执行中/已完成),支持拖拽调整
- **区域管理树**:左侧树形结构展示五级保洁区域架构
- **人员排班表**:周视图展示排班情况,支持点击编辑
### 5. 评价管理页
- **评价汇总统计**:各模块平均评分卡片+星级分布图
- **评价列表**:支持按模块/评分/日期筛选,展示评分星级、评语、关联工单
- **绩效报表**:班组/人员评分排名表,支持导出
### 6. 组织架构管理页
- **班组管理**:卡片式展示各班组,含人员数量、负责区域、班组长信息
- **人员分配**:左右穿梭框选择人员分配到班组,支持批量操作
- **排班日历**:月视图排班管理
### 7. 系统设置页
- **租户信息**公司基本信息、Logo上传
- **角色权限**:角色列表+权限树勾选
- **蓝牙设备管理**Beacon设备列表、位置绑定、状态监控
- **微信配置**:小程序/公众号参数配置
## 全局布局
- 左侧固定侧边栏:可折叠,含模块图标+文字,底部展开/收起按钮
- 顶部导航栏:面包屑导航 + 全局搜索 + 通知铃铛 + 用户头像下拉菜单
- 内容区:带内边距的白色背景卡片式内容区
## Agent Extensions
### Skill
- **TAPD**
- Purpose: 将概要设计中的各模块需求拆解为TAPD需求和任务录入项目管理平台
- Expected outcome: 在TAPD中创建完整的需求树Epic→Story→Task覆盖报修、巡检、保洁、评价、组织架构、系统管理六大模块含优先级、描述、关联关系

@ -0,0 +1,91 @@
---
name: 测试要求增强与权限方案优化
overview: 1. 增强测试要求代码级100%覆盖(每个类/方法/if-else/case分支、测试前置流程修改后必须先测试通过才能继续2. 权限实时生效方案从"数据库轮询标记"改为"Redis Pub/Sub"利用已有Redis实例零额外中间件
todos:
- id: update-08-redis
content: 更新08-项目技术要求.md中权限实时通知方案为Redis Pub/Sub
status: completed
- id: update-08-test
content: 更新08-项目技术要求.md中测试要求100%覆盖+测试前置流程)
status: completed
dependencies:
- update-08-redis
- id: update-08-misc
content: 更新08-项目技术要求.md中缓存策略表、缓存管理表、配置项等关联内容
status: completed
dependencies:
- update-08-redis
- id: update-03-redis
content: 更新03-业务流转逻辑.md中权限实时生效描述
status: completed
- id: update-01-redis
content: 更新01-模块划分.md中权限实时生效描述
status: completed
---
## 用户需求
### 需求1权限实时生效方案优化
当前方案"数据库轮询标记 + Spring Event"每5秒轮询一次数据库资源消耗大。需要改为更轻量的方案利用已有Redis实例实现毫秒级权限变更通知零额外中间件、零数据库压力。
### 需求2测试要求增强
- **100%代码级覆盖**每个类、方法、包括方法里if-else、case分支都必须测试到
- **测试前置流程**:每次代码修改后,必须先进行测试,测试通过后才能进行下一步操作
## 方案概述
### 一、权限实时生效Redis Pub/Sub 替代数据库轮询
**原理**:权限变更时通过 Redis `PUBLISH` 命令发布消息,各服务节点通过 `MessageListener` 订阅频道,收到消息后刷新本地权限缓存。
**优势对比**
| 对比项 | 数据库轮询(原方案) | Redis Pub/Sub新方案 |
| --- | --- | --- |
| 数据库压力 | 每5秒N次SQL | 0不碰数据库 |
| 延迟 | 最大5秒 | 毫秒级(内存操作) |
| 资源消耗 | 定时线程+DB连接 | 复用已有Redis连接几乎零额外开销 |
| 多节点支持 | 需每节点轮询 | 天然支持(每节点各自订阅) |
| 降级策略 | 无 | Redis断连时权限缓存TTL(2小时)到期后自然刷新 |
**实现要点**
- 频道命名:`permission:changed`
- 消息内容版本号Long用于判断是否需要刷新
- 发布时机:角色权限变更、用户角色分配变更
- 订阅方:各服务实例启动时注册 `RedisMessageListenerContainer`
- 降级保障Redis连接断开时不影响业务缓存TTL到期后自动刷新
### 二、测试要求增强
- 单元测试覆盖率从70%提升至100%(行覆盖+分支覆盖)
- 新增JaCoCo分支覆盖指标要求
- 新增"9.6 测试前置流程"章节,定义代码修改→测试→下一步的强制流程
- 在7.8代码审查要求中增加测试覆盖率检查项
### 修改范围
3个文档共约12处修改点
1. **docs/08-项目技术要求.md**9处
- 3.1 权限变更实时通知:方案描述+代码示例
- 3.3 跨实例通信描述
- 5.3 缓存策略表
- 7.8 代码审查要求
- 8.4 缓存管理策略表
- 9.1 单元测试:覆盖率和分支要求
- 9.2 集成测试:权限生效延迟描述
- 新增9.6 测试前置流程
- 10.3 配置项删除轮询间隔新增Redis频道配置
2. **docs/03-业务流转逻辑.md**1处
- 2.3 权限实时生效流程中的描述
3. **docs/01-模块划分.md**1处
- 2.4 权限实时生效机制中的描述

@ -0,0 +1,62 @@
---
name: 移除Docker相关文件和文档描述
overview: 移除所有Docker相关文件并清理3个文档中的Docker/容器化描述改为Windows直接部署方式
todos:
- id: delete-docker-files
content: 删除 backend/Dockerfile、docker-compose.yml、.dockerignore
status: completed
- id: update-readme
content: 更新 README.md删除Docker技术栈、目录项、部署章节替换为Windows部署指引
status: completed
dependencies:
- delete-docker-files
- id: update-08-tech
content: 更新 08-项目技术要求.md基础设施表、部署架构图、8.2部署章节
status: completed
dependencies:
- delete-docker-files
---
## 用户需求
不需要Docker移除所有相关文件并检查文档中内容去掉Docker/容器化相关描述替换为Windows服务部署方式。
### 需要删除的文件3个
1. `backend/Dockerfile`
2. `docker-compose.yml`
3. `.dockerignore`
### 需要修改的文档2个
- `README.md`删除Docker技术栈、Dockerfile目录项、Docker部署章节替换为Windows部署指引
- `docs/08-项目技术要求.md`基础设施表Docker行改为Windows Service、部署架构图去掉Docker Compose、8.2节从Docker Compose改为Windows服务部署要求
### 无需修改的文档
- `docs/01-模块划分.md`、`docs/03-业务流转逻辑.md` 等不含Docker相关内容
## 修改范围
### 删除文件
| 文件 | 说明 |
| --- | --- |
| `backend/Dockerfile` | Docker镜像构建脚本 |
| `docker-compose.yml` | Docker Compose编排文件 |
| `.dockerignore` | Docker构建排除文件 |
### 修改文件
#### README.md3处修改
1. 技术栈表删除 `| 容器化 | Docker + Docker Compose |`
2. 项目结构树删除 `├── Dockerfile` 行,删除 `docker-compose.yml` 相关行
3. 部署章节:将"2. Docker Compose 部署(推荐)"和"3. 仅构建后端镜像"替换为Windows服务部署指引引用 `docs/Windows部署指南.md`
#### docs/08-项目技术要求.md3处修改
1. 1.4 基础设施表:`Docker + Docker Compose | 私有云容器化部署` 改为 `Windows Service + WinSW | Windows服务部署开机自启`
2. 8.1 部署架构图将Docker Compose容器编排图改为Windows同机服务部署架构图
3. 8.2 节:标题从"Docker Compose部署要求"改为"Windows服务部署要求"yaml示例替换为WinSW配置示例

@ -0,0 +1,148 @@
---
name: 详细功能说明文档编写
overview: 基于全部项目文档要求按照02-功能清单-*.md的4个端侧小程序端、物业公司、医院、超级管理员创建对应文件夹每个功能菜单拆分为独立.md文件包含页面名称、页面元素、查询条件、列表字段、界面布局以及需求追溯和功能关联。
todos:
- id: create-folders
content: 创建4个功能说明文件夹
status: completed
- id: write-super-admin
content: 编写超级管理员功能说明4个文件
status: completed
dependencies:
- create-folders
- id: write-property
content: 编写物业公司功能说明9个文件
status: completed
dependencies:
- create-folders
- id: write-hospital
content: 编写医院功能说明5个文件
status: completed
dependencies:
- create-folders
- id: write-miniprogram
content: 编写小程序端功能说明9个文件
status: completed
dependencies:
- create-folders
---
## 产品概述
基于现有项目文档01~06号文档编写4套详细功能说明文档覆盖全部角色端的功能页面设计。
## 核心功能
- 按照02-功能清单的4个文件名创建4个文件夹超级管理员、物业公司、医院、小程序端
- 每个功能菜单独立一个.md文件共约27个文件
- 每个文件包含:菜单/子菜单/页面名称、页面元素、查询条件、列表字段、界面布局描述
- 每个功能标注:对应文档要求来源、后续服务功能、关联功能
- 所有设计需满足01-模块划分、03-业务流转逻辑、05-接口规范、06-项目技术要求中定义的约束
## 文件统计
- 02-功能清单-超级管理员/4个文件账号管理、权限管理、系统配置、操作日志
- 02-功能清单-物业公司/9个文件在线报修、巡检管理、保洁管理、组织架构、考勤打卡、服务评价、统计报表、操作日志、系统配置
- 02-功能清单-医院/5个文件合同管理、分段招标管理、服务监督、服务评价、统计报表
- 02-功能清单-小程序端/9个文件通用功能、报修、巡检、保洁、考勤、组织架构、服务评价、统计概览、管理员功能
## 文档编写规范
### 单文件模板结构
每个.md文件遵循统一模板
```
# {菜单名称}
> 模块编码:{module code}
> 端侧Web/小程序
> 关联文档列出关联的01~06号文档章节
## 功能概览
| 项目 | 说明 |
|------|------|
| 菜单名称 | |
| 子菜单 | |
| 功能编号 | 如 PR-R-01~11 |
| 权限编码 | 如 repair:list:* |
## 页面清单
### 页面N{页面名称}
- **页面路径**:路由路径
- **页面元素**:表单字段、按钮、标签页等
- **查询条件**:筛选器列表
- **列表字段**:表格列定义
- **界面布局**:文字描述布局结构
- **操作按钮**:可用动作及权限要求
## 需求追溯
| 功能点编号 | 功能名称 | 文档来源 | 后续服务 | 关联功能 |
|------------|----------|----------|----------|----------|
## 业务规则
- 从03-业务流转逻辑和06-项目技术要求中提取的约束
## 状态流转(如有)
- 状态机定义
```
### 需求追溯规范
- **文档来源**:标注来自哪个文档的哪个编号,如"02-物业公司 PR-R-01"、"03-物业公司 1.2节"、"06-技术要求 4.4节"
- **后续服务**:该功能完成后触发的下游服务,如"工单分配→触发通知服务"
- **关联功能**:与本功能有数据依赖或交互的其他功能,如"工单列表↔巡检异常上报"
### 数据来源
所有页面设计的数据字段从以下文档综合提取:
- 01-模块划分.md角色权限、数据范围
- 02-功能清单-*.md功能点编号和描述
- 03-业务流转逻辑-*.md流转规则、状态定义、补录标记字段
- 05-接口规范.md权限编码矩阵第九章、模块事件定义
- 06-项目技术要求.md蓝牙策略、审计日志、四级权限体系
### 目录结构
```
docs/
├── 02-功能清单-超级管理员/
│ ├── 01-账号管理.md # SA-A-01~08含医院/物业账号CRUD、有效期、到期管理
│ ├── 02-权限管理.md # SA-P-01~10含角色定义、权限分配、预设模板、审计日志
│ ├── 03-系统配置.md # SA-S-01~02含版本管理、缓存管理
│ └── 04-操作日志.md # SA-L-01~02含权限变更日志、账号操作日志
├── 02-功能清单-物业公司/
│ ├── 01-在线报修.md # PR-R-01~11含工单列表/详情/分配/流转/延期/补录/导出
│ ├── 02-巡检管理.md # PR-I-01~07含计划/看板/记录/异常/区域/补录
│ ├── 03-保洁管理.md # PR-C-01~08含区域/看板/排班/蓝牙/超时/抽查/补录
│ ├── 04-组织架构.md # PR-O-01~11含班组/人员/排班/技能/打卡点/下属账号
│ ├── 05-考勤打卡.md # PR-A-01~05含打卡点/规则/记录/异常审核/补录
│ ├── 06-服务评价.md # PR-E-01~05含汇总/列表/回复/绩效/配置
│ ├── 07-统计报表.md # PR-ST-01~07含报修/巡检/保洁/评价/考勤/综合/自定义
│ ├── 08-操作日志.md # PR-AL-01~05含时间轴/列表/详情/导出/补录日志
│ └── 09-系统配置.md # PR-S-01~05含蓝牙设备/字典/微信/消息模板/补录审核
├── 02-功能清单-医院/
│ ├── 01-合同管理.md # HO-CT-01~11含台账/录入/审批/付款/变更/预警/续签
│ ├── 02-分段招标管理.md # HO-BD-01~09含计划/标段/供应商/发布/投标/评标/定标
│ ├── 03-服务监督.md # HO-SP-01~04含报修/巡检/保洁数据只读查看
│ ├── 04-服务评价.md # HO-EV-01~03含发起评价/汇总查看/列表查看
│ └── 05-统计报表.md # HO-ST-01~08含业务/合同/招标/综合/自定义报表
└── 02-功能清单-小程序端/
├── 01-通用功能.md # MP-C-01~06含登录/个人信息/消息/补录/版本/通讯录
├── 02-报修相关功能.md # MP-R-01~12含扫码/报修/工单/催单/接单/完工/延期/验收/评价
├── 03-巡检相关功能.md # MP-I-01~06含今日任务/蓝牙打卡/执行/异常上报/补录
├── 04-保洁相关功能.md # MP-CL-01~07含今日任务/蓝牙打卡/执行/异常/抽查/补录
├── 05-考勤相关功能.md # MP-AT-01~06含上下班打卡/记录/申诉/日历/审核
├── 06-组织架构相关功能.md # MP-OR-01~02含我的班组/我的排班
├── 07-服务评价相关功能.md # MP-EV-01~03含待评价/评分留言/历史评价
├── 08-统计概览功能.md # MP-ST-01~02含简版概览/个人绩效
└── 09-管理员小程序功能.md # MP-PM-01~06含报修/巡检/保洁/考勤/评价/组织架构管理
```
## Agent Extensions
### SubAgent
- **code-explorer**: 在编写功能说明时,快速检索项目文档中特定功能点的定义、流转规则和权限编码,确保文档内容与现有规范一致

@ -0,0 +1,158 @@
---
name: 项目技术要求文档
overview: 基于已有的7份设计文档和用户补充信息创建项目技术要求文档08-技术要求.md覆盖技术栈选型、架构设计、安全、性能、开发规范、部署运维、测试等维度并纳入无中间件方案、蓝牙策略配置等新增需求。
todos:
- id: write-tech-doc
content: 编写docs/08-项目技术要求.md文档涵盖技术栈、架构、安全、性能、蓝牙策略、去中间件方案、开发规范、部署运维、测试要求全量内容
status: completed
- id: update-model-bluetooth
content: 更新docs/04-核心数据模型.md新增蓝牙策略配置表(sys_bluetooth_policy)和审核表(sys_bluetooth_policy_audit)打卡记录表增加check_type字段
status: completed
dependencies:
- write-tech-doc
- id: update-api-bluetooth
content: 更新docs/05-API接口规范.md新增蓝牙策略配置API和打卡记录check_type返回字段
status: completed
dependencies:
- write-tech-doc
- id: update-flow-bluetooth
content: 更新docs/03-业务流转逻辑.md蓝牙校验流程改为按策略配置判断强制/非强制),补录模式仅在强制蓝牙+蓝牙失败时触发
status: completed
dependencies:
- write-tech-doc
---
## 产品概述
医院物业SaaS管理后台的项目技术要求文档作为内部团队开发标准所有开发人员必须严格按照此标准执行。
## 核心功能
- 明确技术栈选型与版本要求Java+Spring Boot+MariaDB+Vue+uni-app+Redis
- 定义架构设计规范(多租户、模块化、前后端分离、双端、读写分离、去中间件方案)
- 规定安全要求(认证、四级权限、行级数据隔离、蓝牙校验、审计日志)
- 设定性能基线API响应<500ms
- 制定开发规范API规范、代码规范、数据库规范、模块开发规范
- 约定部署运维要求(私有云部署、数据库定时备份与恢复、缓存管理、版本更新策略)
- 新增蓝牙策略配置要求(物业公司配置是否强制蓝牙、医院账号审核、报表区分蓝牙/手动打卡)
## 技术栈选型
### 后端
- 语言Java 17+
- 框架Spring Boot 3.x
- 数据库MariaDB 10.6+(主从复制读写分离)
- 缓存Redis 7.x权限/字典/菜单缓存Pub/Sub用于应用内事件
- ORMMyBatis-Plus 3.5+
- 认证Spring Security + JWT
- 文件存储腾讯云COS
### 前端Web
- 框架Vue 3 + TypeScript
- 构建工具Vite 5.x
- 状态管理Pinia
- 路由Vue Router 4
- UI组件库Element Plus企业级后台管理场景适配度最高
- HTTP客户端Axios
- 图表ECharts
### 小程序
- 框架uni-app + Vue 3
- 蓝牙SDKuni-bleuni-app蓝牙低功耗插件
- UI组件库uni-ui
### 基础设施
- 部署私有云Docker + Docker Compose
- CI/CDJenkins/GitLab CI
- 版本管理Git
## 去中间件方案
不引入RocketMQ/Kafka等消息中间件采用以下替代方案
### 1. 权限变更实时通知
- 方案:数据库轮询标记 + Spring Event
- 实现:权限变更时写入`sys_config`表标记字段`permission_version`递增各服务实例定时5秒轮询该字段检测到版本变化时触发本地权限缓存刷新
- 优势:无额外中间件依赖,实现简单可靠
- 降级轮询间隔内最大5秒延迟对权限场景可接受
### 2. 异步任务处理
- 方案Spring @Async + 自定义线程池
- 实现:配置`ThreadPoolTaskExecutor`,核心线程数=CPU核心数最大线程数=2*CPU核心数队列容量1000拒绝策略CallerRunsPolicy
- 适用场景:报表导出、消息推送、日志异步写入
- 持久化:关键任务落库后异步执行,避免线程池丢失
### 3. 模块间通信
- 方案Spring ApplicationEvent + ApplicationEventPublisher
- 实现模块间通过Spring Event解耦事件定义在共享API模块中
- 跨实例:结合数据库轮询标记实现(事件落库+轮询消费)
- 适用场景:工单完成触发评价、巡检异常触发报修、低分评价通知主管
### 4. 定时任务
- 方案Spring @Scheduled + 数据库分布式锁
- 实现:`@Scheduled`注解声明定时任务,通过`shedlock`或数据库行锁保证集群内单实例执行
- 适用场景巡检任务自动生成、保洁超时预警、合同到期提醒、Beacon心跳检测
## 架构设计
### 整体架构
```
客户端层Vue3 Web管理后台 | uni-app微信小程序
↓ HTTPS
网关层Spring Cloud Gateway / Nginx 反向代理
应用层Spring Boot 单体应用模块化IModulePlugin
数据层MariaDB主从读写分离| Redis缓存 | 腾讯云COS
```
### 读写分离方案
- 实现MariaDB主从复制 + MyBatis-Plus动态数据源切换
- 规则:写操作走主库,读操作走从库
- 注解:自定义`@ReadOnly`注解标记读操作AOP切面切换数据源
- 容错:从库不可用时自动降级到主库读取
### 数据库备份与恢复
- 定时备份每日凌晨2:00全量备份mysqldump每4小时增量备份binlog
- 备份保留全量备份保留30天增量备份保留7天
- 备份存储:私有云本地磁盘 + 异地备份服务器
- 恢复流程:全量恢复 + binlog重放RTO<2RPO<4
- 定期演练:每季度执行一次恢复演练
### 蓝牙策略配置(新增)
- 配置方:物业公司管理员
- 审核方:医院账号
- 配置项:各场景是否强制蓝牙连接(巡检打卡/巡检拍照/保洁打卡/保洁拍照/考勤打卡)
- 数据模型:新增`sys_bluetooth_policy`配置表 + `sys_bluetooth_policy_audit`审核表
- 打卡记录标记:所有打卡记录区分`check_type`BLUETOOTH/MANUAL后台列表和报表明确显示
- 降级逻辑非强制蓝牙场景下蓝牙失败可直接手动打卡记录check_type=MANUAL强制蓝牙场景下蓝牙失败进入补录模式
## 性能要求
- API平均响应时间 < 300msP99 < 500ms
- 列表查询响应 < 500ms
- 报表生成 < 5s
- 并发支持单实例100 QPS水平扩展
- 蓝牙校验扫描超时3秒距离计算<100ms
## 开发规范
- API规范遵循已有05-API接口规范.md
- 代码分层Controller → Service → RepositoryDTO/VO/Entity严格分离
- 命名规范:包名小写、类名大驼峰、方法名小驼峰、常量全大写下划线
- 数据库规范遵循已有04-核心数据模型.md公共字段约定
- 模块开发遵循已有06-扩展接口规范.md IModulePlugin规范
- 审计日志所有写操作自动记录使用AOP切面统一处理
- 单元测试核心业务Service覆盖率 >= 70%

@ -0,0 +1,30 @@
# https://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.java]
indent_style = space
indent_size = 4
[*.{yml,yaml}]
indent_style = space
indent_size = 2
[*.xml]
indent_style = space
indent_size = 4
[*.sql]
indent_style = space
indent_size = 4
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

@ -0,0 +1,18 @@
# ============================================================
# 医院物业 SaaS 管理后台 — 环境变量配置
# 复制本文件为 .env 并修改实际值
# ============================================================
# ---- MariaDB ----
DB_PASSWORD=root
DB_PORT=3306
# ---- Redis ----
REDIS_PASSWORD=
REDIS_PORT=6379
# ---- Application ----
APP_PORT=8080
# ---- JWT ----
JWT_SECRET=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6

35
.gitignore vendored

@ -0,0 +1,35 @@
# Compiled class files
*.class
# Log files
*.log
# Package files
*.jar
*.war
*.ear
*.zip
*.tar.gz
*.rar
# Maven
target/
# IDE
.idea/
*.iml
.vscode/
.settings/
.project
.classpath
.factorypath
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.local

@ -0,0 +1,195 @@
# 医院物业 SaaS 管理后台
> 多租户医院物业管理平台,涵盖报修、巡检、保洁、考勤、合同、招标等核心业务模块,支持蓝牙打卡、微信小程序端操作。
## 技术栈
| 类别 | 技术 |
|------|------|
| 语言 | Java 17 |
| 框架 | Spring Boot 3.2.5 |
| ORM | MyBatis-Plus 3.5.5 |
| 安全 | Spring Security + JWT |
| 数据库 | MariaDB 10.6+ |
| 缓存 | Redis 7 |
| API 文档 | SpringDoc / Swagger |
| 构建工具 | Maven 3.9+ |
| 容器化 | Docker + Docker Compose |
## 项目结构
```
backend/
├── src/main/java/com/hospital/mgmt/
│ ├── common/ # 通用组件
│ │ ├── annotation/ # 自定义注解(@AuditLog, @ReadOnly
│ │ ├── base/ # 基础实体类
│ │ ├── context/ # 租户上下文ThreadLocal
│ │ ├── exception/ # 全局异常处理
│ │ └── result/ # 统一响应封装
│ ├── config/ # 配置类
│ ├── security/ # 安全模块JWT、认证、租户拦截
│ └── modules/ # 业务模块
│ ├── auth/ # 认证(登录/刷新Token
│ ├── org/ # 组织架构(医院/物业/班组/员工)
│ ├── permission/ # 权限管理(用户/角色/权限配置)
│ ├── repair/ # 在线报修
│ ├── inspection/ # 巡检管理
│ ├── cleaning/ # 保洁管理
│ ├── evaluation/ # 服务评价
│ ├── attendance/ # 考勤打卡
│ ├── contract/ # 合同管理
│ ├── bidding/ # 分段招标
│ ├── device/ # 蓝牙设备管理
│ ├── audit/ # 操作日志
│ ├── system/ # 系统管理(字典/版本/模板)
│ └── plugin/ # IModulePlugin 扩展接口
├── src/main/resources/
│ ├── application.yml # 默认配置
│ ├── application-prod.yml # 生产配置
│ └── db/init.sql # 数据库初始化脚本
├── Dockerfile
└── pom.xml
```
## 快速开始
### 前置条件
- JDK 17+
- Maven 3.9+
- MariaDB 10.6+
- Redis 7+
### 1. 本地开发
```bash
# 克隆项目
git clone <repository-url>
cd 医院后勤管理
# 初始化数据库
mysql -uroot -proot < backend/src/main/resources/db/init.sql
# 修改配置(如需要)
# 编辑 backend/src/main/resources/application.yml 中的数据库和Redis连接信息
# 编译运行
cd backend
mvn clean package -DskipTests
java -jar target/hospital-mgmt-1.0.0.jar
```
应用启动后访问:
- API 基地址:`http://localhost:8080/api/v1`
- Swagger UI`http://localhost:8080/api/v1/swagger-ui.html`
### 2. Docker Compose 部署(推荐)
```bash
# 复制环境变量模板并修改
cp .env.example .env
# 一键启动MariaDB + Redis + 后端)
docker compose up -d
# 查看日志
docker compose logs -f backend
# 停止
docker compose down
```
### 3. 仅构建后端镜像
```bash
cd backend
docker build -t hospital-mgmt:1.0.0 .
```
## 环境变量
| 变量 | 说明 | 默认值 |
|------|------|--------|
| `DB_HOST` | MariaDB 主机 | localhost |
| `DB_PORT` | MariaDB 端口 | 3306 |
| `DB_NAME` | 数据库名 | hospital_mgmt |
| `DB_USER` | 数据库用户 | root |
| `DB_PASSWORD` | 数据库密码 | root |
| `REDIS_HOST` | Redis 主机 | localhost |
| `REDIS_PORT` | Redis 端口 | 6379 |
| `REDIS_PASSWORD` | Redis 密码 | (空) |
| `JWT_SECRET` | JWT 签名密钥 | (内置默认值) |
| `JAVA_OPTS` | JVM 参数 | -Xms256m -Xmx512m |
| `SPRING_PROFILES_ACTIVE` | Spring Profile | prod |
## 默认账号
| 角色 | 用户名 | 密码 |
|------|--------|------|
| 超级管理员 | superadmin | admin123 |
## 核心功能模块
### 在线报修
- 工单全生命周期:提交 → 智能派单 → 接单 → 处理 → 验收 → 评价
- 支持紧急催单、延期申请、协助工单
- 补录机制(需审批)
### 巡检管理
- 自定义巡检区域与计划
- 支持日/周/月/自定义频率
- 蓝牙打卡验证巡检人员位置
- 异常自动转报修工单
### 保洁管理
- 五级区域树结构管理
- 日保/深度/临时任务类型
- 排班与任务自动生成
- 抽检功能
### 考勤打卡
- 基于蓝牙 Beacon 的近距离验证
- 灵活配置上下班规则
- 异常申诉与补录审批
### 合同管理
- 合同全生命周期
- 付款节点管理与到期预警
- 合同变更审批流
### 分段招标
- 招标计划与标段管理
- 供应商资质审核
- 黑名单机制
### 服务评价
- 多模块统一评价体系
- 低评分自动通知
- 评价回复机制
### 权限管理
- RBAC + 四级数据权限(全院/医院/班组/个人)
- 用户级权限覆盖
- Redis 实时权限变更广播
### 蓝牙策略
- 各院区独立配置打卡策略
- 必选/可选/禁用三级配置
- 策略审批生效机制
### 扩展接口 (IModulePlugin)
- SPI 机制支持模块热插拔
- 自定义菜单、路由、权限、数据表
- 运行时模块启用/禁用
## API 规范
- RESTful 风格
- 统一响应格式:`{ code, message, data, timestamp }`
- 分页响应:`{ list, pagination: { page, pageSize, total, totalPages } }`
- 错误码5 位数字4xxxx 客户端错误5xxxx 服务端错误)
## License
Private — Internal Use Only

@ -0,0 +1,216 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.hospital</groupId>
<artifactId>hospital-mgmt</artifactId>
<version>1.0.0</version>
<name>hospital-mgmt</name>
<description>医院物业SaaS管理后台</description>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.6</mybatis-plus.version>
<jjwt.version>0.12.5</jjwt.version>
<shedlock.version>5.12.0</shedlock.version>
<hutool.version>5.8.26</hutool.version>
</properties>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- MariaDB -->
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- ShedLock -->
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>${shedlock.version}</version>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-jdbc-template</artifactId>
<version>${shedlock.version}</version>
</dependency>
<!-- Hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Jackson for JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<!-- Swagger / OpenAPI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!-- H2 for testing -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>1.00</minimum>
</limit>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>1.00</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,14 @@
package com.hospital.mgmt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class HospitalMgmtApplication {
public static void main(String[] args) {
SpringApplication.run(HospitalMgmtApplication.class, args);
}
}

@ -0,0 +1,13 @@
package com.hospital.mgmt.common.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuditLog {
String moduleCode();
String moduleName();
String operationType();
String operationContent() default "";
}

@ -0,0 +1,9 @@
package com.hospital.mgmt.common.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ReadOnly {
}

@ -0,0 +1,33 @@
package com.hospital.mgmt.common.base;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
public abstract class BaseEntity implements Serializable {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long tenantId;
private Long createdBy;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
private Long updatedBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
private Integer isSupplement;
private String supplementReason;
private String supplementRemark;
private String supplementAuditStatus;
private Long supplementAuditorId;
private LocalDateTime supplementAuditTime;
}

@ -0,0 +1,46 @@
package com.hospital.mgmt.common.context;
import lombok.Data;
@Data
public class TenantContext {
private Long userId;
private Long tenantId;
private String userType;
private Long hospitalId;
private Long propertyCompanyId;
private Long staffId;
private static final ThreadLocal<TenantContext> CONTEXT = new ThreadLocal<>();
public static void set(TenantContext context) {
CONTEXT.set(context);
}
public static TenantContext get() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
public static Long getTenantId() {
TenantContext ctx = CONTEXT.get();
return ctx != null ? ctx.getTenantId() : null;
}
public static Long getUserId() {
TenantContext ctx = CONTEXT.get();
return ctx != null ? ctx.getUserId() : null;
}
public static String getUserType() {
TenantContext ctx = CONTEXT.get();
return ctx != null ? ctx.getUserType() : null;
}
public static boolean isSuperAdmin() {
return "SUPER_ADMIN".equals(getUserType());
}
}

@ -0,0 +1,23 @@
package com.hospital.mgmt.common.exception;
import lombok.Getter;
@Getter
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
}
public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.code = errorCode.getCode();
}
}

@ -0,0 +1,26 @@
package com.hospital.mgmt.common.exception;
import lombok.Getter;
@Getter
public enum ErrorCode {
SUCCESS(0, "成功"),
MISSING_PARAM(40001, "缺少必填参数"),
INVALID_PARAM(40002, "参数格式错误"),
UNAUTHORIZED(40100, "未认证"),
INVALID_TOKEN(40101, "Token无效"),
NO_PERMISSION(40300, "无功能权限"),
NO_DATA_PERMISSION(40301, "无数据权限"),
NOT_FOUND(40400, "资源不存在"),
STATUS_CONFLICT(40900, "状态冲突"),
DATA_EXISTS(40901, "数据已存在"),
INTERNAL_ERROR(50000, "服务器内部错误");
private final int code;
private final String message;
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
}

@ -0,0 +1,70 @@
package com.hospital.mgmt.common.exception;
import com.hospital.mgmt.common.result.ApiResult;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.OK)
public ApiResult<Void> handleBusinessException(BusinessException e) {
log.warn("Business exception: code={}, message={}", e.getCode(), e.getMessage());
return ApiResult.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.OK)
public ApiResult<Void> handleValidationException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ": " + f.getDefaultMessage())
.findFirst()
.orElse("参数校验失败");
return ApiResult.error(ErrorCode.INVALID_PARAM.getCode(), message);
}
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.OK)
public ApiResult<Void> handleBindException(BindException e) {
String message = e.getFieldErrors().stream()
.map(f -> f.getField() + ": " + f.getDefaultMessage())
.findFirst()
.orElse("参数绑定失败");
return ApiResult.error(ErrorCode.INVALID_PARAM.getCode(), message);
}
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.OK)
public ApiResult<Void> handleConstraintViolation(ConstraintViolationException e) {
return ApiResult.error(ErrorCode.INVALID_PARAM.getCode(), e.getMessage());
}
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ApiResult<Void> handleAccessDenied(AccessDeniedException e) {
return ApiResult.error(ErrorCode.NO_PERMISSION.getCode(), ErrorCode.NO_PERMISSION.getMessage());
}
@ExceptionHandler(BadCredentialsException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ApiResult<Void> handleBadCredentials(BadCredentialsException e) {
return ApiResult.error(ErrorCode.UNAUTHORIZED.getCode(), "用户名或密码错误");
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ApiResult<Void> handleException(Exception e) {
log.error("Unexpected error", e);
return ApiResult.error(ErrorCode.INTERNAL_ERROR.getCode(), ErrorCode.INTERNAL_ERROR.getMessage());
}
}

@ -0,0 +1,37 @@
package com.hospital.mgmt.common.result;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serializable;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiResult<T> implements Serializable {
private int code;
private String message;
private T data;
private Long timestamp;
private ApiResult() {}
public static <T> ApiResult<T> success(T data) {
ApiResult<T> result = new ApiResult<>();
result.setCode(0);
result.setMessage("success");
result.setData(data);
result.setTimestamp(System.currentTimeMillis());
return result;
}
public static <T> ApiResult<T> success() {
return success(null);
}
public static <T> ApiResult<T> error(int code, String message) {
ApiResult<T> result = new ApiResult<>();
result.setCode(code);
result.setMessage(message);
result.setTimestamp(System.currentTimeMillis());
return result;
}
}

@ -0,0 +1,15 @@
package com.hospital.mgmt.common.result;
import lombok.Data;
import java.util.List;
@Data
public class PageResult<T> {
private List<T> list;
private Pagination pagination;
public PageResult(List<T> list, int page, int pageSize, long total) {
this.list = list;
this.pagination = new Pagination(page, pageSize, total);
}
}

@ -0,0 +1,19 @@
package com.hospital.mgmt.common.result;
import lombok.Data;
import java.io.Serializable;
@Data
public class Pagination implements Serializable {
private int page;
private int pageSize;
private long total;
private int totalPages;
public Pagination(int page, int pageSize, long total) {
this.page = page;
this.pageSize = pageSize;
this.total = total;
this.totalPages = (int) Math.ceil((double) total / pageSize);
}
}

@ -0,0 +1,25 @@
package com.hospital.mgmt.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(1000);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}

@ -0,0 +1,44 @@
package com.hospital.mgmt.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.hospital.mgmt.common.context.TenantContext;
import com.hospital.mgmt.security.TenantHandler;
import lombok.RequiredArgsConstructor;
import org.apache.ibatis.reflection.MetaObject;
import java.time.LocalDateTime;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@RequiredArgsConstructor
public class MyBatisPlusConfig implements MetaObjectHandler {
private final TenantHandler tenantHandler;
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(tenantHandler));
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MARIADB));
return interceptor;
}
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createdAt", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "tenantId", Long.class, TenantContext.getTenantId());
this.strictInsertFill(metaObject, "createdBy", Long.class, TenantContext.getUserId());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
this.strictUpdateFill(metaObject, "updatedBy", Long.class, TenantContext.getUserId());
}
}

@ -0,0 +1,23 @@
package com.hospital.mgmt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
@Configuration
public class RedisConfig {
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
return new StringRedisTemplate(connectionFactory);
}
}

@ -0,0 +1,25 @@
package com.hospital.mgmt.config;
import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
@Configuration
@EnableSchedulerLock(defaultLockAtLeastFor = "PT1M", defaultLockAtMostFor = "PT30M")
public class ShedLockConfig {
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(
JdbcTemplateLockProvider.Configuration.builder()
.withJdbcTemplate(new JdbcTemplate(dataSource))
.withTableName("shedlock")
.build()
);
}
}

@ -0,0 +1,19 @@
package com.hospital.mgmt.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("http://localhost:*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}

@ -0,0 +1,19 @@
package com.hospital.mgmt.modules.attendance.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("attendance_check_point")
public class AttendanceCheckPoint extends BaseEntity {
private String name;
private Long beaconId;
private Long hospitalId;
private Long campusId;
private String location;
private String teamIds;
private String status;
}

@ -0,0 +1,32 @@
package com.hospital.mgmt.modules.attendance.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("attendance_record")
public class AttendanceRecord extends BaseEntity {
private Long staffId;
private String staffName;
private LocalDate checkDate;
private String checkType;
private String checkMethod;
private LocalDateTime checkTime;
private Long checkPointId;
private Long beaconId;
private String beaconUuid;
private BigDecimal distance;
private String result;
private String abnormalType;
private String appealStatus;
private String appealReason;
private Long appealAuditorId;
private String appealAuditRemark;
private LocalDateTime appealAuditAt;
}

@ -0,0 +1,22 @@
package com.hospital.mgmt.modules.attendance.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("attendance_rule")
public class AttendanceRule extends BaseEntity {
private String name;
private Long teamId;
private LocalTime workStartTime;
private LocalTime workEndTime;
private Integer startBufferMinutes;
private Integer endBufferMinutes;
private Integer lateThresholdMinutes;
private Integer earlyLeaveThresholdMinutes;
private String status;
}

@ -0,0 +1,8 @@
package com.hospital.mgmt.modules.attendance.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hospital.mgmt.modules.attendance.entity.AttendanceRecord;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AttendanceRecordMapper extends BaseMapper<AttendanceRecord> {}

@ -0,0 +1,32 @@
package com.hospital.mgmt.modules.audit.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@TableName("audit_log")
public class AuditLog implements Serializable {
private Long id;
private Long tenantId;
private Long operatorId;
private String operatorName;
private String operatorIp;
private String moduleCode;
private String moduleName;
private String pageCode;
private String pageName;
private String actionCode;
private String actionName;
private String operationType;
private String operationContent;
private Long businessId;
private String businessNo;
private String beforeData;
private String afterData;
private String requestParams;
private String responseStatus;
private String errorMessage;
private LocalDateTime operatedAt;
}

@ -0,0 +1,8 @@
package com.hospital.mgmt.modules.audit.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hospital.mgmt.modules.audit.entity.AuditLog;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AuditLogMapper extends BaseMapper<AuditLog> {}

@ -0,0 +1,39 @@
package com.hospital.mgmt.modules.auth.controller;
import com.hospital.mgmt.common.result.ApiResult;
import com.hospital.mgmt.modules.auth.dto.LoginRequest;
import com.hospital.mgmt.modules.auth.dto.LoginResponse;
import com.hospital.mgmt.modules.auth.dto.RefreshTokenRequest;
import com.hospital.mgmt.modules.auth.service.AuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@Tag(name = "认证管理")
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@Operation(summary = "Web端账号登录")
@PostMapping("/login")
public ApiResult<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
return ApiResult.success(authService.login(request));
}
@Operation(summary = "刷新Token")
@PostMapping("/refresh")
public ApiResult<LoginResponse> refresh(@Valid @RequestBody RefreshTokenRequest request) {
return ApiResult.success(authService.refreshToken(request.getRefreshToken()));
}
@Operation(summary = "获取当前用户权限集")
@GetMapping("/permissions")
public ApiResult<Object> getPermissions() {
return ApiResult.success(authService.getCurrentUserPermissions());
}
}

@ -0,0 +1,13 @@
package com.hospital.mgmt.modules.auth.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class LoginRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}

@ -0,0 +1,28 @@
package com.hospital.mgmt.modules.auth.dto;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class LoginResponse {
private String accessToken;
private String refreshToken;
private long expiresIn;
private UserInfo user;
@Data
@Builder
public static class UserInfo {
private Long id;
private String realName;
private String phone;
private String userType;
private String avatarUrl;
private Long tenantId;
private Long hospitalId;
private Long propertyCompanyId;
private List<String> permissions;
}
}

@ -0,0 +1,10 @@
package com.hospital.mgmt.modules.auth.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class RefreshTokenRequest {
@NotBlank(message = "refreshToken不能为空")
private String refreshToken;
}

@ -0,0 +1,97 @@
package com.hospital.mgmt.modules.auth.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hospital.mgmt.common.exception.BusinessException;
import com.hospital.mgmt.common.exception.ErrorCode;
import com.hospital.mgmt.modules.auth.dto.LoginRequest;
import com.hospital.mgmt.modules.auth.dto.LoginResponse;
import com.hospital.mgmt.modules.permission.entity.User;
import com.hospital.mgmt.modules.permission.mapper.UserMapper;
import com.hospital.mgmt.security.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class AuthService {
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
public LoginResponse login(LoginRequest request) {
User user = userMapper.selectOne(
new LambdaQueryWrapper<User>().eq(User::getUsername, request.getUsername())
);
if (user == null) {
throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户名或密码错误");
}
if (!passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) {
throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户名或密码错误");
}
if ("DISABLED".equals(user.getStatus())) {
throw new BusinessException(ErrorCode.STATUS_CONFLICT, "账号已被禁用");
}
String accessToken = jwtTokenProvider.createAccessToken(
user.getId(), user.getTenantId(), user.getUserType(),
user.getHospitalId(), user.getPropertyCompanyId(), user.getStaffId()
);
String refreshToken = jwtTokenProvider.createRefreshToken(user.getId());
return LoginResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.expiresIn(7200)
.user(LoginResponse.UserInfo.builder()
.id(user.getId())
.realName(user.getRealName())
.phone(user.getPhone())
.userType(user.getUserType())
.avatarUrl(user.getAvatarUrl())
.tenantId(user.getTenantId())
.hospitalId(user.getHospitalId())
.propertyCompanyId(user.getPropertyCompanyId())
.permissions(new ArrayList<>())
.build())
.build();
}
public LoginResponse refreshToken(String refreshToken) {
if (!jwtTokenProvider.validateToken(refreshToken)) {
throw new BusinessException(ErrorCode.INVALID_TOKEN, "refreshToken无效或已过期");
}
Long userId = jwtTokenProvider.getUserIdFromToken(refreshToken);
User user = userMapper.selectById(userId);
if (user == null) {
throw new BusinessException(ErrorCode.NOT_FOUND, "用户不存在");
}
String newAccessToken = jwtTokenProvider.createAccessToken(
user.getId(), user.getTenantId(), user.getUserType(),
user.getHospitalId(), user.getPropertyCompanyId(), user.getStaffId()
);
String newRefreshToken = jwtTokenProvider.createRefreshToken(user.getId());
return LoginResponse.builder()
.accessToken(newAccessToken)
.refreshToken(newRefreshToken)
.expiresIn(7200)
.build();
}
public Object getCurrentUserPermissions() {
// TODO: 从缓存或数据库加载完整权限集
Map<String, Object> result = new HashMap<>();
result.put("permissions", new ArrayList<>());
result.put("menus", new ArrayList<>());
result.put("dataScope", "SELF");
return result;
}
}

@ -0,0 +1,24 @@
package com.hospital.mgmt.modules.bidding.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("bidding_plan")
public class BiddingPlan extends BaseEntity {
private String planNo;
private String title;
private String biddingType;
private Long hospitalId;
private BigDecimal budgetAmount;
private LocalDate publishDate;
private LocalDate deadlineDate;
private LocalDate openBidDate;
private String status;
private String description;
}

@ -0,0 +1,21 @@
package com.hospital.mgmt.modules.bidding.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("bidding_section")
public class BiddingSection extends BaseEntity {
private Long planId;
private String sectionNo;
private String name;
private String scope;
private String requirements;
private BigDecimal budgetAmount;
private Long winnerSupplierId;
private String status;
}

@ -0,0 +1,20 @@
package com.hospital.mgmt.modules.bidding.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("bidding_supplier")
public class BiddingSupplier extends BaseEntity {
private String name;
private String unifiedSocialCode;
private String contactPerson;
private String contactPhone;
private String address;
private String qualificationStatus;
private Integer blacklist;
private String remark;
}

@ -0,0 +1,21 @@
package com.hospital.mgmt.modules.cleaning.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("cleaning_attachment")
public class CleaningAttachment extends BaseEntity {
private Long taskId;
private String fileType;
private String fileUrl;
private String fileName;
private String uploadStage;
private LocalDateTime watermarkTime;
private String watermarkLocation;
private Integer bluetoothConnected;
}

@ -0,0 +1,18 @@
package com.hospital.mgmt.modules.cleaning.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDate;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("cleaning_schedule")
public class CleaningSchedule extends BaseEntity {
private Long areaId;
private Long staffId;
private LocalDate scheduleDate;
private String shiftType;
private String status;
}

@ -0,0 +1,35 @@
package com.hospital.mgmt.modules.cleaning.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("cleaning_task")
public class CleaningTask extends BaseEntity {
private String taskNo;
private Long areaId;
private String areaName;
private String areaPath;
private Long assignedStaffId;
private String assignedStaffName;
private LocalDate taskDate;
private String taskType;
private String status;
private LocalDateTime checkInTime;
private Long checkInBeaconId;
private BigDecimal checkInDistance;
private String checkType;
private LocalDateTime completedAt;
private LocalDateTime timeoutAt;
private Integer isSpotChecked;
private String spotCheckResult;
private Long spotCheckerId;
private LocalDateTime spotCheckAt;
private String spotCheckRemark;
}

@ -0,0 +1,8 @@
package com.hospital.mgmt.modules.cleaning.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hospital.mgmt.modules.cleaning.entity.CleaningTask;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CleaningTaskMapper extends BaseMapper<CleaningTask> {}

@ -0,0 +1,30 @@
package com.hospital.mgmt.modules.contract.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("contract")
public class Contract extends BaseEntity {
private String contractNo;
private String title;
private String contractType;
private Long propertyCompanyId;
private Long hospitalId;
private Long campusId;
private BigDecimal totalAmount;
private LocalDate startDate;
private LocalDate endDate;
private String status;
private String paymentStatus;
private BigDecimal paidAmount;
private LocalDate signDate;
private String attachmentUrls;
private String remark;
private Integer warnDaysBefore;
}

@ -0,0 +1,23 @@
package com.hospital.mgmt.modules.contract.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("contract_change")
public class ContractChange extends BaseEntity {
private Long contractId;
private String changeType;
private String changeContent;
private String beforeData;
private String afterData;
private Long applicantId;
private String auditStatus;
private Long auditorId;
private String auditRemark;
private LocalDateTime auditAt;
}

@ -0,0 +1,22 @@
package com.hospital.mgmt.modules.contract.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("contract_payment")
public class ContractPayment extends BaseEntity {
private Long contractId;
private String nodeName;
private BigDecimal amount;
private LocalDate planDate;
private LocalDate actualDate;
private String status;
private Long confirmerId;
private String confirmRemark;
}

@ -0,0 +1,8 @@
package com.hospital.mgmt.modules.contract.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hospital.mgmt.modules.contract.entity.Contract;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ContractMapper extends BaseMapper<Contract> {}

@ -0,0 +1,32 @@
package com.hospital.mgmt.modules.device.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("device_beacon")
public class DeviceBeacon extends BaseEntity {
private String uuid;
private Integer major;
private Integer minor;
private String name;
private String macAddress;
private Long hospitalId;
private Long campusId;
private String bindType;
private Long bindId;
private String location;
private BigDecimal latitude;
private BigDecimal longitude;
private Integer rssiThreshold;
private BigDecimal distanceThreshold;
private Integer batteryLevel;
private Integer batteryWarnThreshold;
private LocalDateTime lastHeartbeatAt;
private String status;
}

@ -0,0 +1,8 @@
package com.hospital.mgmt.modules.device.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hospital.mgmt.modules.device.entity.DeviceBeacon;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DeviceBeaconMapper extends BaseMapper<DeviceBeacon> {}

@ -0,0 +1,26 @@
package com.hospital.mgmt.modules.evaluation.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("evaluation")
public class Evaluation extends BaseEntity {
private String moduleType;
private Long businessId;
private String businessNo;
private Integer score;
private String content;
private Long evaluatorId;
private String evaluatorName;
private String evaluatorType;
private Long targetStaffId;
private Long targetTeamId;
private String replyContent;
private Long replierId;
private LocalDateTime repliedAt;
}

@ -0,0 +1,18 @@
package com.hospital.mgmt.modules.evaluation.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("evaluation_config")
public class EvaluationConfig extends BaseEntity {
private String moduleType;
private Integer autoTrigger;
private Integer triggerDelayMinutes;
private Integer lowScoreThreshold;
private String lowScoreNotifyType;
private Integer mustReplyHours;
}

@ -0,0 +1,19 @@
package com.hospital.mgmt.modules.inspection.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("inspection_abnormal")
public class InspectionAbnormal extends BaseEntity {
private Long taskId;
private Long areaId;
private String itemName;
private String severity;
private String description;
private Long repairOrderId;
private String status;
}

@ -0,0 +1,20 @@
package com.hospital.mgmt.modules.inspection.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("inspection_area")
public class InspectionArea extends BaseEntity {
private String name;
private String code;
private Long hospitalId;
private Long campusId;
private Long parentId;
private Long beaconId;
private Integer sortOrder;
private String status;
}

@ -0,0 +1,22 @@
package com.hospital.mgmt.modules.inspection.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("inspection_attachment")
public class InspectionAttachment extends BaseEntity {
private Long taskId;
private Long abnormalId;
private String fileType;
private String fileUrl;
private String fileName;
private String uploadStage;
private LocalDateTime watermarkTime;
private String watermarkLocation;
private Integer bluetoothConnected;
}

@ -0,0 +1,25 @@
package com.hospital.mgmt.modules.inspection.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDate;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("inspection_plan")
public class InspectionPlan extends BaseEntity {
private String name;
private Long areaId;
private String frequency;
private String frequencyConfig;
private Long teamId;
private String staffIds;
private String assignRule;
private String checkItems;
private LocalDate startDate;
private LocalDate endDate;
private Integer remindBefore;
private String status;
}

@ -0,0 +1,35 @@
package com.hospital.mgmt.modules.inspection.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("inspection_task")
public class InspectionTask extends BaseEntity {
private Long planId;
private String taskNo;
private Long areaId;
private String areaName;
private Long assignedStaffId;
private String assignedStaffName;
private LocalDate planDate;
private LocalTime planTimeStart;
private LocalTime planTimeEnd;
private String status;
private LocalDateTime checkInTime;
private Long checkInBeaconId;
private BigDecimal checkInLatitude;
private BigDecimal checkInLongitude;
private BigDecimal checkInDistance;
private String checkType;
private String checkResult;
private Integer abnormalCount;
private LocalDateTime completedAt;
}

@ -0,0 +1,8 @@
package com.hospital.mgmt.modules.inspection.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hospital.mgmt.modules.inspection.entity.InspectionTask;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface InspectionTaskMapper extends BaseMapper<InspectionTask> {}

@ -0,0 +1,22 @@
package com.hospital.mgmt.modules.org.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("org_cleaning_area")
public class CleaningArea extends BaseEntity {
private Long parentId;
private Integer level;
private String name;
private String code;
private Long hospitalId;
private Long campusId;
private Long responsibleStaffId;
private Long beaconId;
private Integer sortOrder;
private String status;
}

@ -0,0 +1,18 @@
package com.hospital.mgmt.modules.org.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_hospital")
public class Hospital extends BaseEntity {
private String name;
private String address;
private String contactPerson;
private String contactPhone;
private String logoUrl;
private String status;
}

@ -0,0 +1,17 @@
package com.hospital.mgmt.modules.org.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
@Data
@TableName("sys_hospital_campus")
public class HospitalCampus implements Serializable {
private Long id;
private Long hospitalId;
private String name;
private String address;
private String contactPerson;
private String contactPhone;
private Integer sortOrder;
}

@ -0,0 +1,21 @@
package com.hospital.mgmt.modules.org.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@TableName("sys_property_company")
public class PropertyCompany implements Serializable {
private Long id;
private String name;
private String address;
private String contactPerson;
private String contactPhone;
private String licenseNo;
private String logoUrl;
private String status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

@ -0,0 +1,19 @@
package com.hospital.mgmt.modules.org.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@TableName("sys_property_hospital")
public class PropertyHospital implements Serializable {
private Long id;
private Long propertyCompanyId;
private Long hospitalId;
private String campusIds;
private LocalDate startDate;
private LocalDate endDate;
private String status;
}

@ -0,0 +1,19 @@
package com.hospital.mgmt.modules.org.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("org_staff")
public class Staff extends BaseEntity {
private String name;
private String phone;
private String gender;
private String avatarUrl;
private String employeeNo;
private String skills;
private String status;
}

@ -0,0 +1,17 @@
package com.hospital.mgmt.modules.org.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDate;
@Data
@TableName("org_staff_team")
public class StaffTeam implements Serializable {
private Long id;
private Long tenantId;
private Long staffId;
private Long teamId;
private Integer isPrimary;
private LocalDate joinDate;
}

@ -0,0 +1,20 @@
package com.hospital.mgmt.modules.org.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("org_team")
public class Team extends BaseEntity {
private String name;
private String teamType;
private Long leaderId;
private Long hospitalId;
private Long campusId;
private String description;
private String skills;
private String status;
}

@ -0,0 +1,8 @@
package com.hospital.mgmt.modules.org.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hospital.mgmt.modules.org.entity.HospitalCampus;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface HospitalCampusMapper extends BaseMapper<HospitalCampus> {}

@ -0,0 +1,8 @@
package com.hospital.mgmt.modules.org.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hospital.mgmt.modules.org.entity.Hospital;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface HospitalMapper extends BaseMapper<Hospital> {}

@ -0,0 +1,8 @@
package com.hospital.mgmt.modules.org.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hospital.mgmt.modules.org.entity.PropertyCompany;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PropertyCompanyMapper extends BaseMapper<PropertyCompany> {}

@ -0,0 +1,8 @@
package com.hospital.mgmt.modules.org.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hospital.mgmt.modules.org.entity.Staff;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface StaffMapper extends BaseMapper<Staff> {}

@ -0,0 +1,8 @@
package com.hospital.mgmt.modules.org.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hospital.mgmt.modules.org.entity.Team;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TeamMapper extends BaseMapper<Team> {}

@ -0,0 +1,24 @@
package com.hospital.mgmt.modules.permission.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
@Data
@TableName("sys_permission_config")
public class PermissionConfig implements Serializable {
private Long id;
private Long parentId;
private Integer level;
private String menuCode;
private String menuName;
private String pageCode;
private String pageName;
private String actionCode;
private String actionName;
private String operationCode;
private String operationName;
private Integer sortOrder;
private String moduleCode;
private String status;
}

@ -0,0 +1,19 @@
package com.hospital.mgmt.modules.permission.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
@Data
@TableName("sys_role")
public class Role implements Serializable {
private Long id;
private Long tenantId;
private String name;
private String code;
private String description;
private String scope;
private Integer isPreset;
private String dataScope;
private String status;
}

@ -0,0 +1,13 @@
package com.hospital.mgmt.modules.permission.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
@Data
@TableName("sys_role_permission")
public class RolePermission implements Serializable {
private Long id;
private Long roleId;
private Long permissionId;
}

@ -0,0 +1,28 @@
package com.hospital.mgmt.modules.permission.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@TableName("sys_user")
public class User implements Serializable {
private Long id;
private Long tenantId;
private String username;
private String passwordHash;
private String userType;
private String realName;
private String phone;
private String email;
private String avatarUrl;
private Long hospitalId;
private Long propertyCompanyId;
private Long staffId;
private String wechatOpenid;
private String wechatUnionid;
private String status;
private LocalDateTime lastLoginAt;
private String lastLoginIp;
}

@ -0,0 +1,14 @@
package com.hospital.mgmt.modules.permission.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
@Data
@TableName("sys_user_permission_override")
public class UserPermissionOverride implements Serializable {
private Long id;
private Long userId;
private Long permissionId;
private String grantType;
}

@ -0,0 +1,13 @@
package com.hospital.mgmt.modules.permission.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
@Data
@TableName("sys_user_role")
public class UserRole implements Serializable {
private Long id;
private Long userId;
private Long roleId;
}

@ -0,0 +1,8 @@
package com.hospital.mgmt.modules.permission.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hospital.mgmt.modules.permission.entity.PermissionConfig;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PermissionConfigMapper extends BaseMapper<PermissionConfig> {}

@ -0,0 +1,8 @@
package com.hospital.mgmt.modules.permission.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hospital.mgmt.modules.permission.entity.Role;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface RoleMapper extends BaseMapper<Role> {}

@ -0,0 +1,8 @@
package com.hospital.mgmt.modules.permission.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hospital.mgmt.modules.permission.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {}

@ -0,0 +1,30 @@
package com.hospital.mgmt.modules.plugin;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class ConfigDefinition {
private String code;
private String name;
private String description;
private String valueType;
private Object defaultValue;
private List<SelectOption> options;
private boolean tenantLevel;
private boolean required;
private String group;
@Data
@Builder
public static class SelectOption {
private String value;
private String label;
public static SelectOption of(String value, String label) {
return SelectOption.builder().value(value).label(label).build();
}
}
}

@ -0,0 +1,33 @@
package com.hospital.mgmt.modules.plugin;
import java.util.Collections;
import java.util.List;
public interface IModulePlugin {
ModuleInfo getModuleInfo();
List<MenuDefinition> getMenuDefinitions();
List<PermissionDefinition> getPermissionDefinitions();
default List<RouteDefinition> getRouteDefinitions() {
return Collections.emptyList();
}
default List<TableDefinition> getTableDefinitions() {
return Collections.emptyList();
}
default void onInitialize(ModuleContext context) {}
default void onDestroy() {}
default List<String> getDependencies() {
return Collections.emptyList();
}
default List<ConfigDefinition> getConfigDefinitions() {
return Collections.emptyList();
}
}

@ -0,0 +1,20 @@
package com.hospital.mgmt.modules.plugin;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class MenuDefinition {
private String code;
private String name;
private String icon;
private String path;
private int sortOrder;
private boolean visible;
private String platform;
private List<MenuDefinition> children;
private List<String> scopeRestrictions;
private String externalLink;
}

@ -0,0 +1,7 @@
package com.hospital.mgmt.modules.plugin;
public interface ModuleContext {
Long getTenantId();
void subscribe(Class<?> eventType, java.util.function.Consumer<?> listener);
void scheduleCron(String cron, Runnable task);
}

@ -0,0 +1,26 @@
package com.hospital.mgmt.modules.plugin;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class ModuleInfo {
private String code;
private String name;
private String description;
private String version;
private String author;
private String icon;
private int sortOrder;
private boolean builtIn;
private boolean defaultEnabled;
private ModuleType type;
private String platform;
private String minSystemVersion;
private String homePath;
public enum ModuleType {
CORE, BUSINESS, EXTENSION
}
}

@ -0,0 +1,46 @@
package com.hospital.mgmt.modules.plugin;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class PermissionDefinition {
private String menuCode;
private String menuName;
private List<PagePermission> pages;
@Data
@Builder
public static class PagePermission {
private String pageCode;
private String pageName;
private String pagePath;
private List<ActionPermission> actions;
}
@Data
@Builder
public static class ActionPermission {
private String actionCode;
private String actionName;
private List<OperationDef> operations;
}
@Data
@Builder
public static class OperationDef {
private String operationCode;
private String operationName;
private boolean defaultGranted;
public static OperationDef of(String code, String name, boolean defaultGranted) {
return OperationDef.builder()
.operationCode(code)
.operationName(name)
.defaultGranted(defaultGranted)
.build();
}
}
}

@ -0,0 +1,28 @@
package com.hospital.mgmt.modules.plugin;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class RouteDefinition {
private String path;
private String name;
private String component;
private RouteMeta meta;
private List<RouteDefinition> children;
private boolean requiresAuth;
private List<String> allowedRoles;
@Data
@Builder
public static class RouteMeta {
private String title;
private String icon;
private boolean keepAlive;
private boolean hidden;
private List<String> breadcrumb;
private List<String> permissions;
}
}

@ -0,0 +1,35 @@
package com.hospital.mgmt.modules.plugin;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class TableDefinition {
private String tableName;
private String description;
private List<ColumnDefinition> columns;
private List<IndexDefinition> indexes;
private String initialDataSql;
@Data
@Builder
public static class ColumnDefinition {
private String name;
private String dataType;
private boolean nullable;
private boolean primaryKey;
private String defaultValue;
private String comment;
private boolean tenantField;
}
@Data
@Builder
public static class IndexDefinition {
private String name;
private List<String> columns;
private boolean unique;
}
}

@ -0,0 +1,15 @@
package com.hospital.mgmt.modules.plugin.event;
import lombok.Getter;
import java.time.LocalDateTime;
@Getter
public class PermissionChangedEvent {
private final Long tenantId;
private final LocalDateTime timestamp;
public PermissionChangedEvent(Long tenantId) {
this.tenantId = tenantId;
this.timestamp = LocalDateTime.now();
}
}

@ -0,0 +1,50 @@
package com.hospital.mgmt.modules.plugin.listener;
import com.hospital.mgmt.modules.plugin.event.PermissionChangedEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
@Slf4j
@Component
@RequiredArgsConstructor
public class PermissionChangeListener implements MessageListener {
private final StringRedisTemplate redisTemplate;
private final RedisMessageListenerContainer listenerContainer;
private final org.springframework.context.ApplicationEventPublisher eventPublisher;
@PostConstruct
public void subscribe() {
listenerContainer.addMessageListener(this, new ChannelTopic("permission:changed"));
log.info("Subscribed to Redis channel: permission:changed");
}
@Override
public void onMessage(Message message, byte[] pattern) {
String channel = new String(message.getChannel());
String body = new String(message.getBody());
log.info("Received Redis message on channel: {}, body: {}", channel, body);
eventPublisher.publishEvent(new PermissionChangedEvent(null));
}
@EventListener
public void onPermissionChanged(PermissionChangedEvent event) {
log.info("Permission changed event received, refreshing local permission cache");
// TODO: 刷新本地权限缓存
}
public void publishPermissionChange(Long tenantId) {
redisTemplate.convertAndSend("permission:changed",
String.valueOf(System.currentTimeMillis()));
log.info("Published permission change notification");
}
}

@ -0,0 +1,21 @@
package com.hospital.mgmt.modules.repair.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("repair_delay_apply")
public class RepairDelayApply extends BaseEntity {
private Long orderId;
private Long applicantId;
private String reason;
private LocalDateTime expectedFinishAt;
private String auditStatus;
private Long auditorId;
private String auditRemark;
private LocalDateTime auditAt;
}

@ -0,0 +1,40 @@
package com.hospital.mgmt.modules.repair.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("repair_order")
public class RepairOrder extends BaseEntity {
private String orderNo;
private Long repairTypeId;
private String title;
private String description;
private String urgency;
private String status;
private Long hospitalId;
private Long campusId;
private String location;
private String locationDetail;
private Long reporterId;
private String reporterType;
private String reporterName;
private String reporterPhone;
private Long assignedTeamId;
private Long assignedStaffId;
private LocalDateTime assignedAt;
private LocalDateTime expectedFinishAt;
private LocalDateTime actualFinishAt;
private LocalDateTime completedAt;
private LocalDateTime closedAt;
private String closeReason;
private Long parentOrderId;
private Integer isAssist;
private Integer remindCount;
private LocalDateTime lastRemindAt;
}

@ -0,0 +1,22 @@
package com.hospital.mgmt.modules.repair.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("repair_order_attachment")
public class RepairOrderAttachment extends BaseEntity {
private Long orderId;
private String fileType;
private String fileUrl;
private String fileName;
private Long fileSize;
private String uploadStage;
private LocalDateTime watermarkTime;
private String watermarkLocation;
private Integer sortOrder;
}

@ -0,0 +1,21 @@
package com.hospital.mgmt.modules.repair.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("repair_order_log")
public class RepairOrderLog extends BaseEntity {
private Long orderId;
private String fromStatus;
private String toStatus;
private String action;
private Long operatorId;
private String operatorName;
private String remark;
private LocalDateTime operatedAt;
}

@ -0,0 +1,19 @@
package com.hospital.mgmt.modules.repair.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("repair_type")
public class RepairType extends BaseEntity {
private String name;
private String code;
private Long defaultTeamId;
private String iconUrl;
private Integer sortOrder;
private String status;
}

@ -0,0 +1,8 @@
package com.hospital.mgmt.modules.repair.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hospital.mgmt.modules.repair.entity.RepairOrder;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface RepairOrderMapper extends BaseMapper<RepairOrder> {}

@ -0,0 +1,25 @@
package com.hospital.mgmt.modules.system.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hospital.mgmt.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_bluetooth_policy")
public class BluetoothPolicy extends BaseEntity {
private Long hospitalId;
private Long campusId;
private String inspectionCheckIn;
private String inspectionPhoto;
private String cleaningCheckIn;
private String cleaningPhoto;
private String attendanceCheck;
private String auditStatus;
private String auditRemark;
private Long auditorId;
private java.time.LocalDateTime auditAt;
private java.time.LocalDateTime effectiveAt;
private String status;
}

@ -0,0 +1,17 @@
package com.hospital.mgmt.modules.system.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
@Data
@TableName("sys_dict_data")
public class DictData implements Serializable {
private Long id;
private Long tenantId;
private String typeCode;
private String label;
private String value;
private Integer sortOrder;
private String status;
}

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

Loading…
Cancel
Save