forked from admin/innovation-platform
feat: add user management workflows
This commit is contained in:
10
README.md
10
README.md
@@ -5,6 +5,7 @@ Innovation Platform is a Spring Boot based demo for university innovation and en
|
|||||||
## What is included
|
## What is included
|
||||||
|
|
||||||
- Authentication endpoints powered by Sa-Token
|
- Authentication endpoints powered by Sa-Token
|
||||||
|
- User management APIs for admin and profile workflows
|
||||||
- Project CRUD APIs with pagination
|
- Project CRUD APIs with pagination
|
||||||
- Project dashboard statistics endpoint
|
- Project dashboard statistics endpoint
|
||||||
- Static admin dashboard prototype at `backend/src/main/resources/static/index.html`
|
- Static admin dashboard prototype at `backend/src/main/resources/static/index.html`
|
||||||
@@ -21,6 +22,14 @@ Innovation Platform is a Spring Boot based demo for university innovation and en
|
|||||||
- `POST /api/auth/login`
|
- `POST /api/auth/login`
|
||||||
- `POST /api/auth/register`
|
- `POST /api/auth/register`
|
||||||
- `POST /api/auth/logout`
|
- `POST /api/auth/logout`
|
||||||
|
- `GET /api/users`
|
||||||
|
- `GET /api/users/{id}`
|
||||||
|
- `POST /api/users`
|
||||||
|
- `PUT /api/users/{id}`
|
||||||
|
- `DELETE /api/users/{id}`
|
||||||
|
- `GET /api/users/teachers`
|
||||||
|
- `POST /api/users/change-password`
|
||||||
|
- `PUT /api/users/profile`
|
||||||
- `GET /api/projects`
|
- `GET /api/projects`
|
||||||
- `GET /api/projects/stats`
|
- `GET /api/projects/stats`
|
||||||
- `GET /api/projects/{id}`
|
- `GET /api/projects/{id}`
|
||||||
@@ -43,3 +52,4 @@ When the backend is running, open `/index.html` to use the lightweight dashboard
|
|||||||
- This repository snapshot was recovered from an archive, so local build and runtime validation depend on the target machine having Java and Maven available.
|
- This repository snapshot was recovered from an archive, so local build and runtime validation depend on the target machine having Java and Maven available.
|
||||||
- The current environment used for editing did not include a working Maven installation, so changes were verified statically only.
|
- The current environment used for editing did not include a working Maven installation, so changes were verified statically only.
|
||||||
- The SQL bootstrap files were refreshed into a clean UTF-8 seed set and made idempotent with `ON DUPLICATE KEY UPDATE`.
|
- The SQL bootstrap files were refreshed into a clean UTF-8 seed set and made idempotent with `ON DUPLICATE KEY UPDATE`.
|
||||||
|
- Upstream user-management changes were reconciled into this branch with DTO-based responses so password hashes are not exposed from `/api/users`.
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package com.innovation.platform.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.innovation.platform.common.PageResult;
|
||||||
|
import com.innovation.platform.common.Result;
|
||||||
|
import com.innovation.platform.dto.ChangePasswordRequest;
|
||||||
|
import com.innovation.platform.dto.UserCreateRequest;
|
||||||
|
import com.innovation.platform.dto.UserProfileUpdateRequest;
|
||||||
|
import com.innovation.platform.dto.UserQueryRequest;
|
||||||
|
import com.innovation.platform.dto.UserResponse;
|
||||||
|
import com.innovation.platform.dto.UserUpdateRequest;
|
||||||
|
import com.innovation.platform.service.SysUserService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Tag(name = "User management", description = "User listing, maintenance, teacher lookup, and profile operations")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/users")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@SaCheckLogin
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
private final SysUserService sysUserService;
|
||||||
|
|
||||||
|
@Operation(summary = "List users with pagination and filters")
|
||||||
|
@GetMapping
|
||||||
|
public Result<PageResult<UserResponse>> page(UserQueryRequest request) {
|
||||||
|
IPage<UserResponse> page = sysUserService.getUserList(request);
|
||||||
|
return Result.success(PageResult.of(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Get user detail by id")
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public Result<UserResponse> getById(@Parameter(description = "User id") @PathVariable Long id) {
|
||||||
|
return Result.success(sysUserService.getUserById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Create a user")
|
||||||
|
@PostMapping
|
||||||
|
public Result<Long> create(@Valid @RequestBody UserCreateRequest request) {
|
||||||
|
return Result.success("User created successfully.", sysUserService.createUser(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Update a user")
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public Result<UserResponse> update(
|
||||||
|
@Parameter(description = "User id") @PathVariable Long id,
|
||||||
|
@Valid @RequestBody UserUpdateRequest request
|
||||||
|
) {
|
||||||
|
return Result.success("User updated successfully.", sysUserService.updateUser(id, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Delete a user")
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public Result<Void> delete(@Parameter(description = "User id") @PathVariable Long id) {
|
||||||
|
sysUserService.deleteUser(id);
|
||||||
|
return Result.success("User deleted successfully.", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "List active teachers")
|
||||||
|
@GetMapping("/teachers")
|
||||||
|
public Result<List<UserResponse>> getTeacherList() {
|
||||||
|
return Result.success(sysUserService.getTeacherList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Change the current user's password")
|
||||||
|
@PostMapping("/change-password")
|
||||||
|
public Result<Void> changePassword(@Valid @RequestBody ChangePasswordRequest request) {
|
||||||
|
sysUserService.changePassword(request);
|
||||||
|
return Result.success("Password changed successfully.", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Update the current user's profile")
|
||||||
|
@PutMapping("/profile")
|
||||||
|
public Result<UserResponse> updateProfile(@Valid @RequestBody UserProfileUpdateRequest request) {
|
||||||
|
return Result.success("Profile updated successfully.", sysUserService.updateProfile(request));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.innovation.platform.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ChangePasswordRequest {
|
||||||
|
@NotBlank(message = "Current password is required.")
|
||||||
|
private String oldPassword;
|
||||||
|
|
||||||
|
@NotBlank(message = "New password is required.")
|
||||||
|
@Size(min = 6, max = 100, message = "New password must be between 6 and 100 characters.")
|
||||||
|
private String newPassword;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.innovation.platform.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class UserCreateRequest {
|
||||||
|
@NotBlank(message = "Username is required.")
|
||||||
|
@Size(max = 50, message = "Username must stay within 50 characters.")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@Size(max = 100, message = "Password must stay within 100 characters.")
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
@NotBlank(message = "Real name is required.")
|
||||||
|
@Size(max = 50, message = "Real name must stay within 50 characters.")
|
||||||
|
private String realName;
|
||||||
|
|
||||||
|
private Integer gender;
|
||||||
|
|
||||||
|
@Pattern(regexp = "^$|^1[3-9]\\d{9}$", message = "Phone number format is invalid.")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@Pattern(
|
||||||
|
regexp = "^$|^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
|
||||||
|
message = "Email format is invalid."
|
||||||
|
)
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@Size(max = 255, message = "Avatar URL must stay within 255 characters.")
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
private Integer roleType;
|
||||||
|
private Integer status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.innovation.platform.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class UserProfileUpdateRequest {
|
||||||
|
@Size(max = 50, message = "Real name must stay within 50 characters.")
|
||||||
|
private String realName;
|
||||||
|
|
||||||
|
@Pattern(regexp = "^$|^1[3-9]\\d{9}$", message = "Phone number format is invalid.")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@Pattern(
|
||||||
|
regexp = "^$|^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
|
||||||
|
message = "Email format is invalid."
|
||||||
|
)
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@Size(max = 255, message = "Avatar URL must stay within 255 characters.")
|
||||||
|
private String avatar;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.innovation.platform.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class UserQueryRequest {
|
||||||
|
private String keyword;
|
||||||
|
private Integer roleType;
|
||||||
|
private Integer status;
|
||||||
|
private Integer current = 1;
|
||||||
|
private Integer size = 10;
|
||||||
|
|
||||||
|
public Integer getCurrent() {
|
||||||
|
return current == null || current < 1 ? 1 : current;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getSize() {
|
||||||
|
if (size == null || size < 1) {
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
return Math.min(size, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.innovation.platform.dto;
|
||||||
|
|
||||||
|
import com.innovation.platform.entity.SysUser;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class UserResponse {
|
||||||
|
private Long id;
|
||||||
|
private String username;
|
||||||
|
private String realName;
|
||||||
|
private Integer gender;
|
||||||
|
private String phone;
|
||||||
|
private String email;
|
||||||
|
private String avatar;
|
||||||
|
private Integer status;
|
||||||
|
private String statusLabel;
|
||||||
|
private Integer roleType;
|
||||||
|
private String roleTypeLabel;
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
public static UserResponse fromEntity(SysUser user) {
|
||||||
|
return UserResponse.builder()
|
||||||
|
.id(user.getId())
|
||||||
|
.username(user.getUsername())
|
||||||
|
.realName(user.getRealName())
|
||||||
|
.gender(user.getGender())
|
||||||
|
.phone(user.getPhone())
|
||||||
|
.email(user.getEmail())
|
||||||
|
.avatar(user.getAvatar())
|
||||||
|
.status(user.getStatus())
|
||||||
|
.statusLabel(statusLabel(user.getStatus()))
|
||||||
|
.roleType(user.getRoleType())
|
||||||
|
.roleTypeLabel(roleTypeLabel(user.getRoleType()))
|
||||||
|
.createTime(user.getCreateTime())
|
||||||
|
.updateTime(user.getUpdateTime())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String statusLabel(Integer value) {
|
||||||
|
if (value == null) {
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
return switch (value) {
|
||||||
|
case 0 -> "Disabled";
|
||||||
|
case 1 -> "Active";
|
||||||
|
default -> "Other";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String roleTypeLabel(Integer value) {
|
||||||
|
if (value == null) {
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
return switch (value) {
|
||||||
|
case 1 -> "Student";
|
||||||
|
case 2 -> "Teacher";
|
||||||
|
case 3 -> "Administrator";
|
||||||
|
default -> "Other";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.innovation.platform.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class UserUpdateRequest {
|
||||||
|
@Size(max = 50, message = "Real name must stay within 50 characters.")
|
||||||
|
private String realName;
|
||||||
|
|
||||||
|
private Integer gender;
|
||||||
|
|
||||||
|
@Pattern(regexp = "^$|^1[3-9]\\d{9}$", message = "Phone number format is invalid.")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@Pattern(
|
||||||
|
regexp = "^$|^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
|
||||||
|
message = "Email format is invalid."
|
||||||
|
)
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@Size(max = 255, message = "Avatar URL must stay within 255 characters.")
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
private Integer roleType;
|
||||||
|
private Integer status;
|
||||||
|
}
|
||||||
@@ -1,37 +1,44 @@
|
|||||||
package com.innovation.platform.service;
|
package com.innovation.platform.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.innovation.platform.dto.ChangePasswordRequest;
|
||||||
import com.innovation.platform.dto.LoginRequest;
|
import com.innovation.platform.dto.LoginRequest;
|
||||||
import com.innovation.platform.dto.LoginResponse;
|
import com.innovation.platform.dto.LoginResponse;
|
||||||
import com.innovation.platform.dto.RegisterRequest;
|
import com.innovation.platform.dto.RegisterRequest;
|
||||||
|
import com.innovation.platform.dto.UserCreateRequest;
|
||||||
|
import com.innovation.platform.dto.UserProfileUpdateRequest;
|
||||||
|
import com.innovation.platform.dto.UserQueryRequest;
|
||||||
|
import com.innovation.platform.dto.UserResponse;
|
||||||
|
import com.innovation.platform.dto.UserUpdateRequest;
|
||||||
import com.innovation.platform.entity.SysUser;
|
import com.innovation.platform.entity.SysUser;
|
||||||
|
|
||||||
/**
|
import java.util.List;
|
||||||
* 用户服务接口
|
|
||||||
*/
|
|
||||||
public interface SysUserService {
|
public interface SysUserService {
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户登录
|
|
||||||
*/
|
|
||||||
LoginResponse login(LoginRequest request);
|
LoginResponse login(LoginRequest request);
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户注册
|
|
||||||
*/
|
|
||||||
void register(RegisterRequest request);
|
void register(RegisterRequest request);
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户登出
|
|
||||||
*/
|
|
||||||
void logout();
|
void logout();
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据用户名查询用户
|
|
||||||
*/
|
|
||||||
SysUser getByUsername(String username);
|
SysUser getByUsername(String username);
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据ID查询用户
|
|
||||||
*/
|
|
||||||
SysUser getById(Long id);
|
SysUser getById(Long id);
|
||||||
|
|
||||||
|
IPage<UserResponse> getUserList(UserQueryRequest request);
|
||||||
|
|
||||||
|
UserResponse getUserById(Long id);
|
||||||
|
|
||||||
|
Long createUser(UserCreateRequest request);
|
||||||
|
|
||||||
|
UserResponse updateUser(Long id, UserUpdateRequest request);
|
||||||
|
|
||||||
|
void deleteUser(Long id);
|
||||||
|
|
||||||
|
List<UserResponse> getTeacherList();
|
||||||
|
|
||||||
|
void changePassword(ChangePasswordRequest request);
|
||||||
|
|
||||||
|
UserResponse updateProfile(UserProfileUpdateRequest request);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,26 @@ package com.innovation.platform.service.impl;
|
|||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import cn.hutool.crypto.digest.BCrypt;
|
import cn.hutool.crypto.digest.BCrypt;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.innovation.platform.dto.ChangePasswordRequest;
|
||||||
import com.innovation.platform.dto.LoginRequest;
|
import com.innovation.platform.dto.LoginRequest;
|
||||||
import com.innovation.platform.dto.LoginResponse;
|
import com.innovation.platform.dto.LoginResponse;
|
||||||
import com.innovation.platform.dto.RegisterRequest;
|
import com.innovation.platform.dto.RegisterRequest;
|
||||||
|
import com.innovation.platform.dto.UserCreateRequest;
|
||||||
|
import com.innovation.platform.dto.UserProfileUpdateRequest;
|
||||||
|
import com.innovation.platform.dto.UserQueryRequest;
|
||||||
|
import com.innovation.platform.dto.UserResponse;
|
||||||
|
import com.innovation.platform.dto.UserUpdateRequest;
|
||||||
import com.innovation.platform.entity.SysUser;
|
import com.innovation.platform.entity.SysUser;
|
||||||
import com.innovation.platform.mapper.SysUserMapper;
|
import com.innovation.platform.mapper.SysUserMapper;
|
||||||
import com.innovation.platform.service.SysUserService;
|
import com.innovation.platform.service.SysUserService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -53,14 +64,16 @@ public class SysUserServiceImpl implements SysUserService {
|
|||||||
throw new RuntimeException("Username already exists.");
|
throw new RuntimeException("Username already exists.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensureEmailAvailable(request.getEmail(), null);
|
||||||
|
|
||||||
SysUser user = new SysUser();
|
SysUser user = new SysUser();
|
||||||
user.setUsername(request.getUsername());
|
user.setUsername(clean(request.getUsername()));
|
||||||
user.setPassword(BCrypt.hashpw(request.getPassword()));
|
user.setPassword(BCrypt.hashpw(request.getPassword()));
|
||||||
user.setRealName(request.getRealName());
|
user.setRealName(clean(request.getRealName()));
|
||||||
user.setPhone(request.getPhone());
|
user.setPhone(clean(request.getPhone()));
|
||||||
user.setEmail(request.getEmail());
|
user.setEmail(clean(request.getEmail()));
|
||||||
user.setGender(request.getGender());
|
user.setGender(request.getGender());
|
||||||
user.setRoleType(request.getRoleType() != null ? request.getRoleType() : 0);
|
user.setRoleType(request.getRoleType() != null ? request.getRoleType() : 1);
|
||||||
user.setStatus(1);
|
user.setStatus(1);
|
||||||
|
|
||||||
sysUserMapper.insert(user);
|
sysUserMapper.insert(user);
|
||||||
@@ -83,4 +96,192 @@ public class SysUserServiceImpl implements SysUserService {
|
|||||||
public SysUser getById(Long id) {
|
public SysUser getById(Long id) {
|
||||||
return sysUserMapper.selectById(id);
|
return sysUserMapper.selectById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IPage<UserResponse> getUserList(UserQueryRequest request) {
|
||||||
|
Page<SysUser> page = new Page<>(request.getCurrent(), request.getSize());
|
||||||
|
|
||||||
|
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
String keyword = clean(request.getKeyword());
|
||||||
|
wrapper.and(StringUtils.hasText(keyword), nested -> nested
|
||||||
|
.like(SysUser::getUsername, keyword)
|
||||||
|
.or()
|
||||||
|
.like(SysUser::getRealName, keyword)
|
||||||
|
.or()
|
||||||
|
.like(SysUser::getPhone, keyword)
|
||||||
|
.or()
|
||||||
|
.like(SysUser::getEmail, keyword))
|
||||||
|
.eq(request.getRoleType() != null, SysUser::getRoleType, request.getRoleType())
|
||||||
|
.eq(request.getStatus() != null, SysUser::getStatus, request.getStatus())
|
||||||
|
.orderByDesc(SysUser::getCreateTime);
|
||||||
|
|
||||||
|
return sysUserMapper.selectPage(page, wrapper).convert(UserResponse::fromEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserResponse getUserById(Long id) {
|
||||||
|
return UserResponse.fromEntity(requireUser(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Long createUser(UserCreateRequest request) {
|
||||||
|
if (getByUsername(request.getUsername()) != null) {
|
||||||
|
throw new RuntimeException("Username already exists.");
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureEmailAvailable(request.getEmail(), null);
|
||||||
|
|
||||||
|
SysUser user = new SysUser();
|
||||||
|
user.setUsername(clean(request.getUsername()));
|
||||||
|
user.setPassword(BCrypt.hashpw(resolvePassword(request.getPassword())));
|
||||||
|
user.setRealName(clean(request.getRealName()));
|
||||||
|
user.setGender(request.getGender());
|
||||||
|
user.setPhone(clean(request.getPhone()));
|
||||||
|
user.setEmail(clean(request.getEmail()));
|
||||||
|
user.setAvatar(clean(request.getAvatar()));
|
||||||
|
user.setRoleType(request.getRoleType() != null ? request.getRoleType() : 1);
|
||||||
|
user.setStatus(request.getStatus() != null ? request.getStatus() : 1);
|
||||||
|
applyAuditFields(user, true);
|
||||||
|
|
||||||
|
sysUserMapper.insert(user);
|
||||||
|
return user.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public UserResponse updateUser(Long id, UserUpdateRequest request) {
|
||||||
|
SysUser user = requireUser(id);
|
||||||
|
ensureEmailAvailable(request.getEmail(), id);
|
||||||
|
|
||||||
|
user.setRealName(clean(request.getRealName()));
|
||||||
|
user.setGender(request.getGender());
|
||||||
|
user.setPhone(clean(request.getPhone()));
|
||||||
|
user.setEmail(clean(request.getEmail()));
|
||||||
|
user.setAvatar(clean(request.getAvatar()));
|
||||||
|
user.setRoleType(request.getRoleType() != null ? request.getRoleType() : user.getRoleType());
|
||||||
|
user.setStatus(request.getStatus() != null ? request.getStatus() : user.getStatus());
|
||||||
|
applyAuditFields(user, false);
|
||||||
|
|
||||||
|
sysUserMapper.updateById(user);
|
||||||
|
return UserResponse.fromEntity(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void deleteUser(Long id) {
|
||||||
|
SysUser user = requireUser(id);
|
||||||
|
Long currentUserId = currentUserId();
|
||||||
|
if (currentUserId != null && currentUserId.equals(id)) {
|
||||||
|
throw new RuntimeException("You cannot delete the current account.");
|
||||||
|
}
|
||||||
|
sysUserMapper.deleteById(user.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<UserResponse> getTeacherList() {
|
||||||
|
return sysUserMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<SysUser>()
|
||||||
|
.eq(SysUser::getRoleType, 2)
|
||||||
|
.eq(SysUser::getStatus, 1)
|
||||||
|
.orderByAsc(SysUser::getRealName)
|
||||||
|
).stream().map(UserResponse::fromEntity).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void changePassword(ChangePasswordRequest request) {
|
||||||
|
Long userId = currentUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new RuntimeException("Current login session is invalid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
SysUser user = requireUser(userId);
|
||||||
|
if (!BCrypt.checkpw(request.getOldPassword(), user.getPassword())) {
|
||||||
|
throw new RuntimeException("Current password is incorrect.");
|
||||||
|
}
|
||||||
|
if (request.getOldPassword().equals(request.getNewPassword())) {
|
||||||
|
throw new RuntimeException("New password must be different from the current password.");
|
||||||
|
}
|
||||||
|
|
||||||
|
user.setPassword(BCrypt.hashpw(request.getNewPassword()));
|
||||||
|
applyAuditFields(user, false);
|
||||||
|
sysUserMapper.updateById(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public UserResponse updateProfile(UserProfileUpdateRequest request) {
|
||||||
|
Long userId = currentUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new RuntimeException("Current login session is invalid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
SysUser user = requireUser(userId);
|
||||||
|
ensureEmailAvailable(request.getEmail(), userId);
|
||||||
|
|
||||||
|
user.setRealName(clean(request.getRealName()));
|
||||||
|
user.setPhone(clean(request.getPhone()));
|
||||||
|
user.setEmail(clean(request.getEmail()));
|
||||||
|
user.setAvatar(clean(request.getAvatar()));
|
||||||
|
applyAuditFields(user, false);
|
||||||
|
|
||||||
|
sysUserMapper.updateById(user);
|
||||||
|
return UserResponse.fromEntity(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysUser requireUser(Long id) {
|
||||||
|
SysUser user = sysUserMapper.selectById(id);
|
||||||
|
if (user == null) {
|
||||||
|
throw new RuntimeException("User does not exist.");
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureEmailAvailable(String email, Long currentUserId) {
|
||||||
|
String cleanEmail = clean(email);
|
||||||
|
if (!StringUtils.hasText(cleanEmail)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SysUser existing = getByEmail(cleanEmail);
|
||||||
|
if (existing != null && (currentUserId == null || !existing.getId().equals(currentUserId))) {
|
||||||
|
throw new RuntimeException("Email is already in use.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysUser getByEmail(String email) {
|
||||||
|
String cleanEmail = clean(email);
|
||||||
|
if (!StringUtils.hasText(cleanEmail)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return sysUserMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<SysUser>()
|
||||||
|
.eq(SysUser::getEmail, cleanEmail)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyAuditFields(SysUser user, boolean create) {
|
||||||
|
Long userId = currentUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (create) {
|
||||||
|
user.setCreateBy(userId);
|
||||||
|
}
|
||||||
|
user.setUpdateBy(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long currentUserId() {
|
||||||
|
return StpUtil.isLogin() ? StpUtil.getLoginIdAsLong() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolvePassword(String password) {
|
||||||
|
String cleanPassword = clean(password);
|
||||||
|
return StringUtils.hasText(cleanPassword) ? cleanPassword : "123456";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String clean(String value) {
|
||||||
|
return value == null ? null : value.trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ server:
|
|||||||
|
|
||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:mysql://mysql:3306/innovation_platform?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
|
url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/innovation_platform?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai}
|
||||||
username: innovation
|
username: ${SPRING_DATASOURCE_USERNAME:innovation}
|
||||||
password: innovation123
|
password: ${SPRING_DATASOURCE_PASSWORD:innovation123}
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
sql:
|
sql:
|
||||||
init:
|
init:
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
-- Seed users
|
-- Seed users
|
||||||
INSERT INTO sys_user (id, username, password, real_name, gender, role_type, status) VALUES
|
INSERT INTO sys_user (id, username, password, real_name, gender, phone, email, role_type, status) VALUES
|
||||||
(1, 'admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'System Admin', 1, 3, 1),
|
(1, 'admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'System Admin', 1, '13900000001', 'admin@innovation.local', 3, 1),
|
||||||
(2, 'teacher001', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Alice Chen', 2, 2, 1),
|
(2, 'teacher001', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Alice Chen', 2, '13900000002', 'alice.chen@innovation.local', 2, 1),
|
||||||
(3, 'teacher002', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Michael Zhao', 1, 2, 1),
|
(3, 'teacher002', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Michael Zhao', 1, '13900000003', 'michael.zhao@innovation.local', 2, 1),
|
||||||
(4, 'student001', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Olivia Lin', 2, 1, 1),
|
(4, 'student001', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Olivia Lin', 2, '13900000004', 'olivia.lin@innovation.local', 1, 1),
|
||||||
(5, 'student002', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Ethan Wu', 1, 1, 1),
|
(5, 'student002', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Ethan Wu', 1, '13900000005', 'ethan.wu@innovation.local', 1, 1),
|
||||||
(6, 'student003', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Sophia Xu', 2, 1, 1)
|
(6, 'student003', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Sophia Xu', 2, '13900000006', 'sophia.xu@innovation.local', 1, 1)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
password = VALUES(password),
|
password = VALUES(password),
|
||||||
real_name = VALUES(real_name),
|
real_name = VALUES(real_name),
|
||||||
gender = VALUES(gender),
|
gender = VALUES(gender),
|
||||||
|
phone = VALUES(phone),
|
||||||
|
email = VALUES(email),
|
||||||
role_type = VALUES(role_type),
|
role_type = VALUES(role_type),
|
||||||
status = VALUES(status);
|
status = VALUES(status);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- Add user uniqueness and lookup indexes.
|
||||||
|
ALTER TABLE sys_user
|
||||||
|
ADD CONSTRAINT uk_email UNIQUE (email);
|
||||||
|
|
||||||
|
CREATE INDEX idx_phone ON sys_user(phone);
|
||||||
|
CREATE INDEX idx_role_type ON sys_user(role_type);
|
||||||
|
CREATE INDEX idx_status ON sys_user(status);
|
||||||
@@ -16,7 +16,11 @@ CREATE TABLE IF NOT EXISTS sys_user (
|
|||||||
update_by BIGINT DEFAULT NULL,
|
update_by BIGINT DEFAULT NULL,
|
||||||
deleted TINYINT DEFAULT 0,
|
deleted TINYINT DEFAULT 0,
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
UNIQUE KEY uk_username (username)
|
UNIQUE KEY uk_username (username),
|
||||||
|
UNIQUE KEY uk_email (email),
|
||||||
|
KEY idx_phone (phone),
|
||||||
|
KEY idx_role_type (role_type),
|
||||||
|
KEY idx_status (status)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- Student profile table
|
-- Student profile table
|
||||||
|
|||||||
Reference in New Issue
Block a user