diff --git a/pjcloud-integrated/pjcloud-integrated-api/src/main/java/com/bsd/pjcloud/integrated/vo/meeting/app/MeetingRoomBookingDetailVo.java b/pjcloud-integrated/pjcloud-integrated-api/src/main/java/com/bsd/pjcloud/integrated/vo/meeting/app/MeetingRoomBookingDetailVo.java new file mode 100644 index 000000000..681cb5b02 --- /dev/null +++ b/pjcloud-integrated/pjcloud-integrated-api/src/main/java/com/bsd/pjcloud/integrated/vo/meeting/app/MeetingRoomBookingDetailVo.java @@ -0,0 +1,171 @@ +package com.bsd.pjcloud.integrated.vo.meeting.app; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDate; +import java.util.List; + +/** + * 会议室预约详情页面VO + * + * @author zjf + * @date 2025-01-20 + */ +@Data +@Schema(description = "会议室预约详情页面VO") +public class MeetingRoomBookingDetailVo { + + /** + * 会议室ID + */ + @Schema(description = "会议室ID") + private Long roomId; + + /** + * 会议室名称 + */ + @Schema(description = "会议室名称") + private String roomName; + + /** + * 查询日期 + */ + @Schema(description = "查询日期") + private LocalDate queryDate; + + /** + * 时间段状态列表 + */ + @Schema(description = "时间段状态列表") + private List timeSlots; + + /** + * 当日预约列表 + */ + @Schema(description = "当日预约列表") + private List dailyBookings; + + /** + * 时间段状态VO + */ + @Data + @Schema(description = "时间段状态VO") + public static class TimeSlotStatusVo { + /** + * 开始时间(如:08:00) + */ + @Schema(description = "开始时间") + private String startTime; + + /** + * 结束时间(如:08:30) + */ + @Schema(description = "结束时间") + private String endTime; + + /** + * 状态:available-可预约,booked-已预约,reviewing-审核中 + */ + @Schema(description = "状态") + private String status; + + /** + * 状态描述 + */ + @Schema(description = "状态描述") + private String statusText; + + /** + * 预约信息(当状态为已预约或审核中时) + */ + @Schema(description = "预约信息") + private BookingInfoVo bookingInfo; + } + + /** + * 预约信息VO + */ + @Data + @Schema(description = "预约信息VO") + public static class BookingInfoVo { + /** + * 会议主题 + */ + @Schema(description = "会议主题") + private String title; + + /** + * 申请人 + */ + @Schema(description = "申请人") + private String applicantName; + + /** + * 审核状态 + */ + @Schema(description = "审核状态") + private String checkStatus; + + /** + * 审核状态描述 + */ + @Schema(description = "审核状态描述") + private String checkStatusText; + } + + /** + * 当日预约VO + */ + @Data + @Schema(description = "当日预约VO") + public static class DailyBookingVo { + /** + * 预约ID + */ + @Schema(description = "预约ID") + private Long id; + + /** + * 会议主题 + */ + @Schema(description = "会议主题") + private String title; + + /** + * 申请人 + */ + @Schema(description = "申请人") + private String applicantName; + + /** + * 开始时间 + */ + @Schema(description = "开始时间") + private String startTime; + + /** + * 结束时间 + */ + @Schema(description = "结束时间") + private String endTime; + + /** + * 状态 + */ + @Schema(description = "状态") + private String status; + + /** + * 状态描述 + */ + @Schema(description = "状态描述") + private String statusText; + + /** + * 会议说明 + */ + @Schema(description = "会议说明") + private String description; + } +} diff --git a/pjcloud-integrated/pjcloud-integrated-api/src/main/java/com/bsd/pjcloud/integrated/vo/meeting/app/TimeSlotVo.java b/pjcloud-integrated/pjcloud-integrated-api/src/main/java/com/bsd/pjcloud/integrated/vo/meeting/app/TimeSlotVo.java index a098cccbf..c2942e011 100644 --- a/pjcloud-integrated/pjcloud-integrated-api/src/main/java/com/bsd/pjcloud/integrated/vo/meeting/app/TimeSlotVo.java +++ b/pjcloud-integrated/pjcloud-integrated-api/src/main/java/com/bsd/pjcloud/integrated/vo/meeting/app/TimeSlotVo.java @@ -13,7 +13,7 @@ public class TimeSlotVo { @Schema(description = "结束时间") private String endTime; - @Schema(description = "状态:free-空闲,booked-已预约,disabled-不可用") + @Schema(description = "状态:free-空闲,booked-已预约,reviewing-审核中,disabled-不可用") private String status; @Schema(description = "预约人信息") diff --git a/pjcloud-integrated/pjcloud-integrated-biz/API-INTEGRATION-GUIDE.md b/pjcloud-integrated/pjcloud-integrated-biz/API-INTEGRATION-GUIDE.md deleted file mode 100644 index 635bd9cd9..000000000 --- a/pjcloud-integrated/pjcloud-integrated-biz/API-INTEGRATION-GUIDE.md +++ /dev/null @@ -1,399 +0,0 @@ -# 外部系统滚动通知接口集成指南 - -## 📋 概述 - -本文档介绍如何集成使用 PJCloud 系统的滚动通知接口,该接口提供安全的 API 访问,供外部系统获取滚动通知信息。 - -## 🔐 认证方式 - -接口采用 **AppId + AppSecret** 的认证方式,通过 HTTP 请求头进行身份验证。 - -### 认证参数 - -| 参数名 | 位置 | 必填 | 说明 | -|--------|------|------|------| -| X-App-Id | 请求头 | 是 | 应用ID,由系统管理员分配 | -| X-App-Secret | 请求头 | 是 | 应用密钥,由系统管理员分配 | - -## 🌐 接口信息 - -### 滚动通知接口 - -- **接口地址**: `GET /notice/external/scroll` -- **请求方式**: GET -- **认证方式**: 请求头认证 -- **数据格式**: JSON - -### 请求参数 - -#### 查询参数 -| 参数名 | 必填 | 默认值 | 说明 | -|--------|------|--------|------| -| limit | 否 | 20 | 返回条数,最大50条 | - -#### 请求头示例 -```http -X-App-Id: 202501270001 -X-App-Secret: 550e8400-e29b-41d4-a716-446655440001 -``` - -## 📡 响应格式 - -### 成功响应 -```json -{ - "code": 0, - "msg": "操作成功", - "data": [ - { - "id": 1, - "title": "系统维护通知", - "createTime": "2025-01-27 10:00:00", - "description": "系统将于今晚进行维护升级" - }, - { - "id": 2, - "title": "新功能上线", - "createTime": "2025-01-26 15:30:00", - "description": "新增用户管理功能模块" - } - ] -} -``` - -### 错误响应 -```json -{ - "code": 500, - "msg": "无效的应用ID或密钥", - "data": null -} -``` - -## ⏰ 过期时间功能 - -系统支持为公告设置过期时间,到期后公告将自动下架不再显示: - -- **过期时间字段**: `expireTime` -- **自动处理**: 每天凌晨2点自动检查并处理过期公告 -- **处理逻辑**: 过期公告状态自动更新为"已下架" -- **查询过滤**: 所有查询接口自动过滤已过期公告 -- **灵活设置**: 支持设置具体过期时间,也支持永不过期(不设置过期时间) - -## 💻 集成示例 - -### JavaScript 调用示例 - -#### 基础调用 -```javascript -// 获取滚动通知 -async function getScrollNotices(limit = 20) { - try { - const response = await fetch('/notice/external/scroll?limit=' + limit, { - method: 'GET', - headers: { - 'X-App-Id': '202501270001', - 'X-App-Secret': '550e8400-e29b-41d4-a716-446655440001', - 'Content-Type': 'application/json' - } - }); - - const result = await response.json(); - - if (result.code === 0) { - return result.data; - } else { - throw new Error(result.msg); - } - } catch (error) { - console.error('获取通知失败:', error); - throw error; - } -} - -// 使用示例 -getScrollNotices(15) - .then(notices => { - console.log('获取到通知:', notices); - // 处理通知数据 - displayNotices(notices); - }) - .catch(error => { - console.error('错误:', error.message); - }); -``` - -#### 滚动展示组件 -```javascript -// 滚动通知展示组件 -class ScrollNoticeDisplay { - constructor(containerId, appId, appSecret) { - this.container = document.getElementById(containerId); - this.appId = appId; - this.appSecret = appSecret; - this.notices = []; - this.currentIndex = 0; - this.interval = null; - } - - // 获取通知数据 - async fetchNotices() { - try { - const response = await fetch('/notice/external/scroll?limit=10', { - headers: { - 'X-App-Id': this.appId, - 'X-App-Secret': this.appSecret - } - }); - - const result = await response.json(); - if (result.code === 0) { - this.notices = result.data; - this.startScroll(); - } - } catch (error) { - console.error('获取通知失败:', error); - } - } - - // 开始滚动 - startScroll() { - if (this.notices.length === 0) return; - - this.interval = setInterval(() => { - this.showNextNotice(); - }, 3000); // 每3秒切换一次 - - this.showNextNotice(); // 立即显示第一条 - } - - // 显示下一条通知 - showNextNotice() { - if (this.notices.length === 0) return; - - const notice = this.notices[this.currentIndex]; - this.container.innerHTML = ` -
- ${notice.title} - ${notice.createTime} -
- `; - - this.currentIndex = (this.currentIndex + 1) % this.notices.length; - } - - // 停止滚动 - stopScroll() { - if (this.interval) { - clearInterval(this.interval); - this.interval = null; - } - } -} - -// 使用示例 -const noticeDisplay = new ScrollNoticeDisplay( - 'notice-container', - '202501270001', - '550e8400-e29b-41d4-a716-446655440001' -); - -noticeDisplay.fetchNotices(); -``` - -### cURL 调用示例 - -#### 基础调用 -```bash -curl -X GET "http://your-domain/notice/external/scroll?limit=15" \ - -H "X-App-Id: 202501270001" \ - -H "X-App-Secret: 550e8400-e29b-41d4-a716-446655440001" -``` - -#### 获取最大条数 -```bash -curl -X GET "http://your-domain/notice/external/scroll?limit=50" \ - -H "X-App-Id: 202501270001" \ - -H "X-App-Secret: 550e8400-e29b-41d4-a716-446655440001" -``` - -### Java 调用示例 - -```java -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; -import org.springframework.web.client.RestTemplate; - -public class NoticeApiClient { - - private final String baseUrl; - private final String appId; - private final String appSecret; - private final RestTemplate restTemplate; - - public NoticeApiClient(String baseUrl, String appId, String appSecret) { - this.baseUrl = baseUrl; - this.appId = appId; - this.appSecret = appSecret; - this.restTemplate = new RestTemplate(); - } - - public List getScrollNotices(int limit) { - HttpHeaders headers = new HttpHeaders(); - headers.set("X-App-Id", appId); - headers.set("X-App-Secret", appSecret); - - HttpEntity entity = new HttpEntity<>(headers); - - String url = baseUrl + "/notice/external/scroll?limit=" + limit; - - ResponseEntity response = restTemplate.exchange( - url, - HttpMethod.GET, - entity, - ApiResponse.class - ); - - if (response.getBody() != null && response.getBody().getCode() == 0) { - return response.getBody().getData(); - } - - throw new RuntimeException("获取通知失败"); - } -} - -// 使用示例 -NoticeApiClient client = new NoticeApiClient( - "http://your-domain", - "202501270001", - "550e8400-e29b-41d4-a716-446655440001" -); - -List notices = client.getScrollNotices(20); -``` - -## 🎨 CSS 样式示例 - -```css -/* 滚动通知容器 */ -.notice-container { - width: 100%; - height: 40px; - background: linear-gradient(90deg, #f8f9fa, #e9ecef); - border: 1px solid #dee2e6; - border-radius: 4px; - overflow: hidden; - position: relative; -} - -/* 通知项 */ -.notice-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 16px; - height: 100%; - animation: slideIn 0.5s ease-in-out; -} - -/* 通知标题 */ -.notice-title { - font-weight: 500; - color: #495057; - flex: 1; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -/* 通知时间 */ -.notice-time { - font-size: 12px; - color: #6c757d; - margin-left: 16px; -} - -/* 滑动动画 */ -@keyframes slideIn { - from { - transform: translateX(100%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -/* 响应式设计 */ -@media (max-width: 768px) { - .notice-container { - height: 50px; - } - - .notice-item { - padding: 12px 16px; - } - - .notice-time { - display: none; /* 移动端隐藏时间 */ - } -} -``` - -## ⚠️ 注意事项 - -### 安全要求 -1. **密钥保护**: 请妥善保管 AppSecret,不要泄露给第三方 -2. **HTTPS 传输**: 建议使用 HTTPS 协议进行数据传输 -3. **访问频率**: 建议控制调用频率,避免频繁请求 - -### 技术限制 -1. **返回条数**: 单次请求最多返回 50 条通知 -2. **数据时效**: 只返回已发布的公告信息 -3. **排序规则**: 按置顶状态和创建时间倒序排列 - -### 错误处理 -1. **认证失败**: 检查 AppId 和 AppSecret 是否正确 -2. **网络异常**: 实现重试机制和超时处理 -3. **数据异常**: 对返回数据进行有效性验证 - -## 🔧 故障排除 - -### 常见问题 - -#### 1. 认证失败 -**错误信息**: "无效的应用ID或密钥" -**解决方案**: -- 检查 AppId 和 AppSecret 是否正确 -- 确认应用是否已启用 -- 联系系统管理员验证配置 - -#### 2. 网络超时 -**错误信息**: 请求超时或无响应 -**解决方案**: -- 检查网络连接 -- 确认服务器地址是否正确 -- 实现请求超时和重试机制 - -#### 3. 数据为空 -**可能原因**: -- 当前没有已发布的公告 -- 查询参数设置不当 -- 系统维护中 - -## 📞 技术支持 - -如果在集成过程中遇到问题,请联系技术支持: - -- **技术文档**: 本文档 -- **API 地址**: `/notice/external/scroll` -- **认证方式**: AppId + AppSecret -- **数据格式**: JSON - ---- - -*最后更新时间: 2025-01-27* -*版本: v1.0* \ No newline at end of file diff --git a/pjcloud-integrated/pjcloud-integrated-biz/MEETING_BOOKING_FLOW_EXAMPLE.md b/pjcloud-integrated/pjcloud-integrated-biz/MEETING_BOOKING_FLOW_EXAMPLE.md deleted file mode 100644 index ee0d9e51a..000000000 --- a/pjcloud-integrated/pjcloud-integrated-biz/MEETING_BOOKING_FLOW_EXAMPLE.md +++ /dev/null @@ -1,230 +0,0 @@ -# 会议室预约流程动态审批人实现示例 - -## 📋 概述 - -本文档展示如何在会议室预约的save方法中实现根据会议室管理员字段动态确定审批人的功能。 - -## 🔧 核心修改 - -### 1. 添加必要的依赖注入 - -```java -@RestController -@RequiredArgsConstructor -public class MeetingBookingController { - - private final MeetingBookingService meetingBookingService; - private final MeetingRoomService meetingRoomService; - private final RemoteFlowApiFlowService remoteFlowApiFlowService; - private final RemoteUserService remoteUserService; // 新增:用于获取用户信息 -} -``` - -### 2. 修改save方法中的流程启动部分 - -```java -@PostMapping -@Transactional -public R save(@RequestBody MeetingBookingSaveDto dto) { - // ... 前面的验证逻辑保持不变 ... - - // 7. 发起流程 - var processByBusinessKey = remoteFlowApiFlowService.getProcessByBusinessKey(IntegratedFlowConstant.INTEGRATED_MEETING_BOOKING); - if (!processByBusinessKey.isOk() || processByBusinessKey.getData() == null) { - throw new ServiceException("流程配置获取失败: " + processByBusinessKey.getMsg()); - } - Process process = processByBusinessKey.getData(); - - // 构建流程参数 - MeetingFlowDto meetingFlowDto = new MeetingFlowDto(); - meetingFlowDto.setRoomName(room.getRoomName()); - meetingFlowDto.setApplicantName(SecurityUtils.getUser().getUsername()); - meetingFlowDto.setMeetingTime(bookingDate + " " + startTime + "-" + endTime); - Map stringObjectMap = BeanUtil.beanToMap(meetingFlowDto); - - // 🔥 关键:动态设置审批人 - 根据会议室管理员 - List approvers = determineApproversByRoomAdmin(room); - if (!approvers.isEmpty()) { - // 注意:这里的 "approval_node_1" 需要根据实际流程设计中的节点ID调整 - stringObjectMap.put("approval_node_1_assignee_select", approvers); - } - - ProcessInstanceParamDto processInstanceParamDto = new ProcessInstanceParamDto(); - processInstanceParamDto.setFlowId(process.getFlowId()); - processInstanceParamDto.setParamMap(stringObjectMap); - var stringR = remoteFlowApiFlowService.startProcessInstance(processInstanceParamDto); - - // ... 后续逻辑保持不变 ... -} -``` - -### 3. 实现动态审批人确定方法 - -```java -/** - * 根据会议室管理员确定审批人 - * @param room 会议室信息 - * @return 审批人列表 - */ -private List determineApproversByRoomAdmin(MeetingRoomEntity room) { - List approvers = new ArrayList<>(); - - // 1. 优先使用会议室指定的管理员 - if (room.getAdminId() != null) { - try { - // 获取管理员用户信息 - R> userResult = remoteUserService.getUserById(Arrays.asList(room.getAdminId())); - if (userResult.isOk() && !userResult.getData().isEmpty()) { - SysUser adminUser = userResult.getData().get(0); - NodeUser approver = NodeUser.builder() - .id(adminUser.getUserId()) - .name(adminUser.getName()) - .type("user") - .selected(true) - .avatar(adminUser.getAvatar()) - .build(); - approvers.add(approver); - } - } catch (Exception e) { - log.error("获取会议室管理员信息失败: {}", e.getMessage()); - } - } - - // 2. 如果会议室没有管理员,使用部门经理 - if (approvers.isEmpty() && room.getDeptId() != null) { - approvers.addAll(getDeptManagerAsApprovers(room.getDeptId())); - } - - // 3. 如果仍然没有审批人,使用默认审批人 - if (approvers.isEmpty()) { - approvers.addAll(getDefaultApprovers()); - } - - return approvers; -} - -/** - * 获取部门经理作为审批人 - */ -private List getDeptManagerAsApprovers(Long deptId) { - List approvers = new ArrayList<>(); - try { - // 这里需要根据实际的部门服务接口获取部门经理 - // 示例:假设有部门服务可以获取部门经理 - /* - R deptResult = remoteDeptService.getDeptById(deptId); - if (deptResult.isOk() && deptResult.getData() != null) { - SysDept dept = deptResult.getData(); - Long managerId = dept.getManagerId(); - if (managerId != null) { - R> userResult = remoteUserService.getUserById(Arrays.asList(managerId)); - if (userResult.isOk() && !userResult.getData().isEmpty()) { - SysUser manager = userResult.getData().get(0); - NodeUser approver = NodeUser.builder() - .id(manager.getUserId()) - .name(manager.getName()) - .type("user") - .selected(true) - .avatar(manager.getAvatar()) - .build(); - approvers.add(approver); - } - } - } - */ - } catch (Exception e) { - log.error("获取部门经理信息失败: {}", e.getMessage()); - } - return approvers; -} - -/** - * 获取默认审批人 - */ -private List getDefaultApprovers() { - List approvers = new ArrayList<>(); - try { - // 设置系统管理员作为默认审批人 - Long systemAdminId = 1L; // 需要替换为实际的系统管理员ID - NodeUser defaultApprover = NodeUser.builder() - .id(systemAdminId) - .name("系统管理员") - .type("user") - .selected(true) - .build(); - approvers.add(defaultApprover); - } catch (Exception e) { - log.error("获取默认审批人失败: {}", e.getMessage()); - } - return approvers; -} -``` - -## 🎯 关键要点 - -### 1. 流程节点ID配置 -```java -// 这个key的格式是:{节点ID}_assignee_select -stringObjectMap.put("approval_node_1_assignee_select", approvers); -``` -- `approval_node_1` 需要替换为实际流程设计中的审批节点ID -- 节点ID可以在流程设计器中查看 - -### 2. NodeUser对象构建 -```java -NodeUser approver = NodeUser.builder() - .id(adminUser.getUserId()) // 用户ID - .name(adminUser.getName()) // 用户姓名 - .type("user") // 用户类型,固定为"user" - .selected(true) // 是否选中 - .avatar(adminUser.getAvatar()) // 用户头像(可选) - .build(); -``` - -### 3. 审批人优先级 -1. **会议室管理员** - 最高优先级 -2. **部门经理** - 次优先级 -3. **默认审批人** - 兜底方案 - -### 4. 错误处理 -- 所有获取用户信息的操作都有异常处理 -- 确保即使获取审批人失败,流程也能正常启动 -- 使用日志记录错误信息,便于排查问题 - -## 🔄 流程设计配置 - -在流程设计器中,需要将审批节点配置为: -- **分配类型**: `SELF_SELECT` (发起人自选) -- **节点ID**: 与代码中的key保持一致 -- **多实例模式**: 根据需要选择会签/或签/顺签 - -## 📝 使用示例 - -```java -// 会议室预约时,系统会自动: -// 1. 检查会议室是否有管理员 -// 2. 如果有管理员,使用管理员作为审批人 -// 3. 如果没有管理员,使用部门经理 -// 4. 如果都没有,使用系统管理员 - -// 流程启动后,审批人会收到待办任务 -// 审批人可以在流程中看到会议预约的详细信息 -``` - -## ⚠️ 注意事项 - -1. **节点ID一致性**: 确保代码中的节点ID与流程设计中的节点ID一致 -2. **用户服务可用性**: 确保用户服务接口可用,避免获取用户信息失败 -3. **权限控制**: 确保选择的审批人有相应的审批权限 -4. **测试验证**: 建议在测试环境先验证流程是否正常工作 - -## 🚀 扩展功能 - -可以根据业务需求进一步扩展: - -1. **多级审批**: 根据会议室等级设置多级审批 -2. **时间限制**: 根据预约时间长度确定审批人 -3. **部门规则**: 根据部门特殊规则确定审批人 -4. **用户角色**: 根据用户角色确定审批人 - - diff --git a/pjcloud-integrated/pjcloud-integrated-biz/MEETING_DESK_LAYOUT_API.md b/pjcloud-integrated/pjcloud-integrated-biz/MEETING_DESK_LAYOUT_API.md deleted file mode 100644 index 81ae2b52d..000000000 --- a/pjcloud-integrated/pjcloud-integrated-biz/MEETING_DESK_LAYOUT_API.md +++ /dev/null @@ -1,137 +0,0 @@ -# 移动端摆放形式选择接口使用说明 - -## 📋 概述 - -本文档介绍移动端预约课程时选择摆放形式的接口使用方法。 - -## 🌐 接口信息 - -### 获取摆放形式列表接口 - -- **接口地址**: `GET /meetingDeskLayout/app/list` -- **请求方式**: GET -- **认证方式**: 无需认证(移动端公开接口) -- **数据格式**: JSON - -## 📡 响应格式 - -### 成功响应 -```json -{ - "code": 0, - "msg": "操作成功", - "data": [ - { - "id": 1, - "layoutName": "U型摆放", - "layoutImage": "https://example.com/images/u-layout.jpg", - "capacityMin": 8, - "capacityMax": 12, - "description": "适合小组讨论的U型摆放方式" - }, - { - "id": 2, - "layoutName": "课桌式摆放", - "layoutImage": "https://example.com/images/classroom-layout.jpg", - "capacityMin": 20, - "capacityMax": 50, - "description": "传统课桌式摆放,适合培训课程" - }, - { - "id": 3, - "layoutName": "圆桌式摆放", - "layoutImage": "https://example.com/images/round-table-layout.jpg", - "capacityMin": 6, - "capacityMax": 10, - "description": "圆桌式摆放,促进互动交流" - } - ] -} -``` - -### 字段说明 - -| 字段名 | 类型 | 说明 | -|--------|------|------| -| id | Long | 摆放形式ID | -| layoutName | String | 摆放形式名称 | -| layoutImage | String | 摆放形式示意图URL | -| capacityMin | Integer | 最小容纳人数 | -| capacityMax | Integer | 最大容纳人数 | -| description | String | 摆放形式描述 | - -## 🔍 业务逻辑说明 - -1. **数据过滤**: 只返回状态为"可用"(isAvailable = "0")的摆放形式 -2. **排序规则**: 按照sortOrder字段升序排列 -3. **数据精简**: 只返回移动端需要的核心字段,减少数据传输量 - -## 📱 使用示例 - -### JavaScript 调用示例 -```javascript -fetch('/meetingDeskLayout/app/list', { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } -}) -.then(response => response.json()) -.then(data => { - if (data.code === 0) { - // 处理摆放形式数据 - const layouts = data.data; - layouts.forEach(layout => { - console.log(`摆放形式: ${layout.layoutName}, 容纳人数: ${layout.capacityMin}-${layout.capacityMax}人`); - }); - } -}) -.catch(error => { - console.error('获取摆放形式失败:', error); -}); -``` - -### React Native 调用示例 -```javascript -const fetchDeskLayouts = async () => { - try { - const response = await fetch('/meetingDeskLayout/app/list'); - const result = await response.json(); - - if (result.code === 0) { - return result.data; - } else { - throw new Error(result.msg); - } - } catch (error) { - console.error('获取摆放形式失败:', error); - return []; - } -}; -``` - -### cURL 调用示例 -```bash -curl -X GET "http://your-domain/meetingDeskLayout/app/list" \ - -H "Content-Type: application/json" -``` - -## 🎯 应用场景 - -1. **课程预约**: 用户在预约课程时选择适合的摆放形式 -2. **会议室配置**: 根据参会人数推荐合适的摆放形式 -3. **活动策划**: 根据活动类型选择合适的桌椅摆放方式 - -## ⚠️ 注意事项 - -1. **数据一致性**: 接口返回的数据与后台管理系统的摆放形式数据保持一致 -2. **图片加载**: layoutImage字段可能为空,前端需要处理图片加载失败的情况 -3. **容量限制**: 建议根据实际参会人数选择合适的摆放形式 -4. **缓存策略**: 建议前端对摆放形式数据进行适当缓存,减少重复请求 - -## 🔄 版本更新 - -- **v1.0.0** (2025-01-27): 初始版本,提供基本的摆放形式选择功能 - - - diff --git a/pjcloud-integrated/pjcloud-integrated-biz/NOTICE-EXPIRATION-GUIDE.md b/pjcloud-integrated/pjcloud-integrated-biz/NOTICE-EXPIRATION-GUIDE.md deleted file mode 100644 index 955c37aeb..000000000 --- a/pjcloud-integrated/pjcloud-integrated-biz/NOTICE-EXPIRATION-GUIDE.md +++ /dev/null @@ -1,217 +0,0 @@ -# 通知公告过期时间功能使用指南 - -## 📋 功能概述 - -通知公告系统新增过期时间功能,支持为公告设置具体的过期时间。到期后,公告将自动下架,不再在前端显示,无需手动操作。 - -## ✨ 主要特性 - -- **自动过期**: 支持设置具体过期时间,到期自动下架 -- **灵活配置**: 可选择设置过期时间或永不过期 -- **自动处理**: 系统定时任务自动处理过期公告 -- **智能过滤**: 所有查询接口自动过滤已过期公告 -- **性能优化**: 添加数据库索引,提高查询效率 - -## 🗄️ 数据库变更 - -### 新增字段 -```sql -ALTER TABLE notice ADD COLUMN expire_time DATETIME COMMENT '过期时间,到期后公告自动下架'; -``` - -### 新增索引 -```sql --- 过期时间索引 -CREATE INDEX idx_notice_expire_time ON notice(expire_time); - --- 状态+过期时间复合索引 -CREATE INDEX idx_notice_status_expire ON notice(status, expire_time); -``` - -## 🔧 配置说明 - -### 1. 启用定时任务 -在启动类或配置类上添加注解: -```java -@EnableScheduling -``` - -### 2. 定时任务配置 -系统默认每天凌晨2点执行过期处理: -```java -@Scheduled(cron = "0 0 2 * * ?") -``` - -如需调整执行时间,可修改 `NoticeExpirationTask` 中的 cron 表达式。 - -## 💻 使用方法 - -### 1. 创建公告时设置过期时间 - -#### 前端表单示例 -```html -
- - - - 不设置表示永不过期,设置后到期自动下架 - -
-``` - -#### 后端实体字段 -```java -@Schema(description="过期时间,到期后公告自动下架") -private LocalDateTime expireTime; -``` - -### 2. 过期时间设置建议 - -#### 短期公告(1-7天) -- 活动通知 -- 临时维护公告 -- 节日祝福 - -#### 中期公告(1-30天) -- 功能更新通知 -- 政策变更公告 -- 培训通知 - -#### 长期公告(30天以上) -- 规章制度 -- 长期政策 -- 重要通知 - -#### 永不过期 -- 系统说明文档 -- 基础操作指南 -- 重要规章制度 - -## 🔍 查询逻辑 - -### 自动过滤规则 -所有查询接口都会自动过滤已过期公告: - -```java -// 过滤已过期的公告 -wrapper.and(w -> w.isNull(NoticeEntity::getExpireTime) - .or() - .gt(NoticeEntity::getExpireTime, LocalDateTime.now())); -``` - -### 查询逻辑说明 -- `expireTime IS NULL`: 永不过期的公告 -- `expireTime > NOW()`: 未过期的公告 -- `expireTime <= NOW()`: 已过期的公告(自动过滤) - -## ⏰ 定时任务详情 - -### 执行时间 -- **频率**: 每天执行一次 -- **时间**: 凌晨2点(可配置) -- **时区**: 系统默认时区 - -### 处理逻辑 -1. 查询已过期但状态仍为"已发布"的公告 -2. 批量更新状态为"已下架" -3. 记录处理日志 -4. 异常处理和错误日志 - -### 日志示例 -``` -2025-01-27 02:00:00 INFO - 开始执行通知公告过期处理任务 -2025-01-27 02:00:01 INFO - 发现 3 条过期公告,开始处理 -2025-01-27 02:00:01 INFO - 公告 [系统维护通知] 已过期,状态已更新为已下架 -2025-01-27 02:00:01 INFO - 通知公告过期处理任务执行完成,共处理 3 条过期公告 -``` - -## 🚀 性能优化 - -### 数据库索引 -- 过期时间字段索引,提高过期查询性能 -- 状态+过期时间复合索引,优化常用查询 - -### 查询优化 -- 使用 MyBatis-Plus 的 Lambda 查询 -- 批量更新过期公告状态 -- 避免 N+1 查询问题 - -## 📊 监控和维护 - -### 1. 日志监控 -- 定时任务执行日志 -- 过期公告处理数量 -- 异常错误日志 - -### 2. 数据统计 -```sql --- 统计过期公告数量 -SELECT COUNT(*) FROM notice -WHERE expire_time IS NOT NULL -AND expire_time <= NOW() -AND status = '0'; - --- 统计即将过期的公告 -SELECT COUNT(*) FROM notice -WHERE expire_time IS NOT NULL -AND expire_time BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL 7 DAY) -AND status = '0'; -``` - -### 3. 定期维护 -- 清理过期的过期时间记录 -- 监控定时任务执行情况 -- 优化数据库查询性能 - -## ⚠️ 注意事项 - -### 1. 时区问题 -- 确保系统时区设置正确 -- 过期时间以系统时区为准 -- 考虑跨时区部署的情况 - -### 2. 性能考虑 -- 大量公告时,定时任务执行时间可能较长 -- 建议在业务低峰期执行 -- 监控数据库性能影响 - -### 3. 数据一致性 -- 定时任务执行期间,公告状态可能短暂不一致 -- 查询接口始终过滤过期公告,保证显示一致性 -- 建议在定时任务执行期间避免重要操作 - -## 🔧 故障排除 - -### 1. 定时任务未执行 -**检查项**: -- 是否启用 `@EnableScheduling` -- 定时任务类是否被 Spring 管理 -- cron 表达式是否正确 -- 系统时间是否正确 - -### 2. 过期公告仍显示 -**检查项**: -- 过期时间字段是否正确设置 -- 数据库索引是否创建 -- 查询逻辑是否正确 -- 定时任务是否正常执行 - -### 3. 性能问题 -**优化建议**: -- 检查数据库索引使用情况 -- 优化查询语句 -- 调整定时任务执行时间 -- 考虑分页处理大量数据 - -## 📞 技术支持 - -如遇到问题,请联系技术支持并提供以下信息: -- 错误日志 -- 数据库表结构 -- 定时任务配置 -- 系统环境信息 - ---- - -*最后更新时间: 2025-01-27* -*版本: v1.0* \ No newline at end of file diff --git a/pjcloud-integrated/pjcloud-integrated-biz/README-EXTERNAL-API.md b/pjcloud-integrated/pjcloud-integrated-biz/README-EXTERNAL-API.md deleted file mode 100644 index 039105fe3..000000000 --- a/pjcloud-integrated/pjcloud-integrated-biz/README-EXTERNAL-API.md +++ /dev/null @@ -1,98 +0,0 @@ -# 外部系统滚动通知接口使用说明 - -## 接口概述 - -提供安全的API访问,供其他系统获取滚动通知信息。 - -## 接口信息 - -- **接口地址**: `GET /notice/external/scroll` -- **请求方式**: GET -- **认证方式**: 请求头认证 - -## 请求参数 - -### 请求头 -| 参数名 | 必填 | 说明 | -|--------|------|------| -| X-App-Id | 是 | 应用ID,在配置文件中配置 | -| X-App-Secret | 是 | 应用密钥,在配置文件中配置 | - -### 查询参数 -| 参数名 | 必填 | 默认值 | 说明 | -|--------|------|--------|------| -| limit | 否 | 20 | 返回条数,最大50条 | - -## 响应格式 - -```json -{ - "code": 0, - "msg": "操作成功", - "data": [ - { - "title": "通知标题", - "createTime": "2025-01-27 10:00:00", - "description": "通知描述" - } - ] -} -``` - -## 使用示例 - -### JavaScript 调用示例 -```javascript -fetch('/notice/external/scroll?limit=15', { - method: 'GET', - headers: { - 'X-App-Id': 'external_app_001', - 'X-App-Secret': 'secret_2024_001' - } -}) -.then(response => response.json()) -.then(data => { - if (data.code === 0) { - // 处理通知数据 - console.log(data.data); - } -}) -.catch(error => { - console.error('获取通知失败:', error); -}); -``` - -### cURL 调用示例 -```bash -curl -X GET "http://your-domain/notice/external/scroll?limit=10" \ - -H "X-App-Id: external_app_001" \ - -H "X-App-Secret: secret_2024_001" -``` - -## 配置说明 - -在 `application.yml` 中添加以下配置: - -```yaml -pjcloud: - external-app: - apps: - - appId: "your_app_id" - appSecret: "your_app_secret" - appName: "你的应用名称" - enabled: true -``` - -## 安全特性 - -1. **应用认证**: 通过 appId 和 appSecret 验证调用方身份 -2. **访问控制**: 只返回已发布的公告 -3. **数据限制**: 限制最大返回条数,防止恶意调用 -4. **信息脱敏**: 只返回必要的滚动信息 - -## 注意事项 - -1. 请妥善保管应用密钥,不要泄露给第三方 -2. 建议使用HTTPS协议进行传输 -3. 可以根据需要调整返回条数限制 -4. 支持动态启用/禁用应用访问权限 \ No newline at end of file diff --git a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/controller/MeetingBookingController.java b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/controller/MeetingBookingController.java index 3373baa38..9ab39cca5 100644 --- a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/controller/MeetingBookingController.java +++ b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/controller/MeetingBookingController.java @@ -30,16 +30,16 @@ import com.bsd.pjcloud.integrated.constant.IntegratedFlowConstant; import com.bsd.pjcloud.integrated.dto.flow.MeetingFlowDto; import cn.hutool.core.bean.BeanUtil; import com.bsd.pjcloud.common.core.constant.enums.YesNoEnum; -import com.bsd.pjcloud.common.core.exception.ServiceException; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.time.LocalTime; import java.util.Map; import java.util.List; -import java.util.Objects; import com.bsd.pjcloud.integrated.dto.meeting.MeetingBookingSaveDto; +import com.bsd.pjcloud.integrated.dto.meeting.MeetingStatsQueryDto; import com.bsd.pjcloud.integrated.vo.meeting.MeetingBookingPageVo; +import com.bsd.pjcloud.integrated.vo.meeting.MeetingStatsVo; import java.util.HashMap; /** @@ -264,4 +264,17 @@ public class MeetingBookingController { // meetingBookingService.noticeVisitDesk(bookingId); // return R.ok(); // } + + /** + * 获取会议统计信息 + * @param queryDto 查询参数 + * @return 统计结果 + */ + @Operation(summary = "获取会议统计信息", description = "获取会议统计信息,支持按时间、部门、负责人等维度统计") + @GetMapping("/stats") + @PreAuthorize("@pms.hasPermission('integrated_meetingBooking_view')") + public R getMeetingStats(@ParameterObject MeetingStatsQueryDto queryDto) { + MeetingStatsVo stats = meetingBookingService.getMeetingStats(queryDto); + return R.ok(stats); + } } \ No newline at end of file diff --git a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/controller/MeetingRoomController.java b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/controller/MeetingRoomController.java index f57fc5016..8a036aeb3 100644 --- a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/controller/MeetingRoomController.java +++ b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/controller/MeetingRoomController.java @@ -6,16 +6,19 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import cn.hutool.core.bean.BeanUtil; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.bsd.pjcloud.admin.api.entity.SysUser; +import com.bsd.pjcloud.admin.api.feign.RemoteUserService; import com.bsd.pjcloud.common.core.constant.enums.YesNoEnum; -import com.bsd.pjcloud.common.core.exception.ServiceException; import com.bsd.pjcloud.common.core.util.R; import com.bsd.pjcloud.common.idempotent.annotation.Idempotent; import com.bsd.pjcloud.common.log.annotation.SysLog; import com.bsd.pjcloud.common.security.annotation.Inner; import com.bsd.pjcloud.common.security.util.SecurityUtils; import com.bsd.pjcloud.flow.task.api.feign.RemoteFlowApiFlowService; +import com.bsd.pjcloud.flow.task.api.feign.RemoteFlowEngineService; import com.bsd.pjcloud.flow.task.constant.TaskCheckStatusEnum; import com.bsd.pjcloud.flow.task.dto.ProcessInstanceParamDto; +import com.bsd.pjcloud.flow.task.dto.TaskParamDto; import com.bsd.pjcloud.flow.task.dto.ProcessNodeRecordParamDto; import com.bsd.pjcloud.flow.task.entity.Process; import com.bsd.pjcloud.flow.task.utils.GetCheckReasonUtils; @@ -35,6 +38,7 @@ import com.bsd.pjcloud.integrated.service.MeetingRoomService; import com.bsd.pjcloud.integrated.vo.meeting.app.MeetingBookingDetailVo; import com.bsd.pjcloud.integrated.vo.meeting.app.MeetingBookingListVo; import com.bsd.pjcloud.integrated.vo.meeting.app.MeetingBookingReqVo; +import com.bsd.pjcloud.integrated.vo.meeting.app.MeetingRoomBookingDetailVo; import com.bsd.pjcloud.integrated.vo.meeting.app.MeetingRoomPageVo; import com.bsd.pjcloud.integrated.vo.meeting.app.TimeSlotVo; import org.springframework.security.access.prepost.PreAuthorize; @@ -48,15 +52,10 @@ import lombok.RequiredArgsConstructor; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.time.LocalTime; import java.time.LocalDate; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.time.LocalDateTime; -import java.util.HashMap; /** * 会议室信息表 @@ -81,6 +80,11 @@ public class MeetingRoomController { private final RemoteFlowApiFlowService remoteFlowApiFlowService; + private final RemoteUserService remoteUserService; + + private final RemoteFlowEngineService remoteFlowEngineService; + + /** * 分页查询 * @param page 分页对象 @@ -472,6 +476,17 @@ public class MeetingRoomController { if (!Objects.equals(booking.getApplicantId(), userId)) { return R.failed("无权取消他人预约"); } + // 先尝试终止流程实例(如果存在且在审批中) + String processInstanceId = booking.getProcessInstanceId(); + if (processInstanceId != null && !processInstanceId.isEmpty()) { + TaskParamDto dto = new TaskParamDto(); + dto.setProcessInstanceIdList(java.util.List.of(processInstanceId)); + var stopRet = remoteFlowEngineService.stopProcessInstance(dto); + if (!stopRet.isOk()) { + return R.failed("取消审批流程失败: " + stopRet.getMsg()); + } + } + // 设置checkStatus为4(已取消) booking.setCheckStatus("4"); boolean updated = meetingBookingService.updateById(booking); @@ -603,24 +618,170 @@ public class MeetingRoomController { } /** - * 会议室下拉选择(仅返回可用会议室id和名称) + * 会议室预约详情页面 - 包含时间段状态和当日预约列表 + * @param roomId 会议室ID + * @param date 查询日期 + * @return 会议室预约详情 */ - @GetMapping("/app/select") - public R>> selectMeetingRooms() { - LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); - wrapper.eq(MeetingRoomEntity::getStatus, YesNoEnum.NO.getCode()); // 只查可用 - wrapper.select(MeetingRoomEntity::getId, MeetingRoomEntity::getRoomName); - List list = meetingRoomService.list(wrapper); - - List> result = new ArrayList<>(); - for (MeetingRoomEntity room : list) { - Map map = new HashMap<>(); - map.put("id", room.getId()); - map.put("name", room.getRoomName()); - result.add(map); + @GetMapping("/app/booking-detail-page/{roomId}") + @Operation(summary = "会议室预约详情页面", description = "获取指定会议室在指定日期的预约详情,包含时间段状态和预约列表") + public R getMeetingRoomBookingDetail(@PathVariable("roomId") Long roomId, + @RequestParam("date") String date) { + // 1. 验证会议室是否存在 + MeetingRoomEntity room = meetingRoomService.getById(roomId); + if (room == null) { + return R.failed("会议室不存在"); } + + // 2. 解析日期 + LocalDate queryDate = LocalDate.parse(date); + + // 3. 生成时间段状态(8:00-18:00,每小时一个时间段) + List timeSlots = generateTimeSlots(queryDate, roomId); + + // 4. 获取当日预约列表 + List dailyBookings = getDailyBookings(queryDate, roomId); + + // 5. 构建返回结果 + MeetingRoomBookingDetailVo result = new MeetingRoomBookingDetailVo(); + result.setRoomId(roomId); + result.setRoomName(room.getRoomName()); + result.setQueryDate(queryDate); + result.setTimeSlots(timeSlots); + result.setDailyBookings(dailyBookings); + return R.ok(result); } + /** + * 生成时间段状态(30分钟间隔) + */ + private List generateTimeSlots(LocalDate date, Long roomId) { + List timeSlots = new ArrayList<>(); + + // 生成30分钟间隔的时间段(8:00-18:00) + LocalTime startTime = LocalTime.of(8, 0); + LocalTime endTime = LocalTime.of(18, 0); + + while (startTime.isBefore(endTime)) { + MeetingRoomBookingDetailVo.TimeSlotStatusVo slot = new MeetingRoomBookingDetailVo.TimeSlotStatusVo(); + slot.setStartTime(startTime.format(DateTimeFormatter.ofPattern("HH:mm"))); + slot.setEndTime(startTime.plusMinutes(30).format(DateTimeFormatter.ofPattern("HH:mm"))); + slot.setStatus("available"); + slot.setStatusText("可预约"); + timeSlots.add(slot); + startTime = startTime.plusMinutes(30); + } + + // 获取当天的所有预约记录 + List bookings = meetingBookingService.list( + Wrappers.lambdaQuery() + .eq(MeetingBookingEntity::getRoomId, roomId) + .eq(MeetingBookingEntity::getBookingDate, date) + ); + + // 标记已预约的时间段 + for (MeetingBookingEntity booking : bookings) { + LocalTime bookingStart = booking.getStartTime(); + LocalTime bookingEnd = booking.getEndTime(); + + // 找到对应的时间段并更新状态 + for (MeetingRoomBookingDetailVo.TimeSlotStatusVo slot : timeSlots) { + LocalTime slotStart = LocalTime.parse(slot.getStartTime(), DateTimeFormatter.ofPattern("HH:mm")); + LocalTime slotEnd = LocalTime.parse(slot.getEndTime(), DateTimeFormatter.ofPattern("HH:mm")); + + // 修改判断逻辑:只有当时间段完全在预约时间范围内时才标记为已预约 + if (!slotStart.isBefore(bookingStart) && !slotEnd.isAfter(bookingEnd)) { + slot.setStatus(getBookingStatus(booking.getCheckStatus())); + slot.setStatusText(getBookingStatusText(booking.getCheckStatus())); + + // 设置预约信息 + MeetingRoomBookingDetailVo.BookingInfoVo bookingInfo = new MeetingRoomBookingDetailVo.BookingInfoVo(); + bookingInfo.setTitle(booking.getTitle()); + bookingInfo.setApplicantName(getApplicantName(booking.getApplicantId())); + bookingInfo.setCheckStatus(booking.getCheckStatus()); + bookingInfo.setCheckStatusText(getBookingStatusText(booking.getCheckStatus())); + slot.setBookingInfo(bookingInfo); + } + } + } + + return timeSlots; + } + + /** + * 获取当日预约列表 + */ + private List getDailyBookings(LocalDate date, Long roomId) { + List bookings = meetingBookingService.list( + Wrappers.lambdaQuery() + .eq(MeetingBookingEntity::getRoomId, roomId) + .eq(MeetingBookingEntity::getBookingDate, date) + .orderByAsc(MeetingBookingEntity::getStartTime) + ); + + return bookings.stream().map(booking -> { + MeetingRoomBookingDetailVo.DailyBookingVo vo = new MeetingRoomBookingDetailVo.DailyBookingVo(); + vo.setId(booking.getId()); + vo.setTitle(booking.getTitle()); + vo.setApplicantName(getApplicantName(booking.getApplicantId())); + vo.setStartTime(booking.getStartTime().format(DateTimeFormatter.ofPattern("HH:mm"))); + vo.setEndTime(booking.getEndTime().format(DateTimeFormatter.ofPattern("HH:mm"))); + vo.setStatus(booking.getCheckStatus()); + vo.setStatusText(getBookingStatusText(booking.getCheckStatus())); + vo.setDescription(booking.getDescription()); + return vo; + }).toList(); + } + + /** + * 根据审核状态获取预约状态 + */ + private String getBookingStatus(String checkStatus) { + if (checkStatus == null) return "available"; + switch (checkStatus) { + case "1": return "reviewing"; // 审核中 + case "2": return "booked"; // 审核通过 + case "3": return "available"; // 审核拒绝,重新可预约 + case "4": return "available"; // 已取消,重新可预约 + case "5": return "available"; // 取消,重新可预约 + case "0": return "booked"; // 已到访 + default: return "available"; + } + } + + /** + * 根据审核状态获取状态描述 + */ + private String getBookingStatusText(String checkStatus) { + if (checkStatus == null) return "可预约"; + switch (checkStatus) { + case "1": return "审核中"; + case "2": return "审核通过"; + case "3": return "审核拒绝"; + case "4": return "已取消"; + case "5": return "取消"; + case "0": return "已到访"; + default: return "可预约"; + } + } + + /** + * 获取申请人姓名(这里简化处理,实际应该调用用户服务) + */ + private String getApplicantName(Long applicantId) { + // TODO: 实际应该调用用户服务获取真实姓名 + + R> userById = remoteUserService.getUserById(Arrays.asList(applicantId)); + if (userById.isOk()) { + List data = userById.getData(); + if (data != null && !data.isEmpty()) { + SysUser user = data.get(0); + return user.getName(); + } + } + return "用户" + applicantId; + } + } \ No newline at end of file diff --git a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/dto/meeting/MeetingStatsQueryDto.java b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/dto/meeting/MeetingStatsQueryDto.java new file mode 100644 index 000000000..36c0594be --- /dev/null +++ b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/dto/meeting/MeetingStatsQueryDto.java @@ -0,0 +1,54 @@ +package com.bsd.pjcloud.integrated.dto.meeting; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDate; + +/** + * 会议统计查询参数 + * + * @author zjf + * @date 2025-01-20 + */ +@Data +@Schema(description = "会议统计查询参数") +public class MeetingStatsQueryDto { + + /** + * 开始日期 + */ + @Schema(description = "开始日期") + private LocalDate startDate; + + /** + * 结束日期 + */ + @Schema(description = "结束日期") + private LocalDate endDate; + + /** + * 统计维度:dept-按部门统计,assignee-按负责人统计,time-按时间统计 + */ + @Schema(description = "统计维度:dept-按部门统计,assignee-按负责人统计,time-按时间统计") + private String dimension; + + /** + * 时间粒度:day-按天,week-按周,month-按月 + */ + @Schema(description = "时间粒度:day-按天,week-按周,month-按月") + private String timeGranularity; + + /** + * 部门ID(可选,用于筛选特定部门) + */ + @Schema(description = "部门ID(可选,用于筛选特定部门)") + private Long deptId; + + /** + * 会议室ID(可选,用于筛选特定会议室) + */ + @Schema(description = "会议室ID(可选,用于筛选特定会议室)") + private Long roomId; +} + diff --git a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/mapper/MeetingBookingMapper.java b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/mapper/MeetingBookingMapper.java index 5d6de3185..eee630e62 100644 --- a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/mapper/MeetingBookingMapper.java +++ b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/mapper/MeetingBookingMapper.java @@ -7,12 +7,57 @@ import org.apache.ibatis.annotations.Param; import java.time.LocalDate; import java.util.List; +import java.util.Map; @Mapper public interface MeetingBookingMapper extends PjcloudBaseMapper { - - - List findByRoomIdAndDate(Long roomId, @Param("date") LocalDate date); + + /** + * 获取总体统计信息 + */ + Map getOverallStats(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("deptId") Long deptId, + @Param("roomId") Long roomId); + + /** + * 按部门统计 + */ + List> getDeptStats(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("roomId") Long roomId); + + /** + * 按负责人统计 + */ + List> getAssigneeStats(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("deptId") Long deptId, + @Param("roomId") Long roomId); + + /** + * 按时间统计 + */ + List> getTimeStats(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("timeGranularity") String timeGranularity, + @Param("deptId") Long deptId, + @Param("roomId") Long roomId); + + /** + * 按状态统计 + */ + List> getStatusStats(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("deptId") Long deptId, + @Param("roomId") Long roomId); + + /** + * 按会议室统计 + */ + List> getRoomStats(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("deptId") Long deptId); } \ No newline at end of file diff --git a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/MeetingBookingService.java b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/MeetingBookingService.java index 11ca9295c..51084776f 100644 --- a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/MeetingBookingService.java +++ b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/MeetingBookingService.java @@ -3,7 +3,9 @@ package com.bsd.pjcloud.integrated.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.IService; +import com.bsd.pjcloud.integrated.dto.meeting.MeetingStatsQueryDto; import com.bsd.pjcloud.integrated.entity.MeetingBookingEntity; +import com.bsd.pjcloud.integrated.vo.meeting.MeetingStatsVo; public interface MeetingBookingService extends IService { @@ -37,4 +39,11 @@ public interface MeetingBookingService extends IService { * @param */ void noticeVisitor(MeetingBookingEntity meetingBookingEntity); + + /** + * 获取会议统计信息 + * @param queryDto 查询参数 + * @return 统计结果 + */ + MeetingStatsVo getMeetingStats(MeetingStatsQueryDto queryDto); } \ No newline at end of file diff --git a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/impl/MeetingBookingServiceImpl.java b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/impl/MeetingBookingServiceImpl.java index abe4dea97..ec500a34f 100644 --- a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/impl/MeetingBookingServiceImpl.java +++ b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/impl/MeetingBookingServiceImpl.java @@ -3,21 +3,31 @@ package com.bsd.pjcloud.integrated.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bsd.pjcloud.common.core.util.R; import com.bsd.pjcloud.common.core.util.WebUtils; import com.bsd.pjcloud.common.data.datascope.DataScope; import com.bsd.pjcloud.common.security.util.SecurityUtils; +import com.bsd.pjcloud.admin.api.entity.SysDept; +import com.bsd.pjcloud.admin.api.feign.RemoteDeptService; +import com.bsd.pjcloud.integrated.dto.meeting.MeetingStatsQueryDto; import com.bsd.pjcloud.integrated.entity.*; import com.bsd.pjcloud.integrated.mapper.CleanUserMapper; import com.bsd.pjcloud.integrated.mapper.MeetingBookingMapper; import com.bsd.pjcloud.integrated.mapper.MeetingRoomMapper; import com.bsd.pjcloud.integrated.service.MeetingBookingService; +import com.bsd.pjcloud.integrated.vo.meeting.MeetingStatsVo; import com.bsd.pjcloud.thirdparty.api.feign.RemoterThirdPartySmsService; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; +import cn.hutool.core.bean.BeanUtil; +import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; /** * 会议室预约 @@ -34,6 +44,8 @@ public class MeetingBookingServiceImpl extends ServiceImpl wrapper) { return baseMapper.selectPageByScope(page, wrapper, DataScope.of()); @@ -42,18 +54,19 @@ public class MeetingBookingServiceImpl extends ServiceImpl stringStringHashMap = new HashMap<>(); @@ -102,6 +115,105 @@ public class MeetingBookingServiceImpl extends ServiceImpl overallStatsMap = baseMapper.getOverallStats(startDate, endDate, deptId, roomId); + MeetingStatsVo.OverallStats overallStats = BeanUtil.toBean(overallStatsMap, MeetingStatsVo.OverallStats.class); + statsVo.setOverallStats(overallStats); + + // 2. 按部门统计 + List> deptStatsMapList = baseMapper.getDeptStats(startDate, endDate, roomId); + + // 批量获取部门信息 + List deptIds = deptStatsMapList.stream() + .map(map -> (Long) map.get("deptId")) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + + Map deptNameMap = new HashMap<>(); + if (!deptIds.isEmpty()) { + R> deptMapR = remoteDeptService.getDeptByDeptIds(deptIds); + if (deptMapR.isOk() && deptMapR.getData() != null) { + for (Map.Entry entry : deptMapR.getData().entrySet()) { + deptNameMap.put(entry.getKey(), entry.getValue().getName()); + } + } + } + + // 转换部门统计结果 + List deptStats = deptStatsMapList.stream() + .map(map -> { + MeetingStatsVo.DeptStats deptStat = BeanUtil.toBean(map, MeetingStatsVo.DeptStats.class); + Long deptIdValue = (Long) map.get("deptId"); + deptStat.setDeptName(deptNameMap.getOrDefault(deptIdValue, "未知部门")); + return deptStat; + }) + .collect(Collectors.toList()); + statsVo.setDeptStats(deptStats); + + // 3. 按负责人统计 + List> assigneeStatsMapList = baseMapper.getAssigneeStats(startDate, endDate, deptId, roomId); + List assigneeStats = assigneeStatsMapList.stream() + .map(map -> BeanUtil.toBean(map, MeetingStatsVo.AssigneeStats.class)) + .collect(Collectors.toList()); + statsVo.setAssigneeStats(assigneeStats); + + // 4. 按时间统计 + final String timeGranularity = queryDto.getTimeGranularity() != null && !queryDto.getTimeGranularity().isEmpty() + ? queryDto.getTimeGranularity() : "day"; + List> timeStatsMapList = baseMapper.getTimeStats(startDate, endDate, timeGranularity, deptId, roomId); + List timeStats = timeStatsMapList.stream() + .map(map -> { + MeetingStatsVo.TimeStats timeStat = BeanUtil.toBean(map, MeetingStatsVo.TimeStats.class); + // 处理日期字段 + if (map.get("date") != null) { + if (timeGranularity.equals("day")) { + timeStat.setDate(LocalDate.parse(map.get("date").toString())); + } else { + // 对于周和月,date字段是字符串格式 + timeStat.setDate(null); + } + } + return timeStat; + }) + .collect(Collectors.toList()); + statsVo.setTimeStats(timeStats); + + // 5. 按状态统计 + List> statusStatsMapList = baseMapper.getStatusStats(startDate, endDate, deptId, roomId); + List statusStats = statusStatsMapList.stream() + .map(map -> BeanUtil.toBean(map, MeetingStatsVo.StatusStats.class)) + .collect(Collectors.toList()); + statsVo.setStatusStats(statusStats); + + // 6. 按会议室统计 + List> roomStatsMapList = baseMapper.getRoomStats(startDate, endDate, deptId); + List roomStats = roomStatsMapList.stream() + .map(map -> BeanUtil.toBean(map, MeetingStatsVo.RoomStats.class)) + .collect(Collectors.toList()); + statsVo.setRoomStats(roomStats); + + return statsVo; + } + // private void sendConfirmEndMeetingSms(VisitorUserEntity visitorUserEntity, VisitorsEntity dataVisitors) { // Map stringStringHashMap = new HashMap<>(); // DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); diff --git a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/vo/meeting/MeetingStatsVo.java b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/vo/meeting/MeetingStatsVo.java new file mode 100644 index 000000000..2e0abc306 --- /dev/null +++ b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/vo/meeting/MeetingStatsVo.java @@ -0,0 +1,282 @@ +package com.bsd.pjcloud.integrated.vo.meeting; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDate; +import java.util.List; + +/** + * 会议统计结果 + * + * @author zjf + * @date 2025-01-20 + */ +@Data +@Schema(description = "会议统计结果") +public class MeetingStatsVo { + + /** + * 总体统计 + */ + @Schema(description = "总体统计") + private OverallStats overallStats; + + /** + * 按部门统计 + */ + @Schema(description = "按部门统计") + private List deptStats; + + /** + * 按负责人统计 + */ + @Schema(description = "按负责人统计") + private List assigneeStats; + + /** + * 按时间统计 + */ + @Schema(description = "按时间统计") + private List timeStats; + + /** + * 按状态统计 + */ + @Schema(description = "按状态统计") + private List statusStats; + + /** + * 按会议室统计 + */ + @Schema(description = "按会议室统计") + private List roomStats; + + @Data + @Schema(description = "总体统计") + public static class OverallStats { + /** + * 总预约数 + */ + @Schema(description = "总预约数") + private Long totalBookings; + + /** + * 审核通过数 + */ + @Schema(description = "审核通过数") + private Long approvedBookings; + + /** + * 审核中数 + */ + @Schema(description = "审核中数") + private Long pendingBookings; + + /** + * 审核拒绝数 + */ + @Schema(description = "审核拒绝数") + private Long rejectedBookings; + + /** + * 已取消数 + */ + @Schema(description = "已取消数") + private Long cancelledBookings; + + /** + * 通过率 + */ + @Schema(description = "通过率") + private Double approvalRate; + + /** + * 平均会议时长(分钟) + */ + @Schema(description = "平均会议时长(分钟)") + private Double avgDuration; + + /** + * 总会议时长(分钟) + */ + @Schema(description = "总会议时长(分钟)") + private Long totalDuration; + } + + @Data + @Schema(description = "部门统计") + public static class DeptStats { + /** + * 部门ID + */ + @Schema(description = "部门ID") + private Long deptId; + + /** + * 部门名称 + */ + @Schema(description = "部门名称") + private String deptName; + + /** + * 预约数量 + */ + @Schema(description = "预约数量") + private Long bookingCount; + + /** + * 通过数量 + */ + @Schema(description = "通过数量") + private Long approvedCount; + + /** + * 通过率 + */ + @Schema(description = "通过率") + private Double approvalRate; + + /** + * 总会议时长(分钟) + */ + @Schema(description = "总会议时长(分钟)") + private Long totalDuration; + } + + @Data + @Schema(description = "负责人统计") + public static class AssigneeStats { + /** + * 负责人姓名 + */ + @Schema(description = "负责人姓名") + private String assigneeName; + + /** + * 预约数量 + */ + @Schema(description = "预约数量") + private Long bookingCount; + + /** + * 通过数量 + */ + @Schema(description = "通过数量") + private Long approvedCount; + + /** + * 通过率 + */ + @Schema(description = "通过率") + private Double approvalRate; + + /** + * 总会议时长(分钟) + */ + @Schema(description = "总会议时长(分钟)") + private Long totalDuration; + } + + @Data + @Schema(description = "时间统计") + public static class TimeStats { + /** + * 时间标签 + */ + @Schema(description = "时间标签") + private String timeLabel; + + /** + * 日期 + */ + @Schema(description = "日期") + private LocalDate date; + + /** + * 预约数量 + */ + @Schema(description = "预约数量") + private Long bookingCount; + + /** + * 通过数量 + */ + @Schema(description = "通过数量") + private Long approvedCount; + + /** + * 总会议时长(分钟) + */ + @Schema(description = "总会议时长(分钟)") + private Long totalDuration; + } + + @Data + @Schema(description = "状态统计") + public static class StatusStats { + /** + * 状态码 + */ + @Schema(description = "状态码") + private String status; + + /** + * 状态名称 + */ + @Schema(description = "状态名称") + private String statusName; + + /** + * 数量 + */ + @Schema(description = "数量") + private Long count; + + /** + * 占比 + */ + @Schema(description = "占比") + private Double percentage; + } + + @Data + @Schema(description = "会议室统计") + public static class RoomStats { + /** + * 会议室ID + */ + @Schema(description = "会议室ID") + private Long roomId; + + /** + * 会议室名称 + */ + @Schema(description = "会议室名称") + private String roomName; + + /** + * 预约数量 + */ + @Schema(description = "预约数量") + private Long bookingCount; + + /** + * 通过数量 + */ + @Schema(description = "通过数量") + private Long approvedCount; + + /** + * 使用率 + */ + @Schema(description = "使用率") + private Double usageRate; + + /** + * 总会议时长(分钟) + */ + @Schema(description = "总会议时长(分钟)") + private Long totalDuration; + } +} diff --git a/pjcloud-integrated/pjcloud-integrated-biz/src/main/resources/mapper/MeetingBookingMapper.xml b/pjcloud-integrated/pjcloud-integrated-biz/src/main/resources/mapper/MeetingBookingMapper.xml index 755d43be2..933c63536 100644 --- a/pjcloud-integrated/pjcloud-integrated-biz/src/main/resources/mapper/MeetingBookingMapper.xml +++ b/pjcloud-integrated/pjcloud-integrated-biz/src/main/resources/mapper/MeetingBookingMapper.xml @@ -30,4 +30,216 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pjcloud-integrated/pjcloud-integrated-biz/src/test/java/com/bsd/pjcloud/integrated/service/MeetingBookingStatsTest.java b/pjcloud-integrated/pjcloud-integrated-biz/src/test/java/com/bsd/pjcloud/integrated/service/MeetingBookingStatsTest.java new file mode 100644 index 000000000..0f98b547e --- /dev/null +++ b/pjcloud-integrated/pjcloud-integrated-biz/src/test/java/com/bsd/pjcloud/integrated/service/MeetingBookingStatsTest.java @@ -0,0 +1,166 @@ +package com.bsd.pjcloud.integrated.service; + +import com.bsd.pjcloud.integrated.dto.meeting.MeetingStatsQueryDto; +import com.bsd.pjcloud.integrated.vo.meeting.MeetingStatsVo; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import javax.annotation.Resource; +import java.time.LocalDate; + +/** + * 会议室预约统计功能测试 + * + * @author zjf + * @date 2025-01-20 + */ +@Slf4j +@SpringBootTest +@ActiveProfiles("test") +public class MeetingBookingStatsTest { + + @Resource + private MeetingBookingService meetingBookingService; + + /** + * 测试获取总体统计信息 + */ + @Test + public void testGetOverallStats() { + MeetingStatsQueryDto queryDto = new MeetingStatsQueryDto(); + queryDto.setStartDate(LocalDate.now().minusDays(30)); + queryDto.setEndDate(LocalDate.now()); + + MeetingStatsVo stats = meetingBookingService.getMeetingStats(queryDto); + + log.info("总体统计信息: {}", stats.getOverallStats()); + assert stats.getOverallStats() != null; + } + + /** + * 测试按部门统计 + */ + @Test + public void testGetDeptStats() { + MeetingStatsQueryDto queryDto = new MeetingStatsQueryDto(); + queryDto.setStartDate(LocalDate.now().minusDays(30)); + queryDto.setEndDate(LocalDate.now()); + queryDto.setDimension("dept"); + + MeetingStatsVo stats = meetingBookingService.getMeetingStats(queryDto); + + log.info("部门统计信息: {}", stats.getDeptStats()); + assert stats.getDeptStats() != null; + } + + /** + * 测试按时间统计(按天) + */ + @Test + public void testGetTimeStatsByDay() { + MeetingStatsQueryDto queryDto = new MeetingStatsQueryDto(); + queryDto.setStartDate(LocalDate.now().minusDays(7)); + queryDto.setEndDate(LocalDate.now()); + queryDto.setTimeGranularity("day"); + + MeetingStatsVo stats = meetingBookingService.getMeetingStats(queryDto); + + log.info("按天统计信息: {}", stats.getTimeStats()); + assert stats.getTimeStats() != null; + } + + /** + * 测试按时间统计(按周) + */ + @Test + public void testGetTimeStatsByWeek() { + MeetingStatsQueryDto queryDto = new MeetingStatsQueryDto(); + queryDto.setStartDate(LocalDate.now().minusDays(30)); + queryDto.setEndDate(LocalDate.now()); + queryDto.setTimeGranularity("week"); + + MeetingStatsVo stats = meetingBookingService.getMeetingStats(queryDto); + + log.info("按周统计信息: {}", stats.getTimeStats()); + assert stats.getTimeStats() != null; + } + + /** + * 测试按时间统计(按月) + */ + @Test + public void testGetTimeStatsByMonth() { + MeetingStatsQueryDto queryDto = new MeetingStatsQueryDto(); + queryDto.setStartDate(LocalDate.now().minusDays(90)); + queryDto.setEndDate(LocalDate.now()); + queryDto.setTimeGranularity("month"); + + MeetingStatsVo stats = meetingBookingService.getMeetingStats(queryDto); + + log.info("按月统计信息: {}", stats.getTimeStats()); + assert stats.getTimeStats() != null; + } + + /** + * 测试按状态统计 + */ + @Test + public void testGetStatusStats() { + MeetingStatsQueryDto queryDto = new MeetingStatsQueryDto(); + queryDto.setStartDate(LocalDate.now().minusDays(30)); + queryDto.setEndDate(LocalDate.now()); + + MeetingStatsVo stats = meetingBookingService.getMeetingStats(queryDto); + + log.info("状态统计信息: {}", stats.getStatusStats()); + assert stats.getStatusStats() != null; + } + + /** + * 测试按会议室统计 + */ + @Test + public void testGetRoomStats() { + MeetingStatsQueryDto queryDto = new MeetingStatsQueryDto(); + queryDto.setStartDate(LocalDate.now().minusDays(30)); + queryDto.setEndDate(LocalDate.now()); + + MeetingStatsVo stats = meetingBookingService.getMeetingStats(queryDto); + + log.info("会议室统计信息: {}", stats.getRoomStats()); + assert stats.getRoomStats() != null; + } + + /** + * 测试按负责人统计 + */ + @Test + public void testGetAssigneeStats() { + MeetingStatsQueryDto queryDto = new MeetingStatsQueryDto(); + queryDto.setStartDate(LocalDate.now().minusDays(30)); + queryDto.setEndDate(LocalDate.now()); + queryDto.setDimension("assignee"); + + MeetingStatsVo stats = meetingBookingService.getMeetingStats(queryDto); + + log.info("负责人统计信息: {}", stats.getAssigneeStats()); + assert stats.getAssigneeStats() != null; + } + + /** + * 测试无参数查询(使用默认时间范围) + */ + @Test + public void testGetStatsWithoutParams() { + MeetingStatsQueryDto queryDto = new MeetingStatsQueryDto(); + + MeetingStatsVo stats = meetingBookingService.getMeetingStats(queryDto); + + log.info("默认统计信息: {}", stats); + assert stats != null; + assert stats.getOverallStats() != null; + } +} +