第三阶段核心业务开发完成

This commit is contained in:
2025-05-18 15:58:16 +08:00
parent 53f7fee73a
commit bab3f719e2
120 changed files with 4114 additions and 387 deletions

View File

@@ -0,0 +1,70 @@
package com.yupi.project.controller;
import com.yupi.project.annotation.AuthCheck;
import com.yupi.project.common.BaseResponse;
import com.yupi.project.common.ResultUtils;
import com.yupi.project.constant.UserConstant;
import com.yupi.project.model.vo.AdminDashboardStatsVO;
import com.yupi.project.service.ChargingRobotService;
import com.yupi.project.service.ChargingSessionService;
import com.yupi.project.service.ParkingSpotService;
import com.yupi.project.service.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.math.BigDecimal;
/**
* 管理员统计信息接口
*/
@RestController
@RequestMapping("/admin/stats")
@Slf4j
@Api(tags = "管理员统计数据接口")
public class AdminStatsController {
@Resource
private UserService userService;
@Resource
private ChargingRobotService chargingRobotService;
@Resource
private ChargingSessionService chargingSessionService;
@Resource
private ParkingSpotService parkingSpotService;
@ApiOperation("获取管理员仪表盘统计数据")
@GetMapping("/summary")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<AdminDashboardStatsVO> getAdminDashboardStats() {
long totalUsers = userService.count();
long totalRobots = chargingRobotService.count();
long onlineRobots = chargingRobotService.countOnlineRobots();
long chargingRobots = chargingRobotService.countChargingRobots();
long idleRobots = chargingRobotService.countIdleRobots();
long activeSessions = chargingSessionService.countActiveSessions();
BigDecimal totalRevenue = chargingSessionService.getTotalRevenue();
long totalParkingSpots = parkingSpotService.count();
long availableParkingSpots = parkingSpotService.countAvailableSpots();
AdminDashboardStatsVO statsVO = new AdminDashboardStatsVO();
statsVO.setTotalUsers(totalUsers);
statsVO.setTotalRobots(totalRobots);
statsVO.setOnlineRobots(onlineRobots);
statsVO.setChargingRobots(chargingRobots);
statsVO.setIdleRobots(idleRobots);
statsVO.setActiveSessions(activeSessions);
statsVO.setTotalRevenue(totalRevenue);
statsVO.setTotalParkingSpots(totalParkingSpots);
statsVO.setAvailableParkingSpots(availableParkingSpots);
return ResultUtils.success(statsVO);
}
}

View File

@@ -55,23 +55,34 @@ public class ChargingRobotAdminController {
if (addRequest.getStatus() == null || RobotStatusEnum.getEnumByValue(addRequest.getStatus()) == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "无效的机器人状态");
}
ChargingRobot robot = chargingRobotService.registerRobot(addRequest.getRobotUid(), RobotStatusEnum.getEnumByValue(addRequest.getStatus()));
if (robot == null || robot.getId() == null) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "添加机器人失败");
if (addRequest.getBatteryLevel() != null && (addRequest.getBatteryLevel() < 0 || addRequest.getBatteryLevel() > 100)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "电池电量必须在0到100之间");
}
// 可以选择性地更新其他字段,如初始电量等
// 检查机器人UID是否已存在
if (chargingRobotService.findByRobotUid(addRequest.getRobotUid()) != null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "机器人UID '" + addRequest.getRobotUid() + "' 已存在");
}
ChargingRobot newRobot = new ChargingRobot();
newRobot.setRobotUid(addRequest.getRobotUid());
newRobot.setStatus(addRequest.getStatus()); // Directly use the validated status string
if (addRequest.getBatteryLevel() != null) {
chargingRobotService.updateBatteryLevel(robot.getId(), addRequest.getBatteryLevel());
newRobot.setBatteryLevel(addRequest.getBatteryLevel());
}
if (StringUtils.isNotBlank(addRequest.getCurrentLocation())) {
ChargingRobot updateRobot = new ChargingRobot();
updateRobot.setId(robot.getId());
updateRobot.setCurrentLocation(addRequest.getCurrentLocation());
chargingRobotService.updateById(updateRobot);
newRobot.setCurrentLocation(addRequest.getCurrentLocation());
}
// Set createTime and updateTime if your entity doesn't auto-set them with @TableField(fill = ...)
// newRobot.setCreateTime(new Date());
// newRobot.setUpdateTime(new Date());
boolean saved = chargingRobotService.save(newRobot);
if (!saved || newRobot.getId() == null) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "添加机器人失败");
}
return ResultUtils.success(robot.getId());
return ResultUtils.success(newRobot.getId());
}
@ApiOperation("删除充电机器人")
@@ -108,9 +119,9 @@ public class ChargingRobotAdminController {
}
@ApiOperation("根据ID获取充电机器人信息")
@GetMapping("/get")
@GetMapping("/get/{id}")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<ChargingRobot> getChargingRobotById(long id, HttpServletRequest request) {
public BaseResponse<ChargingRobot> getChargingRobotById(@PathVariable long id, HttpServletRequest request) {
if (id <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}

View File

@@ -79,6 +79,18 @@ public class ChargingSessionController {
return ResultUtils.success(sessionVOPage);
}
@ApiOperation("用户获取当前激活的充电会话")
@GetMapping("/my/active")
@AuthCheck // 需要登录
public BaseResponse<ChargingSessionVO> getMyActiveChargingSession(HttpServletRequest request) {
User loginUser = userService.getCurrentUser(request);
ChargingSession activeSession = chargingSessionService.getActiveSessionByUserId(loginUser.getId());
if (activeSession == null) {
return ResultUtils.success(null); // 没有激活的会话
}
return ResultUtils.success(ChargingSessionVO.objToVo(activeSession));
}
@ApiOperation("用户获取单个充电会话详情")
@GetMapping("/get")
@AuthCheck

View File

@@ -21,6 +21,7 @@ import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@@ -53,14 +54,23 @@ public class ParkingSpotAdminController {
}
// robotAssignable 默认为true如果 DTO 中是 Boolean则 null 会被处理
ParkingSpot spot = parkingSpotService.addParkingSpot(
addRequest.getSpotUid(),
addRequest.getLocationDescription(),
addRequest.getRobotAssignable() == null ? true : addRequest.getRobotAssignable()
);
ParkingSpot spot;
try {
spot = parkingSpotService.addParkingSpot(
addRequest.getSpotUid(),
addRequest.getLocationDescription(),
addRequest.getRobotAssignable() == null ? true : addRequest.getRobotAssignable()
);
} catch (DuplicateKeyException e) {
//捕获由数据库唯一约束抛出的 DuplicateKeyException
log.warn("添加车位失败车位UID已存在 (数据库约束冲突): {}", addRequest.getSpotUid(), e);
throw new BusinessException(ErrorCode.PARAMS_ERROR, "车位UID '" + addRequest.getSpotUid() + "' 已存在。");
}
// BusinessException (如 "车位UID已存在") 会由 Service 层直接抛出,这里无需再次捕获
if (spot == null || spot.getId() == null) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "添加车位失败");
// 这种情况理论上应该被Service层的异常覆盖或者save失败时抛出异常
throw new BusinessException(ErrorCode.OPERATION_ERROR, "添加车位失败,未知原因。");
}
return ResultUtils.success(spot.getId());
}
@@ -130,7 +140,9 @@ public class ParkingSpotAdminController {
queryWrapper.eq("robot_assignable", queryRequest.getRobotAssignable());
}
if (StringUtils.isNotBlank(queryRequest.getSortField())) {
queryWrapper.orderBy(true, queryRequest.getSortOrder().equals("ascend"), queryRequest.getSortField());
// 确保 queryRequest.getSortOrder() 不为 null并转换为 boolean
boolean isAsc = "asc".equalsIgnoreCase(queryRequest.getSortOrder()) || "ascend".equalsIgnoreCase(queryRequest.getSortOrder());
queryWrapper.orderBy(true, isAsc, queryRequest.getSortField());
}
parkingSpotService.page(page, queryWrapper);

View File

@@ -0,0 +1,38 @@
package com.yupi.project.controller;
import com.yupi.project.common.BaseResponse;
import com.yupi.project.common.ResultUtils;
import com.yupi.project.model.entity.ParkingSpot;
import com.yupi.project.service.ParkingSpotService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
/**
* 车位接口 (用户端)
*/
@RestController
@RequestMapping("/parking-spot") // 修改这里:移除 /api 前缀
@Slf4j
@Api(tags = "车位接口 (用户端)")
public class ParkingSpotController {
@Resource
private ParkingSpotService parkingSpotService;
@ApiOperation("获取可用且可指派的充电车位列表")
@GetMapping("/list/available-assignable") // Specific path frontend is calling
public BaseResponse<List<ParkingSpot>> listAvailableAndAssignableSpots() {
List<ParkingSpot> spots = parkingSpotService.findAvailableAndAssignableSpots();
return ResultUtils.success(spots);
}
// Można tu dodać inne publicznie dostępne endpointy dotyczące miejsc parkingowych, jeśli potrzebne
// np. GET /parking-spot/{id} - publiczne dane o miejscu
}

View File

@@ -1,5 +1,6 @@
package com.yupi.project.controller;
import com.yupi.project.annotation.AuthCheck;
import com.yupi.project.common.BaseResponse;
import com.yupi.project.common.ErrorCode;
import com.yupi.project.common.ResultUtils;
@@ -11,10 +12,12 @@ import com.yupi.project.model.enums.UserRoleEnum;
import com.yupi.project.service.UserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;
import io.swagger.annotations.ApiOperation;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;
/**
* 用户接口
@@ -73,6 +76,15 @@ public class UserController {
return ResultUtils.success(currentUser);
}
@ApiOperation("获取当前登录用户的统计信息")
@GetMapping("/stats/mine")
@AuthCheck // 需要登录
public BaseResponse<Map<String, Object>> getMyStats(HttpServletRequest request) {
User loginUser = userService.getCurrentUser(request);
Map<String, Object> stats = userService.getUserDashboardStats(loginUser.getId());
return ResultUtils.success(stats);
}
// --- 管理员功能 --- //
@GetMapping("/list")

View File

@@ -0,0 +1,20 @@
package com.yupi.project.model.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 管理员仪表盘统计数据视图对象
*/
@Data
public class AdminDashboardStatsVO {
private Long totalUsers;
private Long totalRobots;
private Long onlineRobots;
private Long chargingRobots;
private Long idleRobots;
private Long activeSessions;
private BigDecimal totalRevenue;
private Long totalParkingSpots;
private Long availableParkingSpots;
}

View File

@@ -86,4 +86,25 @@ public interface ChargingRobotService extends IService<ChargingRobot> {
* @return 更新是否成功
*/
boolean updateRobotStatus(String robotUID, RobotStatusEnum status, String location, Integer batteryLevel, Long currentTaskId, Date lastHeartbeatTime);
/**
* 获取在线机器人数量 (状态不为 offline 或 error)
*
* @return 在线机器人数量
*/
long countOnlineRobots();
/**
* 获取正在充电的机器人数量 (状态为 charging)
*
* @return 正在充电的机器人数量
*/
long countChargingRobots();
/**
* 获取空闲机器人数量 (状态为 idle)
*
* @return 空闲机器人数量
*/
long countIdleRobots();
}

View File

@@ -131,4 +131,30 @@ public interface ChargingSessionService extends IService<ChargingSession> {
*/
QueryWrapper<ChargingSession> getQueryWrapper(ChargingSessionQueryRequest queryRequest);
/**
* 根据用户ID获取当前激活的充电会话
*
* @param userId 用户ID
* @return ChargingSession 实体,如果没有激活的则返回 null
*/
ChargingSession getActiveSessionByUserId(Long userId);
/**
* 获取今日的充电会话总数 (包括进行中和已完成)
*
* @return 今日充电会话总数
*/
long countTodaySessions();
/**
* 获取今日的总收入 (基于已支付的会话)
*
* @return 今日总收入
*/
BigDecimal sumTodayRevenue();
long countActiveSessions();
BigDecimal getTotalRevenue();
}

View File

@@ -76,4 +76,11 @@ public interface ParkingSpotService extends IService<ParkingSpot> {
*/
boolean updateSpotStatus(String spotUID, ParkingSpotStatusEnum newStatus, Long currentSessionId);
/**
* 获取可用车位数量 (状态为 available)
*
* @return 可用车位数量
*/
long countAvailableSpots();
}

View File

@@ -5,6 +5,7 @@ import com.yupi.project.model.entity.User;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* @description 针对表【user(用户表)】的数据库操作Service
@@ -104,4 +105,12 @@ public interface UserService extends IService<User> {
* @return 操作是否成功
*/
boolean adminUpdateUser(com.yupi.project.model.dto.user.UserAdminUpdateRequest updateRequest);
/**
* 获取用户的仪表盘统计数据 (例如本月充电次数和消费)
*
* @param userId 用户ID
* @return 包含统计数据的 Map
*/
Map<String, Object> getUserDashboardStats(Long userId);
}

View File

@@ -13,6 +13,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@@ -186,4 +187,26 @@ public class ChargingRobotServiceImpl extends ServiceImpl<ChargingRobotMapper, C
// 将机器人状态设置为空闲并清除当前任务ID
return updateRobotStatus(robot.getRobotUid(), RobotStatusEnum.IDLE, null, null, null, null); // 第二个参数传null以清除任务ID
}
@Override
public long countOnlineRobots() {
QueryWrapper<ChargingRobot> queryWrapper = new QueryWrapper<>();
List<String> offlineStatus = Arrays.asList(RobotStatusEnum.OFFLINE.getValue(), RobotStatusEnum.ERROR.getValue());
queryWrapper.notIn("status", offlineStatus);
return this.count(queryWrapper);
}
@Override
public long countChargingRobots() {
QueryWrapper<ChargingRobot> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("status", RobotStatusEnum.CHARGING.getValue());
return this.count(queryWrapper);
}
@Override
public long countIdleRobots() {
QueryWrapper<ChargingRobot> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("status", RobotStatusEnum.IDLE.getValue());
return this.count(queryWrapper);
}
}

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yupi.project.common.ErrorCode;
import com.yupi.project.exception.BusinessException;
import com.yupi.project.exception.ThrowUtils;
import com.yupi.project.mapper.ChargingSessionMapper;
import com.yupi.project.model.dto.charging_session.ChargingRequest;
import com.yupi.project.model.dto.charging_session.ChargingSessionQueryRequest;
@@ -21,8 +22,13 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
@@ -41,9 +47,6 @@ public class ChargingSessionServiceImpl extends ServiceImpl<ChargingSessionMappe
@Resource
private ChargingRobotService chargingRobotService;
@Resource
private UserService userService;
@Resource
private RobotTaskService robotTaskService; // 用于创建和关联机器人任务
@@ -321,45 +324,22 @@ public class ChargingSessionServiceImpl extends ServiceImpl<ChargingSessionMappe
@Override
@Transactional
public boolean processPayment(Long sessionId, Long userId) {
// 省略具体支付逻辑,假设支付成功,更新会话状态
// 实际应用中这里会调用支付网关,并根据支付结果更新
ChargingSession session = this.getById(sessionId);
if (session == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "充电会话不存在");
if (session == null || !session.getUserId().equals(userId)) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "会话不存在或不属于您");
}
if (!session.getUserId().equals(userId)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权操作他人充电记录");
}
if (!ChargingSessionStatusEnum.PAYMENT_PENDING.getValue().equals(session.getStatus()) ||
!PaymentStatusEnum.PENDING.getValue().equals(session.getPaymentStatus())) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "会话状态非待支付,无法处理支付");
}
BigDecimal cost = session.getCost();
if (cost == null || cost.compareTo(BigDecimal.ZERO) <= 0) {
log.info("会话 {} 费用为0或无效标记为已支付 (无需支付)", sessionId);
session.setPaymentStatus(PaymentStatusEnum.PAID.getValue());
session.setStatus(ChargingSessionStatusEnum.PAID.getValue()); // 主状态也更新为已支付
session.setUpdateTime(new Date());
return this.updateById(session);
}
// 模拟扣款
boolean paymentSuccess = userService.decreaseBalance(userId, cost);
if (!paymentSuccess) {
log.warn("用户 {} 为会话 {} 支付 {}元 失败,余额不足或操作失败", userId, sessionId, cost);
session.setPaymentStatus(PaymentStatusEnum.FAILED.getValue());
// 可以考虑是否将会话状态改为ERROR或保持PAYMENT_PENDING
this.updateById(session);
throw new BusinessException(ErrorCode.OPERATION_ERROR, "支付失败,余额不足或系统错误");
if (!ChargingSessionStatusEnum.PAYMENT_PENDING.getValue().equals(session.getStatus())) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "会话状态不正确,无法支付");
}
session.setPaymentStatus(PaymentStatusEnum.PAID.getValue());
session.setStatus(ChargingSessionStatusEnum.PAID.getValue()); // 主状态也更新为已支付
session.setUpdateTime(new Date());
boolean updated = this.updateById(session);
if (updated) {
log.info("用户 {} 为会话 {} 成功支付 {}元", userId, sessionId, cost);
}
return updated;
session.setStatus(ChargingSessionStatusEnum.PAID.getValue());
boolean success = this.updateById(session);
ThrowUtils.throwIf(!success, ErrorCode.SYSTEM_ERROR, "支付状态更新失败");
// TODO: 触发其他后续逻辑,如发送通知
return true;
}
@Override
@@ -579,4 +559,97 @@ public class ChargingSessionServiceImpl extends ServiceImpl<ChargingSessionMappe
log.info("用户 {} 已请求停止充电会话 {}。等待机器人确认。", userId, sessionId);
return true;
}
@Override
public ChargingSession getActiveSessionByUserId(Long userId) {
if (userId == null) {
return null;
}
QueryWrapper<ChargingSession> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", userId);
queryWrapper.eq("status", ChargingSessionStatusEnum.CHARGING_STARTED.getValue());
// 通常一个用户只有一个正在进行的会话,但为了严谨可以取最新的一个(如果有多条脏数据)
queryWrapper.orderByDesc("create_time");
queryWrapper.last("LIMIT 1");
return this.getOne(queryWrapper);
}
@Override
public long countTodaySessions() {
QueryWrapper<ChargingSession> queryWrapper = new QueryWrapper<>();
LocalDateTime startOfDay = LocalDateTime.of(LocalDate.now(), LocalTime.MIN);
LocalDateTime endOfDay = LocalDateTime.of(LocalDate.now(), LocalTime.MAX);
queryWrapper.ge("create_time", startOfDay); // 或者用 start_time取决于业务定义
queryWrapper.le("create_time", endOfDay);
// 包括正在进行的和已完成/已支付的
List<String> relevantStatuses = Arrays.asList(
ChargingSessionStatusEnum.CHARGING_STARTED.getValue(), // 充电进行中
// ChargingSessionStatusEnum.ROBOT_EN_ROUTE.getValue(), // 如果也算今日会话可以加上
// ChargingSessionStatusEnum.ROBOT_ARRIVED.getValue(), // 如果也算
ChargingSessionStatusEnum.CHARGING_COMPLETED.getValue(), // 充电已完成
ChargingSessionStatusEnum.PAID.getValue() // 已支付
);
queryWrapper.in("status", relevantStatuses);
return this.count(queryWrapper);
}
@Override
public BigDecimal sumTodayRevenue() {
// 获取今天的起始和结束时间
LocalDateTime startOfDay = LocalDate.now().atStartOfDay();
LocalDateTime endOfDay = LocalDate.now().atTime(LocalTime.MAX);
Date startDate = Date.from(startOfDay.atZone(java.time.ZoneId.systemDefault()).toInstant());
Date endDate = Date.from(endOfDay.atZone(java.time.ZoneId.systemDefault()).toInstant());
QueryWrapper<ChargingSession> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("status", ChargingSessionStatusEnum.PAID.getValue());
queryWrapper.between("charge_end_time", startDate, endDate); // 假设以充电结束时间为准
queryWrapper.select("IFNULL(SUM(cost), 0) as totalRevenue"); // 使用IFNULL处理没有记录的情况
Map<String, Object> result = this.getMap(queryWrapper);
if (result != null && result.get("totalRevenue") != null) {
// SUM(cost) 返回的可能是 BigDecimal 或 Double取决于数据库和驱动
Object revenueObj = result.get("totalRevenue");
if (revenueObj instanceof BigDecimal) {
return (BigDecimal) revenueObj;
} else if (revenueObj instanceof Number) {
return BigDecimal.valueOf(((Number) revenueObj).doubleValue());
}
}
return BigDecimal.ZERO;
}
@Override
public long countActiveSessions() {
QueryWrapper<ChargingSession> queryWrapper = new QueryWrapper<>();
// 定义哪些状态算作"活动中"
List<String> activeStatuses = Arrays.asList(
ChargingSessionStatusEnum.REQUESTED.getValue(),
ChargingSessionStatusEnum.ROBOT_ASSIGNED.getValue(),
ChargingSessionStatusEnum.ROBOT_ARRIVED.getValue(),
ChargingSessionStatusEnum.CHARGING_STARTED.getValue(),
ChargingSessionStatusEnum.PAYMENT_PENDING.getValue() // 用户停止充电后,等待支付也算活动
);
queryWrapper.in("status", activeStatuses);
return this.count(queryWrapper);
}
@Override
public BigDecimal getTotalRevenue() {
QueryWrapper<ChargingSession> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("status", ChargingSessionStatusEnum.PAID.getValue());
queryWrapper.select("IFNULL(SUM(cost), 0) as totalRevenue");
Map<String, Object> result = this.getMap(queryWrapper);
if (result != null && result.get("totalRevenue") != null) {
Object revenueObj = result.get("totalRevenue");
if (revenueObj instanceof BigDecimal) {
return (BigDecimal) revenueObj;
} else if (revenueObj instanceof Number) {
return BigDecimal.valueOf(((Number) revenueObj).doubleValue());
}
}
return BigDecimal.ZERO;
}
}

View File

@@ -32,8 +32,13 @@ public class MqttServiceImpl implements MqttService {
// 1. Check if the robot has pending or already sent tasks
if (robotTaskService.hasPendingOrSentTask(robotId)) {
log.warn("Robot {} is busy (has PENDING or SENT tasks). Command {} aborted.", robotId, commandType);
return false;
// 添加优先级处理STOP_CHARGE 命令应该可以覆盖其他任务
if (CommandTypeEnum.STOP_CHARGE.equals(commandType)) {
log.info("Robot {} has pending tasks, but STOP_CHARGE is a priority command, proceeding anyway.", robotId);
} else {
log.warn("Robot {} is busy (has PENDING or SENT tasks). Command {} aborted.", robotId, commandType);
return false;
}
}
// 2. Create a new task in PENDING state

View File

@@ -167,4 +167,11 @@ public class ParkingSpotServiceImpl extends ServiceImpl<ParkingSpotMapper, Parki
}
return success;
}
@Override
public long countAvailableSpots() {
QueryWrapper<ParkingSpot> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("status", ParkingSpotStatusEnum.AVAILABLE.getValue());
return this.count(queryWrapper);
}
}

View File

@@ -8,6 +8,9 @@ import com.yupi.project.exception.BusinessException;
import com.yupi.project.mapper.UserMapper;
import com.yupi.project.model.entity.User;
import com.yupi.project.service.UserService;
import com.yupi.project.service.ChargingSessionService;
import com.yupi.project.model.entity.ChargingSession;
import com.yupi.project.model.enums.ChargingSessionStatusEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
@@ -27,6 +30,12 @@ import java.util.regex.Pattern;
import java.util.List;
import java.util.stream.Collectors;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.TemporalAdjusters;
import java.util.Date;
/**
* 用户服务实现类
@@ -42,6 +51,9 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private ChargingSessionService chargingSessionService;
// 用户名校验正则允许字母、数字、下划线长度4到16位
private static final String USERNAME_PATTERN = "^[a-zA-Z0-9_]{4,16}$";
// 密码校验正则至少包含字母和数字长度至少6位
@@ -395,4 +407,42 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
return false;
}
}
}
@Override
public Map<String, Object> getUserDashboardStats(Long userId) {
if (userId == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户ID不能为空");
}
// 获取本月的第一天和最后一天
LocalDateTime now = LocalDateTime.now();
LocalDateTime firstDayOfMonth = now.with(TemporalAdjusters.firstDayOfMonth()).withHour(0).withMinute(0).withSecond(0);
LocalDateTime lastDayOfMonth = now.with(TemporalAdjusters.lastDayOfMonth()).withHour(23).withMinute(59).withSecond(59);
Date startDate = Date.from(firstDayOfMonth.atZone(ZoneId.systemDefault()).toInstant());
Date endDate = Date.from(lastDayOfMonth.atZone(ZoneId.systemDefault()).toInstant());
// 查询本月已完成的充电会话
QueryWrapper<ChargingSession> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", userId);
queryWrapper.eq("status", ChargingSessionStatusEnum.PAID.getValue()); // 修正: COMPLETED -> PAID (统计已支付完成的会话)
queryWrapper.between("charge_end_time", startDate, endDate); // 假设用 charge_end_time 判断是否在本月完成
List<ChargingSession> monthlySessions = chargingSessionService.list(queryWrapper);
long monthlyCharges = monthlySessions.size();
BigDecimal monthlySpending = BigDecimal.ZERO;
for (ChargingSession session : monthlySessions) {
if (session.getCost() != null) {
monthlySpending = monthlySpending.add(session.getCost());
}
}
Map<String, Object> stats = new HashMap<>();
stats.put("monthlyCharges", monthlyCharges);
stats.put("monthlySpending", monthlySpending.setScale(2, BigDecimal.ROUND_HALF_UP));
return stats;
}
}