Compare commits

...

8 Commits

Author SHA1 Message Date
6e89255f1e 在线列表刷新逻辑
All checks were successful
构建并部署 Spring Boot 应用 / build-and-deploy (push) Successful in 15m20s
2025-10-19 15:59:05 +08:00
272c674b4a 签退
All checks were successful
构建并部署 Spring Boot 应用 / build-and-deploy (push) Successful in 34m2s
2025-10-19 13:39:45 +08:00
c33cbd799d 修改签到和更新逻辑
All checks were successful
构建并部署 Spring Boot 应用 / build-and-deploy (push) Successful in 8m53s
2025-10-19 05:41:24 +08:00
74c0908b58 位置逻辑更新,代码修改
All checks were successful
构建并部署 Spring Boot 应用 / build-and-deploy (push) Successful in 52m56s
2025-10-18 22:20:07 +08:00
c173c480b2 apt更新
All checks were successful
构建并部署 Spring Boot 应用 / build-and-deploy (push) Successful in 34m42s
2025-10-18 00:10:20 +08:00
050fdd7137 https还原
Some checks failed
构建并部署 Spring Boot 应用 / build-and-deploy (push) Failing after 13s
2025-10-17 23:21:29 +08:00
ca14774891 修改用户角色,添加管理员相关逻辑
Some checks failed
构建并部署 Spring Boot 应用 / build-and-deploy (push) Failing after 9m4s
2025-10-17 02:17:28 +08:00
349cc22069 修改用户角色,添加管理员相关逻辑 2025-10-17 02:16:14 +08:00
20 changed files with 1487 additions and 386 deletions

View File

@@ -40,7 +40,9 @@ jobs:
- name: 安装 Maven
run: |
echo "安装 Maven..."
sudo apt update
# 修复GPG签名错误问题
sudo apt-get clean
sudo apt-get update --allow-releaseinfo-change
sudo apt install -y maven
echo "Maven 版本:"
mvn --version
@@ -90,14 +92,7 @@ jobs:
docker stop light-delivery-container 2>/dev/null || echo "没有运行中的容器"
docker rm light-delivery-container 2>/dev/null || echo "没有可删除的容器"
- name: 备份当前镜像(可选)
run: |
# 为当前运行中的镜像创建备份标签
if docker images light-delivery-app:latest --quiet | grep -q .; then
BACKUP_TAG="backup-$(date +%Y%m%d-%H%M%S)"
docker tag light-delivery-app:latest light-delivery-app:$BACKUP_TAG
echo "已创建备份: light-delivery-app:$BACKUP_TAG"
fi
# 已移除备份镜像逻辑,节省存储空间
- name: 运行新容器
run: |
@@ -140,15 +135,6 @@ jobs:
- name: 清理资源
run: |
# 清理旧的备份镜像保留最近5个
echo "清理旧的备份镜像..."
docker images light-delivery-app --filter "reference=light-delivery-app:backup-*" \
--format "{{.Tag}}\t{{.CreatedAt}}" | \
sort -k2 -r | \
tail -n +6 | \
awk '{print $1}' | \
xargs -r -I {} docker rmi light-delivery-app:{} || echo "无需清理"
# 清理无用镜像和容器
docker system prune -f

248
README.md
View File

@@ -86,6 +86,99 @@ src
位置信息现在存储在服务器内存缓存中,而不是持久化到数据库,以提高访问速度和减少数据库负载。
#### WebSocket接口和交互逻辑
系统通过WebSocket实现实时位置同步端点为`/ws/location`
##### 连接建立
客户端通过WebSocket连接到`/ws/location?userId={userId}`端点建立连接需要在查询参数中提供用户ID。
##### 消息格式
所有消息都使用JSON格式。
###### 1. 客户端发送的消息类型
1. **updateLocation位置更新** - 更新用户位置
```json
{
"type": "updateLocation",
"userId": 123,
"latitude": 25.0342,
"longitude": 102.7057,
"timestamp": 1634567890000
}
```
###### 2. 服务器发送的消息类型
1. **subscribed订阅成功响应** - 服务器确认订阅成功
```json
{
"type": "subscribed",
"userId": 123
}
```
2. **onlineUserList在线用户列表** - 服务器发送当前在线用户列表
```json
{
"type": "onlineUserList",
"users": [
{
"userId": 123,
"name": "张三",
"role": "DELIVERY_PERSON",
"userStatus": true,
"lastUpdateTime": 1634567890000,
"locationData": {
"latitude": 25.0342,
"longitude": 102.7057,
"timestamp": 1634567890000
}
}
]
}
```
3. **userLocationList用户位置列表** - 服务器每30秒发送所有在线用户的位置列表
```json
{
"type": "userLocationList",
"users": [
{
"userId": 123,
"locationData": {
"latitude": 25.0342,
"longitude": 102.7057,
"timestamp": 1634567890000
}
}
]
}
```
##### 交互流程
```
[用户签到] --> B{签到成功?}
B -->|是| C[建立WebSocket连接]
B -->|否| D[签到失败]
C --> E[自动订阅位置更新]
E --> F[接收subscribed确认]
F --> G[接收在线用户列表]
G --> H[开始位置更新循环]
H --> I[发送位置更新]
I --> J[每30秒接收一次位置列表]
K[用户签退] --> L{签退成功?}
L -->|是| M[自动取消订阅并关闭WebSocket连接]
L -->|否| N[签退失败]
```
##### 角色区分
- 管理员ADMIN和配送员DELIVERY_PERSON都可以接收位置更新
- 位置广播消息中包含用户角色信息,便于客户端区分显示
- 只有已签到用户才会收到位置更新广播
### 数据模型
主要实体包括:
@@ -167,6 +260,12 @@ java -jar target/light-delivery-1.0.0.jar --spring.profiles.active=local
java -jar target/light-delivery-1.0.0.jar --spring.profiles.active=test
```
在PowerShell中也可以使用以下命令运行本地环境
```powershell
$env:SPRING_PROFILES_ACTIVE = "local"
mvn spring-boot:run
```
### 运行Docker容器
```bash
@@ -238,6 +337,7 @@ deploy.bat
- `GET /user/info` - 获取用户信息
- `POST /user/logout` - 用户登出
- `POST /user/signin` - 用户签到
- `POST /user/signout` - 用户签退
- `POST /user/register` - 注册为配送员
### 订单相关
@@ -248,7 +348,7 @@ deploy.bat
### 配送员相关
- `GET /delivery-persons` - 获取配送员列表
- `GET /delivery-persons/{id}` - 获取配送员详情
- `PUT /delivery-persons/{id}/location` - 更新配送员位置
- `GET /delivery-persons/{id}/orders` - 获取配送员订单
### 员工管理相关(仅限管理员访问)
- `GET /employees` - 获取员工列表
@@ -258,9 +358,151 @@ deploy.bat
### 位置同步相关
- `GET /location-sync/delivery-persons/locations` - 获取所有配送员位置
- WebSocket端点: `/ws/location` - 实时位置同步
### WebSocket消息格式
客户端和服务器通过WebSocket发送JSON格式的消息。
#### 客户端发送的消息类型
1. **updateLocation位置更新** - 更新用户位置
```json
{
"type": "updateLocation",
"userId": 123,
"latitude": 25.0342,
"longitude": 102.7057,
"timestamp": 1634567890000
}
```
#### 服务器发送的消息类型
1. **subscribed订阅成功响应** - 服务器确认订阅成功
```json
{
"type": "subscribed",
"userId": 123
}
```
2. **onlineUserList在线用户列表** - 服务器发送当前在线用户列表
```json
{
"type": "onlineUserList",
"users": [
{
"userId": 123,
"name": "张三",
"role": "DELIVERY_PERSON",
"userStatus": true,
"lastUpdateTime": 1634567890000,
"locationData": {
"latitude": 25.0342,
"longitude": 102.7057,
"timestamp": 1634567890000
}
}
]
}
```
3. **userLocationList用户位置列表** - 服务器每30秒发送所有在线用户的位置列表
```json
{
"type": "userLocationList",
"users": [
{
"userId": 123,
"locationData": {
"latitude": 25.0342,
"longitude": 102.7057,
"timestamp": 1634567890000
}
}
]
}
```
## API接口响应格式
### 用户签到接口 `/user/signin`
#### 请求参数
- Method: `POST`
- Headers: `Authorization: Bearer <token>`
- Body:
```json
{
"latitude": 25.0342,
"longitude": 102.7057,
"timestamp": 1634567890000
}
```
#### 成功响应
```json
{
"success": true,
"userInfo": {
"id": 123,
"name": "张三",
"phone": "13800138000",
"role": "DELIVERY_PERSON",
"openid": "oK4fS5ABCDEF123456"
}
}
```
#### 失败响应
```json
{
"success": false,
"message": "错误信息"
}
```
### 用户签退接口 `/user/signout`
#### 请求参数
- Method: `POST`
- Headers: `Authorization: Bearer <token>`
#### 成功响应
```json
{
"success": true,
"message": "签退成功"
}
```
#### 失败响应
```json
{
"success": false,
"message": "错误信息"
}
```
## 最近更新
### WebSocket使用方式变更重要变更
为了更好地分离关注点和提高系统安全性我们对WebSocket使用方式进行了重要变更
- 移除了WebSocket中的签到/签退功能这些操作现在完全通过REST API进行
- WebSocket现在仅用于位置同步不再处理用户状态变更
- 用户需要先通过REST API签到然后才能建立WebSocket连接并接收位置更新
- 用户签退时服务器会自动取消订阅并关闭WebSocket连接
- 客户端不再需要发送subscribe/unsubscribe消息这些操作由服务器自动处理
- 服务器现在每30秒批量发送一次所有在线用户的位置信息而不是实时发送单个用户位置更新
### WebSocket位置同步增强新增
为了支持管理员和配送员都能接收位置更新信息我们对WebSocket位置同步功能进行了增强
- 修改了LocationWebSocketHandler使其支持所有用户类型包括管理员和配送员
- 在位置消息中添加了用户角色信息,便于客户端区分并使用不同图标显示
- 重构了WebSocket处理逻辑使其更加通用和可扩展
- 实现了在线用户列表管理和广播机制
### 用户角色管理优化(新增)
为解决数据一致性问题,我们优化了用户角色管理机制:
- 移除了User表中的role字段
@@ -292,4 +534,6 @@ deploy.bat
3. 配送员位置信息具有时效性默认5分钟内有效
4. 位置历史记录功能已被移除,如有需要可使用第三方服务进行位置追踪
5. 员工管理接口仅限管理员角色访问
6. 用户角色信息现在通过员工表动态获取,确保数据一致性
6. 用户角色信息现在通过员工表动态获取,确保数据一致性
7. WebSocket位置同步现在支持所有已签到用户包括管理员和配送员
8. WebSocket仅用于位置同步签到/签退操作需通过REST API完成

View File

@@ -68,18 +68,6 @@ public class DeliveryPersonController {
response.setCurrentLocation(location);
return ResponseEntity.ok(response);
}
/**
* 更新指定货运人员位置。
* @param id 货运人员ID
* @param locationRequest 包含经纬度
* @return 操作结果
*/
@PutMapping("/{id}/location")
public ResponseEntity<String> updateLocation(@PathVariable Long id, @RequestBody LocationRequest locationRequest) {
deliveryPersonService.updateLocation(id, locationRequest.getLongitude(), locationRequest.getLatitude());
return ResponseEntity.ok("位置更新成功");
}
/**
* 获取货运人员的当前订单。
@@ -112,13 +100,4 @@ public class DeliveryPersonController {
}).collect(Collectors.toList());
return ResponseEntity.ok(orders);
}
/**
* 位置请求体
*/
@Data
public static class LocationRequest {
private Double longitude;
private Double latitude;
}
}

View File

@@ -0,0 +1,138 @@
package com.light.delivery.controller;
import com.light.delivery.model.Employee;
import com.light.delivery.model.User;
import com.light.delivery.model.UserRole;
import com.light.delivery.service.EmployeeService;
import com.light.delivery.service.UserService;
import com.light.delivery.service.impl.UserServiceImpl;
import com.light.delivery.util.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 员工管理控制器,提供员工信息的增删改查功能。
* 仅限管理员角色访问。
*/
@RestController
@RequestMapping("/employees")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@Autowired
private UserService userService;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserServiceImpl userServiceImpl;
/**
* 获取所有员工列表
* @return 员工信息列表
*/
@GetMapping
public ResponseEntity<?> getAllEmployees(HttpServletRequest request) {
try {
User user = getUserFromToken(request);
UserRole userRole = userServiceImpl.getUserRole(user);
if (!UserRole.ADMIN.equals(userRole)) {
return ResponseEntity.status(403).body("权限不足,仅管理员可访问");
}
List<Employee> employees = employeeService.getAllEmployees();
return ResponseEntity.ok(employees);
} catch (Exception e) {
return ResponseEntity.status(401).body("认证失败: " + e.getMessage());
}
}
/**
* 添加新员工
* @param employee 员工信息
* @return 添加结果
*/
@PostMapping
public ResponseEntity<?> addEmployee(@RequestBody Employee employee, HttpServletRequest request) {
try {
User user = getUserFromToken(request);
UserRole userRole = userServiceImpl.getUserRole(user);
if (!UserRole.ADMIN.equals(userRole)) {
return ResponseEntity.status(403).body("权限不足,仅管理员可访问");
}
Employee savedEmployee = employeeService.saveEmployee(employee);
return ResponseEntity.ok(savedEmployee);
} catch (Exception e) {
return ResponseEntity.status(401).body("认证失败: " + e.getMessage());
}
}
/**
* 更新员工信息
* @param id 员工ID
* @param employee 员工信息
* @return 更新结果
*/
@PutMapping("/{id}")
public ResponseEntity<?> updateEmployee(@PathVariable Long id, @RequestBody Employee employee, HttpServletRequest request) {
try {
User user = getUserFromToken(request);
UserRole userRole = userServiceImpl.getUserRole(user);
if (!UserRole.ADMIN.equals(userRole)) {
return ResponseEntity.status(403).body("权限不足,仅管理员可访问");
}
Employee updatedEmployee = employeeService.updateEmployee(id, employee);
if (updatedEmployee == null) {
return ResponseEntity.status(404).body("员工不存在");
}
return ResponseEntity.ok(updatedEmployee);
} catch (Exception e) {
return ResponseEntity.status(401).body("认证失败: " + e.getMessage());
}
}
/**
* 删除员工
* @param id 员工ID
* @return 删除结果
*/
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteEmployee(@PathVariable Long id, HttpServletRequest request) {
try {
User user = getUserFromToken(request);
UserRole userRole = userServiceImpl.getUserRole(user);
if (!UserRole.ADMIN.equals(userRole)) {
return ResponseEntity.status(403).body("权限不足,仅管理员可访问");
}
employeeService.deleteEmployee(id);
return ResponseEntity.ok("员工删除成功");
} catch (Exception e) {
return ResponseEntity.status(401).body("认证失败: " + e.getMessage());
}
}
/**
* 从请求中提取用户信息
* @param request HTTP请求
* @return 用户对象
*/
private User getUserFromToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
String token = bearerToken.substring(7);
String openid = jwtUtil.extractUsername(token);
return userService.getUserInfo(token);
}
throw new IllegalArgumentException("Authorization token is missing");
}
}

View File

@@ -1,76 +0,0 @@
package com.light.delivery.controller;
import com.light.delivery.model.DeliveryPerson;
import com.light.delivery.service.DeliveryPersonService;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* 位置同步控制器,提供配送员位置信息查询接口。
* 用于获取所有已签到配送员的实时位置信息。
*/
@RestController
@RequestMapping("/location-sync")
public class LocationSyncController {
/**
* 配送员服务依赖注入
*/
@Autowired
private DeliveryPersonService deliveryPersonService;
/**
* 获取所有已签到配送员的实时位置。
* 仅返回有位置信息的配送员(最近有更新位置的)。
* @return 配送员位置列表
*/
@GetMapping("/delivery-persons/locations")
public ResponseEntity<List<DeliveryPersonLocation>> getDeliveryPersonLocations() {
List<DeliveryPerson> allDeliveryPersons = deliveryPersonService.getAllDeliveryPersons();
// 过滤出有位置信息的配送员(最近有更新位置的)
List<DeliveryPersonLocation> locations = allDeliveryPersons.stream()
.filter(person -> person.getLatitude() != null && person.getLongitude() != null)
.map(person -> {
DeliveryPersonLocation location = new DeliveryPersonLocation();
location.setDeliveryPersonId(person.getId());
location.setLatitude(person.getLatitude());
location.setLongitude(person.getLongitude());
location.setStatus(person.getStatus());
return location;
})
.collect(Collectors.toList());
return ResponseEntity.ok(locations);
}
/**
* 配送员位置信息DTO用于向前端返回配送员位置数据。
*/
@Data
public static class DeliveryPersonLocation {
/**
* 配送员ID
*/
private Long deliveryPersonId;
/**
* 纬度
*/
private Double latitude;
/**
* 经度
*/
private Double longitude;
/**
* 状态
*/
private String status;
}
}

View File

@@ -1,17 +1,21 @@
package com.light.delivery.controller;
import com.light.delivery.dto.UserInfoResponse;
import com.light.delivery.dto.SignOutResponse;
import com.light.delivery.dto.SignInResponse;
import com.light.delivery.model.LoginResponse;
import com.light.delivery.model.RegisterRequest;
import com.light.delivery.model.User;
import com.light.delivery.model.UserRole;
import com.light.delivery.model.WxLoginRequest;
import com.light.delivery.service.LocationWebSocketHandler;
import com.light.delivery.service.UserService;
import com.light.delivery.service.impl.UserServiceImpl;
import com.light.delivery.util.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -45,6 +49,12 @@ public class UserController {
*/
@Autowired
private UserServiceImpl userServiceImpl;
/**
* ApplicationContext依赖注入用于获取其他Bean。
*/
@Autowired
private ApplicationContext applicationContext;
/**
* 获取当前用户状态接口。
@@ -172,33 +182,82 @@ public class UserController {
/**
* 用户签到接口。
* @param request HTTP请求对象用于提取认证令牌
* @param initialLocation 初始位置信息
* @return 更新后的用户信息
*/
@PostMapping("/signin")
public ResponseEntity<?> signIn(HttpServletRequest request) {
public ResponseEntity<?> signIn(HttpServletRequest request, @RequestBody LocationWebSocketHandler.LocationData initialLocation) {
String token = extractToken(request);
if (token == null) {
return ResponseEntity.badRequest().body("Authorization token is missing");
return ResponseEntity.badRequest().body(new SignInResponse(false, "Authorization token is missing"));
}
// 检查位置信息是否为空
if (initialLocation == null) {
return ResponseEntity.badRequest().body(new SignInResponse(false, "Initial location information is required"));
}
// 检查位置信息的必要字段是否为空
if (initialLocation.getLatitude() == null || initialLocation.getLongitude() == null) {
return ResponseEntity.badRequest().body(new SignInResponse(false, "Latitude and longitude are required in location information"));
}
try {
User user = userService.getUserInfo(token);
User updatedUser = userService.signIn(user.getId());
UserInfoResponse response = toUserInfoResponse(updatedUser);
return ResponseEntity.ok(response);
UserInfoResponse userInfoResponse = toUserInfoResponse(updatedUser);
// 通知WebSocket处理器用户已签到并自动订阅
try {
LocationWebSocketHandler handler = applicationContext.getBean(LocationWebSocketHandler.class);
handler.userSignedIn(user.getId(), initialLocation);
} catch (Exception e) {
System.err.println("通知WebSocket签到状态时出错: " + e.getMessage());
}
return ResponseEntity.ok(new SignInResponse(true, userInfoResponse));
} catch (Exception e) {
return ResponseEntity.badRequest().body("Invalid token: " + e.getMessage());
return ResponseEntity.badRequest().body(new SignInResponse(false, "Invalid token: " + e.getMessage()));
}
}
/**
* 注册为骑手接口。
* 用户签退接口。
* @param request HTTP请求对象用于提取认证令牌
* @return 操作结果
*/
@PostMapping("/signout")
public ResponseEntity<?> signOut(HttpServletRequest request) {
String token = extractToken(request);
if (token == null) {
return ResponseEntity.badRequest().body(new SignOutResponse(false, "Authorization token is missing"));
}
try {
User user = userService.getUserInfo(token);
// 通知WebSocket处理器用户已签退并自动取消订阅
try {
LocationWebSocketHandler handler = applicationContext.getBean(LocationWebSocketHandler.class);
handler.userSignedOut(user.getId());
} catch (Exception e) {
System.err.println("通知WebSocket签退状态时出错: " + e.getMessage());
}
return ResponseEntity.ok(new SignOutResponse(true, "签退成功"));
} catch (Exception e) {
return ResponseEntity.badRequest().body(new SignOutResponse(false, "Invalid token: " + e.getMessage()));
}
}
/**
* 注册为正式员工接口。
* @param request HTTP请求对象用于提取认证令牌
* @param registerRequest 注册请求对象,包含姓名和手机号
* @return 更新后的用户信息
*/
@PostMapping("/register")
public ResponseEntity<UserInfoResponse> registerAsDeliveryPerson(
public ResponseEntity<UserInfoResponse> registerAsEmployee(
HttpServletRequest request,
@RequestBody RegisterRequest registerRequest) {
try {
@@ -213,7 +272,7 @@ public class UserController {
User user = userService.getUserInfo(token);
System.out.println("获取到用户信息: " + user);
User updatedUser = userService.registerAsDeliveryPerson(
User updatedUser = userService.registerAsEmployee(
user.getId(),
registerRequest.getName(),
registerRequest.getPhone());
@@ -221,12 +280,12 @@ public class UserController {
return ResponseEntity.ok(response);
} catch (IllegalArgumentException e) {
// 记录错误日志
System.err.println("注册配送员时发生错误: " + e.getMessage());
System.err.println("注册员时发生错误: " + e.getMessage());
e.printStackTrace();
return ResponseEntity.badRequest().build();
} catch (Exception e) {
// 记录未预期的错误
System.err.println("注册配送员时发生未预期错误: " + e.getMessage());
System.err.println("注册员时发生未预期错误: " + e.getMessage());
e.printStackTrace();
return ResponseEntity.status(500).build();
}

View File

@@ -0,0 +1,15 @@
package com.light.delivery.dto;
import lombok.Data;
/**
* 员工信息DTO用于员工信息的传输
*/
@Data
public class EmployeeDto {
private Long id;
private String name;
private String phone;
private String role;
private String openid;
}

View File

@@ -0,0 +1,42 @@
package com.light.delivery.dto;
import lombok.Data;
/**
* 签到响应 DTO用于统一签到接口的响应格式
*/
@Data
public class SignInResponse {
/**
* 操作是否成功
*/
private boolean success;
/**
* 响应消息(可选)
*/
private String message;
/**
* 用户信息(签到成功时返回)
*/
private UserInfoResponse userInfo;
public SignInResponse() {}
public SignInResponse(boolean success, String message) {
this.success = success;
this.message = message;
}
public SignInResponse(boolean success, UserInfoResponse userInfo) {
this.success = success;
this.userInfo = userInfo;
}
public SignInResponse(boolean success, String message, UserInfoResponse userInfo) {
this.success = success;
this.message = message;
this.userInfo = userInfo;
}
}

View File

@@ -0,0 +1,26 @@
package com.light.delivery.dto;
import lombok.Data;
/**
* 签退响应 DTO用于统一签退接口的响应格式
*/
@Data
public class SignOutResponse {
/**
* 操作是否成功
*/
private boolean success;
/**
* 响应消息(可选)
*/
private String message;
public SignOutResponse() {}
public SignOutResponse(boolean success, String message) {
this.success = success;
this.message = message;
}
}

View File

@@ -0,0 +1,49 @@
package com.light.delivery.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.HashMap;
import java.util.Map;
/**
* 全局异常处理器,用于统一处理系统中的异常
*/
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理权限不足异常
*/
@ExceptionHandler(SecurityException.class)
public ResponseEntity<Map<String, String>> handleSecurityException(SecurityException e) {
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("error", "权限不足");
errorResponse.put("message", e.getMessage());
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorResponse);
}
/**
* 处理非法参数异常
*/
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, String>> handleIllegalArgumentException(IllegalArgumentException e) {
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("error", "参数错误");
errorResponse.put("message", e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
/**
* 处理通用异常
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, String>> handleGenericException(Exception e) {
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("error", "系统错误");
errorResponse.put("message", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}
}

View File

@@ -20,13 +20,6 @@ public interface DeliveryPersonService {
* @return 配送员对象
*/
DeliveryPerson getDeliveryPersonById(Long id);
/**
* 更新配送员当前位置。
* @param id 配送员ID
* @param longitude 经度
* @param latitude 纬度
*/
void updateLocation(Long id, Double longitude, Double latitude);
/**
* 获取指定配送员的所有订单。
* @param id 配送员ID

View File

@@ -0,0 +1,52 @@
package com.light.delivery.service;
import com.light.delivery.dto.EmployeeDto;
import com.light.delivery.model.Employee;
import java.util.List;
/**
* 员工服务接口,定义员工相关的业务操作。
*/
public interface EmployeeService {
/**
* 获取所有员工信息
* @return 员工列表
*/
List<Employee> getAllEmployees();
/**
* 保存员工信息
* @param employee 员工信息
* @return 保存后的员工信息
*/
Employee saveEmployee(Employee employee);
/**
* 更新员工信息
* @param id 员工ID
* @param employee 员工信息
* @return 更新后的员工信息
*/
Employee updateEmployee(Long id, Employee employee);
/**
* 删除员工
* @param id 员工ID
*/
void deleteEmployee(Long id);
/**
* 将Employee实体转换为EmployeeDto
* @param employee 员工实体
* @return 员工DTO
*/
EmployeeDto toDto(Employee employee);
/**
* 将EmployeeDto转换为Employee实体
* @param dto 员工DTO
* @return 员工实体
*/
Employee toEntity(EmployeeDto dto);
}

View File

@@ -5,14 +5,6 @@ package com.light.delivery.service;
*/
public interface LocationSyncService {
/**
* 处理配送员位置更新
* @param deliveryPersonId 配送员ID
* @param longitude 经度
* @param latitude 纬度
*/
void handleLocationUpdate(Long deliveryPersonId, Double longitude, Double latitude);
/**
* 订阅位置更新
* @param sessionId 会话ID

View File

@@ -23,11 +23,14 @@ public interface UserService {
/**
* 微信小程序登录接收code返回登录响应。
* @param code 微信登录凭证
* @return 登录响应
*/
LoginResponse wxLogin(String code);
/**
* 获取当前已登录用户(从上下文或模拟实现中)
* @return 当前用户
*/
User getCurrentUser();
@@ -39,19 +42,25 @@ public interface UserService {
*/
User signIn(Long userId);
/**
* 注册为骑手
* @param userId 用户ID
* @param name 姓名
* @param phone 手机号
* @return 注册结果
*/
User registerAsDeliveryPerson(Long userId, String name, String phone);
/**
* 检查用户是否已签到
* @param userId 用户ID
* @return true表示已签到false表示未签到
*/
boolean isUserSignedIn(Long userId);
/**
* 用户签退功能
* @param userId 用户ID
*/
void signOut(Long userId);
/**
* 注册为正式员工
* @param userId 用户ID
* @param name 姓名
* @param phone 手机号
* @return 注册结果
*/
User registerAsEmployee(Long userId, String name, String phone);
}

View File

@@ -34,12 +34,6 @@ public class DeliveryPersonServiceImpl implements DeliveryPersonService {
return person.orElse(null);
}
@Override
public void updateLocation(Long id, Double longitude, Double latitude) {
// 使用 LocationSyncService 更新位置信息到服务器缓存,而不是直接更新数据库
locationSyncService.handleLocationUpdate(id, longitude, latitude);
}
@Override
public List<Order> getCurrentOrders(Long id) {
return orderRepository.findByDeliveryPersonId(id);

View File

@@ -0,0 +1,80 @@
package com.light.delivery.service.impl;
import com.light.delivery.dto.EmployeeDto;
import com.light.delivery.model.Employee;
import com.light.delivery.repository.EmployeeRepository;
import com.light.delivery.service.EmployeeService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* 员工服务实现类,处理员工相关的业务逻辑。
*/
@Service
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeRepository employeeRepository;
@Override
public List<Employee> getAllEmployees() {
return employeeRepository.findAll();
}
@Override
public Employee saveEmployee(Employee employee) {
return employeeRepository.save(employee);
}
@Override
public Employee updateEmployee(Long id, Employee employee) {
Optional<Employee> existingEmployee = employeeRepository.findById(id);
if (existingEmployee.isPresent()) {
Employee emp = existingEmployee.get();
emp.setName(employee.getName());
emp.setPhone(employee.getPhone());
emp.setRole(employee.getRole());
// 注意不更新openid字段该字段由用户注册时自动填充
return employeeRepository.save(emp);
}
return null;
}
@Override
public void deleteEmployee(Long id) {
employeeRepository.deleteById(id);
}
/**
* 将Employee实体转换为EmployeeDto
* @param employee 员工实体
* @return 员工DTO
*/
public EmployeeDto toDto(Employee employee) {
if (employee == null) {
return null;
}
EmployeeDto dto = new EmployeeDto();
BeanUtils.copyProperties(employee, dto);
return dto;
}
/**
* 将EmployeeDto转换为Employee实体
* @param dto 员工DTO
* @return 员工实体
*/
public Employee toEntity(EmployeeDto dto) {
if (dto == null) {
return null;
}
Employee employee = new Employee();
BeanUtils.copyProperties(dto, employee);
return employee;
}
}

View File

@@ -34,19 +34,7 @@ public class LocationSyncServiceImpl implements LocationSyncService {
// 位置过期时间(分钟)
private static final int LOCATION_EXPIRE_MINUTES = 5;
@Override
public void handleLocationUpdate(Long deliveryPersonId, Double longitude, Double latitude) {
try {
// 更新内存缓存中的配送员位置,而不是数据库
deliveryPersonLongitudeMap.put(deliveryPersonId, longitude);
deliveryPersonLatitudeMap.put(deliveryPersonId, latitude);
// 更新最后更新时间
lastUpdateTimes.put(deliveryPersonId, LocalDateTime.now());
} catch (Exception e) {
System.err.println("更新配送员位置时出错: " + e.getMessage());
}
}
@Override
public void subscribe(String sessionId, Long deliveryPersonId) {

View File

@@ -15,6 +15,7 @@ import com.light.delivery.service.UserService;
import com.light.delivery.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@@ -43,16 +44,16 @@ public class UserServiceImpl implements UserService {
private EmployeeRepository employeeRepository;
/**
* JWT工具类用于生成和解析JWT令牌
* Jwt工具类用于生成和解析JWT令牌
*/
@Autowired
private JwtUtil jwtUtil;
/**
* WebSocket处理器用于管理WebSocket连接
* Spring应用上下文用于获取其他Bean
*/
@Autowired
private LocationWebSocketHandler locationWebSocketHandler;
private ApplicationContext applicationContext;
/**
* 微信小程序appId
@@ -103,6 +104,25 @@ public class UserServiceImpl implements UserService {
throw new IllegalArgumentException("Invalid token: " + e.getMessage());
}
}
/**
* 根据用户ID获取用户信息。
* @param userId 用户ID
* @return 用户实体对象
* @throws IllegalArgumentException 当用户不存在时抛出异常
*/
public User getUserInfoById(Long userId) {
if (userId == null) {
throw new IllegalArgumentException("User ID is null");
}
Optional<User> userOptional = userRepository.findById(userId);
if (!userOptional.isPresent()) {
throw new IllegalArgumentException("用户不存在");
}
return userOptional.get();
}
/**
* 用户登出逻辑。
@@ -127,14 +147,18 @@ public class UserServiceImpl implements UserService {
return;
}
// 如果用户是配送员,通知WebSocket处理器清理连接
// 注意这里需要根据实际业务逻辑获取配送员ID
// 可能需要通过其他方式关联用户ID和配送员ID
// 这里假设用户ID和配送员ID相同根据项目实际情况调整
// locationWebSocketHandler.removeUserConnection(user.getId());
// 通知WebSocket处理器清理连接
try {
LocationWebSocketHandler handler = applicationContext.getBean(LocationWebSocketHandler.class);
// 清理用户状态
handler.cleanupUserConnection(user.getId());
} catch (Exception e) {
// 记录日志但不中断登出流程
System.err.println("清理WebSocket连接时出错: " + e.getMessage());
}
} catch (Exception e) {
// 记录日志但不中断登出流程
System.err.println("清理WebSocket连接时出错: " + e.getMessage());
System.err.println("登出时出错: " + e.getMessage());
}
}
@@ -227,6 +251,12 @@ public class UserServiceImpl implements UserService {
return user;
}
@Override
public void signOut(Long userId) {
// 签退操作在REST控制器中通过WebSocket处理器完成
// 这里可以添加其他签退逻辑(如记录签退时间等)
}
/**
* 用户注册成为配送员。
@@ -239,8 +269,8 @@ public class UserServiceImpl implements UserService {
* @throws IllegalArgumentException 当用户不存在、手机号未找到或姓名不匹配时抛出异常
*/
@Override
public User registerAsDeliveryPerson(Long userId, String name, String phone) {
System.out.println("尝试注册配送用户ID: " + userId);
public User registerAsEmployee(Long userId, String name, String phone) {
System.out.println("尝试注册员用户ID: " + userId);
Optional<User> userOptional = userRepository.findById(userId);
if (!userOptional.isPresent()) {
@@ -330,22 +360,18 @@ public class UserServiceImpl implements UserService {
*/
@Override
public boolean isUserSignedIn(Long userId) {
// 只有配送员角色才有签到状态
Optional<User> userOptional = userRepository.findById(userId);
if (!userOptional.isPresent()) {
return false;
}
User user = userOptional.get();
UserRole userRole = getUserRole(user);
if (userRole != UserRole.DELIVERY_PERSON) {
// 非配送员角色没有签到状态概念
return false;
}
// 检查WebSocket中的签到状态
return locationWebSocketHandler.isDeliveryPersonSignedIn(userId);
// 注意由于循环依赖问题这里不能直接注入LocationWebSocketHandler
// 在实际应用中,可能需要通过其他方式获取签到状态
// 现在通过静态方法或上下文获取LocationWebSocketHandler实例
try {
// 使用Spring上下文获取LocationWebSocketHandler实例
LocationWebSocketHandler handler = applicationContext.getBean(LocationWebSocketHandler.class);
return handler.isUserSignedIn(userId);
} catch (Exception e) {
System.err.println("获取WebSocket签到状态时出错: " + e.getMessage());
return false; // 默认返回false
}
}
/**

View File

@@ -1,8 +1,10 @@
spring.application.name=Light
server.port=443
# 默认禁用SSL通过profile启用
server.ssl.enabled=false
server.ssl.enabled=true
server.ssl.key-store-type=JKS
server.ssl.key-store=/etc/ssl/certs/www.doubleyin.cn.jks
server.ssl.key-store-password=${KEY_STORE_PASSWORD}
spring.datasource.url=jdbc:mysql://115.190.121.151:3306/light_delivery?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
spring.datasource.username=double