feat(erp): 添加ERP用户管理系统

- 在ResultCode中新增用户相关错误码(用户不存在、用户已存在、密码错误、Token无效等)
- 创建ERP用户实体类ErpUser,包含账号、密码、店铺号等字段
- 实现用户注册功能,支持账号、手机号、邮箱唯一性校验
- 实现用户登录功能,支持密码验证和Token生成
- 添加Token验证机制,支持Bearer和自定义Header方式
- 创建用户管理API文档,包含注册、登录接口说明和错误码说明
- 实现IP地址获取功能,记录用户最后登录IP
- 添加MD5密码加密和Token生成解析工具类
- 实现用户状态管理(激活/禁用)功能
This commit is contained in:
2025-12-25 10:03:36 +08:00
parent b321750d63
commit f7fbcc4138
12 changed files with 900 additions and 1 deletions

View File

@@ -88,7 +88,27 @@ public enum ResultCode {
/**
* 业务错误
*/
BUSINESS_ERROR("6000", "业务错误");
BUSINESS_ERROR("6000", "业务错误"),
/**
* 用户不存在
*/
USER_NOT_FOUND("7001", "用户不存在"),
/**
* 用户已存在
*/
USER_EXISTS("7002", "用户已存在"),
/**
* 密码错误
*/
PASSWORD_ERROR("7003", "密码错误"),
/**
* Token无效或已过期
*/
TOKEN_INVALID("7004", "Token无效或已过期");
/**
* 响应码

View File

@@ -0,0 +1,80 @@
package com.mtkj.mtpay.controller;
import com.mtkj.mtpay.common.Result;
import com.mtkj.mtpay.dto.request.ErpUserLoginRequestDTO;
import com.mtkj.mtpay.dto.request.ErpUserRegisterRequestDTO;
import com.mtkj.mtpay.dto.response.ErpUserLoginResponseDTO;
import com.mtkj.mtpay.dto.response.ErpUserRegisterResponseDTO;
import com.mtkj.mtpay.service.ErpUserService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* ERP用户控制器
*/
@Slf4j
@RestController
@RequestMapping("/api/erp/user")
@RequiredArgsConstructor
public class ErpUserController {
private final ErpUserService erpUserService;
/**
* 用户注册
*/
@PostMapping("/register")
public Result<ErpUserRegisterResponseDTO> register(@Valid @RequestBody ErpUserRegisterRequestDTO request) {
log.info("用户注册请求,账号: {}", request.getUsername());
ErpUserRegisterResponseDTO response = erpUserService.register(request);
return Result.success("注册成功", response);
}
/**
* 用户登录
*/
@PostMapping("/login")
public Result<ErpUserLoginResponseDTO> login(@Valid @RequestBody ErpUserLoginRequestDTO request,
HttpServletRequest httpRequest) {
log.info("用户登录请求,账号: {}", request.getUsername());
// 获取客户端IP
String loginIp = getClientIp(httpRequest);
ErpUserLoginResponseDTO response = erpUserService.login(request, loginIp);
return Result.success("登录成功", response);
}
/**
* 获取客户端IP地址
*/
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 处理多个IP的情况取第一个IP
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}

View File

@@ -0,0 +1,24 @@
package com.mtkj.mtpay.dto.request;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* ERP用户登录请求DTO
*/
@Data
public class ErpUserLoginRequestDTO {
/**
* 账号
*/
@NotBlank(message = "账号不能为空")
private String username;
/**
* 密码
*/
@NotBlank(message = "密码不能为空")
private String password;
}

View File

@@ -0,0 +1,56 @@
package com.mtkj.mtpay.dto.request;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* ERP用户注册请求DTO
*/
@Data
public class ErpUserRegisterRequestDTO {
/**
* 账号
*/
@NotBlank(message = "账号不能为空")
@Size(min = 3, max = 50, message = "账号长度必须在3-50个字符之间")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "账号只能包含字母、数字和下划线")
private String username;
/**
* 密码
*/
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间")
private String password;
/**
* 店铺号
*/
@NotBlank(message = "店铺号不能为空")
@Size(max = 50, message = "店铺号长度不能超过50个字符")
private String storeCode;
/**
* 用户名称
*/
@Size(max = 50, message = "用户名称长度不能超过50个字符")
private String nickName;
/**
* 手机号
*/
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
/**
* 邮箱
*/
@Email(message = "邮箱格式不正确")
@Size(max = 100, message = "邮箱长度不能超过100个字符")
private String email;
}

View File

@@ -0,0 +1,74 @@
package com.mtkj.mtpay.dto.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* ERP用户登录响应DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ErpUserLoginResponseDTO {
/**
* 用户ID
*/
private Long id;
/**
* 账号
*/
private String username;
/**
* 用户名称
*/
private String nickName;
/**
* 手机号
*/
private String phone;
/**
* 邮箱
*/
private String email;
/**
* 店铺号
*/
private String storeCode;
/**
* 状态
*/
private String status;
/**
* Token用于后续接口认证
*/
private String token;
/**
* Token过期时间毫秒时间戳
*/
private Long tokenExpireTime;
/**
* 最后登录时间
*/
private LocalDateTime lastLoginTime;
/**
* 最后登录IP
*/
private String lastLoginIp;
}

View File

@@ -0,0 +1,59 @@
package com.mtkj.mtpay.dto.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* ERP用户注册响应DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ErpUserRegisterResponseDTO {
/**
* 用户ID
*/
private Long id;
/**
* 账号
*/
private String username;
/**
* 用户名称
*/
private String nickName;
/**
* 手机号
*/
private String phone;
/**
* 邮箱
*/
private String email;
/**
* 店铺号
*/
private String storeCode;
/**
* 状态
*/
private String status;
/**
* 创建时间
*/
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,87 @@
package com.mtkj.mtpay.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* ERP用户实体类
*/
@TableName(value = "erp_user")
@Data
public class ErpUser {
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 账号(唯一)
*/
@TableField(value = "username", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String username;
/**
* 密码MD5加密
*/
@TableField(value = "password", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String password;
/**
* 用户名称
*/
@TableField(value = "nick_name", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String nickName;
/**
* 手机号
*/
@TableField(value = "phone", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String phone;
/**
* 邮箱
*/
@TableField(value = "email", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String email;
/**
* 店铺号
*/
@TableField(value = "store_code", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String storeCode;
/**
* 状态ACTIVE-激活DISABLED-禁用
*/
@TableField(value = "status", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String status;
/**
* 最后登录时间
*/
@TableField(value = "last_login_time", jdbcType = org.apache.ibatis.type.JdbcType.TIMESTAMP)
private LocalDateTime lastLoginTime;
/**
* 最后登录IP
*/
@TableField(value = "last_login_ip", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String lastLoginIp;
/**
* 创建时间
*/
@TableField(value = "create_time", fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,13 @@
package com.mtkj.mtpay.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mtkj.mtpay.entity.ErpUser;
import org.apache.ibatis.annotations.Mapper;
/**
* ERP用户Mapper接口
*/
@Mapper
public interface ErpUserMapper extends BaseMapper<ErpUser> {
}

View File

@@ -0,0 +1,42 @@
package com.mtkj.mtpay.service;
import com.mtkj.mtpay.dto.request.ErpUserLoginRequestDTO;
import com.mtkj.mtpay.dto.request.ErpUserRegisterRequestDTO;
import com.mtkj.mtpay.dto.response.ErpUserLoginResponseDTO;
import com.mtkj.mtpay.dto.response.ErpUserRegisterResponseDTO;
/**
* ERP用户服务接口
*/
public interface ErpUserService {
/**
* 用户注册
* @param request 注册请求
* @return 注册响应
*/
ErpUserRegisterResponseDTO register(ErpUserRegisterRequestDTO request);
/**
* 用户登录
* @param request 登录请求
* @param loginIp 登录IP
* @return 登录响应
*/
ErpUserLoginResponseDTO login(ErpUserLoginRequestDTO request, String loginIp);
/**
* 根据用户名查询用户
* @param username 用户名
* @return 用户实体
*/
com.mtkj.mtpay.entity.ErpUser getUserByUsername(String username);
/**
* 验证Token
* @param token Token
* @return 用户实体如果Token无效则返回null
*/
com.mtkj.mtpay.entity.ErpUser validateToken(String token);
}

View File

@@ -0,0 +1,190 @@
package com.mtkj.mtpay.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mtkj.mtpay.common.ResultCode;
import com.mtkj.mtpay.dto.request.ErpUserLoginRequestDTO;
import com.mtkj.mtpay.dto.request.ErpUserRegisterRequestDTO;
import com.mtkj.mtpay.dto.response.ErpUserLoginResponseDTO;
import com.mtkj.mtpay.dto.response.ErpUserRegisterResponseDTO;
import com.mtkj.mtpay.entity.ErpUser;
import com.mtkj.mtpay.exception.BusinessException;
import com.mtkj.mtpay.mapper.ErpUserMapper;
import com.mtkj.mtpay.service.ErpUserService;
import com.mtkj.mtpay.util.MD5;
import com.mtkj.mtpay.util.TokenUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Map;
/**
* ERP用户服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ErpUserServiceImpl implements ErpUserService {
private final ErpUserMapper erpUserMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public ErpUserRegisterResponseDTO register(ErpUserRegisterRequestDTO request) {
log.info("用户注册请求,账号: {}, 店铺号: {}", request.getUsername(), request.getStoreCode());
// 检查用户名是否已存在
ErpUser existingUser = erpUserMapper.selectOne(
new LambdaQueryWrapper<ErpUser>()
.eq(ErpUser::getUsername, request.getUsername())
);
if (existingUser != null) {
throw new BusinessException(ResultCode.BUSINESS_ERROR.getCode(), "账号已存在");
}
// 检查手机号是否已存在(如果提供了手机号)
if (request.getPhone() != null && !request.getPhone().trim().isEmpty()) {
ErpUser existingPhone = erpUserMapper.selectOne(
new LambdaQueryWrapper<ErpUser>()
.eq(ErpUser::getPhone, request.getPhone())
);
if (existingPhone != null) {
throw new BusinessException(ResultCode.BUSINESS_ERROR.getCode(), "手机号已被注册");
}
}
// 检查邮箱是否已存在(如果提供了邮箱)
if (request.getEmail() != null && !request.getEmail().trim().isEmpty()) {
ErpUser existingEmail = erpUserMapper.selectOne(
new LambdaQueryWrapper<ErpUser>()
.eq(ErpUser::getEmail, request.getEmail())
);
if (existingEmail != null) {
throw new BusinessException(ResultCode.BUSINESS_ERROR.getCode(), "邮箱已被注册");
}
}
// 创建新用户
ErpUser user = new ErpUser();
user.setUsername(request.getUsername());
// 密码使用MD5加密
user.setPassword(MD5.md5(request.getPassword()));
user.setNickName(request.getNickName());
user.setPhone(request.getPhone());
user.setEmail(request.getEmail());
user.setStoreCode(request.getStoreCode());
user.setStatus("ACTIVE");
// 保存用户
erpUserMapper.insert(user);
log.info("用户注册成功用户ID: {}, 账号: {}", user.getId(), user.getUsername());
// 构建响应
ErpUserRegisterResponseDTO response = ErpUserRegisterResponseDTO.builder()
.id(user.getId())
.username(user.getUsername())
.nickName(user.getNickName())
.phone(user.getPhone())
.email(user.getEmail())
.storeCode(user.getStoreCode())
.status(user.getStatus())
.createTime(user.getCreateTime())
.build();
return response;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ErpUserLoginResponseDTO login(ErpUserLoginRequestDTO request, String loginIp) {
log.info("用户登录请求,账号: {}", request.getUsername());
// 查询用户
ErpUser user = erpUserMapper.selectOne(
new LambdaQueryWrapper<ErpUser>()
.eq(ErpUser::getUsername, request.getUsername())
);
if (user == null) {
throw new BusinessException(ResultCode.UNAUTHORIZED.getCode(), "账号或密码错误");
}
// 检查用户状态
if (!"ACTIVE".equals(user.getStatus())) {
throw new BusinessException(ResultCode.FORBIDDEN.getCode(), "账号已被禁用");
}
// 验证密码
String encryptedPassword = MD5.md5(request.getPassword());
if (!encryptedPassword.equals(user.getPassword())) {
throw new BusinessException(ResultCode.UNAUTHORIZED.getCode(), "账号或密码错误");
}
// 更新最后登录时间和IP
user.setLastLoginTime(LocalDateTime.now());
user.setLastLoginIp(loginIp);
erpUserMapper.updateById(user);
// 生成Token
String token = TokenUtils.generateToken(user.getId(), user.getUsername());
Long tokenExpireTime = TokenUtils.getTokenExpireTime(token);
log.info("用户登录成功用户ID: {}, 账号: {}", user.getId(), user.getUsername());
// 构建响应
ErpUserLoginResponseDTO response = ErpUserLoginResponseDTO.builder()
.id(user.getId())
.username(user.getUsername())
.nickName(user.getNickName())
.phone(user.getPhone())
.email(user.getEmail())
.storeCode(user.getStoreCode())
.status(user.getStatus())
.token(token)
.tokenExpireTime(tokenExpireTime)
.lastLoginTime(user.getLastLoginTime())
.lastLoginIp(user.getLastLoginIp())
.build();
return response;
}
@Override
public ErpUser getUserByUsername(String username) {
return erpUserMapper.selectOne(
new LambdaQueryWrapper<ErpUser>()
.eq(ErpUser::getUsername, username)
);
}
@Override
public ErpUser validateToken(String token) {
if (token == null || token.trim().isEmpty()) {
return null;
}
// 解析Token
Map<String, Object> userInfo = TokenUtils.parseToken(token);
if (userInfo == null) {
return null;
}
// 查询用户
Long userId = (Long) userInfo.get("userId");
ErpUser user = erpUserMapper.selectById(userId);
if (user == null || !"ACTIVE".equals(user.getStatus())) {
return null;
}
return user;
}
}