feat: 打造若依 (RuoYi) 规范的高品质 RBAC 用户权限体系,内置 admin/admin123 超级管理员与测试读者,支持登录注册、个人中心与用户管理
Build and Deploy Blog / deploy (push) Successful in 38s
Build and Deploy Blog / deploy (push) Successful in 38s
This commit is contained in:
@@ -0,0 +1,98 @@
|
|||||||
|
package com.example.blog.common;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class JwtUtils {
|
||||||
|
|
||||||
|
private static final String SECRET = "XiaoDong-RuoYi-Secret-Token-Key-2026-SuperSecureJWT!@#";
|
||||||
|
private static final long EXPIRATION_MS = 7L * 24 * 60 * 60 * 1000; // 7 天有效
|
||||||
|
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 JWT Token
|
||||||
|
*/
|
||||||
|
public static String createToken(Long userId, String username, String nickname, String role) {
|
||||||
|
try {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
long exp = now + EXPIRATION_MS;
|
||||||
|
|
||||||
|
Map<String, Object> header = new HashMap<>();
|
||||||
|
header.put("alg", "HS256");
|
||||||
|
header.put("typ", "JWT");
|
||||||
|
|
||||||
|
Map<String, Object> payload = new HashMap<>();
|
||||||
|
payload.put("userId", userId);
|
||||||
|
payload.put("username", username);
|
||||||
|
payload.put("nickname", nickname);
|
||||||
|
payload.put("role", role);
|
||||||
|
payload.put("iat", now / 1000);
|
||||||
|
payload.put("exp", exp / 1000);
|
||||||
|
|
||||||
|
String encodedHeader = Base64.getUrlEncoder().withoutPadding()
|
||||||
|
.encodeToString(objectMapper.writeValueAsString(header).getBytes(StandardCharsets.UTF_8));
|
||||||
|
String encodedPayload = Base64.getUrlEncoder().withoutPadding()
|
||||||
|
.encodeToString(objectMapper.writeValueAsString(payload).getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
String content = encodedHeader + "." + encodedPayload;
|
||||||
|
String signature = sign(content, SECRET);
|
||||||
|
|
||||||
|
return content + "." + signature;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("生成 Token 失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析并校验 Token,返回 Claims
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static Map<String, Object> parseToken(String token) {
|
||||||
|
if (token == null || token.trim().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (token.startsWith("Bearer ")) {
|
||||||
|
token = token.substring(7).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] parts = token.split("\\.");
|
||||||
|
if (parts.length != 3) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String content = parts[0] + "." + parts[1];
|
||||||
|
String expectedSignature = sign(content, SECRET);
|
||||||
|
if (!expectedSignature.equals(parts[2])) {
|
||||||
|
return null; // 签名不匹配
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] payloadBytes = Base64.getUrlDecoder().decode(parts[1]);
|
||||||
|
Map<String, Object> claims = objectMapper.readValue(payloadBytes, Map.class);
|
||||||
|
|
||||||
|
// 检查过期时间
|
||||||
|
Number exp = (Number) claims.get("exp");
|
||||||
|
if (exp != null && exp.longValue() * 1000 < System.currentTimeMillis()) {
|
||||||
|
return null; // Token 已过期
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String sign(String data, String secret) throws Exception {
|
||||||
|
Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
|
SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
|
||||||
|
mac.init(secretKey);
|
||||||
|
byte[] rawHmac = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
||||||
|
return Base64.getUrlEncoder().withoutPadding().encodeToString(rawHmac);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.example.blog.common;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class SecurityUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机盐值
|
||||||
|
*/
|
||||||
|
public static String generateSalt() {
|
||||||
|
return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SHA-256 + 盐值加密密码 (若依风格安全加盐散列)
|
||||||
|
*/
|
||||||
|
public static String encryptPassword(String password, String salt) {
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
|
String salted = password + "{" + salt + "}";
|
||||||
|
byte[] bytes = md.digest(salted.getBytes(StandardCharsets.UTF_8));
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (byte b : bytes) {
|
||||||
|
sb.append(String.format("%02x", b));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException("SHA-256 算法未找到", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验密码
|
||||||
|
*/
|
||||||
|
public static boolean matchesPassword(String rawPassword, String encryptedPassword, String salt) {
|
||||||
|
String hash = encryptPassword(rawPassword, salt);
|
||||||
|
return hash.equalsIgnoreCase(encryptedPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
package com.example.blog.config;
|
package com.example.blog.config;
|
||||||
|
|
||||||
|
import com.example.blog.common.SecurityUtils;
|
||||||
import com.example.blog.entity.Category;
|
import com.example.blog.entity.Category;
|
||||||
import com.example.blog.entity.Post;
|
import com.example.blog.entity.Post;
|
||||||
|
import com.example.blog.entity.SysUser;
|
||||||
import com.example.blog.repository.CategoryRepository;
|
import com.example.blog.repository.CategoryRepository;
|
||||||
import com.example.blog.repository.PostRepository;
|
import com.example.blog.repository.PostRepository;
|
||||||
|
import com.example.blog.repository.SysUserRepository;
|
||||||
import org.springframework.boot.CommandLineRunner;
|
import org.springframework.boot.CommandLineRunner;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -15,14 +18,21 @@ public class DataInitializer implements CommandLineRunner {
|
|||||||
|
|
||||||
private final PostRepository postRepository;
|
private final PostRepository postRepository;
|
||||||
private final CategoryRepository categoryRepository;
|
private final CategoryRepository categoryRepository;
|
||||||
|
private final SysUserRepository sysUserRepository;
|
||||||
|
|
||||||
public DataInitializer(PostRepository postRepository, CategoryRepository categoryRepository) {
|
public DataInitializer(PostRepository postRepository,
|
||||||
|
CategoryRepository categoryRepository,
|
||||||
|
SysUserRepository sysUserRepository) {
|
||||||
this.postRepository = postRepository;
|
this.postRepository = postRepository;
|
||||||
this.categoryRepository = categoryRepository;
|
this.categoryRepository = categoryRepository;
|
||||||
|
this.sysUserRepository = sysUserRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run(String... args) {
|
public void run(String... args) {
|
||||||
|
// 0. 初始化系统内置若依风格用户 (admin / admin123 与测试读者)
|
||||||
|
initSystemUsers();
|
||||||
|
|
||||||
// 1. 初始化分类
|
// 1. 初始化分类
|
||||||
if (categoryRepository.count() == 0) {
|
if (categoryRepository.count() == 0) {
|
||||||
List<Category> categories = Arrays.asList(
|
List<Category> categories = Arrays.asList(
|
||||||
@@ -113,4 +123,36 @@ public class DataInitializer implements CommandLineRunner {
|
|||||||
postRepository.saveAll(existingPosts);
|
postRepository.saveAll(existingPosts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void initSystemUsers() {
|
||||||
|
// 1. 初始化内置超级管理员 admin / admin123
|
||||||
|
if (!sysUserRepository.existsByUsername("admin")) {
|
||||||
|
SysUser admin = new SysUser();
|
||||||
|
admin.setUsername("admin");
|
||||||
|
String salt = SecurityUtils.generateSalt();
|
||||||
|
admin.setSalt(salt);
|
||||||
|
admin.setPassword(SecurityUtils.encryptPassword("admin123", salt));
|
||||||
|
admin.setNickname("小东 (管理员)");
|
||||||
|
admin.setEmail("j2570852518@gmail.com");
|
||||||
|
admin.setAvatar("👨💻");
|
||||||
|
admin.setRole("ADMIN");
|
||||||
|
admin.setStatus("0");
|
||||||
|
sysUserRepository.save(admin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 初始化内置测试访客读者 ry_user / user123
|
||||||
|
if (!sysUserRepository.existsByUsername("ry_user")) {
|
||||||
|
SysUser visitor = new SysUser();
|
||||||
|
visitor.setUsername("ry_user");
|
||||||
|
String salt = SecurityUtils.generateSalt();
|
||||||
|
visitor.setSalt(salt);
|
||||||
|
visitor.setPassword(SecurityUtils.encryptPassword("user123", salt));
|
||||||
|
visitor.setNickname("若依读者");
|
||||||
|
visitor.setEmail("visitor@example.com");
|
||||||
|
visitor.setAvatar("🚀");
|
||||||
|
visitor.setRole("USER");
|
||||||
|
visitor.setStatus("0");
|
||||||
|
sysUserRepository.save(visitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.example.blog.controller;
|
||||||
|
|
||||||
|
import com.example.blog.common.JwtUtils;
|
||||||
|
import com.example.blog.common.Result;
|
||||||
|
import com.example.blog.dto.LoginBody;
|
||||||
|
import com.example.blog.dto.RegisterBody;
|
||||||
|
import com.example.blog.service.SysUserService;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/auth")
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
public class AuthController {
|
||||||
|
|
||||||
|
private final SysUserService sysUserService;
|
||||||
|
|
||||||
|
public AuthController(SysUserService sysUserService) {
|
||||||
|
this.sysUserService = sysUserService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/login")
|
||||||
|
public Result<Map<String, Object>> login(@RequestBody LoginBody loginBody, HttpServletRequest request) {
|
||||||
|
String ip = request.getHeader("X-Forwarded-For");
|
||||||
|
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||||
|
ip = request.getRemoteAddr();
|
||||||
|
}
|
||||||
|
return Result.success(sysUserService.login(loginBody, ip));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/register")
|
||||||
|
public Result<Void> register(@RequestBody RegisterBody registerBody) {
|
||||||
|
sysUserService.register(registerBody);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/info")
|
||||||
|
public Result<Map<String, Object>> getInfo(@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
||||||
|
if (authHeader == null || authHeader.isEmpty()) {
|
||||||
|
return Result.error(401, "请先登录");
|
||||||
|
}
|
||||||
|
Map<String, Object> claims = JwtUtils.parseToken(authHeader);
|
||||||
|
if (claims == null) {
|
||||||
|
return Result.error(401, "登录凭证无效或已过期,请重新登录");
|
||||||
|
}
|
||||||
|
Number userIdNum = (Number) claims.get("userId");
|
||||||
|
if (userIdNum == null) {
|
||||||
|
return Result.error(401, "用户凭证异常");
|
||||||
|
}
|
||||||
|
return Result.success(sysUserService.getUserInfo(userIdNum.longValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/logout")
|
||||||
|
public Result<Void> logout() {
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
package com.example.blog.controller;
|
package com.example.blog.controller;
|
||||||
|
|
||||||
|
import com.example.blog.common.JwtUtils;
|
||||||
import com.example.blog.common.Result;
|
import com.example.blog.common.Result;
|
||||||
import com.example.blog.entity.Comment;
|
import com.example.blog.entity.Comment;
|
||||||
import com.example.blog.service.CommentService;
|
import com.example.blog.service.CommentService;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/posts/{postId}/comments")
|
@RequestMapping("/api/posts/{postId}/comments")
|
||||||
@@ -24,7 +26,24 @@ public class CommentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public Result<Comment> addComment(@PathVariable Long postId, @RequestBody Comment comment) {
|
public Result<Comment> addComment(@PathVariable Long postId,
|
||||||
|
@RequestBody Comment comment,
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
||||||
|
if (authHeader != null && !authHeader.isEmpty()) {
|
||||||
|
Map<String, Object> claims = JwtUtils.parseToken(authHeader);
|
||||||
|
if (claims != null) {
|
||||||
|
String nickname = (String) claims.get("nickname");
|
||||||
|
String role = (String) claims.get("role");
|
||||||
|
if (nickname != null && !nickname.trim().isEmpty()) {
|
||||||
|
comment.setAuthor(nickname);
|
||||||
|
}
|
||||||
|
if (role != null) {
|
||||||
|
comment.setRole(role);
|
||||||
|
}
|
||||||
|
comment.setAvatar("ADMIN".equalsIgnoreCase(role) ? "👑" : "👨💻");
|
||||||
|
}
|
||||||
|
}
|
||||||
return Result.success(commentService.addComment(postId, comment));
|
return Result.success(commentService.addComment(postId, comment));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package com.example.blog.controller;
|
package com.example.blog.controller;
|
||||||
|
|
||||||
|
import com.example.blog.common.JwtUtils;
|
||||||
import com.example.blog.common.Result;
|
import com.example.blog.common.Result;
|
||||||
import com.example.blog.entity.Post;
|
import com.example.blog.entity.Post;
|
||||||
import com.example.blog.service.PostService;
|
import com.example.blog.service.PostService;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/posts")
|
@RequestMapping("/api/posts")
|
||||||
@@ -18,6 +20,20 @@ public class PostController {
|
|||||||
this.postService = postService;
|
this.postService = postService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void checkAdmin(String authHeader) {
|
||||||
|
if (authHeader == null || authHeader.isEmpty()) {
|
||||||
|
throw new RuntimeException("无权操作:请使用管理员账号登录");
|
||||||
|
}
|
||||||
|
Map<String, Object> claims = JwtUtils.parseToken(authHeader);
|
||||||
|
if (claims == null) {
|
||||||
|
throw new RuntimeException("登录已失效,请重新登录");
|
||||||
|
}
|
||||||
|
String role = (String) claims.get("role");
|
||||||
|
if (!"ADMIN".equalsIgnoreCase(role)) {
|
||||||
|
throw new RuntimeException("无权操作:此功能仅限超级管理员使用");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public Result<List<Post>> listPosts(
|
public Result<List<Post>> listPosts(
|
||||||
@RequestParam(required = false) String category,
|
@RequestParam(required = false) String category,
|
||||||
@@ -31,17 +47,31 @@ public class PostController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public Result<Post> createPost(@RequestBody Post post) {
|
public Result<Post> createPost(@RequestBody Post post,
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
||||||
|
checkAdmin(authHeader);
|
||||||
|
Map<String, Object> claims = JwtUtils.parseToken(authHeader);
|
||||||
|
if (claims != null) {
|
||||||
|
String nickname = (String) claims.get("nickname");
|
||||||
|
if (nickname != null && !nickname.trim().isEmpty()) {
|
||||||
|
post.setAuthor(nickname);
|
||||||
|
}
|
||||||
|
}
|
||||||
return Result.success(postService.createPost(post));
|
return Result.success(postService.createPost(post));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
public Result<Post> updatePost(@PathVariable Long id, @RequestBody Post post) {
|
public Result<Post> updatePost(@PathVariable Long id,
|
||||||
|
@RequestBody Post post,
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
||||||
|
checkAdmin(authHeader);
|
||||||
return Result.success(postService.updatePost(id, post));
|
return Result.success(postService.updatePost(id, post));
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
public Result<Void> deletePost(@PathVariable Long id) {
|
public Result<Void> deletePost(@PathVariable Long id,
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
||||||
|
checkAdmin(authHeader);
|
||||||
postService.deletePost(id);
|
postService.deletePost(id);
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
@@ -51,3 +81,4 @@ public class PostController {
|
|||||||
return Result.success(postService.likePost(id));
|
return Result.success(postService.likePost(id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package com.example.blog.controller;
|
||||||
|
|
||||||
|
import com.example.blog.common.JwtUtils;
|
||||||
|
import com.example.blog.common.Result;
|
||||||
|
import com.example.blog.dto.UpdateProfileBody;
|
||||||
|
import com.example.blog.dto.UpdatePwdBody;
|
||||||
|
import com.example.blog.entity.SysUser;
|
||||||
|
import com.example.blog.service.SysUserService;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/users")
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
public class SysUserController {
|
||||||
|
|
||||||
|
private final SysUserService sysUserService;
|
||||||
|
|
||||||
|
public SysUserController(SysUserService sysUserService) {
|
||||||
|
this.sysUserService = sysUserService;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> checkAuth(String authHeader) {
|
||||||
|
if (authHeader == null || authHeader.isEmpty()) {
|
||||||
|
throw new RuntimeException("请先登录后操作");
|
||||||
|
}
|
||||||
|
Map<String, Object> claims = JwtUtils.parseToken(authHeader);
|
||||||
|
if (claims == null) {
|
||||||
|
throw new RuntimeException("登录凭证已失效,请重新登录");
|
||||||
|
}
|
||||||
|
return claims;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkAdmin(String authHeader) {
|
||||||
|
Map<String, Object> claims = checkAuth(authHeader);
|
||||||
|
String role = (String) claims.get("role");
|
||||||
|
if (!"ADMIN".equalsIgnoreCase(role)) {
|
||||||
|
throw new RuntimeException("无权操作,此功能仅限超级管理员使用");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public Result<List<SysUser>> listUsers(@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
||||||
|
checkAdmin(authHeader);
|
||||||
|
return Result.success(sysUserService.listUsers());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public Result<SysUser> createUser(@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
|
@RequestBody SysUser user) {
|
||||||
|
checkAdmin(authHeader);
|
||||||
|
return Result.success(sysUserService.createUser(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}/status")
|
||||||
|
public Result<Void> changeStatus(@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestParam String status) {
|
||||||
|
checkAdmin(authHeader);
|
||||||
|
sysUserService.changeStatus(id, status);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public Result<Void> deleteUser(@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
|
@PathVariable Long id) {
|
||||||
|
checkAdmin(authHeader);
|
||||||
|
sysUserService.deleteUser(id);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/profile")
|
||||||
|
public Result<Void> updateProfile(@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
|
@RequestBody UpdateProfileBody body) {
|
||||||
|
Map<String, Object> claims = checkAuth(authHeader);
|
||||||
|
Number userId = (Number) claims.get("userId");
|
||||||
|
sysUserService.updateProfile(userId.longValue(), body);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/password")
|
||||||
|
public Result<Void> updatePassword(@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
|
@RequestBody UpdatePwdBody body) {
|
||||||
|
Map<String, Object> claims = checkAuth(authHeader);
|
||||||
|
Number userId = (Number) claims.get("userId");
|
||||||
|
sysUserService.updatePassword(userId.longValue(), body);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.example.blog.dto;
|
||||||
|
|
||||||
|
public class LoginBody {
|
||||||
|
private String username;
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
public String getUsername() { return username; }
|
||||||
|
public void setUsername(String username) { this.username = username; }
|
||||||
|
public String getPassword() { return password; }
|
||||||
|
public void setPassword(String password) { this.password = password; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.example.blog.dto;
|
||||||
|
|
||||||
|
public class RegisterBody {
|
||||||
|
private String username;
|
||||||
|
private String password;
|
||||||
|
private String nickname;
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
public String getUsername() { return username; }
|
||||||
|
public void setUsername(String username) { this.username = username; }
|
||||||
|
public String getPassword() { return password; }
|
||||||
|
public void setPassword(String password) { this.password = password; }
|
||||||
|
public String getNickname() { return nickname; }
|
||||||
|
public void setNickname(String nickname) { this.nickname = nickname; }
|
||||||
|
public String getEmail() { return email; }
|
||||||
|
public void setEmail(String email) { this.email = email; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.example.blog.dto;
|
||||||
|
|
||||||
|
public class UpdateProfileBody {
|
||||||
|
private String nickname;
|
||||||
|
private String email;
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
public String getNickname() { return nickname; }
|
||||||
|
public void setNickname(String nickname) { this.nickname = nickname; }
|
||||||
|
public String getEmail() { return email; }
|
||||||
|
public void setEmail(String email) { this.email = email; }
|
||||||
|
public String getAvatar() { return avatar; }
|
||||||
|
public void setAvatar(String avatar) { this.avatar = avatar; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.example.blog.dto;
|
||||||
|
|
||||||
|
public class UpdatePwdBody {
|
||||||
|
private String oldPassword;
|
||||||
|
private String newPassword;
|
||||||
|
|
||||||
|
public String getOldPassword() { return oldPassword; }
|
||||||
|
public void setOldPassword(String oldPassword) { this.oldPassword = oldPassword; }
|
||||||
|
public String getNewPassword() { return newPassword; }
|
||||||
|
public void setNewPassword(String newPassword) { this.newPassword = newPassword; }
|
||||||
|
}
|
||||||
@@ -17,6 +17,12 @@ public class Comment {
|
|||||||
@Column(nullable = false, length = 50)
|
@Column(nullable = false, length = 50)
|
||||||
private String author = "访客";
|
private String author = "访客";
|
||||||
|
|
||||||
|
@Column(length = 255)
|
||||||
|
private String avatar = "👨💻";
|
||||||
|
|
||||||
|
@Column(length = 20)
|
||||||
|
private String role = "USER";
|
||||||
|
|
||||||
@Column(nullable = false, columnDefinition = "TEXT")
|
@Column(nullable = false, columnDefinition = "TEXT")
|
||||||
private String content;
|
private String content;
|
||||||
|
|
||||||
@@ -43,6 +49,10 @@ public class Comment {
|
|||||||
public void setPostId(Long postId) { this.postId = postId; }
|
public void setPostId(Long postId) { this.postId = postId; }
|
||||||
public String getAuthor() { return author; }
|
public String getAuthor() { return author; }
|
||||||
public void setAuthor(String author) { this.author = author; }
|
public void setAuthor(String author) { this.author = author; }
|
||||||
|
public String getAvatar() { return avatar; }
|
||||||
|
public void setAvatar(String avatar) { this.avatar = avatar; }
|
||||||
|
public String getRole() { return role; }
|
||||||
|
public void setRole(String role) { this.role = role; }
|
||||||
public String getContent() { return content; }
|
public String getContent() { return content; }
|
||||||
public void setContent(String content) { this.content = content; }
|
public void setContent(String content) { this.content = content; }
|
||||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package com.example.blog.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "sys_user")
|
||||||
|
public class SysUser {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, unique = true, length = 50)
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 128)
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
@Column(length = 50)
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
@Column(length = 100)
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@Column(length = 255)
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 20)
|
||||||
|
private String role = "USER"; // ADMIN, USER
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 1)
|
||||||
|
private String status = "0"; // 0: 正常, 1: 停用
|
||||||
|
|
||||||
|
@Column(length = 64)
|
||||||
|
private String salt;
|
||||||
|
|
||||||
|
@Column(name = "login_ip", length = 50)
|
||||||
|
private String loginIp;
|
||||||
|
|
||||||
|
@Column(name = "login_date")
|
||||||
|
private LocalDateTime loginDate;
|
||||||
|
|
||||||
|
@Column(name = "created_at", updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "updated_at")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
public void prePersist() {
|
||||||
|
if (createdAt == null) createdAt = LocalDateTime.now();
|
||||||
|
if (updatedAt == null) updatedAt = LocalDateTime.now();
|
||||||
|
if (role == null) role = "USER";
|
||||||
|
if (status == null) status = "0";
|
||||||
|
if (nickname == null || nickname.trim().isEmpty()) nickname = username;
|
||||||
|
if (avatar == null) avatar = "👨💻";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
public void preUpdate() {
|
||||||
|
updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SysUser() {}
|
||||||
|
|
||||||
|
public SysUser(String username, String password, String nickname, String email, String role) {
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
this.nickname = nickname;
|
||||||
|
this.email = email;
|
||||||
|
this.role = role;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getUsername() { return username; }
|
||||||
|
public void setUsername(String username) { this.username = username; }
|
||||||
|
|
||||||
|
public String getPassword() { return password; }
|
||||||
|
public void setPassword(String password) { this.password = password; }
|
||||||
|
|
||||||
|
public String getNickname() { return nickname; }
|
||||||
|
public void setNickname(String nickname) { this.nickname = nickname; }
|
||||||
|
|
||||||
|
public String getEmail() { return email; }
|
||||||
|
public void setEmail(String email) { this.email = email; }
|
||||||
|
|
||||||
|
public String getAvatar() { return avatar; }
|
||||||
|
public void setAvatar(String avatar) { this.avatar = avatar; }
|
||||||
|
|
||||||
|
public String getRole() { return role; }
|
||||||
|
public void setRole(String role) { this.role = role; }
|
||||||
|
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
|
||||||
|
public String getSalt() { return salt; }
|
||||||
|
public void setSalt(String salt) { this.salt = salt; }
|
||||||
|
|
||||||
|
public String getLoginIp() { return loginIp; }
|
||||||
|
public void setLoginIp(String loginIp) { this.loginIp = loginIp; }
|
||||||
|
|
||||||
|
public LocalDateTime getLoginDate() { return loginDate; }
|
||||||
|
public void setLoginDate(LocalDateTime loginDate) { this.loginDate = loginDate; }
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||||
|
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.example.blog.repository;
|
||||||
|
|
||||||
|
import com.example.blog.entity.SysUser;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface SysUserRepository extends JpaRepository<SysUser, Long> {
|
||||||
|
Optional<SysUser> findByUsername(String username);
|
||||||
|
boolean existsByUsername(String username);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.example.blog.service;
|
||||||
|
|
||||||
|
import com.example.blog.dto.LoginBody;
|
||||||
|
import com.example.blog.dto.RegisterBody;
|
||||||
|
import com.example.blog.dto.UpdateProfileBody;
|
||||||
|
import com.example.blog.dto.UpdatePwdBody;
|
||||||
|
import com.example.blog.entity.SysUser;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface SysUserService {
|
||||||
|
|
||||||
|
Map<String, Object> login(LoginBody loginBody, String ip);
|
||||||
|
|
||||||
|
void register(RegisterBody registerBody);
|
||||||
|
|
||||||
|
Map<String, Object> getUserInfo(Long userId);
|
||||||
|
|
||||||
|
List<SysUser> listUsers();
|
||||||
|
|
||||||
|
void changeStatus(Long userId, String status);
|
||||||
|
|
||||||
|
void updateProfile(Long userId, UpdateProfileBody body);
|
||||||
|
|
||||||
|
void updatePassword(Long userId, UpdatePwdBody body);
|
||||||
|
|
||||||
|
void deleteUser(Long userId);
|
||||||
|
|
||||||
|
SysUser createUser(SysUser user);
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
package com.example.blog.service.impl;
|
||||||
|
|
||||||
|
import com.example.blog.common.JwtUtils;
|
||||||
|
import com.example.blog.common.SecurityUtils;
|
||||||
|
import com.example.blog.dto.LoginBody;
|
||||||
|
import com.example.blog.dto.RegisterBody;
|
||||||
|
import com.example.blog.dto.UpdateProfileBody;
|
||||||
|
import com.example.blog.dto.UpdatePwdBody;
|
||||||
|
import com.example.blog.entity.SysUser;
|
||||||
|
import com.example.blog.repository.SysUserRepository;
|
||||||
|
import com.example.blog.service.SysUserService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class SysUserServiceImpl implements SysUserService {
|
||||||
|
|
||||||
|
private final SysUserRepository sysUserRepository;
|
||||||
|
|
||||||
|
public SysUserServiceImpl(SysUserRepository sysUserRepository) {
|
||||||
|
this.sysUserRepository = sysUserRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public Map<String, Object> login(LoginBody loginBody, String ip) {
|
||||||
|
if (loginBody.getUsername() == null || loginBody.getUsername().trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("用户名不能为空");
|
||||||
|
}
|
||||||
|
if (loginBody.getPassword() == null || loginBody.getPassword().trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("密码不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
SysUser user = sysUserRepository.findByUsername(loginBody.getUsername().trim())
|
||||||
|
.orElseThrow(() -> new RuntimeException("用户名或密码错误"));
|
||||||
|
|
||||||
|
if ("1".equals(user.getStatus())) {
|
||||||
|
throw new RuntimeException("对不起,该账号已被停用,请联系管理员");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SecurityUtils.matchesPassword(loginBody.getPassword(), user.getPassword(), user.getSalt())) {
|
||||||
|
throw new RuntimeException("用户名或密码错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新登录信息
|
||||||
|
user.setLoginIp(ip != null ? ip : "127.0.0.1");
|
||||||
|
user.setLoginDate(LocalDateTime.now());
|
||||||
|
sysUserRepository.save(user);
|
||||||
|
|
||||||
|
// 生成 RuoYi 风格 Token 与鉴权负载
|
||||||
|
String token = JwtUtils.createToken(user.getId(), user.getUsername(), user.getNickname(), user.getRole());
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("token", token);
|
||||||
|
|
||||||
|
Map<String, Object> safeUser = new HashMap<>();
|
||||||
|
safeUser.put("id", user.getId());
|
||||||
|
safeUser.put("username", user.getUsername());
|
||||||
|
safeUser.put("nickname", user.getNickname());
|
||||||
|
safeUser.put("email", user.getEmail());
|
||||||
|
safeUser.put("avatar", user.getAvatar());
|
||||||
|
safeUser.put("role", user.getRole());
|
||||||
|
safeUser.put("status", user.getStatus());
|
||||||
|
safeUser.put("loginDate", user.getLoginDate());
|
||||||
|
result.put("user", safeUser);
|
||||||
|
|
||||||
|
// 角色与权限标识 (若依标准格式)
|
||||||
|
result.put("roles", Collections.singletonList(user.getRole()));
|
||||||
|
if ("ADMIN".equalsIgnoreCase(user.getRole())) {
|
||||||
|
result.put("permissions", Collections.singletonList("*:*:*"));
|
||||||
|
} else {
|
||||||
|
result.put("permissions", Arrays.asList("post:view", "post:like", "comment:add"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void register(RegisterBody registerBody) {
|
||||||
|
String username = registerBody.getUsername() != null ? registerBody.getUsername().trim() : "";
|
||||||
|
String password = registerBody.getPassword() != null ? registerBody.getPassword().trim() : "";
|
||||||
|
|
||||||
|
if (username.length() < 3 || username.length() > 20) {
|
||||||
|
throw new IllegalArgumentException("用户名长度必须在 3 到 20 个字符之间");
|
||||||
|
}
|
||||||
|
if (password.length() < 5 || password.length() > 30) {
|
||||||
|
throw new IllegalArgumentException("密码长度必须在 5 到 30 个字符之间");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sysUserRepository.existsByUsername(username)) {
|
||||||
|
throw new RuntimeException("该用户名已被注册,请尝试其他用户名");
|
||||||
|
}
|
||||||
|
|
||||||
|
String salt = SecurityUtils.generateSalt();
|
||||||
|
String encryptedPassword = SecurityUtils.encryptPassword(password, salt);
|
||||||
|
|
||||||
|
SysUser user = new SysUser();
|
||||||
|
user.setUsername(username);
|
||||||
|
user.setPassword(encryptedPassword);
|
||||||
|
user.setSalt(salt);
|
||||||
|
user.setNickname(registerBody.getNickname() != null && !registerBody.getNickname().trim().isEmpty() ? registerBody.getNickname().trim() : username);
|
||||||
|
user.setEmail(registerBody.getEmail() != null ? registerBody.getEmail().trim() : "");
|
||||||
|
user.setAvatar("👨💻");
|
||||||
|
user.setRole("USER");
|
||||||
|
user.setStatus("0");
|
||||||
|
|
||||||
|
sysUserRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public Map<String, Object> getUserInfo(Long userId) {
|
||||||
|
SysUser user = sysUserRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("用户不存在"));
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
Map<String, Object> safeUser = new HashMap<>();
|
||||||
|
safeUser.put("id", user.getId());
|
||||||
|
safeUser.put("username", user.getUsername());
|
||||||
|
safeUser.put("nickname", user.getNickname());
|
||||||
|
safeUser.put("email", user.getEmail());
|
||||||
|
safeUser.put("avatar", user.getAvatar());
|
||||||
|
safeUser.put("role", user.getRole());
|
||||||
|
safeUser.put("status", user.getStatus());
|
||||||
|
safeUser.put("loginDate", user.getLoginDate());
|
||||||
|
safeUser.put("createdAt", user.getCreatedAt());
|
||||||
|
|
||||||
|
result.put("user", safeUser);
|
||||||
|
result.put("roles", Collections.singletonList(user.getRole()));
|
||||||
|
if ("ADMIN".equalsIgnoreCase(user.getRole())) {
|
||||||
|
result.put("permissions", Collections.singletonList("*:*:*"));
|
||||||
|
} else {
|
||||||
|
result.put("permissions", Arrays.asList("post:view", "post:like", "comment:add"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<SysUser> listUsers() {
|
||||||
|
List<SysUser> users = sysUserRepository.findAll();
|
||||||
|
// 清除敏感字段
|
||||||
|
for (SysUser u : users) {
|
||||||
|
u.setPassword(null);
|
||||||
|
u.setSalt(null);
|
||||||
|
}
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void changeStatus(Long userId, String status) {
|
||||||
|
SysUser user = sysUserRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("用户不存在"));
|
||||||
|
if ("admin".equalsIgnoreCase(user.getUsername())) {
|
||||||
|
throw new RuntimeException("不允许停用内置超级管理员账号");
|
||||||
|
}
|
||||||
|
user.setStatus("1".equals(status) ? "1" : "0");
|
||||||
|
sysUserRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void updateProfile(Long userId, UpdateProfileBody body) {
|
||||||
|
SysUser user = sysUserRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("用户不存在"));
|
||||||
|
if (body.getNickname() != null && !body.getNickname().trim().isEmpty()) {
|
||||||
|
user.setNickname(body.getNickname().trim());
|
||||||
|
}
|
||||||
|
if (body.getEmail() != null) {
|
||||||
|
user.setEmail(body.getEmail().trim());
|
||||||
|
}
|
||||||
|
if (body.getAvatar() != null && !body.getAvatar().trim().isEmpty()) {
|
||||||
|
user.setAvatar(body.getAvatar().trim());
|
||||||
|
}
|
||||||
|
sysUserRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void updatePassword(Long userId, UpdatePwdBody body) {
|
||||||
|
SysUser user = sysUserRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("用户不存在"));
|
||||||
|
|
||||||
|
if (!SecurityUtils.matchesPassword(body.getOldPassword(), user.getPassword(), user.getSalt())) {
|
||||||
|
throw new RuntimeException("原密码错误,修改失败");
|
||||||
|
}
|
||||||
|
if (body.getNewPassword() == null || body.getNewPassword().length() < 5) {
|
||||||
|
throw new IllegalArgumentException("新密码长度不能少于 5 位");
|
||||||
|
}
|
||||||
|
|
||||||
|
String newSalt = SecurityUtils.generateSalt();
|
||||||
|
String newHash = SecurityUtils.encryptPassword(body.getNewPassword(), newSalt);
|
||||||
|
user.setSalt(newSalt);
|
||||||
|
user.setPassword(newHash);
|
||||||
|
sysUserRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void deleteUser(Long userId) {
|
||||||
|
SysUser user = sysUserRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("用户不存在"));
|
||||||
|
if ("admin".equalsIgnoreCase(user.getUsername())) {
|
||||||
|
throw new RuntimeException("不允许删除内置超级管理员账号");
|
||||||
|
}
|
||||||
|
sysUserRepository.deleteById(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public SysUser createUser(SysUser user) {
|
||||||
|
if (sysUserRepository.existsByUsername(user.getUsername())) {
|
||||||
|
throw new RuntimeException("用户名已存在");
|
||||||
|
}
|
||||||
|
String salt = SecurityUtils.generateSalt();
|
||||||
|
user.setSalt(salt);
|
||||||
|
user.setPassword(SecurityUtils.encryptPassword(user.getPassword(), salt));
|
||||||
|
return sysUserRepository.save(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
+640
-17
@@ -299,13 +299,69 @@
|
|||||||
<span>{{ backendConnected ? 'Spring Boot 3 + MySQL 8.0 在线' : '本地安全模式' }}</span>
|
<span>{{ backendConnected ? 'Spring Boot 3 + MySQL 8.0 在线' : '本地安全模式' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 写文章按钮 (唤起现代化 Markdown 写作台) -->
|
<!-- 写文章按钮 (仅超级管理员可见) -->
|
||||||
<button @click="openPublishModal"
|
<button v-if="isAdmin" @click="openPublishModal"
|
||||||
class="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-sm font-semibold bg-gradient-to-r from-sky-600 to-indigo-600 hover:from-sky-700 hover:to-indigo-700 text-white shadow-md shadow-sky-600/25 transition-all hover:shadow active:scale-95">
|
class="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-sm font-semibold bg-gradient-to-r from-sky-600 to-indigo-600 hover:from-sky-700 hover:to-indigo-700 text-white shadow-md shadow-sky-600/25 transition-all hover:shadow active:scale-95">
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
|
||||||
<span class="hidden sm:inline">写新文章</span>
|
<span class="hidden sm:inline">写新文章</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- 若依风格用户系统入口 -->
|
||||||
|
<div v-if="isLoggedIn" class="relative">
|
||||||
|
<button @click="showUserDropdown = !showUserDropdown"
|
||||||
|
class="flex items-center gap-2 px-3 py-1.5 rounded-xl bg-white dark:bg-slate-900 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors border border-slate-200 dark:border-slate-800 shadow-sm">
|
||||||
|
<span class="text-base">{{ currentUser.avatar || '👨💻' }}</span>
|
||||||
|
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 max-w-[80px] sm:max-w-[120px] truncate">{{ currentUser.nickname }}</span>
|
||||||
|
<span :class="isAdmin ? 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300 border-amber-200' : 'bg-sky-100 text-sky-800 dark:bg-sky-950 dark:text-sky-300 border-sky-200'"
|
||||||
|
class="px-2 py-0.5 text-[10px] font-bold rounded-full border">
|
||||||
|
{{ isAdmin ? '👑 管理员' : '👤 读者' }}
|
||||||
|
</span>
|
||||||
|
<svg class="w-3 h-3 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 下拉菜单抽屉 -->
|
||||||
|
<div v-show="showUserDropdown" @click.outside="showUserDropdown = false"
|
||||||
|
class="absolute right-0 mt-2 w-56 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 shadow-2xl py-2 z-50">
|
||||||
|
<div class="px-4 py-2.5 border-b border-slate-100 dark:border-slate-800">
|
||||||
|
<div class="font-bold text-xs text-slate-800 dark:text-slate-100 flex items-center gap-1.5">
|
||||||
|
<span>{{ currentUser.avatar || '👨💻' }}</span>
|
||||||
|
<span>{{ currentUser.nickname }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-[11px] text-slate-400 font-mono">@{{ currentUser.username }}</div>
|
||||||
|
<div class="text-[10px] text-slate-400 mt-0.5 truncate">{{ currentUser.email || '未绑定邮箱' }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button v-if="isAdmin" @click="showUserDropdown = false; openPublishModal()"
|
||||||
|
class="w-full text-left px-4 py-2 text-xs text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 flex items-center gap-2">
|
||||||
|
<span>✍️</span> <span>写新文章</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="isAdmin" @click="showUserDropdown = false; openUserMgmtModal()"
|
||||||
|
class="w-full text-left px-4 py-2 text-xs text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 flex items-center gap-2">
|
||||||
|
<span>👥</span> <span>用户管理中心 (RuoYi RBAC)</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button @click="showUserDropdown = false; openProfileModal()"
|
||||||
|
class="w-full text-left px-4 py-2 text-xs text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 flex items-center gap-2">
|
||||||
|
<span>👤</span> <span>个人中心设置</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="my-1 border-t border-slate-100 dark:border-slate-800"></div>
|
||||||
|
|
||||||
|
<button @click="showUserDropdown = false; handleLogout()"
|
||||||
|
class="w-full text-left px-4 py-2 text-xs text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-950/40 flex items-center gap-2 font-medium">
|
||||||
|
<span>🚪</span> <span>退出登录</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 未登录状态展示登录/注册按钮 -->
|
||||||
|
<button v-else @click="openAuthModal('login')"
|
||||||
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold bg-sky-50 hover:bg-sky-100 dark:bg-sky-950/60 dark:hover:bg-sky-900/60 text-sky-700 dark:text-sky-300 border border-sky-200 dark:border-sky-800 transition-all shadow-sm active:scale-95">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path></svg>
|
||||||
|
<span>登录 / 注册</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- 深色/浅色模式切换 -->
|
<!-- 深色/浅色模式切换 -->
|
||||||
<button @click="toggleDarkMode"
|
<button @click="toggleDarkMode"
|
||||||
class="p-2 rounded-xl text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-100 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors border border-transparent dark:border-slate-800"
|
class="p-2 rounded-xl text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-100 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors border border-transparent dark:border-slate-800"
|
||||||
@@ -608,10 +664,25 @@
|
|||||||
|
|
||||||
<!-- 撰写评论卡片 -->
|
<!-- 撰写评论卡片 -->
|
||||||
<div class="bg-slate-50/80 dark:bg-slate-800/40 p-4 sm:p-5 rounded-2xl border border-slate-200/60 dark:border-slate-700/60 mb-6 space-y-3">
|
<div class="bg-slate-50/80 dark:bg-slate-800/40 p-4 sm:p-5 rounded-2xl border border-slate-200/60 dark:border-slate-700/60 mb-6 space-y-3">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center justify-between text-xs">
|
||||||
<input v-model="newComment.author" type="text" placeholder="你的技术昵称 (默认: 访客)"
|
<div v-if="isLoggedIn" class="flex items-center gap-2">
|
||||||
class="px-3.5 py-1.5 rounded-xl bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 text-xs w-48 focus:outline-none focus:ring-2 focus:ring-sky-500">
|
<span class="text-base">{{ currentUser.avatar || '👨💻' }}</span>
|
||||||
<span class="text-xs text-slate-400">评论将实时持久化写入 MySQL 8.0</span>
|
<span class="text-slate-600 dark:text-slate-300">正在以</span>
|
||||||
|
<strong class="text-slate-900 dark:text-white">{{ currentUser.nickname }}</strong>
|
||||||
|
<span :class="isAdmin ? 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300' : 'bg-sky-100 text-sky-800 dark:bg-sky-950 dark:text-sky-300'"
|
||||||
|
class="px-1.5 py-0.2 text-[10px] font-bold rounded-full">
|
||||||
|
{{ isAdmin ? '👑 博主' : '👤 读者' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex items-center gap-2">
|
||||||
|
<input v-model="newComment.author" type="text" placeholder="你的昵称 (默认: 访客)"
|
||||||
|
class="px-3 py-1.5 rounded-xl bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 text-xs w-40 focus:outline-none focus:ring-2 focus:ring-sky-500">
|
||||||
|
<span class="text-slate-400">或</span>
|
||||||
|
<button @click="openAuthModal('login')" class="text-sky-600 dark:text-sky-400 font-bold hover:underline">
|
||||||
|
👉 登录若依账号
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span class="text-slate-400 hidden sm:inline font-mono">直写 MySQL 8.0</span>
|
||||||
</div>
|
</div>
|
||||||
<textarea v-model="newComment.content" rows="3" placeholder="写下你的技术思考、疑问或独到见解..."
|
<textarea v-model="newComment.content" rows="3" placeholder="写下你的技术思考、疑问或独到见解..."
|
||||||
class="w-full px-3.5 py-2.5 rounded-xl bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-sky-500"></textarea>
|
class="w-full px-3.5 py-2.5 rounded-xl bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-sky-500"></textarea>
|
||||||
@@ -630,14 +701,20 @@
|
|||||||
class="p-4 rounded-2xl bg-slate-50/60 dark:bg-slate-800/30 border border-slate-100 dark:border-slate-800 text-xs sm:text-sm">
|
class="p-4 rounded-2xl bg-slate-50/60 dark:bg-slate-800/30 border border-slate-100 dark:border-slate-800 text-xs sm:text-sm">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="w-6 h-6 rounded-full bg-sky-500/20 text-sky-600 dark:text-sky-400 font-bold flex items-center justify-center text-xs">
|
<div class="w-7 h-7 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-sm shadow-sm">
|
||||||
{{ c.author ? c.author.charAt(0).toUpperCase() : 'U' }}
|
{{ c.avatar || (c.role === 'ADMIN' ? '👑' : '👨💻') }}
|
||||||
</div>
|
</div>
|
||||||
<span class="font-bold text-slate-900 dark:text-slate-100">{{ c.author || '匿名访客' }}</span>
|
<span class="font-bold text-slate-900 dark:text-slate-100">{{ c.author || '匿名访客' }}</span>
|
||||||
|
<span v-if="c.role === 'ADMIN'" class="px-1.5 py-0.5 rounded text-[10px] font-bold bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300 border border-amber-200 dark:border-amber-800">
|
||||||
|
👑 博主/管理员
|
||||||
|
</span>
|
||||||
|
<span v-else-if="c.role === 'USER'" class="px-1.5 py-0.5 rounded text-[10px] font-medium bg-sky-100 text-sky-800 dark:bg-sky-950 dark:text-sky-300 border border-sky-200 dark:border-sky-800">
|
||||||
|
读者
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-xs text-slate-400 font-mono">{{ formatDate(c.createdAt) }}</span>
|
<span class="text-xs text-slate-400 font-mono">{{ formatDate(c.createdAt) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-slate-700 dark:text-slate-300 leading-relaxed pl-8">{{ c.content }}</p>
|
<p class="text-slate-700 dark:text-slate-300 leading-relaxed pl-9">{{ c.content }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-center py-8 text-xs text-slate-400">
|
<div v-else class="text-center py-8 text-xs text-slate-400">
|
||||||
@@ -949,6 +1026,249 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ======================= 若依风格 1:用户认证登录/注册中心模态框 ======================= -->
|
||||||
|
<div v-if="showAuthModal"
|
||||||
|
class="fixed inset-0 z-50 overflow-y-auto bg-slate-900/70 backdrop-blur-sm flex items-center justify-center p-4"
|
||||||
|
@click.self="showAuthModal = false">
|
||||||
|
<div class="w-full max-w-md bg-white dark:bg-slate-900 rounded-3xl shadow-2xl border border-slate-200 dark:border-slate-800 overflow-hidden animate-in fade-in zoom-in-95 duration-200">
|
||||||
|
<!-- 头部 -->
|
||||||
|
<div class="px-6 pt-6 pb-4 text-center border-b border-slate-100 dark:border-slate-800 relative">
|
||||||
|
<button @click="showAuthModal = false" class="absolute right-4 top-4 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 text-lg">✕</button>
|
||||||
|
<div class="w-12 h-12 mx-auto rounded-2xl bg-gradient-to-tr from-sky-500 via-indigo-500 to-purple-600 flex items-center justify-center text-white text-xl font-black shadow-md shadow-sky-500/20 mb-3">
|
||||||
|
RY
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-black text-slate-900 dark:text-white">小东全栈博客 · 认证中心</h3>
|
||||||
|
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">若依 (RuoYi) 规范 RBAC 权限与用户体系</p>
|
||||||
|
|
||||||
|
<!-- 登录/注册 Tab 切换 -->
|
||||||
|
<div class="flex mt-4 bg-slate-100 dark:bg-slate-800 p-1 rounded-xl text-xs font-bold">
|
||||||
|
<button @click="authTab = 'login'" :class="authTab === 'login' ? 'bg-white dark:bg-slate-700 text-sky-600 dark:text-sky-400 shadow-sm' : 'text-slate-500'" class="flex-1 py-1.5 rounded-lg transition-all">
|
||||||
|
账号登录
|
||||||
|
</button>
|
||||||
|
<button @click="authTab = 'register'" :class="authTab === 'register' ? 'bg-white dark:bg-slate-700 text-sky-600 dark:text-sky-400 shadow-sm' : 'text-slate-500'" class="flex-1 py-1.5 rounded-lg transition-all">
|
||||||
|
读者注册
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录表单 -->
|
||||||
|
<div v-if="authTab === 'login'" class="p-6 space-y-4 text-xs">
|
||||||
|
<div>
|
||||||
|
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">用户名 *</label>
|
||||||
|
<input v-model="loginForm.username" type="text" placeholder="输入账号 (如 admin)"
|
||||||
|
class="w-full px-3.5 py-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500 focus:outline-none text-sm font-medium">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">密码 *</label>
|
||||||
|
<input v-model="loginForm.password" type="password" placeholder="输入密码 (如 admin123)" @keyup.enter="handleLogin"
|
||||||
|
class="w-full px-3.5 py-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500 focus:outline-none text-sm font-medium">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 快捷一键填入账号 (若依经典便民测试功能) -->
|
||||||
|
<div class="pt-2">
|
||||||
|
<div class="text-[11px] text-slate-400 mb-2 font-mono">⚡ 快捷填入内置测试账号:</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button @click="quickFill('admin', 'admin123')"
|
||||||
|
class="flex-1 py-1.5 px-2 rounded-xl bg-amber-50 hover:bg-amber-100 dark:bg-amber-950/40 dark:hover:bg-amber-950/70 text-amber-700 dark:text-amber-300 border border-amber-200 dark:border-amber-800 font-semibold text-[11px] transition-colors">
|
||||||
|
👑 超级管理员 (admin)
|
||||||
|
</button>
|
||||||
|
<button @click="quickFill('ry_user', 'user123')"
|
||||||
|
class="flex-1 py-1.5 px-2 rounded-xl bg-sky-50 hover:bg-sky-100 dark:bg-sky-950/40 dark:hover:bg-sky-950/70 text-sky-700 dark:text-sky-300 border border-sky-200 dark:border-sky-800 font-semibold text-[11px] transition-colors">
|
||||||
|
👤 测试读者 (ry_user)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="handleLogin" :disabled="authLoading"
|
||||||
|
class="w-full mt-4 py-2.5 rounded-xl bg-gradient-to-r from-sky-600 to-indigo-600 hover:from-sky-700 hover:to-indigo-700 disabled:opacity-50 text-white font-bold text-sm shadow-md shadow-sky-600/30 flex items-center justify-center gap-2">
|
||||||
|
<span v-if="authLoading" class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></span>
|
||||||
|
<span>{{ authLoading ? '验证登录中...' : '立即登录' }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 注册表单 -->
|
||||||
|
<div v-else class="p-6 space-y-3.5 text-xs">
|
||||||
|
<div>
|
||||||
|
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">用户名 *</label>
|
||||||
|
<input v-model="registerForm.username" type="text" placeholder="3-20 位字符,用于登录"
|
||||||
|
class="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500 focus:outline-none">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">个性昵称</label>
|
||||||
|
<input v-model="registerForm.nickname" type="text" placeholder="例如: 极客小明"
|
||||||
|
class="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500 focus:outline-none">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">登录密码 *</label>
|
||||||
|
<input v-model="registerForm.password" type="password" placeholder="不少于 5 位"
|
||||||
|
class="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500 focus:outline-none">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">联系邮箱</label>
|
||||||
|
<input v-model="registerForm.email" type="email" placeholder="name@example.com"
|
||||||
|
class="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500 focus:outline-none">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="handleRegister" :disabled="authLoading"
|
||||||
|
class="w-full mt-4 py-2.5 rounded-xl bg-gradient-to-r from-sky-600 to-indigo-600 hover:from-sky-700 hover:to-indigo-700 disabled:opacity-50 text-white font-bold text-sm shadow-md shadow-sky-600/30 flex items-center justify-center gap-2">
|
||||||
|
<span v-if="authLoading" class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></span>
|
||||||
|
<span>{{ authLoading ? '创建账号中...' : '立即注册读者账号' }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ======================= 若依风格 2:用户权限管理中心模态框 (RBAC 后台) ======================= -->
|
||||||
|
<div v-if="showUserMgmtModal"
|
||||||
|
class="fixed inset-0 z-50 overflow-y-auto bg-slate-900/70 backdrop-blur-sm flex items-center justify-center p-3 sm:p-6"
|
||||||
|
@click.self="showUserMgmtModal = false">
|
||||||
|
<div class="w-full max-w-4xl bg-white dark:bg-slate-900 rounded-3xl shadow-2xl border border-slate-200 dark:border-slate-800 overflow-hidden flex flex-col max-h-[90vh] animate-in fade-in zoom-in-95 duration-200">
|
||||||
|
<div class="px-6 py-4 border-b border-slate-100 dark:border-slate-800 flex items-center justify-between shrink-0">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-2xl">👥</span>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-base text-slate-900 dark:text-white">若依风格 · 用户权限管理中心 (RBAC)</h3>
|
||||||
|
<p class="text-xs text-slate-400">管理全站注册用户、启用/停用状态及权限角色</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button @click="showUserMgmtModal = false" class="text-slate-400 hover:text-slate-600 text-lg">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户表格内容区 -->
|
||||||
|
<div class="p-6 overflow-y-auto flex-1">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<div class="text-xs text-slate-500 font-mono">共 {{ userList.length }} 位注册用户</div>
|
||||||
|
<button @click="fetchUserList" class="px-3 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-xs font-semibold flex items-center gap-1.5 transition-colors">
|
||||||
|
<span>🔄</span> <span>刷新列表</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto rounded-2xl border border-slate-200 dark:border-slate-800">
|
||||||
|
<table class="w-full text-left text-xs border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-slate-50 dark:bg-slate-800/60 border-b border-slate-200 dark:border-slate-800 text-slate-500 dark:text-slate-400 font-bold">
|
||||||
|
<th class="p-3.5">ID</th>
|
||||||
|
<th class="p-3.5">用户名</th>
|
||||||
|
<th class="p-3.5">昵称</th>
|
||||||
|
<th class="p-3.5">角色权限</th>
|
||||||
|
<th class="p-3.5">账号状态</th>
|
||||||
|
<th class="p-3.5">最近登录</th>
|
||||||
|
<th class="p-3.5 text-right">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100 dark:divide-slate-800">
|
||||||
|
<tr v-for="u in userList" :key="u.id" class="hover:bg-slate-50/50 dark:hover:bg-slate-800/40 transition-colors">
|
||||||
|
<td class="p-3.5 font-mono">{{ u.id }}</td>
|
||||||
|
<td class="p-3.5 font-bold text-slate-900 dark:text-slate-100">{{ u.username }}</td>
|
||||||
|
<td class="p-3.5 flex items-center gap-1.5">
|
||||||
|
<span>{{ u.avatar || '👨💻' }}</span>
|
||||||
|
<span>{{ u.nickname }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="p-3.5">
|
||||||
|
<span :class="u.role === 'ADMIN' ? 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300 border-amber-200' : 'bg-sky-100 text-sky-800 dark:bg-sky-950 dark:text-sky-300 border-sky-200'"
|
||||||
|
class="px-2 py-0.5 rounded-full font-bold text-[10px] border">
|
||||||
|
{{ u.role === 'ADMIN' ? '👑 超级管理员' : '👤 访客读者' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="p-3.5">
|
||||||
|
<span :class="u.status === '0' ? 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-400 border-emerald-200' : 'bg-rose-100 text-rose-800 dark:bg-rose-950 dark:text-rose-400 border-rose-200'"
|
||||||
|
class="px-2 py-0.5 rounded-full font-bold text-[10px] border">
|
||||||
|
{{ u.status === '0' ? '正常' : '已停用' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="p-3.5 text-slate-400 font-mono">{{ u.loginDate ? formatDate(u.loginDate) : '未登录' }}</td>
|
||||||
|
<td class="p-3.5 text-right font-medium">
|
||||||
|
<template v-if="u.username !== 'admin'">
|
||||||
|
<button @click="toggleUserStatus(u)" class="text-sky-600 hover:underline mr-3">
|
||||||
|
{{ u.status === '0' ? '停用' : '启用' }}
|
||||||
|
</button>
|
||||||
|
<button @click="deleteUser(u)" class="text-rose-600 hover:underline">
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<span v-else class="text-[11px] text-slate-400 font-mono">系统保护</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ======================= 若依风格 3:个人中心设置模态框 ======================= -->
|
||||||
|
<div v-if="showProfileModal"
|
||||||
|
class="fixed inset-0 z-50 overflow-y-auto bg-slate-900/70 backdrop-blur-sm flex items-center justify-center p-4"
|
||||||
|
@click.self="showProfileModal = false">
|
||||||
|
<div class="w-full max-w-md bg-white dark:bg-slate-900 rounded-3xl shadow-2xl border border-slate-200 dark:border-slate-800 overflow-hidden animate-in fade-in zoom-in-95 duration-200">
|
||||||
|
<div class="px-6 py-4 border-b border-slate-100 dark:border-slate-800 flex items-center justify-between">
|
||||||
|
<h3 class="font-bold text-base text-slate-900 dark:text-white flex items-center gap-2">
|
||||||
|
<span>👤 个人中心设置</span>
|
||||||
|
</h3>
|
||||||
|
<button @click="showProfileModal = false" class="text-slate-400 hover:text-slate-600">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabs -->
|
||||||
|
<div class="flex border-b border-slate-100 dark:border-slate-800 text-xs font-bold">
|
||||||
|
<button @click="profileTab = 'info'" :class="profileTab === 'info' ? 'border-b-2 border-sky-500 text-sky-600 dark:text-sky-400' : 'text-slate-400'" class="flex-1 py-3 text-center transition-colors">
|
||||||
|
基本资料
|
||||||
|
</button>
|
||||||
|
<button @click="profileTab = 'pwd'" :class="profileTab === 'pwd' ? 'border-b-2 border-sky-500 text-sky-600 dark:text-sky-400' : 'text-slate-400'" class="flex-1 py-3 text-center transition-colors">
|
||||||
|
修改密码
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 修改基本资料 -->
|
||||||
|
<div v-if="profileTab === 'info'" class="p-6 space-y-4 text-xs">
|
||||||
|
<div>
|
||||||
|
<label class="block font-bold mb-1 text-slate-400">用户名</label>
|
||||||
|
<input :value="currentUser.username" disabled class="w-full px-3.5 py-2 rounded-xl bg-slate-100 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-slate-400 cursor-not-allowed font-mono">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">修改昵称</label>
|
||||||
|
<input v-model="profileForm.nickname" type="text" class="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">联系邮箱</label>
|
||||||
|
<input v-model="profileForm.email" type="email" class="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block font-bold mb-1.5 text-slate-700 dark:text-slate-300">选择头像 Emoji</label>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button v-for="av in ['👨💻', '🚀', '💻', '🧠', '⚡', '🐬', '🌟', '🛡️']" :key="av"
|
||||||
|
@click="profileForm.avatar = av"
|
||||||
|
:class="profileForm.avatar === av ? 'ring-2 ring-sky-500 scale-110' : 'opacity-70 hover:opacity-100'"
|
||||||
|
class="w-9 h-9 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-lg transition-all">
|
||||||
|
{{ av }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button @click="handleUpdateProfile" class="w-full mt-2 py-2.5 rounded-xl bg-sky-600 hover:bg-sky-700 text-white font-bold text-xs shadow-md shadow-sky-600/30 transition-all">
|
||||||
|
保存资料修改
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 修改密码 -->
|
||||||
|
<div v-else class="p-6 space-y-4 text-xs">
|
||||||
|
<div>
|
||||||
|
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">原密码 *</label>
|
||||||
|
<input v-model="pwdForm.oldPassword" type="password" placeholder="输入当前使用的密码" class="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">新密码 *</label>
|
||||||
|
<input v-model="pwdForm.newPassword" type="password" placeholder="不少于 5 位" class="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">确认新密码 *</label>
|
||||||
|
<input v-model="pwdForm.confirmPassword" type="password" placeholder="再次输入新密码" class="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500">
|
||||||
|
</div>
|
||||||
|
<button @click="handleUpdatePwd" class="w-full mt-2 py-2.5 rounded-xl bg-sky-600 hover:bg-sky-700 text-white font-bold text-xs shadow-md shadow-sky-600/30 transition-all">
|
||||||
|
确认修改密码
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 返回顶部浮动胶囊 -->
|
<!-- 返回顶部浮动胶囊 -->
|
||||||
<button v-show="showScrollTop" @click="scrollToTop"
|
<button v-show="showScrollTop" @click="scrollToTop"
|
||||||
class="fixed bottom-6 right-6 z-40 p-3 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 text-slate-700 dark:text-slate-300 shadow-xl hover:scale-110 active:scale-95 transition-all"
|
class="fixed bottom-6 right-6 z-40 p-3 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 text-slate-700 dark:text-slate-300 shadow-xl hover:scale-110 active:scale-95 transition-all"
|
||||||
@@ -1004,6 +1324,253 @@
|
|||||||
const showScrollTop = ref(false);
|
const showScrollTop = ref(false);
|
||||||
const toasts = ref([]);
|
const toasts = ref([]);
|
||||||
|
|
||||||
|
// ============ 若依 (RuoYi) 规范 RBAC 用户认证与权限体系 ============
|
||||||
|
const token = ref(localStorage.getItem('ry_token') || '');
|
||||||
|
const currentUser = ref(null);
|
||||||
|
const roles = ref([]);
|
||||||
|
const permissions = ref([]);
|
||||||
|
const showUserDropdown = ref(false);
|
||||||
|
|
||||||
|
const showAuthModal = ref(false);
|
||||||
|
const authTab = ref('login'); // 'login' | 'register'
|
||||||
|
const authLoading = ref(false);
|
||||||
|
const loginForm = ref({ username: '', password: '' });
|
||||||
|
const registerForm = ref({ username: '', password: '', nickname: '', email: '' });
|
||||||
|
|
||||||
|
const showUserMgmtModal = ref(false);
|
||||||
|
const userList = ref([]);
|
||||||
|
|
||||||
|
const showProfileModal = ref(false);
|
||||||
|
const profileTab = ref('info'); // 'info' | 'pwd'
|
||||||
|
const profileForm = ref({ nickname: '', email: '', avatar: '👨💻' });
|
||||||
|
const pwdForm = ref({ oldPassword: '', newPassword: '', confirmPassword: '' });
|
||||||
|
|
||||||
|
const isLoggedIn = computed(() => !!token.value && !!currentUser.value);
|
||||||
|
const isAdmin = computed(() => currentUser.value && currentUser.value.role === 'ADMIN');
|
||||||
|
const authHeaders = computed(() => {
|
||||||
|
return token.value ? { 'Authorization': 'Bearer ' + token.value } : {};
|
||||||
|
});
|
||||||
|
|
||||||
|
function openAuthModal(tab = 'login') {
|
||||||
|
authTab.value = tab;
|
||||||
|
loginForm.value = { username: '', password: '' };
|
||||||
|
registerForm.value = { username: '', password: '', nickname: '', email: '' };
|
||||||
|
showAuthModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function quickFill(u, p) {
|
||||||
|
loginForm.value.username = u;
|
||||||
|
loginForm.value.password = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogin() {
|
||||||
|
if (!loginForm.value.username.trim() || !loginForm.value.password.trim()) {
|
||||||
|
showToast('请输入用户名和密码', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
authLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/auth/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(loginForm.value)
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (res.ok && result.code === 200) {
|
||||||
|
token.value = result.data.token;
|
||||||
|
localStorage.setItem('ry_token', result.data.token);
|
||||||
|
currentUser.value = result.data.user;
|
||||||
|
roles.value = result.data.roles;
|
||||||
|
permissions.value = result.data.permissions;
|
||||||
|
showAuthModal.value = false;
|
||||||
|
showToast(`欢迎回来,${currentUser.value.nickname}!`, 'success');
|
||||||
|
} else {
|
||||||
|
showToast(result.msg || '登录失败,请检查账号密码', 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast('登录服务不可用,请稍后重试', 'error');
|
||||||
|
}
|
||||||
|
authLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRegister() {
|
||||||
|
if (!registerForm.value.username.trim() || !registerForm.value.password.trim()) {
|
||||||
|
showToast('用户名和密码不能为空', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
authLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/auth/register`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(registerForm.value)
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (res.ok && result.code === 200) {
|
||||||
|
showToast('注册成功!正在为您自动登录...', 'success');
|
||||||
|
loginForm.value.username = registerForm.value.username;
|
||||||
|
loginForm.value.password = registerForm.value.password;
|
||||||
|
await handleLogin();
|
||||||
|
} else {
|
||||||
|
showToast(result.msg || '注册失败', 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast('注册请求失败', 'error');
|
||||||
|
}
|
||||||
|
authLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
token.value = '';
|
||||||
|
currentUser.value = null;
|
||||||
|
roles.value = [];
|
||||||
|
permissions.value = [];
|
||||||
|
localStorage.removeItem('ry_token');
|
||||||
|
showToast('已安全退出登录', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchUserInfo() {
|
||||||
|
if (!token.value) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/auth/info`, {
|
||||||
|
headers: { ...authHeaders.value }
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.code === 200) {
|
||||||
|
currentUser.value = result.data.user;
|
||||||
|
roles.value = result.data.roles;
|
||||||
|
permissions.value = result.data.permissions;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
localStorage.removeItem('ry_token');
|
||||||
|
token.value = '';
|
||||||
|
currentUser.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户管理中心
|
||||||
|
function openUserMgmtModal() {
|
||||||
|
showUserMgmtModal.value = true;
|
||||||
|
fetchUserList();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchUserList() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/users`, {
|
||||||
|
headers: { ...authHeaders.value }
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
const result = await res.json();
|
||||||
|
userList.value = result.data || [];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast('获取用户列表失败', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleUserStatus(u) {
|
||||||
|
const newStatus = u.status === '0' ? '1' : '0';
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/users/${u.id}/status?status=${newStatus}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { ...authHeaders.value }
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (res.ok && result.code === 200) {
|
||||||
|
u.status = newStatus;
|
||||||
|
showToast(`已${newStatus === '1' ? '停用' : '启用'}用户 [${u.username}]`, 'success');
|
||||||
|
} else {
|
||||||
|
showToast(result.msg || '操作失败', 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast('网络请求失败', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteUser(u) {
|
||||||
|
if (!confirm(`确定要永久删除用户 [${u.username}] 吗?`)) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/users/${u.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { ...authHeaders.value }
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (res.ok && result.code === 200) {
|
||||||
|
showToast(`用户 [${u.username}] 已被删除`, 'success');
|
||||||
|
fetchUserList();
|
||||||
|
} else {
|
||||||
|
showToast(result.msg || '删除失败', 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast('网络请求失败', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 个人中心
|
||||||
|
function openProfileModal() {
|
||||||
|
profileTab.value = 'info';
|
||||||
|
profileForm.value = {
|
||||||
|
nickname: currentUser.value.nickname,
|
||||||
|
email: currentUser.value.email || '',
|
||||||
|
avatar: currentUser.value.avatar || '👨💻'
|
||||||
|
};
|
||||||
|
pwdForm.value = { oldPassword: '', newPassword: '', confirmPassword: '' };
|
||||||
|
showProfileModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUpdateProfile() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/users/profile`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json', ...authHeaders.value },
|
||||||
|
body: JSON.stringify(profileForm.value)
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (res.ok && result.code === 200) {
|
||||||
|
currentUser.value.nickname = profileForm.value.nickname;
|
||||||
|
currentUser.value.email = profileForm.value.email;
|
||||||
|
currentUser.value.avatar = profileForm.value.avatar;
|
||||||
|
showToast('个人资料已成功更新', 'success');
|
||||||
|
showProfileModal.value = false;
|
||||||
|
} else {
|
||||||
|
showToast(result.msg || '修改失败', 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast('请求失败', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUpdatePwd() {
|
||||||
|
if (!pwdForm.value.oldPassword || !pwdForm.value.newPassword) {
|
||||||
|
showToast('旧密码和新密码不能为空', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pwdForm.value.newPassword !== pwdForm.value.confirmPassword) {
|
||||||
|
showToast('两次输入的新密码不一致', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/users/password`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json', ...authHeaders.value },
|
||||||
|
body: JSON.stringify(pwdForm.value)
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (res.ok && result.code === 200) {
|
||||||
|
showToast('密码修改成功,请使用新密码重新登录', 'success');
|
||||||
|
handleLogout();
|
||||||
|
showProfileModal.value = false;
|
||||||
|
openAuthModal('login');
|
||||||
|
} else {
|
||||||
|
showToast(result.msg || '修改密码失败', 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast('请求失败', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const categories = ref(['全部']);
|
const categories = ref(['全部']);
|
||||||
const categoryList = ref([]);
|
const categoryList = ref([]);
|
||||||
const availableCategories = computed(() => categories.value.filter(c => c !== '全部'));
|
const availableCategories = computed(() => categories.value.filter(c => c !== '全部'));
|
||||||
@@ -1284,17 +1851,26 @@
|
|||||||
showToast('评论内容不能为空', 'error');
|
showToast('评论内容不能为空', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isLoggedIn.value && currentUser.value) {
|
||||||
|
newComment.value.author = currentUser.value.nickname;
|
||||||
|
}
|
||||||
submittingComment.value = true;
|
submittingComment.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/posts/${activePost.value.id}/comments`, {
|
const res = await fetch(`${API_BASE}/posts/${activePost.value.id}/comments`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...authHeaders.value
|
||||||
|
},
|
||||||
body: JSON.stringify(newComment.value)
|
body: JSON.stringify(newComment.value)
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
const result = await res.json();
|
||||||
|
if (res.ok && result.code === 200) {
|
||||||
newComment.value.content = '';
|
newComment.value.content = '';
|
||||||
showToast('评论已持久化写入 MySQL 8.0', 'success');
|
showToast('评论已持久化写入 MySQL 8.0', 'success');
|
||||||
fetchComments(activePost.value.id);
|
fetchComments(activePost.value.id);
|
||||||
|
} else {
|
||||||
|
showToast(result.msg || '评论发表失败', 'error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('评论提交失败,请检查网络连接', 'error');
|
showToast('评论提交失败,请检查网络连接', 'error');
|
||||||
@@ -1304,16 +1880,26 @@
|
|||||||
|
|
||||||
// 删除文章
|
// 删除文章
|
||||||
async function deletePost(post) {
|
async function deletePost(post) {
|
||||||
|
if (!isAdmin.value) {
|
||||||
|
showToast('无权删除:仅超级管理员可用', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!confirm(`确定要永久删除文章《${post.title}》吗?`)) return;
|
if (!confirm(`确定要永久删除文章《${post.title}》吗?`)) return;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/posts/${post.id}`, { method: 'DELETE' });
|
const res = await fetch(`${API_BASE}/posts/${post.id}`, {
|
||||||
if (res.ok) {
|
method: 'DELETE',
|
||||||
|
headers: { ...authHeaders.value }
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (res.ok && result.code === 200) {
|
||||||
showToast('文章已从 MySQL 数据库中移除', 'success');
|
showToast('文章已从 MySQL 数据库中移除', 'success');
|
||||||
navigateView('home');
|
navigateView('home');
|
||||||
fetchPosts();
|
fetchPosts();
|
||||||
|
} else {
|
||||||
|
showToast(result.msg || '删除失败', 'error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('删除失败', 'error');
|
showToast('删除失败,权限不足或服务器错误', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1372,6 +1958,10 @@
|
|||||||
|
|
||||||
// 持久化发布文章至 MySQL
|
// 持久化发布文章至 MySQL
|
||||||
async function publishPost() {
|
async function publishPost() {
|
||||||
|
if (!isAdmin.value) {
|
||||||
|
showToast('无权发布:请先使用超级管理员账号登录', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!newPost.value.title.trim() || !newPost.value.content.trim()) {
|
if (!newPost.value.title.trim() || !newPost.value.content.trim()) {
|
||||||
showToast('文章标题和正文内容不能为空!', 'error');
|
showToast('文章标题和正文内容不能为空!', 'error');
|
||||||
return;
|
return;
|
||||||
@@ -1381,15 +1971,19 @@
|
|||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/posts`, {
|
const res = await fetch(`${API_BASE}/posts`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...authHeaders.value
|
||||||
|
},
|
||||||
body: JSON.stringify(newPost.value)
|
body: JSON.stringify(newPost.value)
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
const result = await res.json();
|
||||||
|
if (res.ok && result.code === 200) {
|
||||||
showToast('文章已成功持久化写入 MySQL 8.0!', 'success');
|
showToast('文章已成功持久化写入 MySQL 8.0!', 'success');
|
||||||
showPublishModal.value = false;
|
showPublishModal.value = false;
|
||||||
fetchPosts();
|
fetchPosts();
|
||||||
} else {
|
} else {
|
||||||
showToast('发布失败,请稍后重试', 'error');
|
showToast(result.msg || '发布失败,请稍后重试', 'error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('发布失败,无法连接服务器', 'error');
|
showToast('发布失败,无法连接服务器', 'error');
|
||||||
@@ -1500,6 +2094,7 @@
|
|||||||
window.addEventListener('scroll', handleScroll);
|
window.addEventListener('scroll', handleScroll);
|
||||||
window.addEventListener('hashchange', handleHashRoute);
|
window.addEventListener('hashchange', handleHashRoute);
|
||||||
|
|
||||||
|
await fetchUserInfo();
|
||||||
await fetchCategories();
|
await fetchCategories();
|
||||||
await fetchPosts();
|
await fetchPosts();
|
||||||
handleHashRoute();
|
handleHashRoute();
|
||||||
@@ -1516,6 +2111,34 @@
|
|||||||
backendConnected,
|
backendConnected,
|
||||||
currentView,
|
currentView,
|
||||||
navigateView,
|
navigateView,
|
||||||
|
token,
|
||||||
|
currentUser,
|
||||||
|
isLoggedIn,
|
||||||
|
isAdmin,
|
||||||
|
showUserDropdown,
|
||||||
|
showAuthModal,
|
||||||
|
authTab,
|
||||||
|
authLoading,
|
||||||
|
loginForm,
|
||||||
|
registerForm,
|
||||||
|
openAuthModal,
|
||||||
|
quickFill,
|
||||||
|
handleLogin,
|
||||||
|
handleRegister,
|
||||||
|
handleLogout,
|
||||||
|
showUserMgmtModal,
|
||||||
|
userList,
|
||||||
|
openUserMgmtModal,
|
||||||
|
fetchUserList,
|
||||||
|
toggleUserStatus,
|
||||||
|
deleteUser,
|
||||||
|
showProfileModal,
|
||||||
|
profileTab,
|
||||||
|
profileForm,
|
||||||
|
pwdForm,
|
||||||
|
openProfileModal,
|
||||||
|
handleUpdateProfile,
|
||||||
|
handleUpdatePwd,
|
||||||
posts,
|
posts,
|
||||||
stats,
|
stats,
|
||||||
categories,
|
categories,
|
||||||
|
|||||||
Reference in New Issue
Block a user