Files
ERP/oa-backend/src/main/java/com/kaidi/oa/web/AdminConferenceServiceController.java
T
QiufengandClaude Opus 4.8 5e51dc3f56 SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)
恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。

== 此快照内容 ==
- 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091)
- 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static)
- 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB)
- 交接文档 go.md + go-code-reference/endpoints/entities/database.md
- 多代理建设脚本 .claude/wf-*.js

== 状态 ==
- 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板)
- 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用
- W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成)
- 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456

== 排除(gitignore, 可再生) ==
node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui)
完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules)

时间戳: 20260615-191517

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:19:15 +08:00

550 lines
26 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.kaidi.oa.web;
import com.kaidi.oa.common.ApiException;
import com.kaidi.oa.common.ApiResp;
import com.kaidi.oa.common.NotFoundException;
import com.kaidi.oa.domain.AdminConferenceMaterial;
import com.kaidi.oa.domain.AdminConferenceStaff;
import com.kaidi.oa.domain.AdminEquipSetup;
import com.kaidi.oa.domain.AdminRecurringBooking;
import com.kaidi.oa.domain.Meeting;
import com.kaidi.oa.domain.MeetingRoom;
import com.kaidi.oa.repository.AdminConferenceMaterialRepository;
import com.kaidi.oa.repository.AdminConferenceStaffRepository;
import com.kaidi.oa.repository.AdminEquipSetupRepository;
import com.kaidi.oa.repository.AdminRecurringBookingRepository;
import com.kaidi.oa.repository.MeetingRepository;
import com.kaidi.oa.repository.MeetingRoomRepository;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeParseException;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 行政/办公室·接待与会务服务——补深「会务服务物料管理」PARTIAL 缺口(GAP 8, med)。
*
* 覆盖:
* 1. 会议室 CRUD(新建/编辑/删除,原 MeetingRoomController 仅 GET
* URL: /api/oa/meeting-rooms POST/PATCH/DELETE
* 2. 会务物料准备清单(席卡/茶歇/签到表等)
* URL: /api/oa/admin-conference-materials
* 3. 设备调试记录(投影/音响调试记录及异常闭环)
* URL: /api/oa/admin-equip-setups
* 4. 现场服务人员安排(迎宾/签到/引导/摄影等)
* URL: /api/oa/admin-conference-staff
* 5. 周期会议预订规则管理 + @Scheduled 自动展开(每日向未来 7 天预生成)
* URL: /api/oa/admin-recurring-bookings
*
* 注意:GET /api/oa/meeting-rooms 仍由 MeetingRoomController 处理;
* 本控制器处理同 base path 下的 POST/PATCH/DELETESpring MVC 按 HTTP 方法区分,无冲突)。
*/
@RestController
public class AdminConferenceServiceController {
private final MeetingRoomRepository roomRepo;
private final AdminConferenceMaterialRepository materialRepo;
private final AdminEquipSetupRepository equipRepo;
private final AdminConferenceStaffRepository staffRepo;
private final AdminRecurringBookingRepository recurRepo;
private final MeetingRepository meetingRepo;
public AdminConferenceServiceController(
MeetingRoomRepository roomRepo,
AdminConferenceMaterialRepository materialRepo,
AdminEquipSetupRepository equipRepo,
AdminConferenceStaffRepository staffRepo,
AdminRecurringBookingRepository recurRepo,
MeetingRepository meetingRepo) {
this.roomRepo = roomRepo;
this.materialRepo = materialRepo;
this.equipRepo = equipRepo;
this.staffRepo = staffRepo;
this.recurRepo = recurRepo;
this.meetingRepo = meetingRepo;
}
// ================================================================
// 1. 会议室 CRUD(补 POST/PATCH/DELETEGET 已在 MeetingRoomController
// ================================================================
public record RoomRequest(String name, Integer capacity, String location) {}
@PostMapping("/api/oa/meeting-rooms")
@Transactional
public ApiResp<MeetingRoom> createRoom(@RequestBody RoomRequest req) {
if (req.name() == null || req.name().isBlank()) {
throw new ApiException(400, "会议室名称(name)不能为空");
}
MeetingRoom r = new MeetingRoom();
r.setName(req.name().trim());
r.setCapacity(req.capacity());
r.setLocation(req.location());
return ApiResp.ok(roomRepo.save(r));
}
@PatchMapping("/api/oa/meeting-rooms/{id}")
@Transactional
public ApiResp<MeetingRoom> updateRoom(@PathVariable Long id, @RequestBody RoomRequest req) {
MeetingRoom r = roomRepo.findById(id)
.orElseThrow(() -> new NotFoundException("会议室不存在: " + id));
if (req.name() != null && !req.name().isBlank()) r.setName(req.name().trim());
if (req.capacity() != null) r.setCapacity(req.capacity());
if (req.location() != null) r.setLocation(req.location());
return ApiResp.ok(roomRepo.save(r));
}
@DeleteMapping("/api/oa/meeting-rooms/{id}")
@Transactional
public ApiResp<Void> deleteRoom(@PathVariable Long id) {
if (!roomRepo.existsById(id)) {
throw new NotFoundException("会议室不存在: " + id);
}
List<Meeting> bound = meetingRepo.findByRoomId(id);
long futureMeetings = bound.stream()
.filter(m -> m.getStartTime() != null && m.getStartTime().isAfter(Instant.now()))
.count();
if (futureMeetings > 0) {
throw new ApiException(409, "该会议室有 " + futureMeetings + " 条未来会议占用,请先取消或迁移");
}
roomRepo.deleteById(id);
return ApiResp.ok(null);
}
// ================================================================
// 2. 会务物料准备清单 /api/oa/admin-conference-materials
// ================================================================
@GetMapping("/api/oa/admin-conference-materials")
public ApiResp<List<AdminConferenceMaterial>> listMaterials(
@RequestParam(required = false) Long meetingId,
@RequestParam(required = false) String prepStatus) {
if (meetingId != null) return ApiResp.ok(materialRepo.findByMeetingId(meetingId));
if (prepStatus != null && !prepStatus.isBlank()) return ApiResp.ok(materialRepo.findByPrepStatus(prepStatus));
return ApiResp.ok(materialRepo.findAllByOrderByCreatedAtDesc());
}
@GetMapping("/api/oa/admin-conference-materials/{id}")
public ApiResp<AdminConferenceMaterial> getMaterial(@PathVariable Long id) {
return ApiResp.ok(materialRepo.findById(id)
.orElseThrow(() -> new NotFoundException("会务物料记录不存在: " + id)));
}
public record MaterialRequest(
Long meetingId, String meetingTitle, String materialType,
String materialName, Integer planQty, Integer actualQty,
String unitName, String handler, String readyDate, String remark) {}
@PostMapping("/api/oa/admin-conference-materials")
@Transactional
public ApiResp<AdminConferenceMaterial> createMaterial(@RequestBody MaterialRequest req) {
if (req.meetingTitle() == null || req.meetingTitle().isBlank()) {
throw new ApiException(400, "会议名称(meetingTitle)不能为空");
}
if (req.materialType() == null || req.materialType().isBlank()) {
throw new ApiException(400, "物料类型(materialType)不能为空");
}
AdminConferenceMaterial m = new AdminConferenceMaterial();
m.setMeetingId(req.meetingId());
m.setMeetingTitle(req.meetingTitle().trim());
m.setMaterialType(req.materialType());
m.setMaterialName(req.materialName());
m.setPlanQty(req.planQty() == null ? 1 : req.planQty());
m.setActualQty(req.actualQty());
m.setUnitName(req.unitName() == null ? "份" : req.unitName());
m.setHandler(req.handler());
m.setReadyDate(req.readyDate());
m.setRemark(req.remark());
m.setPrepStatus("待准备");
m.setCreatedAt(Instant.now());
m.setUpdatedAt(Instant.now());
return ApiResp.ok(materialRepo.save(m));
}
public record MaterialPatchRequest(String prepStatus, Integer actualQty, String handler, String remark) {}
@PatchMapping("/api/oa/admin-conference-materials/{id}")
@Transactional
public ApiResp<AdminConferenceMaterial> updateMaterial(
@PathVariable Long id, @RequestBody MaterialPatchRequest req) {
AdminConferenceMaterial m = materialRepo.findById(id)
.orElseThrow(() -> new NotFoundException("会务物料记录不存在: " + id));
Set<String> allowed = Set.of("待准备", "准备中", "已就绪", "缺货");
if (req.prepStatus() != null && !allowed.contains(req.prepStatus())) {
throw new ApiException(400, "prepStatus 仅支持: 待准备/准备中/已就绪/缺货");
}
if (req.prepStatus() != null) m.setPrepStatus(req.prepStatus());
if (req.actualQty() != null) m.setActualQty(req.actualQty());
if (req.handler() != null) m.setHandler(req.handler());
if (req.remark() != null) m.setRemark(req.remark());
m.setUpdatedAt(Instant.now());
return ApiResp.ok(materialRepo.save(m));
}
@DeleteMapping("/api/oa/admin-conference-materials/{id}")
@Transactional
public ApiResp<Void> deleteMaterial(@PathVariable Long id) {
if (!materialRepo.existsById(id)) throw new NotFoundException("会务物料记录不存在: " + id);
materialRepo.deleteById(id);
return ApiResp.ok(null);
}
// ================================================================
// 3. 设备调试记录 /api/oa/admin-equip-setups
// ================================================================
@GetMapping("/api/oa/admin-equip-setups")
public ApiResp<List<AdminEquipSetup>> listEquips(
@RequestParam(required = false) Long meetingId,
@RequestParam(required = false) String setupResult) {
if (meetingId != null) return ApiResp.ok(equipRepo.findByMeetingId(meetingId));
if (setupResult != null && !setupResult.isBlank()) return ApiResp.ok(equipRepo.findBySetupResult(setupResult));
return ApiResp.ok(equipRepo.findAllByOrderByCreatedAtDesc());
}
@GetMapping("/api/oa/admin-equip-setups/{id}")
public ApiResp<AdminEquipSetup> getEquip(@PathVariable Long id) {
return ApiResp.ok(equipRepo.findById(id)
.orElseThrow(() -> new NotFoundException("设备调试记录不存在: " + id)));
}
public record EquipRequest(
Long meetingId, String meetingTitle, String equipType,
String equipModel, String technician, String setupDate,
String setupResult, String issueDesc, String resolution, String finalStatus) {}
@PostMapping("/api/oa/admin-equip-setups")
@Transactional
public ApiResp<AdminEquipSetup> createEquip(@RequestBody EquipRequest req) {
if (req.meetingTitle() == null || req.meetingTitle().isBlank()) {
throw new ApiException(400, "会议名称(meetingTitle)不能为空");
}
if (req.equipType() == null || req.equipType().isBlank()) {
throw new ApiException(400, "设备类型(equipType)不能为空");
}
AdminEquipSetup e = new AdminEquipSetup();
e.setMeetingId(req.meetingId());
e.setMeetingTitle(req.meetingTitle().trim());
e.setEquipType(req.equipType());
e.setEquipModel(req.equipModel());
e.setTechnician(req.technician());
e.setSetupDate(req.setupDate() == null ? LocalDate.now().toString() : req.setupDate());
e.setSetupResult(req.setupResult() == null ? "正常" : req.setupResult());
e.setIssueDesc(req.issueDesc());
e.setResolution(req.resolution());
e.setFinalStatus(req.finalStatus() == null
? (req.setupResult() == null ? "正常" : req.setupResult())
: req.finalStatus());
e.setCreatedAt(Instant.now());
e.setUpdatedAt(Instant.now());
return ApiResp.ok(equipRepo.save(e));
}
@PatchMapping("/api/oa/admin-equip-setups/{id}")
@Transactional
public ApiResp<AdminEquipSetup> updateEquip(@PathVariable Long id, @RequestBody EquipRequest req) {
AdminEquipSetup e = equipRepo.findById(id)
.orElseThrow(() -> new NotFoundException("设备调试记录不存在: " + id));
if (req.setupResult() != null) e.setSetupResult(req.setupResult());
if (req.issueDesc() != null) e.setIssueDesc(req.issueDesc());
if (req.resolution() != null) e.setResolution(req.resolution());
if (req.finalStatus() != null) e.setFinalStatus(req.finalStatus());
if (req.technician() != null) e.setTechnician(req.technician());
e.setUpdatedAt(Instant.now());
return ApiResp.ok(equipRepo.save(e));
}
@DeleteMapping("/api/oa/admin-equip-setups/{id}")
@Transactional
public ApiResp<Void> deleteEquip(@PathVariable Long id) {
if (!equipRepo.existsById(id)) throw new NotFoundException("设备调试记录不存在: " + id);
equipRepo.deleteById(id);
return ApiResp.ok(null);
}
// ================================================================
// 4. 现场服务人员安排 /api/oa/admin-conference-staff
// ================================================================
@GetMapping("/api/oa/admin-conference-staff")
public ApiResp<List<AdminConferenceStaff>> listStaff(
@RequestParam(required = false) Long meetingId,
@RequestParam(required = false) String arrivalStatus) {
if (meetingId != null) return ApiResp.ok(staffRepo.findByMeetingId(meetingId));
if (arrivalStatus != null && !arrivalStatus.isBlank()) return ApiResp.ok(staffRepo.findByArrivalStatus(arrivalStatus));
return ApiResp.ok(staffRepo.findAllByOrderByCreatedAtDesc());
}
@GetMapping("/api/oa/admin-conference-staff/{id}")
public ApiResp<AdminConferenceStaff> getStaff(@PathVariable Long id) {
return ApiResp.ok(staffRepo.findById(id)
.orElseThrow(() -> new NotFoundException("服务人员安排不存在: " + id)));
}
public record StaffRequest(
Long meetingId, String meetingTitle, String roleType,
String staffName, String dept, String phone,
String serviceTime, String arrivalStatus, String remark) {}
@PostMapping("/api/oa/admin-conference-staff")
@Transactional
public ApiResp<AdminConferenceStaff> createStaff(@RequestBody StaffRequest req) {
if (req.meetingTitle() == null || req.meetingTitle().isBlank()) {
throw new ApiException(400, "会议名称(meetingTitle)不能为空");
}
if (req.staffName() == null || req.staffName().isBlank()) {
throw new ApiException(400, "服务人员姓名(staffName)不能为空");
}
if (req.roleType() == null || req.roleType().isBlank()) {
throw new ApiException(400, "服务岗位(roleType)不能为空");
}
AdminConferenceStaff s = new AdminConferenceStaff();
s.setMeetingId(req.meetingId());
s.setMeetingTitle(req.meetingTitle().trim());
s.setRoleType(req.roleType());
s.setStaffName(req.staffName().trim());
s.setDept(req.dept());
s.setPhone(req.phone());
s.setServiceTime(req.serviceTime());
s.setArrivalStatus(req.arrivalStatus() == null ? "待到岗" : req.arrivalStatus());
s.setRemark(req.remark());
s.setCreatedAt(Instant.now());
s.setUpdatedAt(Instant.now());
return ApiResp.ok(staffRepo.save(s));
}
@PatchMapping("/api/oa/admin-conference-staff/{id}")
@Transactional
public ApiResp<AdminConferenceStaff> updateStaff(@PathVariable Long id, @RequestBody StaffRequest req) {
AdminConferenceStaff s = staffRepo.findById(id)
.orElseThrow(() -> new NotFoundException("服务人员安排不存在: " + id));
if (req.arrivalStatus() != null) s.setArrivalStatus(req.arrivalStatus());
if (req.staffName() != null && !req.staffName().isBlank()) s.setStaffName(req.staffName());
if (req.roleType() != null && !req.roleType().isBlank()) s.setRoleType(req.roleType());
if (req.serviceTime() != null) s.setServiceTime(req.serviceTime());
if (req.remark() != null) s.setRemark(req.remark());
if (req.phone() != null) s.setPhone(req.phone());
s.setUpdatedAt(Instant.now());
return ApiResp.ok(staffRepo.save(s));
}
@DeleteMapping("/api/oa/admin-conference-staff/{id}")
@Transactional
public ApiResp<Void> deleteStaff(@PathVariable Long id) {
if (!staffRepo.existsById(id)) throw new NotFoundException("服务人员安排不存在: " + id);
staffRepo.deleteById(id);
return ApiResp.ok(null);
}
// ================================================================
// 5. 周期会议预订 /api/oa/admin-recurring-bookings
// ================================================================
@GetMapping("/api/oa/admin-recurring-bookings")
public ApiResp<List<AdminRecurringBooking>> listRecurring(
@RequestParam(required = false) String bookingStatus) {
if (bookingStatus != null && !bookingStatus.isBlank()) {
return ApiResp.ok(recurRepo.findByBookingStatus(bookingStatus));
}
return ApiResp.ok(recurRepo.findAllByOrderByCreatedAtDesc());
}
@GetMapping("/api/oa/admin-recurring-bookings/{id}")
public ApiResp<AdminRecurringBooking> getRecurring(@PathVariable Long id) {
return ApiResp.ok(recurRepo.findById(id)
.orElseThrow(() -> new NotFoundException("周期预订规则不存在: " + id)));
}
public record RecurringRequest(
String ruleName, String subject, Long roomId, String organizer,
String attendees, String recurType, String recurParam,
String startTime, String endTime, String effectFrom, String effectTo) {}
@PostMapping("/api/oa/admin-recurring-bookings")
@Transactional
public ApiResp<AdminRecurringBooking> createRecurring(@RequestBody RecurringRequest req) {
if (req.ruleName() == null || req.ruleName().isBlank()) {
throw new ApiException(400, "规则名称(ruleName)不能为空");
}
if (req.recurType() == null || req.recurType().isBlank()) {
throw new ApiException(400, "周期类型(recurType)不能为空,支持DAILY/WEEKLY/MONTHLY");
}
AdminRecurringBooking rb = new AdminRecurringBooking();
rb.setRuleName(req.ruleName().trim());
rb.setSubject(req.subject() == null ? req.ruleName() : req.subject());
rb.setRoomId(req.roomId());
rb.setOrganizer(req.organizer());
rb.setAttendees(req.attendees());
rb.setRecurType(req.recurType().toUpperCase());
rb.setRecurParam(req.recurParam());
rb.setStartTime(req.startTime() == null ? "09:00" : req.startTime());
rb.setEndTime(req.endTime() == null ? "10:00" : req.endTime());
rb.setEffectFrom(req.effectFrom() == null ? LocalDate.now().toString() : req.effectFrom());
rb.setEffectTo(req.effectTo());
rb.setBookingStatus("生效中");
rb.setLastExpandedDate(LocalDate.now().minusDays(1).toString());
rb.setCreatedAt(Instant.now());
rb.setUpdatedAt(Instant.now());
return ApiResp.ok(recurRepo.save(rb));
}
@PatchMapping("/api/oa/admin-recurring-bookings/{id}")
@Transactional
public ApiResp<AdminRecurringBooking> updateRecurring(@PathVariable Long id, @RequestBody RecurringRequest req) {
AdminRecurringBooking rb = recurRepo.findById(id)
.orElseThrow(() -> new NotFoundException("周期预订规则不存在: " + id));
if (req.ruleName() != null && !req.ruleName().isBlank()) rb.setRuleName(req.ruleName().trim());
if (req.subject() != null) rb.setSubject(req.subject());
if (req.roomId() != null) rb.setRoomId(req.roomId());
if (req.organizer() != null) rb.setOrganizer(req.organizer());
if (req.attendees() != null) rb.setAttendees(req.attendees());
if (req.startTime() != null) rb.setStartTime(req.startTime());
if (req.endTime() != null) rb.setEndTime(req.endTime());
if (req.effectTo() != null) rb.setEffectTo(req.effectTo());
rb.setUpdatedAt(Instant.now());
return ApiResp.ok(recurRepo.save(rb));
}
@PostMapping("/api/oa/admin-recurring-bookings/{id}/status")
@Transactional
public ApiResp<AdminRecurringBooking> changeStatus(
@PathVariable Long id, @RequestBody Map<String, String> body) {
AdminRecurringBooking rb = recurRepo.findById(id)
.orElseThrow(() -> new NotFoundException("周期预订规则不存在: " + id));
String newStatus = body.get("bookingStatus");
if (newStatus == null || !Set.of("生效中", "已暂停", "已结束").contains(newStatus)) {
throw new ApiException(400, "bookingStatus 仅支持: 生效中/已暂停/已结束");
}
rb.setBookingStatus(newStatus);
rb.setUpdatedAt(Instant.now());
return ApiResp.ok(recurRepo.save(rb));
}
@DeleteMapping("/api/oa/admin-recurring-bookings/{id}")
@Transactional
public ApiResp<Void> deleteRecurring(@PathVariable Long id) {
if (!recurRepo.existsById(id)) throw new NotFoundException("周期预订规则不存在: " + id);
recurRepo.deleteById(id);
return ApiResp.ok(null);
}
/**
* @Scheduled 每天凌晨1点:把「生效中」的周期规则向未来7天展开为具体 Meeting 记录。
* 幂等:同一规则同一会议室同一开始时刻已存在则跳过。
*/
@Scheduled(cron = "0 0 1 * * *")
@Transactional
public void expandRecurringBookings() {
LocalDate today = LocalDate.now();
LocalDate horizon = today.plusDays(7);
for (AdminRecurringBooking rb : recurRepo.findByBookingStatus("生效中")) {
LocalDate effectFrom = parseDateSafe(rb.getEffectFrom(), today);
LocalDate effectTo = rb.getEffectTo() == null ? null : parseDateSafe(rb.getEffectTo(), null);
LocalDate lastExpanded = parseDateSafe(rb.getLastExpandedDate(), effectFrom.minusDays(1));
LocalDate startExpand = lastExpanded.plusDays(1);
if (startExpand.isBefore(today)) startExpand = today;
for (LocalDate d = startExpand; !d.isAfter(horizon); d = d.plusDays(1)) {
if (effectTo != null && d.isAfter(effectTo)) break;
if (d.isBefore(effectFrom)) continue;
if (!matchesRecurRule(rb, d)) continue;
LocalTime st = parseTimeSafe(rb.getStartTime(), LocalTime.of(9, 0));
LocalTime et = parseTimeSafe(rb.getEndTime(), LocalTime.of(10, 0));
Instant startInst = d.atTime(st).toInstant(ZoneOffset.ofHours(8));
Instant endInst = d.atTime(et).toInstant(ZoneOffset.ofHours(8));
// 幂等:同房间同开始时刻已存在则跳过
boolean alreadyExists = false;
if (rb.getRoomId() != null) {
List<Meeting> existing = meetingRepo.findByRoomId(rb.getRoomId());
for (Meeting mx : existing) {
if (startInst.equals(mx.getStartTime())) {
alreadyExists = true;
break;
}
}
}
if (alreadyExists) continue;
Meeting m = new Meeting();
m.setSubject(rb.getSubject() + "" + d + "");
m.setStartTime(startInst);
m.setEndTime(endInst);
m.setRoomId(rb.getRoomId());
m.setOrganizer(rb.getOrganizer());
m.setAttendees(rb.getAttendees());
m.setStatus("已预定");
m.setDescription("[周期预订] 规则: " + rb.getRuleName());
meetingRepo.save(m);
}
LocalDate newLast = (effectTo == null || horizon.isBefore(effectTo)) ? horizon : effectTo;
rb.setLastExpandedDate(newLast.toString());
rb.setUpdatedAt(Instant.now());
recurRepo.save(rb);
}
}
/** 手动触发周期展开(前端调试用)。 */
@PostMapping("/api/oa/admin-recurring-bookings/expand-now")
@Transactional
public ApiResp<String> expandNow() {
expandRecurringBookings();
return ApiResp.ok("周期会议已展开完毕");
}
// ================================================================
// 工具方法
// ================================================================
private boolean matchesRecurRule(AdminRecurringBooking rb, LocalDate d) {
String type = rb.getRecurType();
if (type == null) return false;
switch (type) {
case "DAILY": return true;
case "WEEKLY": {
if (rb.getRecurParam() == null || rb.getRecurParam().isBlank()) return false;
int dow = d.getDayOfWeek().getValue();
for (String p : rb.getRecurParam().split(",")) {
try { if (Integer.parseInt(p.trim()) == dow) return true; }
catch (NumberFormatException ignored) {}
}
return false;
}
case "MONTHLY": {
if (rb.getRecurParam() == null || rb.getRecurParam().isBlank()) return false;
try { return d.getDayOfMonth() == Integer.parseInt(rb.getRecurParam().trim()); }
catch (NumberFormatException e) { return false; }
}
default: return false;
}
}
private LocalDate parseDateSafe(String s, LocalDate fallback) {
if (s == null || s.isBlank()) return fallback;
try { return LocalDate.parse(s.trim().substring(0, Math.min(10, s.trim().length()))); }
catch (DateTimeParseException e) { return fallback; }
}
private LocalTime parseTimeSafe(String s, LocalTime fallback) {
if (s == null || s.isBlank()) return fallback;
try { return LocalTime.parse(s.trim()); }
catch (DateTimeParseException e) { return fallback; }
}
}