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.AdminShuttleBus; import com.kaidi.oa.domain.AdminShuttlePassenger; import com.kaidi.oa.repository.AdminShuttleBusRepository; import com.kaidi.oa.repository.AdminShuttlePassengerRepository; 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.util.List; import java.util.Map; /** * 行政/办公室·通勤班车管理。 * * 功能:班车路线台账(起终点/途经站/发车时间/返程时间/关联车辆/司机/运行日/座位数) + * 乘客预约(员工每日预约乘坐/取消 + 乘客统计/满座预警) + * 路线状态管理(运营中/暂停/停运)。 * * 预约逻辑:同一乘客同日同方向只能预约一次(重复预约抛 409);预约后自动更新班车 bookedCount。 */ @RestController @RequestMapping("/api/oa/admin-shuttle-buses") public class AdminShuttleBusController { private final AdminShuttleBusRepository busRepo; private final AdminShuttlePassengerRepository passengerRepo; public AdminShuttleBusController(AdminShuttleBusRepository busRepo, AdminShuttlePassengerRepository passengerRepo) { this.busRepo = busRepo; this.passengerRepo = passengerRepo; } // ==================== 班车路线 ==================== @GetMapping public ApiResp> listBuses( @RequestParam(required = false) String status) { if (status != null && !status.isBlank()) { return ApiResp.ok(busRepo.findByStatus(status)); } return ApiResp.ok(busRepo.findAllByOrderByRouteNameAsc()); } @GetMapping("/{id}") public ApiResp getBus(@PathVariable Long id) { return ApiResp.ok(busRepo.findById(id) .orElseThrow(() -> new NotFoundException("班车路线不存在: " + id))); } public record BusRequest( String routeName, String startStop, String endStop, String stops, String departureTime, String returnTime, String vehiclePlate, String driver, Integer seats, String runDays, String status, String remark) { } @PostMapping @Transactional public ApiResp createBus(@RequestBody BusRequest req) { if (req.routeName() == null || req.routeName().isBlank()) { throw new ApiException(400, "路线名称(routeName) 不能为空"); } AdminShuttleBus bus = new AdminShuttleBus(); bus.setRouteName(req.routeName()); bus.setStartStop(req.startStop()); bus.setEndStop(req.endStop()); bus.setStops(req.stops()); bus.setDepartureTime(req.departureTime()); bus.setReturnTime(req.returnTime()); bus.setVehiclePlate(req.vehiclePlate()); bus.setDriver(req.driver()); bus.setSeats(req.seats() == null ? 45 : req.seats()); bus.setBookedCount(0); bus.setRunDays(req.runDays() == null ? "工作日" : req.runDays()); bus.setStatus(req.status() == null ? "运营中" : req.status()); bus.setRemark(req.remark()); bus.setCreatedAt(Instant.now()); bus.setUpdatedAt(Instant.now()); return ApiResp.ok(busRepo.save(bus)); } @PatchMapping("/{id}") @Transactional public ApiResp updateBus(@PathVariable Long id, @RequestBody BusRequest req) { AdminShuttleBus bus = busRepo.findById(id) .orElseThrow(() -> new NotFoundException("班车路线不存在: " + id)); if (req.routeName() != null && !req.routeName().isBlank()) bus.setRouteName(req.routeName()); if (req.startStop() != null) bus.setStartStop(req.startStop()); if (req.endStop() != null) bus.setEndStop(req.endStop()); if (req.stops() != null) bus.setStops(req.stops()); if (req.departureTime() != null) bus.setDepartureTime(req.departureTime()); if (req.returnTime() != null) bus.setReturnTime(req.returnTime()); if (req.vehiclePlate() != null) bus.setVehiclePlate(req.vehiclePlate()); if (req.driver() != null) bus.setDriver(req.driver()); if (req.seats() != null) bus.setSeats(req.seats()); if (req.runDays() != null) bus.setRunDays(req.runDays()); if (req.status() != null) bus.setStatus(req.status()); if (req.remark() != null) bus.setRemark(req.remark()); bus.setUpdatedAt(Instant.now()); return ApiResp.ok(busRepo.save(bus)); } /** 暂停班车路线 */ @PostMapping("/{id}/suspend") @Transactional public ApiResp suspend(@PathVariable Long id) { AdminShuttleBus bus = busRepo.findById(id) .orElseThrow(() -> new NotFoundException("班车路线不存在: " + id)); if (!"运营中".equals(bus.getStatus())) { throw new ApiException(409, "只有「运营中」状态可暂停"); } bus.setStatus("暂停"); bus.setUpdatedAt(Instant.now()); return ApiResp.ok(busRepo.save(bus)); } /** 恢复运营 */ @PostMapping("/{id}/resume") @Transactional public ApiResp resume(@PathVariable Long id) { AdminShuttleBus bus = busRepo.findById(id) .orElseThrow(() -> new NotFoundException("班车路线不存在: " + id)); bus.setStatus("运营中"); bus.setUpdatedAt(Instant.now()); return ApiResp.ok(busRepo.save(bus)); } @DeleteMapping("/{id}") @Transactional public ApiResp deleteBus(@PathVariable Long id) { AdminShuttleBus bus = busRepo.findById(id) .orElseThrow(() -> new NotFoundException("班车路线不存在: " + id)); busRepo.delete(bus); return ApiResp.ok(null); } // ==================== 乘客预约 ==================== @GetMapping("/{id}/passengers") public ApiResp> listPassengers( @PathVariable Long id, @RequestParam(required = false) String rideDate) { if (rideDate != null && !rideDate.isBlank()) { return ApiResp.ok(passengerRepo.findByShuttleIdAndRideDate(id, rideDate)); } return ApiResp.ok(passengerRepo.findByStatus("已预约").stream() .filter(p -> id.equals(p.getShuttleId())).toList()); } public record BookRequest(String passenger, String dept, String rideDate, String direction, String boardStop) { } /** 预约乘坐:员工在线预约班车,满座时抛 409 */ @PostMapping("/{id}/book") @Transactional public ApiResp book(@PathVariable Long id, @RequestBody BookRequest req) { AdminShuttleBus bus = busRepo.findById(id) .orElseThrow(() -> new NotFoundException("班车路线不存在: " + id)); if (!"运营中".equals(bus.getStatus())) { throw new ApiException(409, "班车路线当前状态为「" + bus.getStatus() + "」,无法预约"); } if (req.passenger() == null || req.passenger().isBlank()) { throw new ApiException(400, "乘客姓名(passenger) 不能为空"); } String rideDate = req.rideDate() == null ? LocalDate.now().toString() : req.rideDate(); String direction = req.direction() == null ? "去" : req.direction(); // 重复预约校验 List existing = passengerRepo.findByShuttleIdAndRideDate(id, rideDate); boolean alreadyBooked = existing.stream() .anyMatch(p -> req.passenger().equals(p.getPassenger()) && direction.equals(p.getDirection()) && !"已取消".equals(p.getStatus())); if (alreadyBooked) { throw new ApiException(409, "该乘客(" + req.passenger() + ")当日已预约此方向班车"); } // 满座检验 int booked = (int) existing.stream() .filter(p -> direction.equals(p.getDirection()) && !"已取消".equals(p.getStatus())) .count(); int seats = bus.getSeats() == null ? 45 : bus.getSeats(); if (booked >= seats) { throw new ApiException(409, "班车座位已满(" + seats + "座)"); } AdminShuttlePassenger sp = new AdminShuttlePassenger(); sp.setShuttleId(id); sp.setPassenger(req.passenger()); sp.setDept(req.dept()); sp.setRideDate(rideDate); sp.setDirection(direction); sp.setBoardStop(req.boardStop()); sp.setStatus("已预约"); sp.setCreatedAt(Instant.now()); AdminShuttlePassenger saved = passengerRepo.save(sp); // 更新班车预约计数 bus.setBookedCount(booked + 1); bus.setUpdatedAt(Instant.now()); busRepo.save(bus); return ApiResp.ok(saved); } /** 取消预约 */ @PostMapping("/passengers/{pid}/cancel") @Transactional public ApiResp cancelBooking(@PathVariable Long pid) { AdminShuttlePassenger p = passengerRepo.findById(pid) .orElseThrow(() -> new NotFoundException("预约记录不存在: " + pid)); if ("已取消".equals(p.getStatus())) { throw new ApiException(409, "预约已取消"); } p.setStatus("已取消"); // 更新班车预约计数 busRepo.findById(p.getShuttleId()).ifPresent(bus -> { int cur = bus.getBookedCount() == null ? 0 : bus.getBookedCount(); bus.setBookedCount(Math.max(0, cur - 1)); bus.setUpdatedAt(Instant.now()); busRepo.save(bus); }); return ApiResp.ok(passengerRepo.save(p)); } /** 乘客统计:某日各路线预约数 / 到达数 */ @GetMapping("/stats") public ApiResp> stats(@RequestParam(required = false) String date) { String d = date == null ? LocalDate.now().toString() : date; List buses = busRepo.findAllByOrderByRouteNameAsc(); List> rows = new java.util.ArrayList<>(); int totalBooked = 0; int totalRode = 0; for (AdminShuttleBus bus : buses) { List dayPassengers = passengerRepo.findByShuttleIdAndRideDate(bus.getId(), d); int booked = (int) dayPassengers.stream().filter(p -> !"已取消".equals(p.getStatus())).count(); int rode = (int) dayPassengers.stream().filter(p -> "已乘坐".equals(p.getStatus())).count(); totalBooked += booked; totalRode += rode; rows.add(Map.of( "busId", bus.getId(), "routeName", bus.getRouteName(), "status", bus.getStatus() == null ? "" : bus.getStatus(), "seats", bus.getSeats() == null ? 0 : bus.getSeats(), "bookedToday", booked, "rodToday", rode, "loadRate", bus.getSeats() == null || bus.getSeats() == 0 ? 0.0 : Math.round((double) booked / bus.getSeats() * 100.0) / 100.0)); } return ApiResp.ok(Map.of("date", d, "totalBooked", totalBooked, "totalRode", totalRode, "routes", rows)); } }