第三阶段核心业务开发完成
This commit is contained in:
@@ -126,6 +126,44 @@
|
||||
* 对于超时的任务,更新其状态为 `TIMED_OUT`,记录错误信息。
|
||||
* 根据业务需要,可能需要同步更新关联的 `charging_robot` 和 `charging_session` 的状态。
|
||||
|
||||
### 阶段 3: 管理端核心功能完善 与 前后端数据对接 (进行中)
|
||||
|
||||
**目标**: 搭建核心管理页面框架,并将部分用户端和管理端页面与真实后端数据对接。
|
||||
|
||||
**进行中 / 即将进行:**
|
||||
|
||||
1. **用户仪表盘 (`/dashboard`) 数据对接 (规划中)**
|
||||
* **目的**: 将用户仪表盘的模拟数据替换为真实的后端数据,包括"当前充电状态"和"用户统计数据"(本月充电次数、本月总消费)。
|
||||
* **后端接口需求**:
|
||||
* `GET /api/session/my/active`: 获取用户当前激活的充电会话。若无激活会话,返回 null 或明确的空状态。
|
||||
* **Controller**: `ChargingSessionController`
|
||||
* **Service**: 查询用户状态为进行中(如 `CHARGING_IN_PROGRESS`)的会话。
|
||||
* `GET /api/user/stats/mine`: 获取当前登录用户的统计数据。
|
||||
* **响应示例**: `{ "monthlyCharges": 15, "monthlySpending": 250.75 }`
|
||||
* **Controller**: `UserController` 或新的 `UserStatsController`
|
||||
* **Service**: 计算用户本月已完成/已支付的充电次数和总消费。
|
||||
* **前端修改 (`dashboard/page.tsx`)**:
|
||||
* 移除相关模拟数据加载逻辑。
|
||||
* 定义与后端响应匹配的数据接口。
|
||||
* 使用 `axios` (api 实例) 并行调用上述两个接口。
|
||||
* 根据真实数据更新UI组件的显示。
|
||||
* 处理API错误和加载状态。
|
||||
* **状态**: 等待后端接口实现。
|
||||
|
||||
2. **"我的充电记录" (`/my-sessions`) 页面数据对接 (已完成)**
|
||||
* 前端已修改为调用 `POST /api/session/my/list/page` 接口。
|
||||
* 后端 `ChargingSessionController` 和 `ChargingSessionVO` 已确认基本满足需求。
|
||||
* **状态**: 已完成,等待用户测试确认。
|
||||
|
||||
3. **管理员仪表盘 (`/admin/dashboard`) 框架 (已完成)**
|
||||
* 包含系统概览统计卡片(模拟数据)和管理模块导航卡片。
|
||||
* **状态**: 初步完成,后续需对接真实统计数据和启用所有导航链接。
|
||||
|
||||
4. **机器人管理页面 (`/admin/robots`) 框架 (已完成)**
|
||||
* 包含列表展示、搜索、筛选、分页(模拟数据)。
|
||||
* 模拟的添加、编辑、删除按钮。
|
||||
* **状态**: 初步完成,后续需对接真实数据和实现CRUD操作。
|
||||
|
||||
## 4. 数据库 Schema (初步 DDL)
|
||||
|
||||
```sql
|
||||
@@ -228,6 +266,6 @@ CREATE TABLE `activation_code` (
|
||||
* 用于指令 (`robot/command/{clientId}`) 和状态 (`robot/status/{clientId}`) Topic 的用户名和密码。
|
||||
* 与硬件团队确认最终的 MQTT Topic 和 Payload 结构。
|
||||
2. **开始开发**: 在获取 MQTT 信息后,可以并行开始:
|
||||
* **阶段一**: 数据库初始化、用户模块开发、**`robot_task` 相关基础 Service 开发**。
|
||||
* **阶段二**: MQTT 配置、基础连接、订阅实现、**集成 `robot_task` 检查与更新逻辑**、**任务超时处理实现**。
|
||||
* **阶段一**: 数据库初始化、用户模块开发、`robot_task` 相关基础 Service 开发。
|
||||
* **阶段二**: MQTT 配置、基础连接、订阅实现、集成 `robot_task` 检查与更新逻辑、任务超时处理实现。
|
||||
3. 按照开发阶段逐步推进。
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -76,4 +76,11 @@ public interface ParkingSpotService extends IService<ParkingSpot> {
|
||||
*/
|
||||
boolean updateSpotStatus(String spotUID, ParkingSpotStatusEnum newStatus, Long currentSessionId);
|
||||
|
||||
/**
|
||||
* 获取可用车位数量 (状态为 available)
|
||||
*
|
||||
* @return 可用车位数量
|
||||
*/
|
||||
long countAvailableSpots();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"name": "mqtt",
|
||||
"type": "com.yupi.project.config.properties.MqttProperties",
|
||||
"sourceType": "com.yupi.project.config.properties.MqttProperties"
|
||||
}
|
||||
],
|
||||
"properties": [
|
||||
{
|
||||
"name": "mqtt.broker-url",
|
||||
"type": "java.lang.String",
|
||||
"sourceType": "com.yupi.project.config.properties.MqttProperties"
|
||||
},
|
||||
{
|
||||
"name": "mqtt.client-id-prefix",
|
||||
"type": "java.lang.String",
|
||||
"sourceType": "com.yupi.project.config.properties.MqttProperties"
|
||||
},
|
||||
{
|
||||
"name": "mqtt.command-topic-base",
|
||||
"type": "java.lang.String",
|
||||
"sourceType": "com.yupi.project.config.properties.MqttProperties"
|
||||
},
|
||||
{
|
||||
"name": "mqtt.connection-timeout",
|
||||
"type": "java.lang.Integer",
|
||||
"sourceType": "com.yupi.project.config.properties.MqttProperties"
|
||||
},
|
||||
{
|
||||
"name": "mqtt.default-qos",
|
||||
"type": "java.lang.Integer",
|
||||
"sourceType": "com.yupi.project.config.properties.MqttProperties"
|
||||
},
|
||||
{
|
||||
"name": "mqtt.keep-alive-interval",
|
||||
"type": "java.lang.Integer",
|
||||
"sourceType": "com.yupi.project.config.properties.MqttProperties"
|
||||
},
|
||||
{
|
||||
"name": "mqtt.password",
|
||||
"type": "java.lang.String",
|
||||
"sourceType": "com.yupi.project.config.properties.MqttProperties"
|
||||
},
|
||||
{
|
||||
"name": "mqtt.status-topic-base",
|
||||
"type": "java.lang.String",
|
||||
"sourceType": "com.yupi.project.config.properties.MqttProperties"
|
||||
},
|
||||
{
|
||||
"name": "mqtt.username",
|
||||
"type": "java.lang.String",
|
||||
"sourceType": "com.yupi.project.config.properties.MqttProperties"
|
||||
}
|
||||
],
|
||||
"hints": []
|
||||
}
|
||||
BIN
springboot-init-main/target/classes/TaskTimeoutHandler.class
Normal file
BIN
springboot-init-main/target/classes/TaskTimeoutHandler.class
Normal file
Binary file not shown.
6
springboot-init-main/target/classes/application-prod.yml
Normal file
6
springboot-init-main/target/classes/application-prod.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
spring:
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/my_db
|
||||
username: root
|
||||
password: 123456
|
||||
65
springboot-init-main/target/classes/application.yml
Normal file
65
springboot-init-main/target/classes/application.yml
Normal file
@@ -0,0 +1,65 @@
|
||||
spring:
|
||||
application:
|
||||
name: mqtt-charging-system
|
||||
# DataSource Config
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://yuyun-us1.stormrain.cn:3306/mqtt_power?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: mysql_a4MQ4P
|
||||
mvc:
|
||||
pathmatch:
|
||||
matching-strategy: ANT_PATH_MATCHER
|
||||
# session 失效时间(秒)
|
||||
session:
|
||||
timeout: 86400
|
||||
server:
|
||||
port: 7529
|
||||
servlet:
|
||||
context-path: /api
|
||||
session:
|
||||
timeout: 86400 # 设置session的过期时间,单位为秒,这里设置为1天
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
logic-delete-field: isDelete # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
|
||||
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
|
||||
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
|
||||
|
||||
# Logging configuration
|
||||
logging:
|
||||
level:
|
||||
# Set root logger level (e.g., INFO, WARN, ERROR, DEBUG)
|
||||
root: INFO
|
||||
# Set specific package levels
|
||||
com.yupi.project: DEBUG # Example: Set your project's base package to DEBUG
|
||||
org.springframework.web: INFO # Set Spring Web logging level
|
||||
org.springframework.security: DEBUG # Enable Spring Security DEBUG logging
|
||||
org.mybatis: INFO # Set MyBatis logging level
|
||||
# ... other specific loggers
|
||||
#file:
|
||||
#name: logs/application.log # Log file name
|
||||
#path: ./logs # Log file path
|
||||
#pattern:
|
||||
#console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
|
||||
#file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
|
||||
|
||||
# ===================================================================
|
||||
# MQTT Configurations
|
||||
# ===================================================================
|
||||
mqtt:
|
||||
broker-url: tcp://broker.emqx.io:1883
|
||||
username: # Public broker, no credentials specified for connection
|
||||
password: # Public broker, no credentials specified for connection
|
||||
client-id-prefix: backend-yupi-mqtt-power- # Unique client ID prefix for our project
|
||||
default-qos: 1 # Default Quality of Service (0, 1, 2)
|
||||
connection-timeout: 30 # Connection timeout in seconds
|
||||
keep-alive-interval: 60 # Keep alive interval in seconds
|
||||
command-topic-base: yupi_mqtt_power_project/robot/command # Prefixed base topic for sending commands
|
||||
status-topic-base: yupi_mqtt_power_project/robot/status # Prefixed base topic for receiving status
|
||||
task: # Task specific configurations
|
||||
timeoutSeconds: 300 # Default 300 seconds (5 minutes) for a task to be considered timed out
|
||||
timeoutCheckRateMs: 60000 # Default 60000 ms (1 minute) for how often to check for timed out tasks
|
||||
1
springboot-init-main/target/classes/banner.txt
Normal file
1
springboot-init-main/target/classes/banner.txt
Normal file
@@ -0,0 +1 @@
|
||||
我的项目 by 程序员鱼皮 https://github.com/liyupi
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user