This commit is contained in:
2025-05-13 21:30:06 +08:00
commit 60abc68c60
115 changed files with 12478 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
package com.yupi.project;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.yupi.project.mapper")
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}

View File

@@ -0,0 +1,32 @@
package com.yupi.project.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 权限校验
*
* @author yupi
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthCheck {
/**
* 有任何一个角色
*
* @return
*/
String[] anyRole() default "";
/**
* 必须有某个角色
*
* @return
*/
String mustRole() default "";
}

View File

@@ -0,0 +1,77 @@
package com.yupi.project.aop;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.yupi.project.annotation.AuthCheck;
import com.yupi.project.common.ErrorCode;
import com.yupi.project.exception.BusinessException;
import com.yupi.project.model.entity.User;
import com.yupi.project.model.enums.UserRoleEnum;
import com.yupi.project.service.UserService;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 权限校验 AOP
*
* @author yupi
*/
@Aspect
@Component
public class AuthInterceptor {
@Resource
private UserService userService;
/**
* 执行拦截
*
* @param joinPoint
* @param authCheck
* @return
*/
@Around("@annotation(authCheck)")
public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {
List<String> anyRole = Arrays.stream(authCheck.anyRole()).filter(StringUtils::isNotBlank).collect(Collectors.toList());
String mustRole = authCheck.mustRole();
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
// 当前登录用户
User user = userService.getCurrentUser(request);
// 拥有任意权限即通过
if (CollectionUtils.isNotEmpty(anyRole)) {
String userRole = user.getRole();
if (!anyRole.contains(userRole)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
}
// 必须有所有权限才通过
if (StringUtils.isNotBlank(mustRole)) {
UserRoleEnum mustUserRoleEnum = UserRoleEnum.getEnumByValue(mustRole);
if (mustUserRoleEnum == null) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
String userRole = user.getRole();
if (UserRoleEnum.ADMIN.equals(mustUserRoleEnum)) {
if (!mustRole.equals(userRole)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
}
}
// 通过权限校验,放行
return joinPoint.proceed();
}
}

View File

@@ -0,0 +1,56 @@
package com.yupi.project.aop;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.UUID;
/**
* 请求响应日志 AOP
*
* @author yupi
**/
@Aspect
@Component
@Slf4j
public class LogInterceptor {
/**
* 执行拦截
*/
@Around("execution(* com.yupi.project.controller.*.*(..))")
public Object doInterceptor(ProceedingJoinPoint point) throws Throwable {
// 计时
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 获取请求路径
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
// 生成请求唯一 id
String requestId = UUID.randomUUID().toString();
String url = httpServletRequest.getRequestURI();
// 获取请求参数
Object[] args = point.getArgs();
String reqParam = "[" + StringUtils.join(args, ", ") + "]";
// 输出请求日志
log.info("request startid: {}, path: {}, ip: {}, params: {}", requestId, url,
httpServletRequest.getRemoteHost(), reqParam);
// 执行原方法
Object result = point.proceed();
// 输出响应日志
stopWatch.stop();
long totalTimeMillis = stopWatch.getTotalTimeMillis();
log.info("request end, id: {}, cost: {}ms", requestId, totalTimeMillis);
return result;
}
}

View File

@@ -0,0 +1,35 @@
package com.yupi.project.common;
import lombok.Data;
import java.io.Serializable;
/**
* 通用返回类
*
* @param <T>
* @author yupi
*/
@Data
public class BaseResponse<T> implements Serializable {
private int code;
private T data;
private String message;
public BaseResponse(int code, T data, String message) {
this.code = code;
this.data = data;
this.message = message;
}
public BaseResponse(int code, T data) {
this(code, data, "");
}
public BaseResponse(ErrorCode errorCode) {
this(errorCode.getCode(), null, errorCode.getMessage());
}
}

View File

@@ -0,0 +1,20 @@
package com.yupi.project.common;
import lombok.Data;
import java.io.Serializable;
/**
* 删除请求
*
* @author yupi
*/
@Data
public class DeleteRequest implements Serializable {
/**
* id
*/
private Long id;
private static final long serialVersionUID = 1L;
}

View File

@@ -0,0 +1,42 @@
package com.yupi.project.common;
/**
* 错误码
*
* @author yupi
*/
public enum ErrorCode {
SUCCESS(0, "ok"),
PARAMS_ERROR(40000, "请求参数错误"),
NOT_LOGIN_ERROR(40100, "未登录"),
NO_AUTH_ERROR(40101, "无权限"),
NOT_FOUND_ERROR(40400, "请求数据不存在"),
FORBIDDEN_ERROR(40300, "禁止访问"),
SYSTEM_ERROR(50000, "系统内部异常"),
OPERATION_ERROR(50001, "操作失败");
/**
* 状态码
*/
private final int code;
/**
* 信息
*/
private final String message;
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}

View File

@@ -0,0 +1,33 @@
package com.yupi.project.common;
import com.yupi.project.constant.CommonConstant;
import lombok.Data;
/**
* 分页请求
*
* @author yupi
*/
@Data
public class PageRequest {
/**
* 当前页号
*/
private long current = 1;
/**
* 页面大小
*/
private long pageSize = 10;
/**
* 排序字段
*/
private String sortField;
/**
* 排序顺序(默认升序)
*/
private String sortOrder = CommonConstant.SORT_ORDER_ASC;
}

View File

@@ -0,0 +1,51 @@
package com.yupi.project.common;
/**
* 返回工具类
*
* @author yupi
*/
public class ResultUtils {
/**
* 成功
*
* @param data
* @param <T>
* @return
*/
public static <T> BaseResponse<T> success(T data) {
return new BaseResponse<>(0, data, "ok");
}
/**
* 失败
*
* @param errorCode
* @return
*/
public static BaseResponse error(ErrorCode errorCode) {
return new BaseResponse<>(errorCode);
}
/**
* 失败
*
* @param code
* @param message
* @return
*/
public static BaseResponse error(int code, String message) {
return new BaseResponse(code, null, message);
}
/**
* 失败
*
* @param errorCode
* @return
*/
public static BaseResponse error(ErrorCode errorCode, String message) {
return new BaseResponse(errorCode.getCode(), null, message);
}
}

View File

@@ -0,0 +1,38 @@
package com.yupi.project.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* Knife4j 接口文档配置
* https://doc.xiaominfo.com/knife4j/documentation/get_start.html
*
* @author yupi
*/
@Configuration
@EnableSwagger2
@Profile("dev")
public class Knife4jConfig {
@Bean
public Docket defaultApi2() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder()
.title("project-backend")
.description("project-backend")
.version("1.0")
.build())
.select()
// 指定 Controller 扫描包路径
.apis(RequestHandlerSelectors.basePackage("com.yupi.project.controller"))
.paths(PathSelectors.any())
.build();
}
}

View File

@@ -0,0 +1,91 @@
package com.yupi.project.config;
import com.yupi.project.config.properties.MqttProperties;
import com.yupi.project.mqtt.MqttCallbackHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.UUID;
@Slf4j
@Configuration
@RequiredArgsConstructor
public class MqttConfig {
private final MqttProperties mqttProperties;
private final MqttCallbackHandler mqttCallbackHandler;
@Autowired // Autowire the MqttClient bean defined below
private MqttClient mqttClient;
@Bean
public MqttConnectOptions mqttConnectOptions() {
MqttConnectOptions options = new MqttConnectOptions();
if (StringUtils.hasText(mqttProperties.getUsername())) {
options.setUserName(mqttProperties.getUsername());
}
if (StringUtils.hasText(mqttProperties.getPassword())) {
options.setPassword(mqttProperties.getPassword().toCharArray());
}
options.setAutomaticReconnect(true); // Enable automatic reconnect
options.setCleanSession(true); // Start with a clean session
options.setConnectionTimeout(mqttProperties.getConnectionTimeout());
options.setKeepAliveInterval(mqttProperties.getKeepAliveInterval());
// options.setWill(...) // Configure Last Will and Testament if needed
return options;
}
@Bean // Bean method name will be the bean name by default: "mqttClientBean"
public MqttClient mqttClientBean(MqttConnectOptions mqttConnectOptions) throws MqttException {
String clientId = mqttProperties.getClientIdPrefix() + UUID.randomUUID().toString().replace("-", "");
MqttClient client = new MqttClient(mqttProperties.getBrokerUrl(), clientId, new MemoryPersistence());
client.setCallback(mqttCallbackHandler);
// Pass the client instance to the handler so it can subscribe on connectComplete
mqttCallbackHandler.setMqttClient(client);
return client;
}
@PostConstruct
public void connect() {
try {
// Use the autowired mqttClient field
if (this.mqttClient != null && !this.mqttClient.isConnected()) {
log.info("Attempting to connect to MQTT broker: {} with client ID: {}", mqttProperties.getBrokerUrl(), this.mqttClient.getClientId());
this.mqttClient.connect(mqttConnectOptions()); // mqttConnectOptions() provides the bean
// Subscription logic is now in MqttCallbackHandler.connectComplete
} else if (this.mqttClient == null) {
log.error("MqttClient (autowired) is null, cannot connect.");
}
} catch (MqttException e) {
log.error("Error connecting to MQTT broker: ", e);
// Consider retry logic or application failure based on requirements
}
}
@PreDestroy
public void disconnect() {
try {
// Use the autowired mqttClient field
if (this.mqttClient != null && this.mqttClient.isConnected()) {
log.info("Disconnecting from MQTT broker: {}", this.mqttClient.getServerURI());
this.mqttClient.disconnect();
log.info("Successfully disconnected from MQTT broker.");
}
if (this.mqttClient != null) {
this.mqttClient.close();
}
} catch (MqttException e) {
log.error("Error disconnecting from MQTT broker: ", e);
}
}
}

View File

@@ -0,0 +1,31 @@
package com.yupi.project.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis Plus 配置
*
* @author yupi
*/
@Configuration
@MapperScan("com.yupi.project.mapper")
public class MyBatisPlusConfig {
/**
* 拦截器配置
*
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

View File

@@ -0,0 +1,73 @@
package com.yupi.project.config;
import com.yupi.project.model.enums.UserRoleEnum;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
/**
* 安全相关配置 (集成CORS)
*/
@Configuration
@EnableWebSecurity // 启用Spring Security的Web安全支持
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
// 使用 BCrypt 进行密码加密
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 集成CORS配置
.cors()
.and()
// 明确禁用CSRF保护
.csrf().disable()
.authorizeRequests(
// 允许对登录和注册接口的匿名访问 (路径不带 /api 前缀)
authorize -> authorize
.antMatchers("/user/login", "/user/register", "/error").permitAll()
.antMatchers("/user/current").authenticated()
// 使用 hasAuthority 确保匹配 UserRoleEnum.ADMIN.getValue() 即 "admin"
.antMatchers("/user/list").hasAuthority(UserRoleEnum.ADMIN.getValue())
.antMatchers(HttpMethod.DELETE, "/user/delete/**").hasAuthority(UserRoleEnum.ADMIN.getValue())
.antMatchers(HttpMethod.POST, "/user/admin/add").hasAuthority(UserRoleEnum.ADMIN.getValue())
.antMatchers(HttpMethod.PUT, "/user/admin/update").hasAuthority(UserRoleEnum.ADMIN.getValue())
// 其他所有请求都需要认证
.anyRequest().authenticated()
)
// 禁用默认的表单登录页面
.formLogin().disable()
// 禁用HTTP Basic认证
.httpBasic().disable();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// TODO: 在生产环境请显式指定允许的源,而不是 "*"
configuration.setAllowedOriginPatterns(Arrays.asList("*")); // 允许所有源模式
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*")); // 允许所有头
configuration.setAllowCredentials(true); // 允许发送 Cookie
configuration.setMaxAge(3600L); // 预检请求的有效期
// configuration.setExposedHeaders(Arrays.asList("YourCustomHeader")); // 如果需要暴露自定义头
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration); // 对所有路径应用配置
return source;
}
}

View File

@@ -0,0 +1,22 @@
package com.yupi.project.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "mqtt")
public class MqttProperties {
private String brokerUrl;
private String username;
private String password;
private String clientIdPrefix;
private int defaultQos;
private int connectionTimeout;
private int keepAliveInterval;
private String commandTopicBase;
private String statusTopicBase;
}

View File

@@ -0,0 +1,19 @@
package com.yupi.project.constant;
/**
* 通用常量
*
* @author yupi
*/
public interface CommonConstant {
/**
* 升序
*/
String SORT_ORDER_ASC = "ascend";
/**
* 降序
*/
String SORT_ORDER_DESC = " descend";
}

View File

@@ -0,0 +1,33 @@
package com.yupi.project.constant;
/**
* 用户常量
*
* @author yupi
*/
public interface UserConstant {
/**
* 用户登录态键
*/
String USER_LOGIN_STATE = "userLoginState";
/**
* 系统用户 id虚拟用户
*/
long SYSTEM_USER_ID = 0;
// region 权限
/**
* 默认角色
*/
String DEFAULT_ROLE = "user";
/**
* 管理员角色
*/
String ADMIN_ROLE = "admin";
// endregion
}

View File

@@ -0,0 +1,152 @@
package com.yupi.project.controller;
import com.yupi.project.common.BaseResponse;
import com.yupi.project.common.ErrorCode;
import com.yupi.project.common.ResultUtils;
import com.yupi.project.exception.BusinessException;
import com.yupi.project.model.dto.user.UserLoginRequest;
import com.yupi.project.model.dto.user.UserRegisterRequest;
import com.yupi.project.model.entity.User;
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 javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
/**
* 用户接口
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
// --- 登录注册相关 --- //
@PostMapping("/register")
public BaseResponse<Long> userRegister(@RequestBody UserRegisterRequest userRegisterRequest) {
if (userRegisterRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
String username = userRegisterRequest.getUsername();
String password = userRegisterRequest.getPassword();
String checkPassword = userRegisterRequest.getCheckPassword();
if (StringUtils.isAnyBlank(username, password, checkPassword)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数不能为空");
}
long result = userService.userRegister(username, password, checkPassword);
return ResultUtils.success(result);
}
@PostMapping("/login")
public BaseResponse<User> userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {
if (userLoginRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
String username = userLoginRequest.getUsername();
String password = userLoginRequest.getPassword();
if (StringUtils.isAnyBlank(username, password)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数不能为空");
}
User user = userService.userLogin(username, password, request);
return ResultUtils.success(user);
}
@PostMapping("/logout")
public BaseResponse<Boolean> userLogout(HttpServletRequest request) {
if (request == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
boolean result = userService.userLogout(request);
return ResultUtils.success(result);
}
@GetMapping("/current")
public BaseResponse<User> getCurrentUser(HttpServletRequest request) {
User currentUser = userService.getCurrentUser(request);
// getCurrentUser 内部已处理未登录情况,并返回脱敏用户
return ResultUtils.success(currentUser);
}
// --- 管理员功能 --- //
@GetMapping("/list")
public BaseResponse<List<User>> listUsers(HttpServletRequest request) {
// 权限校验 (在 SecurityConfig 中配置,这里可以简化或移除,但保留显式检查作为双重保险)
User currentUser = userService.getCurrentUser(request); // 获取当前用户以检查角色
if (currentUser == null || !UserRoleEnum.ADMIN.getValue().equals(currentUser.getRole())) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无管理员权限");
}
List<User> userList = userService.listUsers(); // 调用新的 service 方法,它已经处理了脱敏
return ResultUtils.success(userList);
}
// 新增管理员删除用户接口
@DeleteMapping("/delete/{userId}")
public BaseResponse<Boolean> adminDeleteUser(@PathVariable Long userId, HttpServletRequest request) {
// 权限校验 (虽然 SecurityConfig 会拦截,但 Controller 层再校验一次更安全)
User currentUser = userService.getCurrentUser(request);
if (currentUser == null || !UserRoleEnum.ADMIN.getValue().equals(currentUser.getRole())) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无管理员权限");
}
if (userId == null || userId <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户ID参数错误");
}
// 防止管理员误删自己 (可选逻辑)
if (currentUser.getId().equals(userId)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "不能删除当前登录的管理员账户");
}
boolean result = userService.adminDeleteUser(userId);
return ResultUtils.success(result);
}
// 新增管理员添加用户接口
@PostMapping("/admin/add")
public BaseResponse<Long> adminAddUser(@RequestBody com.yupi.project.model.dto.user.UserAdminAddRequest addRequest, HttpServletRequest request) {
// 权限校验
User currentUser = userService.getCurrentUser(request);
if (currentUser == null || !UserRoleEnum.ADMIN.getValue().equals(currentUser.getRole())) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无管理员权限");
}
if (addRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空");
}
long newUserId = userService.adminAddUser(addRequest);
return ResultUtils.success(newUserId);
}
// 新增管理员更新用户接口
@PutMapping("/admin/update")
public BaseResponse<Boolean> adminUpdateUser(@RequestBody com.yupi.project.model.dto.user.UserAdminUpdateRequest updateRequest, HttpServletRequest request) {
// 权限校验
User currentUser = userService.getCurrentUser(request);
if (currentUser == null || !UserRoleEnum.ADMIN.getValue().equals(currentUser.getRole())) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无管理员权限");
}
if (updateRequest == null || updateRequest.getId() == null || updateRequest.getId() <=0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数错误必须提供用户ID");
}
// 防止管理员通过此接口修改自己的角色把自己改成普通用户或修改关键的ADMIN账户 (可选)
// 如果要更新的用户是当前登录的管理员并且尝试修改角色为非admin则拒绝
if (currentUser.getId().equals(updateRequest.getId()) &&
StringUtils.isNotBlank(updateRequest.getRole()) &&
!UserRoleEnum.ADMIN.getValue().equals(updateRequest.getRole())) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "管理员不能将自己的角色修改为非管理员");
}
boolean result = userService.adminUpdateUser(updateRequest);
return ResultUtils.success(result);
}
}

View File

@@ -0,0 +1,32 @@
package com.yupi.project.exception;
import com.yupi.project.common.ErrorCode;
/**
* 自定义异常类
*
* @author yupi
*/
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
}
public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.code = errorCode.getCode();
}
public int getCode() {
return code;
}
}

View File

@@ -0,0 +1,30 @@
package com.yupi.project.exception;
import com.yupi.project.common.BaseResponse;
import com.yupi.project.common.ErrorCode;
import com.yupi.project.common.ResultUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器
*
* @author yupi
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public BaseResponse<?> businessExceptionHandler(BusinessException e) {
log.error("businessException: " + e.getMessage(), e);
return ResultUtils.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(RuntimeException.class)
public BaseResponse<?> runtimeExceptionHandler(RuntimeException e) {
log.error("runtimeException", e);
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, e.getMessage());
}
}

View File

@@ -0,0 +1,13 @@
package com.yupi.project.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yupi.project.model.entity.User;
/**
* @description 针对表【user(用户表)】的数据库操作Mapper
* @createDate 2023-11-24 10:00:00
* @Entity com.yupi.project.model.entity.User
*/
public interface UserMapper extends BaseMapper<User> {
}

View File

@@ -0,0 +1,34 @@
package com.yupi.project.model.dto.user;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 管理员添加用户请求体
*/
@Data
public class UserAdminAddRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 用户角色 (例如 "user", "admin")
*/
private String role;
/**
* 初始余额 (可选)
*/
private BigDecimal balance;
}

View File

@@ -0,0 +1,41 @@
package com.yupi.project.model.dto.user;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 管理员更新用户请求体
*/
@Data
public class UserAdminUpdateRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户ID (必须)
*/
private Long id;
/**
* 用户名 (可选,如果提供则尝试更新)
*/
private String username;
/**
* 新密码 (可选,如果提供则重置密码)
*/
private String password;
/**
* 用户角色 (可选,如果提供则尝试更新)
*/
private String role;
/**
* 余额 (可选,如果提供则尝试更新)
*/
private BigDecimal balance;
// isDeleted 不应由管理员直接通过此接口修改,应通过删除接口
}

View File

@@ -0,0 +1,18 @@
package com.yupi.project.model.dto.user;
import lombok.Data;
import java.io.Serializable;
/**
* 用户登录请求体
*/
@Data
public class UserLoginRequest implements Serializable {
private static final long serialVersionUID = 3191241716373120793L;
private String username;
private String password;
}

View File

@@ -0,0 +1,21 @@
package com.yupi.project.model.dto.user;
import lombok.Data;
import java.io.Serializable;
/**
* 用户注册请求体
*/
@Data
public class UserRegisterRequest implements Serializable {
private static final long serialVersionUID = 3191241716373120793L;
private String username;
private String password;
private String checkPassword;
}

View File

@@ -0,0 +1,60 @@
package com.yupi.project.model.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 用户表
*/
@TableName(value ="user")
@Data
public class User implements Serializable {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码 (加密存储)
*/
private String password;
/**
* 角色 (user/admin)
*/
private String role;
/**
* 账户余额
*/
private BigDecimal balance;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 逻辑删除标志 (0:未删, 1:已删)
*/
@TableLogic
private Integer isDeleted;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}

View File

@@ -0,0 +1,62 @@
package com.yupi.project.model.enums;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.lang3.ObjectUtils;
/**
* 用户角色枚举
*
* @author yupi
*/
public enum UserRoleEnum {
USER("user", "用户"),
ADMIN("admin", "管理员"),
BAN("ban", "被封号");
private final String value;
private final String text;
UserRoleEnum(String value, String text) {
this.value = value;
this.text = text;
}
/**
* 获取值列表
*
* @return
*/
public static List<String> getValues() {
return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList());
}
/**
* 根据 value 获取枚举
*
* @param value
* @return
*/
public static UserRoleEnum getEnumByValue(String value) {
if (ObjectUtils.isEmpty(value)) {
return null;
}
for (UserRoleEnum anEnum : UserRoleEnum.values()) {
if (anEnum.value.equals(value)) {
return anEnum;
}
}
return null;
}
public String getValue() {
return value;
}
public String getText() {
return text;
}
}

View File

@@ -0,0 +1,77 @@
package com.yupi.project.mqtt;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.springframework.stereotype.Component;
// import com.yupi.project.service.MqttMessageHandler; // Will be uncommented and used later
import com.yupi.project.config.properties.MqttProperties;
@Slf4j
@Component
@RequiredArgsConstructor
public class MqttCallbackHandler implements MqttCallbackExtended {
// private final MqttMessageHandler mqttMessageHandler; // Will be uncommented and used later
private final MqttProperties mqttProperties;
private MqttClient mqttClient; // Setter needed or passed in constructor/method
public void setMqttClient(MqttClient mqttClient) {
this.mqttClient = mqttClient;
}
@Override
public void connectComplete(boolean reconnect, String serverURI) {
log.info("MQTT connection {} to broker: {}", reconnect ? "re-established" : "established", serverURI);
// Subscribe to the status topic upon connection/reconnection
try {
if (mqttClient != null && mqttClient.isConnected()) {
String statusTopic = mqttProperties.getStatusTopicBase() + "/+"; // Subscribe to all robot statuses
mqttClient.subscribe(statusTopic, mqttProperties.getDefaultQos());
log.info("Subscribed to MQTT topic: {}", statusTopic);
} else {
log.warn("MQTT client not available or not connected, cannot subscribe to topic.");
}
} catch (Exception e) {
log.error("Error subscribing to MQTT topic after connection complete: ", e);
}
}
@Override
public void connectionLost(Throwable cause) {
log.error("MQTT connection lost!", cause);
// Automatic reconnect is handled by MqttConnectOptions
}
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
String payload = new String(message.getPayload());
log.debug("MQTT message arrived. Topic: {}, QoS: {}, Payload:\n{}", topic, message.getQos(), payload);
// TODO: Implement application-level authentication/validation of the message payload here or in MqttMessageHandler
// try {
// mqttMessageHandler.handleStatusUpdate(topic, payload); // Will be uncommented and used later
// } catch (Exception e) {
// log.error("Error handling MQTT message for topic {}: ", topic, e);
// }
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
// This callback is invoked when a message published by this client has been successfully delivered
// Used for QoS 1 and 2. For QoS 0, it's called after the message has been handed to the network.
try {
if (token.isComplete() && token.getMessage() != null) {
log.trace("MQTT message delivery complete. Message ID: {}, Payload: {}", token.getMessageId(), new String(token.getMessage().getPayload()));
} else if (token.isComplete()){
log.trace("MQTT message delivery complete. Message ID: {}", token.getMessageId());
}
} catch (Exception e) {
log.error("Error in MQTT deliveryComplete callback: ", e);
}
}
}

View File

@@ -0,0 +1,105 @@
package com.yupi.project.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yupi.project.model.entity.User;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.util.List;
/**
* @description 针对表【user(用户表)】的数据库操作Service
* @createDate 2023-11-24 10:05:00
*/
public interface UserService extends IService<User> {
/**
* 用户注册
*
* @param username 用户名
* @param password 密码
* @param checkPassword 校验密码
* @return 新用户 id
*/
long userRegister(String username, String password, String checkPassword);
/**
* 用户登录
*
* @param username 用户名
* @param password 密码
* @param request HttpServletRequest 用于操作 session
* @return 脱敏后的用户信息
*/
User userLogin(String username, String password, HttpServletRequest request);
/**
* 获取当前登录用户
*
* @param request HttpServletRequest
* @return 当前登录用户,未登录则返回 null
*/
User getCurrentUser(HttpServletRequest request);
/**
* 用户注销
*
* @param request HttpServletRequest
* @return 是否成功
*/
boolean userLogout(HttpServletRequest request);
/**
* 获取脱敏的用户信息
*
* @param originUser 原始用户信息
* @return 脱敏后的用户信息
*/
User getSafetyUser(User originUser);
/**
* 扣减用户余额 (需要保证线程安全和数据一致性)
* @param userId 用户ID
* @param amount 扣减金额 (正数)
* @return 操作是否成功
*/
boolean deductBalance(Long userId, BigDecimal amount);
/**
* 增加用户余额 (需要保证线程安全和数据一致性)
* @param userId 用户ID
* @param amount 增加金额 (正数)
* @return 操作是否成功
*/
boolean addBalance(Long userId, BigDecimal amount);
/**
* 获取用户列表 (仅管理员)
*
* @return 脱敏后的用户列表
*/
List<User> listUsers();
/**
* 管理员删除用户 (逻辑删除)
*
* @param userId 用户ID
* @return 操作是否成功
*/
boolean adminDeleteUser(Long userId);
/**
* 管理员添加新用户
*
* @param addRequest 包含新用户信息的请求体
* @return 新用户的ID
*/
long adminAddUser(com.yupi.project.model.dto.user.UserAdminAddRequest addRequest);
/**
* 管理员更新用户信息
*
* @param updateRequest 包含待更新用户信息的请求体
* @return 操作是否成功
*/
boolean adminUpdateUser(com.yupi.project.model.dto.user.UserAdminUpdateRequest updateRequest);
}

View File

@@ -0,0 +1,392 @@
package com.yupi.project.service.impl;
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.constant.UserConstant;
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 lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.List;
import java.util.stream.Collectors;
import java.util.ArrayList;
/**
* 用户服务实现类
*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
@Resource
private UserMapper userMapper;
@Resource
private PasswordEncoder passwordEncoder;
// 用户名校验正则允许字母、数字、下划线长度4到16位
private static final String USERNAME_PATTERN = "^[a-zA-Z0-9_]{4,16}$";
// 密码校验正则至少包含字母和数字长度至少6位
private static final String PASSWORD_PATTERN = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{6,}$";
@Override
public long userRegister(String username, String password, String checkPassword) {
// 1. 参数校验
if (StringUtils.isAnyBlank(username, password, checkPassword)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数不能为空");
}
if (!Pattern.matches(USERNAME_PATTERN, username)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户名不符合规范4-16位字母、数字、下划线");
}
if (password.length() < 6) { // 使用更宽松的校验,正则校验留给前端
throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码长度不能小于6位");
}
if (!password.equals(checkPassword)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "两次输入的密码不一致");
}
synchronized (username.intern()) {
// 2. 检查用户名是否已存在
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
long count = this.count(queryWrapper);
if (count > 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户名已存在");
}
// 3. 密码加密
String encodedPassword = passwordEncoder.encode(password);
// 4. 创建用户
User userToSave = new User();
userToSave.setUsername(username);
userToSave.setPassword(encodedPassword);
userToSave.setRole(UserConstant.DEFAULT_ROLE); // 默认角色 'user'
userToSave.setBalance(BigDecimal.ZERO); // 初始余额为0
boolean saveResult = this.save(userToSave);
if (!saveResult) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "注册失败,数据库错误");
}
return userToSave.getId();
}
}
@Override
public User userLogin(String username, String password, HttpServletRequest request) {
// 1. 参数校验
if (StringUtils.isAnyBlank(username, password)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户名或密码不能为空");
}
if (!Pattern.matches(USERNAME_PATTERN, username)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户名格式错误");
}
// 2. 查询用户
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
User user = this.getOne(queryWrapper);
// 3. 校验密码 和 用户存在性
if (user == null || !passwordEncoder.matches(password, user.getPassword())) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户名或密码错误");
}
// 4. 用户脱敏
User safetyUser = getSafetyUser(user);
// 5. 创建 Authentication 对象并设置到 SecurityContext
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
if (StringUtils.isNotBlank(user.getRole())) { // 确保角色不为空
authorities.add(new SimpleGrantedAuthority(user.getRole()));
}
Authentication authentication = new UsernamePasswordAuthenticationToken(safetyUser, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
return safetyUser;
}
@Override
public User getCurrentUser(HttpServletRequest request) {
// 优先从 SecurityContextHolder 获取认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated() && !(authentication.getPrincipal() instanceof String && authentication.getPrincipal().equals("anonymousUser"))) {
Object principal = authentication.getPrincipal();
if (principal instanceof User) {
return (User) principal; // principal 已经是 safetyUser
} else if (principal instanceof org.springframework.security.core.userdetails.User) {
// 如果 principal 是 Spring Security 的 User (不太可能在这里,因为我们设置的是 safetyUser)
// 需要转换或重新查询
// For now, assume it's our User object based on login logic
}
}
// 如果 SecurityContextHolder 中没有,尝试从 session (旧逻辑,作为后备或移除)
Object userObj = request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);
if (userObj instanceof User) {
// 最好在这里也验证一下数据库中的用户状态或者确保session中的信息足够可信
return (User) userObj;
}
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
}
@Override
public boolean userLogout(HttpServletRequest request) {
SecurityContextHolder.clearContext(); // 清除安全上下文
if (request.getSession(false) != null) { // 获取session但不创建新的
request.getSession(false).invalidate(); // 使session无效
}
return true;
}
@Override
public User getSafetyUser(User originUser) {
if (originUser == null) {
return null;
}
User safetyUser = new User();
BeanUtils.copyProperties(originUser, safetyUser, "password");
return safetyUser;
}
@Override
@Transactional
public boolean deductBalance(Long userId, BigDecimal amount) {
if (userId == null || userId <= 0 || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数错误");
}
boolean updateResult = this.update()
.setSql("balance = balance - " + amount.doubleValue())
.eq("id", userId)
.ge("balance", amount)
.update();
if (!updateResult) {
User user = this.getById(userId);
if (user == null || user.getIsDeleted() == 1) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "用户不存在");
} else if (user.getBalance().compareTo(amount) < 0) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "余额不足");
} else {
log.warn("Deduct balance failed due to concurrent update for userId: {}", userId);
throw new BusinessException(ErrorCode.OPERATION_ERROR, "扣款失败,请重试");
}
}
log.info("Deducted {} from balance for user {}", amount, userId);
return true;
}
@Override
@Transactional
public boolean addBalance(Long userId, BigDecimal amount) {
if (userId == null || userId <= 0 || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数错误");
}
boolean updateResult = this.update()
.setSql("balance = balance + " + amount.doubleValue())
.eq("id", userId)
.update();
if (!updateResult) {
User user = this.getById(userId);
if (user == null || user.getIsDeleted() == 1) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "用户不存在");
}
log.error("Add balance failed unexpectedly for userId: {}", userId);
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "充值失败,请稍后重试");
}
log.info("Added {} to balance for user {}", amount, userId);
return true;
}
@Override
public List<User> listUsers() {
List<User> userList = this.list(new QueryWrapper<User>().eq("isDeleted", 0));
return userList.stream().map(this::getSafetyUser).collect(Collectors.toList());
}
// 新增管理员删除用户实现
@Override
public boolean adminDeleteUser(Long userId) {
if (userId == null || userId <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户ID参数错误");
}
User user = this.getById(userId);
if (user == null || user.getIsDeleted() == 1) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "用户不存在或已被删除");
}
boolean result = this.removeById(userId);
if (!result) {
log.warn("Attempted to delete user that might already be deleted or does not exist, userId: {}", userId);
User checkUser = this.getById(userId);
if(checkUser == null || checkUser.getIsDeleted() == 1){
return true;
}
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "删除用户失败,数据库操作异常");
}
log.info("User with id: {} logically deleted by admin.", userId);
return true;
}
// 新增管理员添加用户实现
@Override
@Transactional // 确保操作的原子性
public long adminAddUser(com.yupi.project.model.dto.user.UserAdminAddRequest addRequest) {
String username = addRequest.getUsername();
String password = addRequest.getPassword();
String role = addRequest.getRole();
BigDecimal balance = addRequest.getBalance();
// 1. 参数校验
if (StringUtils.isAnyBlank(username, password, role)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户名、密码和角色不能为空");
}
if (!Pattern.matches(USERNAME_PATTERN, username)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户名不符合规范4-16位字母、数字、下划线");
}
if (password.length() < 6) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码长度不能小于6位");
}
// 校验角色是否合法 (例如:必须是 UserRoleEnum 中的值)
try {
com.yupi.project.model.enums.UserRoleEnum.valueOf(role.toUpperCase());
} catch (IllegalArgumentException e) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "无效的用户角色");
}
if (balance != null && balance.compareTo(BigDecimal.ZERO) < 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "初始余额不能为负数");
}
synchronized (username.intern()) {
// 2. 检查用户名是否已存在
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
if (this.count(queryWrapper) > 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户名已存在");
}
// 3. 密码加密
String encodedPassword = passwordEncoder.encode(password);
// 4. 创建用户
User newUser = new User();
newUser.setUsername(username);
newUser.setPassword(encodedPassword);
newUser.setRole(role); // 直接使用管理员指定的角色
newUser.setBalance(balance != null ? balance : BigDecimal.ZERO); // 如果未提供余额默认为0
boolean saveResult = this.save(newUser);
if (!saveResult) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "添加用户失败,数据库错误");
}
log.info("Admin added new user: {}, ID: {}, Role: {}", username, newUser.getId(), role);
return newUser.getId();
}
}
// 新增管理员更新用户实现
@Override
@Transactional
public boolean adminUpdateUser(com.yupi.project.model.dto.user.UserAdminUpdateRequest updateRequest) {
Long userId = updateRequest.getId();
if (userId == null || userId <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户ID不能为空");
}
User existingUser = this.getById(userId);
if (existingUser == null || existingUser.getIsDeleted() == 1) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "用户不存在或已被删除");
}
boolean needsUpdate = false;
User userToUpdate = new User();
userToUpdate.setId(userId);
// 更新用户名 (如果提供)
if (StringUtils.isNotBlank(updateRequest.getUsername())) {
String newUsername = updateRequest.getUsername();
if (!Pattern.matches(USERNAME_PATTERN, newUsername)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "新用户名不符合规范");
}
// 检查新用户名是否与现有其他用户冲突
if (!existingUser.getUsername().equals(newUsername)) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", newUsername).ne("id", userId);
if (this.count(queryWrapper) > 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "新用户名已被其他用户使用");
}
userToUpdate.setUsername(newUsername);
needsUpdate = true;
}
}
// 更新密码 (如果提供)
if (StringUtils.isNotBlank(updateRequest.getPassword())) {
String newPassword = updateRequest.getPassword();
if (newPassword.length() < 6) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "新密码长度不能小于6位");
}
userToUpdate.setPassword(passwordEncoder.encode(newPassword));
needsUpdate = true;
}
// 更新角色 (如果提供)
if (StringUtils.isNotBlank(updateRequest.getRole())) {
String newRole = updateRequest.getRole();
try {
com.yupi.project.model.enums.UserRoleEnum.valueOf(newRole.toUpperCase());
} catch (IllegalArgumentException e) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "无效的新用户角色");
}
userToUpdate.setRole(newRole);
needsUpdate = true;
}
// 更新余额 (如果提供)
if (updateRequest.getBalance() != null) {
BigDecimal newBalance = updateRequest.getBalance();
if (newBalance.compareTo(BigDecimal.ZERO) < 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "余额不能为负数");
}
userToUpdate.setBalance(newBalance);
needsUpdate = true;
}
if (!needsUpdate) {
log.info("No fields to update for user ID: {}", userId);
return true; // 没有字段需要更新,也视为成功
}
boolean updateResult = this.updateById(userToUpdate);
if (!updateResult) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "更新用户信息失败,数据库操作异常");
}
log.info("User ID: {} updated by admin. Fields updated: username={}, role={}, balance={}, password_changed={}",
userId,
userToUpdate.getUsername() != null,
userToUpdate.getRole() != null,
userToUpdate.getBalance() != null,
userToUpdate.getPassword() != null
);
return true;
}
}

View 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

View File

@@ -0,0 +1,62 @@
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

View File

@@ -0,0 +1 @@
我的项目 by 程序员鱼皮 https://github.com/liyupi

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yupi.project.mapper.UserMapper">
<resultMap id="BaseResultMap" type="com.yupi.project.model.entity.User">
<id property="id" column="id" jdbcType="BIGINT"/>
<result property="userName" column="userName" jdbcType="VARCHAR"/>
<result property="userAccount" column="userAccount" jdbcType="VARCHAR"/>
<result property="userAvatar" column="userAvatar" jdbcType="VARCHAR"/>
<result property="gender" column="gender" jdbcType="TINYINT"/>
<result property="userRole" column="userRole" jdbcType="VARCHAR"/>
<result property="userPassword" column="userPassword" jdbcType="VARCHAR"/>
<result property="createTime" column="createTime" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="updateTime" jdbcType="TIMESTAMP"/>
<result property="isDelete" column="isDelete" jdbcType="TINYINT"/>
</resultMap>
<sql id="Base_Column_List">
id,userName,userAccount,
userAvatar,gender,userRole,
userPassword,createTime,updateTime,
isDelete
</sql>
</mapper>