diff --git a/README.md b/README.md index 6e355f7..49fcb7a 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ Innovation Platform is a Spring Boot based demo for university innovation and en ## What is included - Authentication endpoints powered by Sa-Token +- User management APIs for admin and profile workflows - Project CRUD APIs with pagination - Project dashboard statistics endpoint - 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/register` - `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/stats` - `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. - 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`. +- Upstream user-management changes were reconciled into this branch with DTO-based responses so password hashes are not exposed from `/api/users`. diff --git a/backend/src/main/java/com/innovation/platform/controller/UserController.java b/backend/src/main/java/com/innovation/platform/controller/UserController.java new file mode 100644 index 0000000..ac0408c --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/controller/UserController.java @@ -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> page(UserQueryRequest request) { + IPage page = sysUserService.getUserList(request); + return Result.success(PageResult.of(page)); + } + + @Operation(summary = "Get user detail by id") + @GetMapping("/{id}") + public Result getById(@Parameter(description = "User id") @PathVariable Long id) { + return Result.success(sysUserService.getUserById(id)); + } + + @Operation(summary = "Create a user") + @PostMapping + public Result create(@Valid @RequestBody UserCreateRequest request) { + return Result.success("User created successfully.", sysUserService.createUser(request)); + } + + @Operation(summary = "Update a user") + @PutMapping("/{id}") + public Result 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 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> getTeacherList() { + return Result.success(sysUserService.getTeacherList()); + } + + @Operation(summary = "Change the current user's password") + @PostMapping("/change-password") + public Result 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 updateProfile(@Valid @RequestBody UserProfileUpdateRequest request) { + return Result.success("Profile updated successfully.", sysUserService.updateProfile(request)); + } +} diff --git a/backend/src/main/java/com/innovation/platform/dto/ChangePasswordRequest.java b/backend/src/main/java/com/innovation/platform/dto/ChangePasswordRequest.java new file mode 100644 index 0000000..491a398 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/dto/ChangePasswordRequest.java @@ -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; +} diff --git a/backend/src/main/java/com/innovation/platform/dto/UserCreateRequest.java b/backend/src/main/java/com/innovation/platform/dto/UserCreateRequest.java new file mode 100644 index 0000000..b7d7ae8 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/dto/UserCreateRequest.java @@ -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; +} diff --git a/backend/src/main/java/com/innovation/platform/dto/UserProfileUpdateRequest.java b/backend/src/main/java/com/innovation/platform/dto/UserProfileUpdateRequest.java new file mode 100644 index 0000000..30cb103 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/dto/UserProfileUpdateRequest.java @@ -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; +} diff --git a/backend/src/main/java/com/innovation/platform/dto/UserQueryRequest.java b/backend/src/main/java/com/innovation/platform/dto/UserQueryRequest.java new file mode 100644 index 0000000..27e66e0 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/dto/UserQueryRequest.java @@ -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); + } +} diff --git a/backend/src/main/java/com/innovation/platform/dto/UserResponse.java b/backend/src/main/java/com/innovation/platform/dto/UserResponse.java new file mode 100644 index 0000000..cc2e9f8 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/dto/UserResponse.java @@ -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"; + }; + } +} diff --git a/backend/src/main/java/com/innovation/platform/dto/UserUpdateRequest.java b/backend/src/main/java/com/innovation/platform/dto/UserUpdateRequest.java new file mode 100644 index 0000000..ec66ab3 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/dto/UserUpdateRequest.java @@ -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; +} diff --git a/backend/src/main/java/com/innovation/platform/service/SysUserService.java b/backend/src/main/java/com/innovation/platform/service/SysUserService.java index ddc1bcf..cf23bf1 100644 --- a/backend/src/main/java/com/innovation/platform/service/SysUserService.java +++ b/backend/src/main/java/com/innovation/platform/service/SysUserService.java @@ -1,37 +1,44 @@ 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.LoginResponse; 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 java.util.List; + public interface SysUserService { - - /** - * 用户登录 - */ + LoginResponse login(LoginRequest request); - - /** - * 用户注册 - */ + void register(RegisterRequest request); - - /** - * 用户登出 - */ + void logout(); - - /** - * 根据用户名查询用户 - */ + SysUser getByUsername(String username); - - /** - * 根据ID查询用户 - */ + SysUser getById(Long id); + + IPage getUserList(UserQueryRequest request); + + UserResponse getUserById(Long id); + + Long createUser(UserCreateRequest request); + + UserResponse updateUser(Long id, UserUpdateRequest request); + + void deleteUser(Long id); + + List getTeacherList(); + + void changePassword(ChangePasswordRequest request); + + UserResponse updateProfile(UserProfileUpdateRequest request); } diff --git a/backend/src/main/java/com/innovation/platform/service/impl/SysUserServiceImpl.java b/backend/src/main/java/com/innovation/platform/service/impl/SysUserServiceImpl.java index a031446..9905217 100644 --- a/backend/src/main/java/com/innovation/platform/service/impl/SysUserServiceImpl.java +++ b/backend/src/main/java/com/innovation/platform/service/impl/SysUserServiceImpl.java @@ -3,15 +3,26 @@ package com.innovation.platform.service.impl; import cn.dev33.satoken.stp.StpUtil; import cn.hutool.crypto.digest.BCrypt; 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.LoginResponse; 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.mapper.SysUserMapper; import com.innovation.platform.service.SysUserService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.List; @Service @RequiredArgsConstructor @@ -53,14 +64,16 @@ public class SysUserServiceImpl implements SysUserService { throw new RuntimeException("Username already exists."); } + ensureEmailAvailable(request.getEmail(), null); + SysUser user = new SysUser(); - user.setUsername(request.getUsername()); + user.setUsername(clean(request.getUsername())); user.setPassword(BCrypt.hashpw(request.getPassword())); - user.setRealName(request.getRealName()); - user.setPhone(request.getPhone()); - user.setEmail(request.getEmail()); + user.setRealName(clean(request.getRealName())); + user.setPhone(clean(request.getPhone())); + user.setEmail(clean(request.getEmail())); user.setGender(request.getGender()); - user.setRoleType(request.getRoleType() != null ? request.getRoleType() : 0); + user.setRoleType(request.getRoleType() != null ? request.getRoleType() : 1); user.setStatus(1); sysUserMapper.insert(user); @@ -83,4 +96,192 @@ public class SysUserServiceImpl implements SysUserService { public SysUser getById(Long id) { return sysUserMapper.selectById(id); } + + @Override + public IPage getUserList(UserQueryRequest request) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + + LambdaQueryWrapper 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 getTeacherList() { + return sysUserMapper.selectList( + new LambdaQueryWrapper() + .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() + .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(); + } } diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index da681fe..29384fc 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -3,9 +3,9 @@ server: spring: datasource: - url: jdbc:mysql://mysql:3306/innovation_platform?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai - username: innovation - password: innovation123 + url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/innovation_platform?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai} + username: ${SPRING_DATASOURCE_USERNAME:innovation} + password: ${SPRING_DATASOURCE_PASSWORD:innovation123} driver-class-name: com.mysql.cj.jdbc.Driver sql: init: diff --git a/backend/src/main/resources/data.sql b/backend/src/main/resources/data.sql index 7f33310..8380a3e 100644 --- a/backend/src/main/resources/data.sql +++ b/backend/src/main/resources/data.sql @@ -1,15 +1,17 @@ -- Seed users -INSERT INTO sys_user (id, username, password, real_name, gender, role_type, status) VALUES - (1, 'admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'System Admin', 1, 3, 1), - (2, 'teacher001', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Alice Chen', 2, 2, 1), - (3, 'teacher002', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Michael Zhao', 1, 2, 1), - (4, 'student001', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Olivia Lin', 2, 1, 1), - (5, 'student002', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Ethan Wu', 1, 1, 1), - (6, 'student003', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Sophia Xu', 2, 1, 1) +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, '13900000001', 'admin@innovation.local', 3, 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, '13900000003', 'michael.zhao@innovation.local', 2, 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, '13900000005', 'ethan.wu@innovation.local', 1, 1), + (6, 'student003', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Sophia Xu', 2, '13900000006', 'sophia.xu@innovation.local', 1, 1) ON DUPLICATE KEY UPDATE password = VALUES(password), real_name = VALUES(real_name), gender = VALUES(gender), + phone = VALUES(phone), + email = VALUES(email), role_type = VALUES(role_type), status = VALUES(status); diff --git a/backend/src/main/resources/db/migration/V2__add_user_unique_constraints.sql b/backend/src/main/resources/db/migration/V2__add_user_unique_constraints.sql new file mode 100644 index 0000000..4d6c20e --- /dev/null +++ b/backend/src/main/resources/db/migration/V2__add_user_unique_constraints.sql @@ -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); diff --git a/backend/src/main/resources/schema.sql b/backend/src/main/resources/schema.sql index 96a1f82..ca7db21 100644 --- a/backend/src/main/resources/schema.sql +++ b/backend/src/main/resources/schema.sql @@ -16,7 +16,11 @@ CREATE TABLE IF NOT EXISTS sys_user ( update_by BIGINT DEFAULT NULL, deleted TINYINT DEFAULT 0, 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; -- Student profile table