feat: user related logic refactor
This commit is contained in:
2
mvnw.cmd
vendored
2
mvnw.cmd
vendored
@@ -23,7 +23,7 @@
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - userinfo and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -28,11 +28,11 @@ public class SecurityConfig {
|
||||
.authorizeHttpRequests(authz -> authz
|
||||
.requestMatchers(
|
||||
"/error",
|
||||
"/login",
|
||||
"/register",
|
||||
"/shadow/**",
|
||||
"/health",
|
||||
"/code/**",
|
||||
"/v3/api-docs/**"
|
||||
"/v3/api-docs/**",
|
||||
"/account/mail/verify/**"
|
||||
).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package xin.merlin.myplayerbackend.controller;
|
||||
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import xin.merlin.myplayerbackend.entity.http.Code;
|
||||
import xin.merlin.myplayerbackend.service.impl.AccountServiceImpl;
|
||||
import xin.merlin.myplayerbackend.utils.result.Response;
|
||||
|
||||
|
||||
/*
|
||||
* 账户相关接口:
|
||||
* 修改邮箱
|
||||
* 修改密码
|
||||
* */
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
@RequiredArgsConstructor
|
||||
public class AccountController {
|
||||
|
||||
private final AccountServiceImpl accountService;
|
||||
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@PostMapping("/change/email")
|
||||
Response changeEmail(@RequestBody Code code, @RequestParam String email){
|
||||
return accountService.changeEmail(code,email);
|
||||
}
|
||||
|
||||
@PostMapping("/mail/verify/{encode}")
|
||||
Response verifyEmail(@PathVariable String encode){
|
||||
return accountService.verifyEmail(encode);
|
||||
}
|
||||
|
||||
@PostMapping("/change/password")
|
||||
Response changePassword(@RequestBody Code code){
|
||||
return accountService.resetPassword(code);
|
||||
}
|
||||
|
||||
// @PostMapping("/init")
|
||||
// Response init(@RequestBody Account account) {
|
||||
// try {
|
||||
// if(account==null||account.getAccount()==null) return Response.success(ResultCode.ACCOUNT_INFO_LOST);
|
||||
// account.setPassword(passwordEncoder.encode(account.getPassword()));
|
||||
// accountService.init(account);
|
||||
// return Response.success(ResultCode.SUCCESS);
|
||||
// } catch (Exception e) {
|
||||
// log.error(e.getMessage());
|
||||
// return Response.fail(ResultCode.SERVER_ERROR);
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package xin.merlin.myplayerbackend.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import xin.merlin.myplayerbackend.entity.Audit;
|
||||
import xin.merlin.myplayerbackend.service.UploadService;
|
||||
import xin.merlin.myplayerbackend.service.impl.AuditServiceImpl;
|
||||
import xin.merlin.myplayerbackend.utils.JwtUtil;
|
||||
import xin.merlin.myplayerbackend.utils.result.Response;
|
||||
import xin.merlin.myplayerbackend.utils.result.ResultCode;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/audit")
|
||||
@RequiredArgsConstructor
|
||||
public class AuditController {
|
||||
|
||||
private final AuditServiceImpl auditService;
|
||||
|
||||
private final UploadService uploadService;
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
@PostMapping("/upload/avatar")
|
||||
@Transactional
|
||||
Response uploadAvatar(@RequestParam("avatar") MultipartFile file,
|
||||
@RequestParam("type") Integer type,
|
||||
@RequestParam("applicant") Integer applicant,
|
||||
@RequestParam("influence") Integer influence) throws IOException {
|
||||
try {
|
||||
String target = uploadService.uploadAvatar(file,type,influence);
|
||||
Audit oldAudit = auditService.getOne(
|
||||
Wrappers.<Audit>lambdaQuery().eq(Audit::getType, type)
|
||||
.eq(Audit::getApplicant,applicant).eq(Audit::getInfluence,influence));
|
||||
|
||||
Audit audit = new Audit(type,applicant,influence,target);
|
||||
if(oldAudit != null) {
|
||||
audit = new Audit(oldAudit.getA_id(),type,applicant,influence,target);
|
||||
}
|
||||
auditService.saveOrUpdate(audit);
|
||||
return Response.success(ResultCode.SUCCESS);
|
||||
} catch (IOException e) {
|
||||
log.error(e.getMessage());
|
||||
return Response.fail(ResultCode.SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 管理员api
|
||||
|
||||
@GetMapping("/get/all")
|
||||
Response getAll(@RequestHeader("Authorization")String token,@RequestParam Integer size,@RequestParam Integer page) {
|
||||
token = token.substring(7);
|
||||
if(!jwtUtil.isAdmin(token)) return Response.success(ResultCode.ACCOUNT_PERMISSION_DENY);
|
||||
return Response.success(ResultCode.SUCCESS,auditService.page(new Page<Audit>().setCurrent(page).setSize(size), Wrappers.<Audit>lambdaQuery().orderByAsc(Audit::getType)));
|
||||
}
|
||||
|
||||
|
||||
// TODO:完善完基础类别的增删改查之后,统一处理审核
|
||||
@PostMapping("/update/status")
|
||||
Response updateStatus(@RequestHeader("Authorization")String token,@RequestBody Audit audit) {
|
||||
token = token.substring(7);
|
||||
if(!jwtUtil.isAdmin(token)) return Response.success(ResultCode.ACCOUNT_PERMISSION_DENY);
|
||||
|
||||
auditService.passTypeOne(audit);
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -6,11 +6,12 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import xin.merlin.myplayerbackend.entity.Account;
|
||||
import xin.merlin.myplayerbackend.entity.http.Code;
|
||||
import xin.merlin.myplayerbackend.service.CodeService;
|
||||
import xin.merlin.myplayerbackend.service.impl.AccountServiceImpl;
|
||||
import xin.merlin.myplayerbackend.service.LoginService;
|
||||
import xin.merlin.myplayerbackend.utils.result.Response;
|
||||
import xin.merlin.myplayerbackend.utils.result.ResultCode;
|
||||
|
||||
@@ -21,35 +22,41 @@ import xin.merlin.myplayerbackend.utils.result.ResultCode;
|
||||
* */
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/shadow")
|
||||
@RequiredArgsConstructor
|
||||
public class LoginController {
|
||||
|
||||
private final AccountServiceImpl accountService;
|
||||
private final LoginService loginService;
|
||||
|
||||
private final CodeService codeService;
|
||||
|
||||
private final HttpServletRequest request;
|
||||
|
||||
//login
|
||||
// 账户密码登录逻辑
|
||||
@PostMapping("/login")
|
||||
Response login(@RequestBody Account account){
|
||||
|
||||
|
||||
return Response.success(ResultCode.SUCCESS);
|
||||
try {
|
||||
return loginService.login(account);
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage());
|
||||
return Response.fail(ResultCode.SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
//register
|
||||
// 验证码登录逻辑
|
||||
@PostMapping("/login/byCode")
|
||||
Response login(@RequestBody Code code){
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
// 注册逻辑
|
||||
@PostMapping("/register")
|
||||
Response register(@RequestBody Code code){
|
||||
if(code.getCode()==null|| code.getCode().isEmpty() || code.getC_id()==null || code.getC_id().isEmpty()) return Response.success(ResultCode.MAIL_VERIFY_NOT_EXIST);
|
||||
try {
|
||||
Response response = codeService.verify(code);
|
||||
if(response.getCode().equals("200")){
|
||||
Account account = new Account();
|
||||
account.setAccount(code.getAccount());
|
||||
account.setIp(request.getRemoteAddr());
|
||||
accountService.save(account);
|
||||
}
|
||||
return response;
|
||||
return loginService.register(code,request.getRemoteAddr());
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Response.fail(ResultCode.SERVER_ERROR);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package xin.merlin.myplayerbackend.controller;
|
||||
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import xin.merlin.myplayerbackend.entity.UserInfo;
|
||||
import xin.merlin.myplayerbackend.service.impl.UserServiceImpl;
|
||||
import xin.merlin.myplayerbackend.utils.JwtUtil;
|
||||
import xin.merlin.myplayerbackend.utils.result.Response;
|
||||
import xin.merlin.myplayerbackend.utils.result.ResultCode;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
@RequiredArgsConstructor
|
||||
public class UserController {
|
||||
|
||||
private final UserServiceImpl userService;
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
Boolean consistencyTest(String token, Integer id) {
|
||||
token = token.substring(7);
|
||||
Integer i = jwtUtil.getId(token);
|
||||
return Objects.equals(i, id);
|
||||
}
|
||||
|
||||
@GetMapping("/info")
|
||||
Response getInfo(@RequestHeader("Authorization")String token){
|
||||
token = token.substring(7);
|
||||
Integer id = jwtUtil.getId(token);
|
||||
return Response.success(ResultCode.SUCCESS,userService.getOne(Wrappers.<UserInfo>lambdaQuery().eq(UserInfo::getId,id)));
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/update")
|
||||
Response updateInfo(@RequestHeader("Authorization")String token,@RequestBody UserInfo userinfo){
|
||||
if(!consistencyTest(token,userinfo.getId())) return Response.success(ResultCode.USER_ILLEGAL_REQUEST);
|
||||
userService.updateById(userinfo);
|
||||
return Response.success(ResultCode.SUCCESS);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,9 +4,11 @@ package xin.merlin.myplayerbackend.entity;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@TableName("account")
|
||||
@NoArgsConstructor
|
||||
public class Account {
|
||||
@TableId("id")
|
||||
private Integer id;
|
||||
@@ -14,4 +16,13 @@ public class Account {
|
||||
private String account;
|
||||
private String password;
|
||||
private String ip;
|
||||
// 1 user; 0 admin
|
||||
private Integer character;
|
||||
|
||||
public Account(String account, String password, String ip, Integer character) {
|
||||
this.account = account;
|
||||
this.password = password;
|
||||
this.ip = ip;
|
||||
this.character = character;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,27 @@ package xin.merlin.myplayerbackend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@TableName("audit")
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class Audit {
|
||||
@TableId("a_id")
|
||||
private Integer a_id;
|
||||
|
||||
// 约定:type == 0 已通过审核, 1 用户头像待审核, 2 群聊头像待审核, 3 playroom头像待审核, 4 修改邮箱请求, 5 已经通过验证的邮箱修改请求, 6 已过期的审核请求
|
||||
private Integer type;
|
||||
private Integer applicant;
|
||||
private Integer influence;
|
||||
private String changed;
|
||||
|
||||
public Audit(Integer type,Integer applicant,Integer influence,String changed) {
|
||||
this.type = type;
|
||||
this.applicant = applicant;
|
||||
this.influence = influence;
|
||||
this.changed = changed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("group")
|
||||
public class Group {
|
||||
@TableName("groupinfo")
|
||||
public class GroupInfo {
|
||||
@TableId("g_id")
|
||||
private Integer g_id;
|
||||
|
||||
@@ -5,8 +5,8 @@ import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("playroom")
|
||||
public class Playroom {
|
||||
@TableName("playroominfo")
|
||||
public class PlayroomInfo {
|
||||
@TableId("r_id")
|
||||
private Integer r_id;
|
||||
|
||||
@@ -5,8 +5,8 @@ import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("user")
|
||||
public class User {
|
||||
@TableName("userinfo")
|
||||
public class UserInfo {
|
||||
@TableId("u_id")
|
||||
private String u_id;
|
||||
|
||||
@@ -7,5 +7,5 @@ public class Code {
|
||||
private String account;
|
||||
private String c_id;
|
||||
private String code;
|
||||
|
||||
private String password;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package xin.merlin.myplayerbackend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import xin.merlin.myplayerbackend.entity.Group;
|
||||
import xin.merlin.myplayerbackend.entity.GroupInfo;
|
||||
|
||||
@Mapper
|
||||
public interface GroupMapper extends BaseMapper<Group> {
|
||||
public interface GroupMapper extends BaseMapper<GroupInfo> {
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package xin.merlin.myplayerbackend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import xin.merlin.myplayerbackend.entity.Playroom;
|
||||
import xin.merlin.myplayerbackend.entity.PlayroomInfo;
|
||||
|
||||
@Mapper
|
||||
public interface PlayroomMapper extends BaseMapper<Playroom> {
|
||||
public interface PlayroomMapper extends BaseMapper<PlayroomInfo> {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package xin.merlin.myplayerbackend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import xin.merlin.myplayerbackend.entity.UserInfo;
|
||||
|
||||
@Mapper
|
||||
public interface UserMapper extends BaseMapper<UserInfo> {
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import xin.merlin.myplayerbackend.entity.Account;
|
||||
import xin.merlin.myplayerbackend.entity.http.Code;
|
||||
import xin.merlin.myplayerbackend.utils.result.Response;
|
||||
@@ -40,6 +39,16 @@ public class CodeService {
|
||||
.expireAfterWrite(5, TimeUnit.MINUTES)
|
||||
.build();
|
||||
|
||||
// // 验证因子队列
|
||||
// private static final Cache<String, String> authList = Caffeine.newBuilder()
|
||||
// .expireAfterWrite(5, TimeUnit.MINUTES)
|
||||
// .build();
|
||||
//
|
||||
// // 验证因子队列对外可访问
|
||||
// public Cache<String, String> getAuthList() {
|
||||
// return authList;
|
||||
// }
|
||||
|
||||
private final MailService mailService;
|
||||
|
||||
// 发送验证码
|
||||
@@ -68,7 +77,6 @@ public class CodeService {
|
||||
|
||||
|
||||
//验证验证码是否存在、可用、正确
|
||||
@Transactional
|
||||
public Response verify(Code code){
|
||||
String c_id = code.getC_id();
|
||||
String account = code.getAccount();
|
||||
@@ -94,7 +102,10 @@ public class CodeService {
|
||||
if (!tempCode.equals(code.getCode())) return Response.success(ResultCode.MAIL_VERIFY_CODE_ERROR);
|
||||
codeFailCount.invalidate(c_id);
|
||||
emailCooldown.invalidate(account);
|
||||
|
||||
// waitingList.invalidate(c_id);
|
||||
//// 生成认证因子
|
||||
// String auth = UUID.randomUUID().toString();
|
||||
// authList.put(auth, account);
|
||||
return Response.success(ResultCode.SUCCESS);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
|
||||
@@ -1,26 +1,82 @@
|
||||
package xin.merlin.myplayerbackend.service;
|
||||
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import xin.merlin.myplayerbackend.entity.Account;
|
||||
import xin.merlin.myplayerbackend.entity.UserInfo;
|
||||
import xin.merlin.myplayerbackend.entity.http.Code;
|
||||
import xin.merlin.myplayerbackend.mapper.AccountMapper;
|
||||
import xin.merlin.myplayerbackend.mapper.UserMapper;
|
||||
import xin.merlin.myplayerbackend.utils.JwtUtil;
|
||||
import xin.merlin.myplayerbackend.utils.RandomCode;
|
||||
import xin.merlin.myplayerbackend.utils.result.Response;
|
||||
import xin.merlin.myplayerbackend.utils.result.ResultCode;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LoginService{
|
||||
|
||||
private final AccountMapper accountMapper;
|
||||
|
||||
public Response login(Account account){
|
||||
Account ta = accountMapper.selectOne(new QueryWrapper<Account>().eq("account",account.getAccount()));
|
||||
if(ta == null) return Response.success(ResultCode.USER_NOT_FOUND);
|
||||
private final UserMapper userMapper;
|
||||
|
||||
//TODO:111
|
||||
return null;
|
||||
private final CodeService codeService;
|
||||
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
|
||||
// 账户密码登录服务逻辑
|
||||
public Response login(Account account){
|
||||
try {
|
||||
Account ta = accountMapper.selectOne(Wrappers.<Account>lambdaQuery().eq(Account::getAccount,account.getAccount()));
|
||||
if(ta == null) return Response.success(ResultCode.USER_NOT_FOUND);
|
||||
if(!passwordEncoder.matches(account.getPassword(),ta.getPassword())){
|
||||
return Response.success(ResultCode.ACCOUNT_PWD_ERROR);
|
||||
}
|
||||
String token = jwtUtil.generateToken(ta);
|
||||
|
||||
UserInfo userinfo = userMapper.selectOne(Wrappers.<UserInfo>lambdaQuery().eq(UserInfo::getId,ta.getId()));
|
||||
return Response.success(ResultCode.SUCCESS, Map.of("token",token,"token_type","Bearer","userinfo",userinfo));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 注册服务逻辑
|
||||
@Transactional
|
||||
public Response register(Code code, String ip){
|
||||
try {
|
||||
if(accountMapper.selectOne(Wrappers.<Account>lambdaQuery().eq(Account::getAccount,code.getAccount())) != null) return Response.success(ResultCode.ACCOUNT_EXIST);
|
||||
Response response = codeService.verify(code);
|
||||
if(response.getCode().equals("200")){
|
||||
Account account = new Account(code.getAccount(),passwordEncoder.encode(code.getPassword()),ip,1);
|
||||
accountMapper.insert(account);
|
||||
account = accountMapper.selectOne(Wrappers.<Account>lambdaQuery().eq(Account::getAccount, account.getAccount()));
|
||||
UserInfo u = new UserInfo();
|
||||
u.setId(account.getId());
|
||||
do{
|
||||
u.setU_id(RandomCode.generateID());
|
||||
}while (userMapper.selectById(u.getU_id())!=null);
|
||||
userMapper.insert(u);
|
||||
u = userMapper.selectById(u.getId());
|
||||
codeService.getWaitingList().invalidate(code.getC_id());
|
||||
return Response.success(ResultCode.SUCCESS,Map.of("token",jwtUtil.generateToken(account),"userinfo",u));
|
||||
}
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -33,4 +33,19 @@ public class MailService {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public Boolean sendTextMail(String receiver, String text){
|
||||
try {
|
||||
SimpleMailMessage message = new SimpleMailMessage();
|
||||
message.setFrom(mail);
|
||||
message.setTo(receiver);
|
||||
message.setSubject("Welcome to use Merlin`s product");
|
||||
message.setText("欢迎使用Merlin.xin产品! \n"+text);
|
||||
mailSender.send(message);
|
||||
return true;
|
||||
} catch (MailException e) {
|
||||
log.error("e: ", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package xin.merlin.myplayerbackend.service;
|
||||
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UploadService {
|
||||
|
||||
@Value("${resources.user.avatar}")
|
||||
private String userAvatarDir;
|
||||
|
||||
@Value("${resources.group.avatar}")
|
||||
private String groupAvatarDir;
|
||||
|
||||
@Value("${resources.playroom.avatar}")
|
||||
private String playroomAvatarDir;
|
||||
|
||||
@Value("${resources.public}")
|
||||
private String publicAvatarDir;
|
||||
|
||||
private static final List<String> AVATAR_ALLOWED_EXTENSIONS = Arrays.asList(".jpg", ".jpeg", ".png", ".gif");
|
||||
|
||||
/**
|
||||
* 上传用户、群组、放映室头像
|
||||
* @param file 目标文件
|
||||
* @param type 上传类型
|
||||
* @param influence 受影响的id
|
||||
* @return host后的访问url
|
||||
* @throws IOException 抛出io异常
|
||||
*/
|
||||
public String uploadAvatar(MultipartFile file, Integer type,Integer influence) throws IOException {
|
||||
String DIR = switch (type) {
|
||||
case 1 -> userAvatarDir;
|
||||
case 2 -> groupAvatarDir;
|
||||
case 3 -> playroomAvatarDir;
|
||||
default -> publicAvatarDir;
|
||||
};
|
||||
String fileName = getFilename(file, influence, DIR);
|
||||
Path targetPath = Paths.get(DIR, fileName);
|
||||
Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
return "/resources/user/avatar/" + fileName; // 返回访问 URL
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param file 目标文件
|
||||
* @param influence 受影响的id
|
||||
* @param DIR 储存的位置
|
||||
* @return fileName 文件在储存之后的名字
|
||||
* @throws IOException 抛出io异常
|
||||
*/
|
||||
private static String getFilename(MultipartFile file, Integer influence, String DIR) throws IOException {
|
||||
File dir = new File(DIR);
|
||||
if (!dir.exists()) dir.mkdirs();
|
||||
|
||||
// 取文件扩展名并检查是否合法
|
||||
String originalFileName = file.getOriginalFilename();
|
||||
String fileExtension = "";
|
||||
if (originalFileName != null && originalFileName.contains(".")) {
|
||||
fileExtension = originalFileName.substring(originalFileName.lastIndexOf(".")).toLowerCase();
|
||||
}
|
||||
|
||||
if (!AVATAR_ALLOWED_EXTENSIONS.contains(fileExtension)) {
|
||||
throw new IOException("仅支持 JPG, PNG, GIF 格式");
|
||||
}
|
||||
|
||||
// 以id进行命名,方便直接替换
|
||||
return influence.toString() + fileExtension;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,10 +1,166 @@
|
||||
package xin.merlin.myplayerbackend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import xin.merlin.myplayerbackend.entity.Account;
|
||||
import xin.merlin.myplayerbackend.entity.Audit;
|
||||
import xin.merlin.myplayerbackend.entity.http.Code;
|
||||
import xin.merlin.myplayerbackend.mapper.AccountMapper;
|
||||
import xin.merlin.myplayerbackend.mapper.AuditMapper;
|
||||
import xin.merlin.myplayerbackend.mapper.UserMapper;
|
||||
import xin.merlin.myplayerbackend.service.CodeService;
|
||||
import xin.merlin.myplayerbackend.service.MailService;
|
||||
import xin.merlin.myplayerbackend.utils.AESUtil;
|
||||
import xin.merlin.myplayerbackend.utils.result.Response;
|
||||
import xin.merlin.myplayerbackend.utils.result.ResultCode;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> {
|
||||
|
||||
private static final Cache<Integer, String> verifyURL = Caffeine.newBuilder()
|
||||
.expireAfterWrite(24, TimeUnit.HOURS)
|
||||
.build();
|
||||
|
||||
private final AccountMapper accountMapper;
|
||||
|
||||
private final AuditMapper auditMapper;
|
||||
|
||||
private final CodeService codeService;
|
||||
|
||||
private final MailService mailService;
|
||||
|
||||
private final AESUtil aesUtil;
|
||||
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
private Integer findByAccount(String account) {
|
||||
Account accountEntity = accountMapper.selectOne(
|
||||
Wrappers.<Account>lambdaQuery()
|
||||
.select(Account::getId)
|
||||
.eq(Account::getAccount, account)
|
||||
);
|
||||
return accountEntity == null ? null : accountEntity.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param code 验证码因子结构体
|
||||
* @param email 新邮箱
|
||||
* @return Response响应体
|
||||
*/
|
||||
@Transactional
|
||||
public Response changeEmail(Code code, String email) {
|
||||
try {
|
||||
Response response = codeService.verify(code);
|
||||
if (response.getCode().equals("200")){
|
||||
Integer id = findByAccount(code.getAccount());
|
||||
Audit audit = auditMapper.selectOne(Wrappers.<Audit>lambdaQuery().eq(Audit::getType,4).eq(Audit::getApplicant,id).eq(Audit::getInfluence,id));
|
||||
if (audit != null){
|
||||
audit.setType(6);
|
||||
auditMapper.updateById(audit);
|
||||
}
|
||||
String url = aesUtil.encrypt(id+":"+email);
|
||||
if (id != null) {
|
||||
verifyURL.put(id, url);
|
||||
}
|
||||
else {
|
||||
return Response.success(ResultCode.MAIL_INFO_LOST);
|
||||
}
|
||||
mailService.sendTextMail(email,"验证链接:\n"+"https://myplayer.merlin.xin/account/mail/verify/"+ url+"\n链接一天内有效,请尽快验证");
|
||||
auditMapper.insert(new Audit(4,id,id,email));
|
||||
codeService.getWaitingList().invalidate(code.getC_id());
|
||||
return Response.success(ResultCode.SUCCESS);
|
||||
}
|
||||
else{
|
||||
return response;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param encode 认证链接
|
||||
* @return response响应体
|
||||
*/
|
||||
@Transactional
|
||||
public Response verifyEmail(String encode) {
|
||||
try {
|
||||
Map<String, String> decode = aesUtil.decryptAndSplit(encode);
|
||||
Integer id = Integer.parseInt(decode.get("id"));
|
||||
String email = decode.get("email");
|
||||
Audit audit = auditMapper.selectOne(Wrappers.<Audit>lambdaQuery().eq(Audit::getType,4).eq(Audit::getApplicant,id).eq(Audit::getInfluence,id));
|
||||
if (audit == null) return Response.success(ResultCode.AUDIT_NO_RECORD);
|
||||
if (!encode.equals(verifyURL.getIfPresent(id))){
|
||||
audit.setType(6);
|
||||
auditMapper.updateById(audit);
|
||||
return Response.success(ResultCode.AUDIT_NO_RECORD);
|
||||
}
|
||||
if (!audit.getChanged().equals(email)) return Response.success(ResultCode.ACCOUNT_ILLEGAL_CHANGE);
|
||||
Account account = accountMapper.selectById(id);
|
||||
account.setAccount(email);
|
||||
accountMapper.updateById(account);
|
||||
audit.setType(5);
|
||||
auditMapper.updateById(audit);
|
||||
verifyURL.invalidate(id);
|
||||
return Response.success(ResultCode.SUCCESS);
|
||||
} catch (NumberFormatException e) {
|
||||
log.error(e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param code 验证码认证因子
|
||||
* @return response响应体
|
||||
*/
|
||||
@Transactional
|
||||
public Response resetPassword(Code code){
|
||||
try {
|
||||
Response response = codeService.verify(code);
|
||||
if (response.getCode().equals("200")){
|
||||
Integer id = findByAccount(code.getAccount());
|
||||
Account account = accountMapper.selectById(id);
|
||||
account.setPassword(passwordEncoder.encode(code.getPassword()));
|
||||
accountMapper.updateById(account);
|
||||
codeService.getWaitingList().invalidate(code.getC_id());
|
||||
return Response.success(ResultCode.SUCCESS);
|
||||
}
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// @Transactional
|
||||
// public void init(Account account) {
|
||||
// try {
|
||||
// account = accountMapper.selectOne(Wrappers.<Account>lambdaQuery().eq(Account::getAccount, account.getAccount()));
|
||||
// if(accountMapper.updateById(account)==0) throw new RuntimeException(account.getAccount()+"初始化未成功");
|
||||
// User u = new User();
|
||||
// u.setId(account.getId());
|
||||
// do{
|
||||
// u.setU_id(RandomCode.generateID());
|
||||
// }while (userMapper.selectById(u.getU_id())!=null);
|
||||
// userMapper.insert(u);
|
||||
// } catch (RuntimeException e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
package xin.merlin.myplayerbackend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import xin.merlin.myplayerbackend.entity.Audit;
|
||||
import xin.merlin.myplayerbackend.mapper.AuditMapper;
|
||||
import xin.merlin.myplayerbackend.mapper.UserMapper;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AuditServiceImpl extends ServiceImpl<AuditMapper, Audit> {
|
||||
|
||||
private final AuditMapper auditMapper;
|
||||
|
||||
private final UserMapper userMapper;
|
||||
|
||||
public Boolean passTypeOne(Audit audit) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ package xin.merlin.myplayerbackend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import xin.merlin.myplayerbackend.entity.Group;
|
||||
import xin.merlin.myplayerbackend.entity.GroupInfo;
|
||||
import xin.merlin.myplayerbackend.mapper.GroupMapper;
|
||||
|
||||
@Service
|
||||
public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> {
|
||||
public class GroupServiceImpl extends ServiceImpl<GroupMapper, GroupInfo> {
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ package xin.merlin.myplayerbackend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import xin.merlin.myplayerbackend.entity.Playroom;
|
||||
import xin.merlin.myplayerbackend.entity.PlayroomInfo;
|
||||
import xin.merlin.myplayerbackend.mapper.PlayroomMapper;
|
||||
|
||||
@Service
|
||||
public class PlayroomServiceImpl extends ServiceImpl<PlayroomMapper, Playroom> {
|
||||
public class PlayroomServiceImpl extends ServiceImpl<PlayroomMapper, PlayroomInfo> {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package xin.merlin.myplayerbackend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import xin.merlin.myplayerbackend.entity.UserInfo;
|
||||
import xin.merlin.myplayerbackend.mapper.UserMapper;
|
||||
|
||||
@Service
|
||||
public class UserServiceImpl extends ServiceImpl<UserMapper, UserInfo> {
|
||||
}
|
||||
67
src/main/java/xin/merlin/myplayerbackend/utils/AESUtil.java
Normal file
67
src/main/java/xin/merlin/myplayerbackend/utils/AESUtil.java
Normal file
@@ -0,0 +1,67 @@
|
||||
package xin.merlin.myplayerbackend.utils;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
@Component
|
||||
public class AESUtil {
|
||||
|
||||
// 16 位密钥(必须 16/24/32 位)
|
||||
private final String key;
|
||||
|
||||
private static final String ALGORITHM = "AES";
|
||||
|
||||
public AESUtil(@Value("${aes.key}") String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
/** 加密 */
|
||||
public String encrypt(String plainText) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
|
||||
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(encrypted);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("AES 加密失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** 解密 */
|
||||
public String decrypt(String cipherText) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec);
|
||||
byte[] decoded = Base64.getDecoder().decode(cipherText);
|
||||
byte[] original = cipher.doFinal(decoded);
|
||||
return new String(original, StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("AES 解密失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, String> decryptAndSplit(String cipherText) {
|
||||
String plainText = this.decrypt(cipherText); // 先解密
|
||||
String[] parts = plainText.split(":", 2); // 只分割一次,防止 email 里有 :
|
||||
|
||||
if (parts.length != 2) {
|
||||
throw new IllegalArgumentException("解密数据格式不正确:" + plainText);
|
||||
}
|
||||
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("id", parts[0]);
|
||||
result.put("email", parts[1]);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,12 @@ package xin.merlin.myplayerbackend.utils;
|
||||
import io.jsonwebtoken.*;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import xin.merlin.myplayerbackend.config.security.JwtProperties;
|
||||
import xin.merlin.myplayerbackend.entity.Account;
|
||||
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
@@ -13,6 +16,8 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtUtil {
|
||||
@@ -35,17 +40,28 @@ public class JwtUtil {
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void destroy() {
|
||||
log.info("JWT 组件正在关闭...");
|
||||
// 如有线程池、定时任务、IO 连接,在这里 shutdown/close
|
||||
// 本例仅把引用置空,帮助 GC(可选)
|
||||
this.jwtParser = null;
|
||||
this.key = null;
|
||||
log.info("JWT 组件已关闭");
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 JWT Token
|
||||
*/
|
||||
public String generateToken(String account, Integer id) {
|
||||
public String generateToken(Account account) {
|
||||
Date now = new Date();
|
||||
Date expireDate = new Date(now.getTime() + jwtProperties.getExpire() * 1000L);
|
||||
|
||||
return Jwts.builder()
|
||||
.subject(account)
|
||||
.claim("id", id)
|
||||
.claim("account", account)
|
||||
.subject(account.getAccount())
|
||||
.claim("id", account.getId())
|
||||
.claim("account", account.getAccount())
|
||||
.claim("character", account.getCharacter())
|
||||
.id(UUID.randomUUID().toString())
|
||||
.issuedAt(now)
|
||||
.expiration(expireDate)
|
||||
@@ -57,7 +73,7 @@ public class JwtUtil {
|
||||
/**
|
||||
* 解析 Token 获取 Claims
|
||||
*/
|
||||
public Claims getClaims(String token) {
|
||||
private Claims getClaims(String token) {
|
||||
try {
|
||||
Jws<Claims> jws = jwtParser.parseSignedClaims(token);
|
||||
System.out.println(jws.getPayload());
|
||||
@@ -95,6 +111,16 @@ public class JwtUtil {
|
||||
return claims.get("id", Integer.class);
|
||||
}
|
||||
|
||||
// public Integer getCharacter(String token) {
|
||||
// Claims claims = getClaims(token);
|
||||
// return claims.get("character", Integer.class);
|
||||
// }
|
||||
|
||||
public Boolean isAdmin(String token) {
|
||||
Claims claims = getClaims(token);
|
||||
return claims.get("character", Integer.class)==0;
|
||||
}
|
||||
|
||||
// 自定义异常类
|
||||
public static class TokenExpiredException extends RuntimeException {
|
||||
public TokenExpiredException(String message, Throwable cause) {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package xin.merlin.myplayerbackend.utils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
public class SHA256Util {
|
||||
|
||||
public static String sha256(String s) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
byte[] bytes = md.digest(s.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) sb.append(String.format("%02x", b));
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,10 +15,16 @@ public enum ResultCode {
|
||||
NOT_FOUND("404", "资源不存在"),
|
||||
SERVER_ERROR("500", "服务器内部错误"),
|
||||
|
||||
|
||||
// 自定义业务错误码
|
||||
|
||||
//账户相关
|
||||
ACCOUNT_EXIST("2001","账户已存在"),
|
||||
ACCOUNT_EXIST("3001","账户已存在"),
|
||||
ACCOUNT_NOT_INIT("3002","账户未初始化"),
|
||||
ACCOUNT_PWD_ERROR("3003","账户密码错误"),
|
||||
ACCOUNT_INFO_LOST("3004","账户信息丢失"),
|
||||
ACCOUNT_ILLEGAL_CHANGE("3005","账户非法篡改"),
|
||||
ACCOUNT_PERMISSION_DENY("3006","用户权限不足"),
|
||||
|
||||
//用户相关
|
||||
USER_BANNED("4000","用户被封禁"),
|
||||
@@ -28,7 +34,7 @@ public enum ResultCode {
|
||||
USER_VERIFICATION_ERROR("4005","验证码不存在或错误"),
|
||||
USER_SEND_TOO_FAST("4006","用户请求过快"),
|
||||
USER_SEND_TOO_OFTEN("4007","请求次数过多,已被限制"),
|
||||
ORDER_NOT_FOUND("4000", "订单不存在"),
|
||||
USER_ILLEGAL_REQUEST("4008", "用户非法请求"),
|
||||
|
||||
//邮箱相关
|
||||
MAIL_ACCOUNT_NOT_PROVIDED("4101","未提供验证码接受账户"),
|
||||
@@ -36,7 +42,10 @@ public enum ResultCode {
|
||||
MAIL_INFO_LOST("4103","验证信息丢失"),
|
||||
MAIL_VERIFY_FAIL_TOO_MANY("4104","验证码错误过多,请重新申请验证码"),
|
||||
MAIL_VERIFY_NOT_EXIST("4105","验证码元素丢失,请重新申请验证码"),
|
||||
MAIL_VERIFY_CODE_ERROR("4106","验证码错误,请重新输入");
|
||||
MAIL_VERIFY_CODE_ERROR("4106","验证码错误,请重新输入"),
|
||||
|
||||
//审核相关
|
||||
AUDIT_NO_RECORD("4201","无审核记录条目");
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user