完善会议

This commit is contained in:
zjf committed 2025-10-09 18:35:18 +08:00
1 parent 06d2bc5feb
commit 169a2735d9
16 files changed
+1259 -1115

No files matched your search

@@ -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<TimeSlotStatusVo> timeSlots;
/**
* 当日预约列表
*/
@Schema(description = "当日预约列表")
private List<DailyBookingVo> 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;
}
}
@@ -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 = "预约人信息")
@@ -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 = `
<div class="notice-item">
<span class="notice-title">${notice.title}</span>
<span class="notice-time">${notice.createTime}</span>
</div>
`;
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<NoticeScreenVo> getScrollNotices(int limit) {
HttpHeaders headers = new HttpHeaders();
headers.set("X-App-Id", appId);
headers.set("X-App-Secret", appSecret);
HttpEntity<String> entity = new HttpEntity<>(headers);
String url = baseUrl + "/notice/external/scroll?limit=" + limit;
ResponseEntity<ApiResponse> 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<NoticeScreenVo> 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*
@@ -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<String, Object> stringObjectMap = BeanUtil.beanToMap(meetingFlowDto);
// 🔥 关键:动态设置审批人 - 根据会议室管理员
List<NodeUser> 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<NodeUser> determineApproversByRoomAdmin(MeetingRoomEntity room) {
List<NodeUser> approvers = new ArrayList<>();
// 1. 优先使用会议室指定的管理员
if (room.getAdminId() != null) {
try {
// 获取管理员用户信息
R<List<SysUser>> 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<NodeUser> getDeptManagerAsApprovers(Long deptId) {
List<NodeUser> approvers = new ArrayList<>();
try {
// 这里需要根据实际的部门服务接口获取部门经理
// 示例:假设有部门服务可以获取部门经理
/*
R<SysDept> deptResult = remoteDeptService.getDeptById(deptId);
if (deptResult.isOk() && deptResult.getData() != null) {
SysDept dept = deptResult.getData();
Long managerId = dept.getManagerId();
if (managerId != null) {
R<List<SysUser>> 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<NodeUser> getDefaultApprovers() {
List<NodeUser> 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. **用户角色**: 根据用户角色确定审批人
@@ -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): 初始版本,提供基本的摆放形式选择功能
@@ -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
<div class="form-group">
<label>过期时间</label>
<input type="datetime-local" name="expireTime" class="form-control">
<small class="form-text text-muted">
不设置表示永不过期,设置后到期自动下架
</small>
</div>
```
#### 后端实体字段
```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*
@@ -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. 支持动态启用/禁用应用访问权限
@@ -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<MeetingStatsVo> getMeetingStats(@ParameterObject MeetingStatsQueryDto queryDto) {
MeetingStatsVo stats = meetingBookingService.getMeetingStats(queryDto);
return R.ok(stats);
}
}
@@ -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<List<Map<String, Object>>> selectMeetingRooms() {
LambdaQueryWrapper<MeetingRoomEntity> wrapper = Wrappers.lambdaQuery();
wrapper.eq(MeetingRoomEntity::getStatus, YesNoEnum.NO.getCode()); // 只查可用
wrapper.select(MeetingRoomEntity::getId, MeetingRoomEntity::getRoomName);
List<MeetingRoomEntity> list = meetingRoomService.list(wrapper);
List<Map<String, Object>> result = new ArrayList<>();
for (MeetingRoomEntity room : list) {
Map<String, Object> 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<MeetingRoomBookingDetailVo> 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<MeetingRoomBookingDetailVo.TimeSlotStatusVo> timeSlots = generateTimeSlots(queryDate, roomId);
// 4. 获取当日预约列表
List<MeetingRoomBookingDetailVo.DailyBookingVo> 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<MeetingRoomBookingDetailVo.TimeSlotStatusVo> generateTimeSlots(LocalDate date, Long roomId) {
List<MeetingRoomBookingDetailVo.TimeSlotStatusVo> 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<MeetingBookingEntity> bookings = meetingBookingService.list(
Wrappers.<MeetingBookingEntity>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<MeetingRoomBookingDetailVo.DailyBookingVo> getDailyBookings(LocalDate date, Long roomId) {
List<MeetingBookingEntity> bookings = meetingBookingService.list(
Wrappers.<MeetingBookingEntity>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<List<SysUser>> userById = remoteUserService.getUserById(Arrays.asList(applicantId));
if (userById.isOk()) {
List<SysUser> data = userById.getData();
if (data != null && !data.isEmpty()) {
SysUser user = data.get(0);
return user.getName();
}
}
return "用户" + applicantId;
}
}
@@ -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;
}
@@ -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<MeetingBookingEntity> {
List<MeetingBookingEntity> findByRoomIdAndDate(Long roomId, @Param("date") LocalDate date);
/**
* 获取总体统计信息
*/
Map<String, Object> getOverallStats(@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate,
@Param("deptId") Long deptId,
@Param("roomId") Long roomId);
/**
* 按部门统计
*/
List<Map<String, Object>> getDeptStats(@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate,
@Param("roomId") Long roomId);
/**
* 按负责人统计
*/
List<Map<String, Object>> getAssigneeStats(@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate,
@Param("deptId") Long deptId,
@Param("roomId") Long roomId);
/**
* 按时间统计
*/
List<Map<String, Object>> getTimeStats(@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate,
@Param("timeGranularity") String timeGranularity,
@Param("deptId") Long deptId,
@Param("roomId") Long roomId);
/**
* 按状态统计
*/
List<Map<String, Object>> getStatusStats(@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate,
@Param("deptId") Long deptId,
@Param("roomId") Long roomId);
/**
* 按会议室统计
*/
List<Map<String, Object>> getRoomStats(@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate,
@Param("deptId") Long deptId);
}
@@ -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<MeetingBookingEntity> {
@@ -37,4 +39,11 @@ public interface MeetingBookingService extends IService<MeetingBookingEntity> {
* @param
*/
void noticeVisitor(MeetingBookingEntity meetingBookingEntity);
/**
* 获取会议统计信息
* @param queryDto 查询参数
* @return 统计结果
*/
MeetingStatsVo getMeetingStats(MeetingStatsQueryDto queryDto);
}
@@ -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<MeetingBookingMapper,
private final MeetingRoomMapper meetingRoomMapper;
private final CleanUserMapper cleanUserMapper;
private final RemoteDeptService remoteDeptService;
@Override
public Page pageScope(Page page, LambdaQueryWrapper<MeetingBookingEntity> wrapper) {
return baseMapper.selectPageByScope(page, wrapper, DataScope.of());
@@ -42,18 +54,19 @@ public class MeetingBookingServiceImpl extends ServiceImpl<MeetingBookingMapper,
@Override
public Boolean confirmEndMeeting(Long bookingId) {
Long currentUserId = SecurityUtils.getUser().getId();
MeetingBookingEntity booking = getById(bookingId);
//更新会议预约结束状态
MeetingBookingEntity booking = new MeetingBookingEntity();
MeetingBookingEntity updateBooking = new MeetingBookingEntity();
if (!booking.getApplicantId().equals(currentUserId)){
throw new RuntimeException("无权限操作");
}
booking.setId(bookingId);
booking.setIsConfirmEnd(1);
boolean updated = updateById(booking);
updateBooking.setId(bookingId);
updateBooking.setIsConfirmEnd(1);
boolean updated = updateById(updateBooking);
//查询会议室阿姨信息
MeetingRoomEntity meetingRoomEntity = meetingRoomMapper.selectById(booking.getRoomId());
Long cleanUserId = meetingRoomEntity.getCleanUserId();
if (cleanUserId != null){
MeetingRoomEntity meetingRoomEntity = meetingRoomMapper.selectById(updateBooking.getRoomId());
if (meetingRoomEntity != null){
Long cleanUserId = meetingRoomEntity.getCleanUserId();
CleanUserEntity cleanUserEntity = cleanUserMapper.selectById(cleanUserId);
//发送短信通知阿姨
Map<String, String> stringStringHashMap = new HashMap<>();
@@ -102,6 +115,105 @@ public class MeetingBookingServiceImpl extends ServiceImpl<MeetingBookingMapper,
}
@Override
public MeetingStatsVo getMeetingStats(MeetingStatsQueryDto queryDto) {
MeetingStatsVo statsVo = new MeetingStatsVo();
// 设置默认查询条件
LocalDate startDate = queryDto.getStartDate();
LocalDate endDate = queryDto.getEndDate();
Long deptId = queryDto.getDeptId();
Long roomId = queryDto.getRoomId();
// 如果没有设置时间范围,默认查询最近30天
if (startDate == null) {
startDate = LocalDate.now().minusDays(30);
}
if (endDate == null) {
endDate = LocalDate.now();
}
// 1. 获取总体统计
Map<String, Object> overallStatsMap = baseMapper.getOverallStats(startDate, endDate, deptId, roomId);
MeetingStatsVo.OverallStats overallStats = BeanUtil.toBean(overallStatsMap, MeetingStatsVo.OverallStats.class);
statsVo.setOverallStats(overallStats);
// 2. 按部门统计
List<Map<String, Object>> deptStatsMapList = baseMapper.getDeptStats(startDate, endDate, roomId);
// 批量获取部门信息
List<Long> deptIds = deptStatsMapList.stream()
.map(map -> (Long) map.get("deptId"))
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
Map<Long, String> deptNameMap = new HashMap<>();
if (!deptIds.isEmpty()) {
R<Map<Long, SysDept>> deptMapR = remoteDeptService.getDeptByDeptIds(deptIds);
if (deptMapR.isOk() && deptMapR.getData() != null) {
for (Map.Entry<Long, SysDept> entry : deptMapR.getData().entrySet()) {
deptNameMap.put(entry.getKey(), entry.getValue().getName());
}
}
}
// 转换部门统计结果
List<MeetingStatsVo.DeptStats> 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<Map<String, Object>> assigneeStatsMapList = baseMapper.getAssigneeStats(startDate, endDate, deptId, roomId);
List<MeetingStatsVo.AssigneeStats> 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<Map<String, Object>> timeStatsMapList = baseMapper.getTimeStats(startDate, endDate, timeGranularity, deptId, roomId);
List<MeetingStatsVo.TimeStats> 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<Map<String, Object>> statusStatsMapList = baseMapper.getStatusStats(startDate, endDate, deptId, roomId);
List<MeetingStatsVo.StatusStats> statusStats = statusStatsMapList.stream()
.map(map -> BeanUtil.toBean(map, MeetingStatsVo.StatusStats.class))
.collect(Collectors.toList());
statsVo.setStatusStats(statusStats);
// 6. 按会议室统计
List<Map<String, Object>> roomStatsMapList = baseMapper.getRoomStats(startDate, endDate, deptId);
List<MeetingStatsVo.RoomStats> 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<String, String> stringStringHashMap = new HashMap<>();
// DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@@ -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> deptStats;
/**
* 按负责人统计
*/
@Schema(description = "按负责人统计")
private List<AssigneeStats> assigneeStats;
/**
* 按时间统计
*/
@Schema(description = "按时间统计")
private List<TimeStats> timeStats;
/**
* 按状态统计
*/
@Schema(description = "按状态统计")
private List<StatusStats> statusStats;
/**
* 按会议室统计
*/
@Schema(description = "按会议室统计")
private List<RoomStats> 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;
}
}
@@ -30,4 +30,216 @@
<select id="findByRoomIdAndDate" resultType="com.bsd.pjcloud.integrated.entity.MeetingBookingEntity">
SELECT * FROM meeting_booking WHERE room_id = #{roomId} AND start_time >= #{date} AND start_time &lt;= #{date}
</select>
<!-- 总体统计 -->
<select id="getOverallStats" resultType="map">
SELECT
COUNT(*) as totalBookings,
SUM(CASE WHEN check_status = '2' THEN 1 ELSE 0 END) as approvedBookings,
SUM(CASE WHEN check_status = '1' THEN 1 ELSE 0 END) as pendingBookings,
SUM(CASE WHEN check_status = '3' THEN 1 ELSE 0 END) as rejectedBookings,
SUM(CASE WHEN check_status = '4' OR check_status = '5' THEN 1 ELSE 0 END) as cancelledBookings,
ROUND(SUM(CASE WHEN check_status = '2' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) as approvalRate,
ROUND(AVG(TIMESTAMPDIFF(MINUTE, start_time, end_time)), 2) as avgDuration,
SUM(TIMESTAMPDIFF(MINUTE, start_time, end_time)) as totalDuration
FROM meeting_booking mb
WHERE 1=1
<if test="startDate != null">
AND mb.booking_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND mb.booking_date &lt;= #{endDate}
</if>
<if test="deptId != null">
AND mb.dept_id = #{deptId}
</if>
<if test="roomId != null">
AND mb.room_id = #{roomId}
</if>
AND mb.del_flag = '0'
</select>
<!-- 按部门统计 -->
<select id="getDeptStats" resultType="map">
SELECT
mb.dept_id as deptId,
COUNT(*) as bookingCount,
SUM(CASE WHEN mb.check_status = '2' THEN 1 ELSE 0 END) as approvedCount,
ROUND(SUM(CASE WHEN mb.check_status = '2' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) as approvalRate,
SUM(TIMESTAMPDIFF(MINUTE, mb.start_time, mb.end_time)) as totalDuration
FROM meeting_booking mb
WHERE 1=1
<if test="startDate != null">
AND mb.booking_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND mb.booking_date &lt;= #{endDate}
</if>
<if test="roomId != null">
AND mb.room_id = #{roomId}
</if>
AND mb.del_flag = '0'
AND mb.dept_id IS NOT NULL
GROUP BY mb.dept_id
ORDER BY bookingCount DESC
</select>
<!-- 按负责人统计 -->
<select id="getAssigneeStats" resultType="map">
SELECT
mb.assignee_name as assigneeName,
COUNT(*) as bookingCount,
SUM(CASE WHEN mb.check_status = '2' THEN 1 ELSE 0 END) as approvedCount,
ROUND(SUM(CASE WHEN mb.check_status = '2' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) as approvalRate,
SUM(TIMESTAMPDIFF(MINUTE, mb.start_time, mb.end_time)) as totalDuration
FROM meeting_booking mb
WHERE 1=1
<if test="startDate != null">
AND mb.booking_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND mb.booking_date &lt;= #{endDate}
</if>
<if test="deptId != null">
AND mb.dept_id = #{deptId}
</if>
<if test="roomId != null">
AND mb.room_id = #{roomId}
</if>
AND mb.del_flag = '0'
AND mb.assignee_name IS NOT NULL
GROUP BY mb.assignee_name
ORDER BY bookingCount DESC
</select>
<!-- 按时间统计 -->
<select id="getTimeStats" resultType="map">
SELECT
<choose>
<when test="timeGranularity == 'day'">
mb.booking_date as date,
DATE_FORMAT(mb.booking_date, '%Y-%m-%d') as timeLabel
</when>
<when test="timeGranularity == 'week'">
DATE_FORMAT(mb.booking_date, '%Y-%u') as date,
CONCAT(YEAR(mb.booking_date), '年第', WEEK(mb.booking_date), '周') as timeLabel
</when>
<when test="timeGranularity == 'month'">
DATE_FORMAT(mb.booking_date, '%Y-%m') as date,
DATE_FORMAT(mb.booking_date, '%Y年%m月') as timeLabel
</when>
<otherwise>
mb.booking_date as date,
DATE_FORMAT(mb.booking_date, '%Y-%m-%d') as timeLabel
</otherwise>
</choose>,
COUNT(*) as bookingCount,
SUM(CASE WHEN mb.check_status = '2' THEN 1 ELSE 0 END) as approvedCount,
SUM(TIMESTAMPDIFF(MINUTE, mb.start_time, mb.end_time)) as totalDuration
FROM meeting_booking mb
WHERE 1=1
<if test="startDate != null">
AND mb.booking_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND mb.booking_date &lt;= #{endDate}
</if>
<if test="deptId != null">
AND mb.dept_id = #{deptId}
</if>
<if test="roomId != null">
AND mb.room_id = #{roomId}
</if>
AND mb.del_flag = '0'
GROUP BY
<choose>
<when test="timeGranularity == 'day'">
mb.booking_date
</when>
<when test="timeGranularity == 'week'">
YEAR(mb.booking_date), WEEK(mb.booking_date)
</when>
<when test="timeGranularity == 'month'">
YEAR(mb.booking_date), MONTH(mb.booking_date)
</when>
<otherwise>
mb.booking_date
</otherwise>
</choose>
ORDER BY date ASC
</select>
<!-- 按状态统计 -->
<select id="getStatusStats" resultType="map">
SELECT
mb.check_status as status,
CASE mb.check_status
WHEN '1' THEN '审核中'
WHEN '2' THEN '审核通过'
WHEN '3' THEN '审核拒绝'
WHEN '4' THEN '已取消'
WHEN '5' THEN '取消'
WHEN '0' THEN '已到访'
ELSE '未知状态'
END as statusName,
COUNT(*) as count,
ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM meeting_booking WHERE del_flag = '0'
<if test="startDate != null">
AND booking_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND booking_date &lt;= #{endDate}
</if>
<if test="deptId != null">
AND dept_id = #{deptId}
</if>
<if test="roomId != null">
AND room_id = #{roomId}
</if>
), 2) as percentage
FROM meeting_booking mb
WHERE 1=1
<if test="startDate != null">
AND mb.booking_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND mb.booking_date &lt;= #{endDate}
</if>
<if test="deptId != null">
AND mb.dept_id = #{deptId}
</if>
<if test="roomId != null">
AND mb.room_id = #{roomId}
</if>
AND mb.del_flag = '0'
GROUP BY mb.check_status
ORDER BY count DESC
</select>
<!-- 按会议室统计 -->
<select id="getRoomStats" resultType="map">
SELECT
mb.room_id as roomId,
mr.room_name as roomName,
COUNT(*) as bookingCount,
SUM(CASE WHEN mb.check_status = '2' THEN 1 ELSE 0 END) as approvedCount,
ROUND(SUM(CASE WHEN mb.check_status = '2' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) as usageRate,
SUM(TIMESTAMPDIFF(MINUTE, mb.start_time, mb.end_time)) as totalDuration
FROM meeting_booking mb
LEFT JOIN meeting_room mr ON mb.room_id = mr.id
WHERE 1=1
<if test="startDate != null">
AND mb.booking_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND mb.booking_date &lt;= #{endDate}
</if>
<if test="deptId != null">
AND mb.dept_id = #{deptId}
</if>
AND mb.del_flag = '0'
GROUP BY mb.room_id, mr.room_name
ORDER BY bookingCount DESC
</select>
</mapper>
@@ -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;
}
}