Compare commits

...

12 Commits

Author SHA1 Message Date
2cfadf96dd 1.添加邮箱验证 2025-08-06 20:45:49 +08:00
fda3e45dc6 1.修改用户更新逻辑 2025-08-05 22:04:36 +08:00
65c41326f4 1.添加 web 端扫码登录功能 2025-08-05 21:15:01 +08:00
17e07e3985 1.微信用户登录添加 token 2025-08-05 15:52:14 +08:00
3106d787da 1.用户修改邮箱逻辑
2.用户验证邮箱逻辑
2025-08-05 15:39:38 +08:00
200fd90339 1.用户邮箱登录注册接口实现 2025-08-05 15:16:07 +08:00
e0e733bc27 1.用户邮箱登录注册接口实现 2025-08-05 15:16:03 +08:00
f5cbe5cac2 1.添加邮箱登录,注册接口 2025-08-04 21:58:55 +08:00
715e6a1b70 1.修改pk文章详情 2025-08-04 15:03:53 +08:00
ba60cfb550 1.修改pk列表接口 2025-08-04 14:44:45 +08:00
451fdc4556 1.修改创建 创建PK记录 2025-08-04 14:31:38 +08:00
8d6e6b3002 1.Pk添加服务层 2025-08-04 14:10:04 +08:00
32 changed files with 1944 additions and 208 deletions

6
.gitignore vendored
View File

@@ -24,5 +24,7 @@
hs_err_pid*
replay_pid*
!/target/
!/.idea/
/.idea/
/target/

Binary file not shown.

25
.idea/MyBatisCodeHelperDatasource.xml generated Normal file
View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MyBatisCodeHelperDatasource">
<option name="projectProfile">
<ProjectProfile>
<option name="controllerTemplateString" value="&#10;#* @vtlvariable name=&quot;tableName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;entityPackageName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;entityClassName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;servicePackageName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;serviceInterfacePackage&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;serviceClassName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;serviceInterfaceClassName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;mapperPackageName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;mapperClassName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;controllerPackage&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;tableRemark&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;myDate&quot; type=&quot;java.util.Date&quot; *#&#10;#* @vtlvariable name=&quot;simpleDateFormat&quot; type=&quot;java.text.SimpleDateFormat&quot; *#&#10;package $!{controllerPackage};&#10;import $!{entityPackageName}.$!{entityClassName};&#10;###set($realServiceName = $!{serviceClassName}+'Impl')&#10;import $!{servicePackageName}.$!{serviceClassName};&#10;import org.springframework.web.bind.annotation.*;&#10;&#10;#set($serviceFirstLower = $!{serviceClassName.substring(0,1).toLowerCase()}+$!{serviceClassName.substring(1,$!{serviceClassName.length()})})&#10;import org.springframework.beans.factory.annotation.Autowired;&#10;&#10;/**&#10;* $!{tableRemark}($!{tableName})表控制层&#10;*&#10;* @author xxxxx&#10;*/&#10;@RestController&#10;@RequestMapping(&quot;/$!{tableName}&quot;)&#10;public class $!{entityClassName}Controller {&#10;/**&#10;* 服务对象&#10;*/&#10; @Autowired&#10; private $!{serviceClassName} $!{serviceFirstLower};&#10;&#10; /**&#10; * 通过主键查询单条数据&#10; *&#10; * @param id 主键&#10; * @return 单条数据&#10; */&#10; @GetMapping(&quot;selectOne&quot;)&#10; public $!{entityClassName} selectOne(Integer id) {&#10; return $!{serviceFirstLower}.selectByPrimaryKey(id);&#10; }&#10;&#10;}" />
<option name="tableGenerateConfigs">
<map>
<entry key="vv_assistant:user">
<value>
<TableGenerateConfig>
<option name="generatedKey" value="id" />
<option name="javaModelName" value="User" />
<option name="sequenceColumn" value="" />
<option name="sequenceId" value="" />
<option name="useActualColumnName" value="false" />
</TableGenerateConfig>
</value>
</entry>
</map>
</option>
</ProjectProfile>
</option>
</component>
</project>

58
pom.xml
View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.vv.pk.assistant</groupId>
@@ -102,6 +102,62 @@
<version>5.6.227</version>
</dependency>
<!-- mailgun 邮件服务 -->
<dependency>
<groupId>com.mailgun</groupId>
<artifactId>mailgun-java</artifactId>
<version>2.0.0</version>
</dependency>
<!-- caffeine 本地缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- crypto 加密 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>5.5.1</version> <!-- 请检查最新版本 -->
</dependency>
<!-- Sa-Token 权限认证在线文档https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.44.0</version>
</dependency>
<!-- hu-tool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.38</version>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.6.2</version>
</dependency>
<!-- 二维码生成-->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.4.1</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>

View File

@@ -1,8 +1,10 @@
package vvpkassistant;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class Application {
public static void main(String[] args) {

View File

@@ -0,0 +1,22 @@
package vvpkassistant.Tools;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/*
* @author: ziin
* @date: 2025/8/4 18:19
*/
public class BcryptUtils {
public static String encryptPassword(String password) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
return passwordEncoder.encode(password);
}
public static Boolean matchPassword(String rawPassWord,String encodePassword){
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
return passwordEncoder.matches(encodePassword, rawPassWord);
}
}

View File

@@ -0,0 +1,16 @@
package vvpkassistant.Tools;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
/*
* @author: ziin
* @date: 2025/8/6 20:15
*/
public class CacheHolder {
public static final Cache<String, String> VERIFICATION_MAIL = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
}

View File

@@ -0,0 +1,31 @@
package vvpkassistant.Tools;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.Hashtable;
public class QRCodeUtil {
public static String generateQRCode(String content, int width, int height)
throws WriterException, IOException {
Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
BitMatrix matrix = new MultiFormatWriter().encode(
content, BarcodeFormat.QR_CODE, width, height, hints);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(matrix, "PNG", outputStream);
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
}
}

View File

@@ -1,4 +1,7 @@
package vvpkassistant.User.controller;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.extra.mail.Mail;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import vvpkassistant.CoinRecords.CoinRecords;
@@ -7,16 +10,26 @@ import vvpkassistant.Data.ResponseData;
import vvpkassistant.Data.ResponseInfo;
import vvpkassistant.Data.WxChatParam;
import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.User.model.DTO.LoginInfoDTO;
import vvpkassistant.User.model.DTO.ScanInfoDTO;
import vvpkassistant.User.model.DTO.UserModelDTO;
import vvpkassistant.User.model.UserModel;
import vvpkassistant.User.model.UserModelVO;
import vvpkassistant.User.model.enumeration.LoginStatusEnum;
import vvpkassistant.User.service.UserService;
import vvpkassistant.common.ErrorCode;
import vvpkassistant.config.FunctionConfigHolder;
import vvpkassistant.Tools.VVRequester;
import vvpkassistant.Tools.VVTools;
import vvpkassistant.exception.BusinessException;
import vvpkassistant.mail.model.MailModel;
import vvpkassistant.mail.service.MailService;
import vvpkassistant.pk.mapper.PkInfoDao;
import vvpkassistant.pk.model.PkInfoModel;
import vvpkassistant.pk.model.PkRecordDetail;
import vvpkassistant.pk.mapper.PkRecordDetailDao;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -43,6 +56,13 @@ public class UserController {
@Autowired
private VVRequester vvRequester;
@Resource
private UserService userService;
@Resource
private MailService mailService;
// 配置用户信息
@PostMapping("inputUserInfo")
public ResponseData<Object> inputUserInfo(@RequestBody Map<String,Object> param) {
@@ -107,7 +127,10 @@ public class UserController {
// return ResponseData.error(ResponseInfo.ERROR,"创建聊天账号失败,请稍后再试");
// }
Map<String,Object> result = new HashMap<>();
result.put("info", tempModel);
StpUtil.login(tempModel.getId());
UserModelVO userModelVO = BeanUtil.copyProperties(tempModel, UserModelVO.class);
userModelVO.setToken(StpUtil.getTokenValue());
result.put("info", userModelVO);
result.put("newAccount",false);
//否则直接返回用户
return ResponseData.success(result);
@@ -133,7 +156,10 @@ public class UserController {
UserModel model = userDao.queryWithPhoneNumber(phoneNumber);
Map<String,Object> result = new HashMap<>();
if (model != null) { // 老用户
result.put("info",model);
UserModelVO userModelVO = BeanUtil.copyProperties(model, UserModelVO.class);
StpUtil.login(userModelVO.getId());
userModelVO.setToken(StpUtil.getTokenValue());
result.put("info",userModelVO);
result.put("newAccount", false);
result.put("chatInfo",wxChatParam);
return ResponseData.success(result);
@@ -156,45 +182,34 @@ public class UserController {
userDao.updateById(oldUser);
}
result.put("info",tempModel);
UserModelVO userModelVO = BeanUtil.copyProperties(tempModel, UserModelVO.class);
StpUtil.login(userModelVO.getId());
userModelVO.setToken(StpUtil.getTokenValue());
result.put("info",userModelVO);
result.put("newAccount",true);
result.put("chatInfo",wxChatParam);
return ResponseData.success(result);
}
}
//todo 修改用户返回结果
// 修改用户信息
@PostMapping("updateUserInfo")
public ResponseData<Object> updateUserInfo(@RequestBody Map<String,Object> map) {
UserModel userModel = new UserModel();
//设置用户id
userModel.setId(Integer.valueOf(map.get("id").toString()));
//设置用户头像
userModel.setHeaderIcon(map.get("headerIcon").toString());
//设置用户昵称
userModel.setNickName(map.get("nickName").toString());
int i = userDao.updateById(userModel);
// 返回结果
Map<String,Object> result = new HashMap<>();
result.put("info", userDao.selectById(map.get("id").toString()));
result.put("newAccount", false);
if (i == 1){
return ResponseData.success(result);
}else {
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
public ResponseData<Object> updateUserInfo(@RequestBody UserModelDTO userModelDTO) {
UserModelVO userModelVO = userService.updateUserInfo( userModelDTO);
return ResponseData.success(userModelVO);
}
// 获取用户信息
@PostMapping("getUserInfo")
public ResponseData<Object> getUserInfo(@RequestBody Map<String,Integer> map) {
UserModel userModel = userDao.selectById(map.get("id"));
return ResponseData.success(userModel);
UserModelVO userModelVO = BeanUtil.copyProperties(userModel, UserModelVO.class);
userModelVO.setHavaPassword(userModel.getPassword() != null);
return ResponseData.success(userModelVO);
}
// 查询用户所有pk数据
@@ -367,5 +382,81 @@ public class UserController {
return ResponseData.success(coinRecords);
}
@PostMapping("/loginWithMail")
public ResponseData<Object> loginWithMail(@RequestBody UserModelDTO model) {
return ResponseData.success(userService.loginWithMail(model));
}
@PostMapping("/registerWithMail")
public ResponseData<Object> mailRegister(@RequestBody UserModelDTO model){
return ResponseData.success(userService.addUserWithMail(model));
}
@GetMapping("/activateAccount")
public ResponseData<Object> activateAccount(@RequestParam("token") String token){
return ResponseData.success(userService.activateAccount(token));
}
@PostMapping("/resendMail")
public ResponseData<Object> resendMail(@RequestBody MailModel mailModel){
return ResponseData.success(mailService.resendMail(mailModel));
}
@GetMapping("/verificationMail")
public ResponseData<Object> verificationMail(@RequestParam("token") String token){
return ResponseData.success(userService.verificationMail(token));
}
@PostMapping("/forgetMail")
public ResponseData<Object> sendForgetPassWordMail(@RequestBody MailModel mailModel){
return ResponseData.success(userService.sendForgetPassWordMail(mailModel));
}
@GetMapping("/qrcode")
public ResponseData<Object> generatedQrcode(){
return ResponseData.success(userService.generatedQrcode());
}
@GetMapping("/check/{uuid}")
public ResponseData<Object> checkQrcode(@PathVariable String uuid){
return ResponseData.success(userService.checkQrcode(uuid));
}
@PostMapping("/scan")
public ResponseData<?> scanQrCode(@RequestBody ScanInfoDTO scanInfoDTO) {
userService.scanQrcode(scanInfoDTO);
return ResponseData.success("");
}
@PostMapping("/confirm")
public ResponseData<?> confirm(@RequestBody ScanInfoDTO scanInfoDTO) {
userService.confirm(scanInfoDTO);
return ResponseData.success("");
}
@PostMapping("/logout")
public ResponseData<Object> logOut(@RequestBody UserModelDTO userModelDTO){
userService.logOut(userModelDTO.getId());
return ResponseData.success("");
}
@PostMapping("/setPassword")
public ResponseData<Object>setPassWord(@RequestBody UserModelDTO userModelDTO){
return ResponseData.success(userService.setPassWord(userModelDTO));
}
@GetMapping("/resetPassword/")
public ResponseData<Object>resetPassWord(@RequestBody UserModelDTO userModelDTO){
return ResponseData.success(userService.resetPassWord(userModelDTO));
}
@PostMapping("/updateUserMail")
public ResponseData<Object>updateUserMail(@RequestBody MailModel mailModel){
return ResponseData.success(userService.updateUserMail(mailModel));
}
@PostMapping("/sendUpdateMailConfirmMail")
public ResponseData<Object>sendUpdateMailConfirmMail(@RequestBody MailModel mailModel){
return ResponseData.success(mailService.sendUpdateConfirmMail(mailModel));
}
}

View File

@@ -0,0 +1,29 @@
package vvpkassistant.User.model.DTO;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginInfoDTO {
/**
* 唯一标识
*/
private String uuid;
/**
* 设备号
*/
private String device;
/**
* 扫码状态
*/
private String status;
private Integer userId;
}

View File

@@ -0,0 +1,13 @@
package vvpkassistant.User.model.DTO;
import lombok.Data;
/*
* @author: ziin
* @date: 2025/8/5 16:43
*/
@Data
public class ScanInfoDTO {
private String uuid;
private Integer userId;
}

View File

@@ -0,0 +1,28 @@
package vvpkassistant.User.model.DTO;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
@Data
public class UserModelDTO {
@TableId(type = IdType.AUTO)
private Integer id; // 主键
private String nickName; // 昵称
private String phoneNumber; // 手机号码
private String headerIcon; // 头像
private String openid; // openid
private String sessionKey; // session key
private Integer status; // 用户状态 0 正常 其他业务逻辑待定
private Long createTime; // 创建时间
private String userChatId; // 聊天使用的id使用微信的openid作为标识
private Integer points; // 用户积分
private Integer inviterId; // 邀请人id
private String email;
private String newPassword;
private String confirmPassword;
private String oldPassword;
private String password;
private String token;
}

View File

@@ -0,0 +1,13 @@
package vvpkassistant.User.model;
import lombok.Data;
/*
* @author: ziin
* @date: 2025/8/5 20:06
*/
@Data
public class QrcodeEntity {
private String uuid;
private String type;
}

View File

@@ -0,0 +1,15 @@
package vvpkassistant.User.model;
import lombok.Builder;
import lombok.Data;
/*
* @author: ziin
* @date: 2025/8/5 18:14
*/
@Data
@Builder
public class QrcodeVO {
private String uuid;
private String qrcode;
}

View File

@@ -0,0 +1,32 @@
package vvpkassistant.User.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ResponseVO {
/**
* 唯一标识
*/
private String uuid;
/**
* 登录二维码
*/
private String qrcode;
/**
* jwt令牌
*/
private String token;
/**
* 扫码状态
*/
private String status;
}

View File

@@ -20,4 +20,7 @@ public class UserModel {
private String userChatId; // 聊天使用的id使用微信的openid作为标识
private Integer points; // 用户积分
private Integer inviterId; // 邀请人id
private String email;
private String password;
private Integer mailVerification;
}

View File

@@ -0,0 +1,28 @@
package vvpkassistant.User.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import vvpkassistant.Data.WxChatParam;
@Data
public class UserModelVO {
private Integer id; // 主键
private String nickName; // 昵称
private String phoneNumber; // 手机号码
private String headerIcon; // 头像
private String openid; // openid
private String sessionKey; // session key
private Integer status; // 用户状态 0 正常 其他业务逻辑待定
private Long createTime; // 创建时间
private String userChatId; // 聊天使用的id使用微信的openid作为标识
private Integer points; // 用户积分
private Integer inviterId; // 邀请人id
private String email;
private String token;
private Boolean newAccount;
private WxChatParam chatInfo;
private Boolean havaPassword;
private Integer mailVerification;
}

View File

@@ -0,0 +1,17 @@
package vvpkassistant.User.model.enumeration;
import lombok.Getter;
@Getter
public enum LoginStatusEnum {
UNSCANNED("未扫描"),
SCANNED("已扫描"),
CONFIRMED("已确认");
private String desc;
LoginStatusEnum(String desc) {
this.desc = desc;
}
}

View File

@@ -0,0 +1,43 @@
package vvpkassistant.User.service;
import com.baomidou.mybatisplus.extension.service.IService;
import vvpkassistant.User.model.DTO.ScanInfoDTO;
import vvpkassistant.User.model.DTO.UserModelDTO;
import vvpkassistant.User.model.UserModel;
import vvpkassistant.User.model.UserModelVO;
import vvpkassistant.mail.model.MailModel;
/*
* @author: ziin
* @date: 2025/8/4 16:19
*/
public interface UserService extends IService<UserModel> {
UserModelVO loginWithMail(UserModelDTO model);
UserModelVO updateUserInfo(UserModelDTO userModelDTO);
UserModelVO addUserWithMail(UserModelDTO model);
Boolean activateAccount(String token);
Boolean verificationMail(String token);
Object generatedQrcode();
Object checkQrcode(String uuid);
void scanQrcode(ScanInfoDTO scanInfoDTO);
void confirm(ScanInfoDTO scanInfoDTO);
void logOut(Integer id);
boolean setPassWord(UserModelDTO userModelDTO);
Object sendForgetPassWordMail(MailModel mailModel);
Object resetPassWord(UserModelDTO userModelDTO);
Boolean updateUserMail(MailModel mailModel);
}

View File

@@ -0,0 +1,334 @@
package vvpkassistant.User.service;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.temp.SaTempUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.zxing.WriterException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import vvpkassistant.Data.WxChatParam;
import vvpkassistant.Tools.BcryptUtils;
import vvpkassistant.Tools.CacheHolder;
import vvpkassistant.Tools.QRCodeUtil;
import vvpkassistant.Tools.VVTools;
import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.User.model.*;
import vvpkassistant.User.model.DTO.LoginInfoDTO;
import vvpkassistant.User.model.DTO.ScanInfoDTO;
import vvpkassistant.User.model.DTO.UserModelDTO;
import vvpkassistant.User.model.enumeration.LoginStatusEnum;
import vvpkassistant.common.ErrorCode;
import vvpkassistant.exception.BusinessException;
import vvpkassistant.mail.model.MailModel;
import vvpkassistant.mail.service.MailService;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/*
* @author: ziin
* @date: 2025/8/4 16:19
*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserDao, UserModel> implements UserService {
@Resource
private UserDao userDao;
@Resource
private WxChatParam wxChatParam;
@Autowired
private MailService mailService;
private final Cache<String, LoginInfoDTO> qrcodeCache = Caffeine.newBuilder()
.expireAfterWrite(2, TimeUnit.MINUTES)
.build();
@Override
public UserModelVO loginWithMail(UserModelDTO model) {
LambdaQueryWrapper<UserModel> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(UserModel::getEmail,model.getEmail())
.in(UserModel::getStatus, 0,2);
UserModel userModel = userDao.selectOne(lambdaQueryWrapper);
if (userModel == null) {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
String password = userModel.getPassword();
UserModelVO userModelVO = BeanUtil.copyProperties(userModel, UserModelVO.class);
if (BcryptUtils.matchPassword(password, model.getPassword())) {
StpUtil.login(userModel.getId());
userModelVO.setToken(StpUtil.getTokenValue());
userModelVO.setChatInfo(wxChatParam);
return userModelVO;
}else {
throw new BusinessException(ErrorCode.PASSWORD_ERROR);
}
}
@Override
public UserModelVO updateUserInfo(UserModelDTO userModelDTO) {
UserModel userInfo = userDao.selectById(userModelDTO.getId());
if (userInfo == null) {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
// 用户没有密码的情况下设置密码
if (userInfo.getPassword() == null && userModelDTO.getNewPassword() != null) {
if (!userModelDTO.getNewPassword().isEmpty()){
if (userModelDTO.getNewPassword().length()<6){
throw new BusinessException(ErrorCode.PARAMS_ERROR,"密码长度不能小于 6 位");
}
userModelDTO.setPassword(BcryptUtils.encryptPassword(userModelDTO.getNewPassword()));
}
}
if (userModelDTO.getEmail() != null) {
mailService.sendVerificationMail(userModelDTO.getEmail(), userModelDTO.getId());
}
// 用户有密码的情况下重新设置密码
if (userInfo.getPassword() != null && userModelDTO.getOldPassword() != null) {
if (BcryptUtils.matchPassword(userInfo.getPassword(),userModelDTO.getOldPassword())) {
userModelDTO.setPassword(BcryptUtils.encryptPassword(userModelDTO.getNewPassword()));
}else {
throw new BusinessException(ErrorCode.PASSWORD_ERROR,"旧密码不正确");
}
}
UserModel userModel = BeanUtil.copyProperties(userModelDTO, UserModel.class);
int i = userDao.updateById(userModel);
// 返回结果
UserModel afterUserInfo = userDao.selectById(userModel.getId());
UserModelVO userModelVO = BeanUtil.copyProperties(afterUserInfo, UserModelVO.class);
userModelVO.setNewAccount(false);
if (i == 1){
return userModelVO;
}else {
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
}
@Override
public UserModelVO addUserWithMail(UserModelDTO userModelDTO) {
LambdaQueryWrapper<UserModel> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(UserModel::getEmail,userModelDTO.getEmail());
UserModel userModel = userDao.selectOne(lambdaQueryWrapper);
if (userModel != null) {
throw new BusinessException(ErrorCode.MAIL_ALREADY_EXIST);
}
if (userModelDTO.getPassword().length() < 6 ){
throw new BusinessException(ErrorCode.PARAMS_ERROR,"密码长度不能小于 6 位");
}
userModelDTO.setPassword(BcryptUtils.encryptPassword(userModelDTO.getPassword()));
userModelDTO.setCreateTime(VVTools.currentTimeStamp());
//设置状态为待验证
userModelDTO.setStatus(2);
//设置积分为0
userModelDTO.setPoints(0);
UserModel userModelEntity = BeanUtil.copyProperties(userModelDTO, UserModel.class);
userModelEntity.setMailVerification(1);
if ( userDao.insert(userModelEntity) != 1){
throw new BusinessException(ErrorCode.ADD_FAILED,"用户注册失败");
}
mailService.sendMail(userModelDTO.getEmail(),userModelEntity.getId());
// 判断用户是否为邀请用户
if (userModelDTO.getInviterId() != null) {
UserModel oldUser = userDao.selectById(userModelDTO.getInviterId());
oldUser.setPoints(oldUser.getPoints() + 10);
userDao.updateById(oldUser);
}
UserModelVO userModelVO = BeanUtil.copyProperties(userModelEntity, UserModelVO.class);
userModelVO.setHavaPassword(true);
userModelVO.setNewAccount(true);
userModelVO.setChatInfo(wxChatParam);
return userModelVO;
}
@Override
public Boolean activateAccount(String token) {
Integer userId = SaTempUtil.parseToken(token, Integer.class);
UserModel userModel = userDao.selectById(userId);
if (userModel == null) {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
userModel.setStatus(0);
userModel.setMailVerification(0);
if (userDao.updateById(userModel) == 1){
return true;
}else {
throw new BusinessException(ErrorCode.UPDATE_FAILED,"激活失败");
}
}
@Override
public Boolean verificationMail(String token) {
Integer userId = SaTempUtil.parseToken(token, Integer.class);
UserModel userModel = userDao.selectById(userId);
userModel.setMailVerification(0);
if (userDao.updateById(userModel) == 1){
return true;
}
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"邮箱验证失败");
}
@Override
public QrcodeVO generatedQrcode() {
String uuid = UUID.randomUUID().toString();
QrcodeEntity qrcodeEntity = new QrcodeEntity();
qrcodeEntity.setUuid(uuid);
qrcodeEntity.setType("qrcdoe");
String base64QR = null;
try {
base64QR = QRCodeUtil.generateQRCode(JSONUtil.toJsonStr(qrcodeEntity), 200, 200);
} catch (WriterException | IOException e) {
log.error(e.getMessage());
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"二维码生成失败");
}
LoginInfoDTO loginInfoDTO = new LoginInfoDTO();
loginInfoDTO.setStatus(LoginStatusEnum.UNSCANNED.name());
loginInfoDTO.setUuid(uuid);
// 二维码uuid绑定存入缓存
qrcodeCache.put(uuid,loginInfoDTO);
// 返回生成的二维码信息
QrcodeVO vo = QrcodeVO.builder().uuid(uuid).qrcode("data:image/png;base64," + base64QR).build();
log.info("-------生成二维码成功:{}-------", uuid);
return vo;
}
@Override
public Object checkQrcode(String uuid) {
LoginInfoDTO loginInfoDTO = qrcodeCache.getIfPresent(uuid);
if (loginInfoDTO == null) {
throw new BusinessException(ErrorCode.QRCODE_EXPIRED);
}
if (Objects.equals(loginInfoDTO.getStatus(), LoginStatusEnum.SCANNED.name())) {
return loginInfoDTO;
}
if (LoginStatusEnum.CONFIRMED.name().equals(loginInfoDTO.getStatus())) {
UserModel userModel = userDao.selectById(loginInfoDTO.getUserId());
StpUtil.login(userModel.getId());
UserModelVO userModelVO = BeanUtil.copyProperties(userModel, UserModelVO.class);
userModelVO.setToken(StpUtil.getTokenValue());
userModelVO.setChatInfo(wxChatParam);
return userModelVO;
}
return null;
}
@Override
public void scanQrcode(ScanInfoDTO scanInfoDTO) {
LoginInfoDTO loginInfoDTO = qrcodeCache.getIfPresent(scanInfoDTO.getUuid());
if (loginInfoDTO != null) {
loginInfoDTO.setStatus(LoginStatusEnum.SCANNED.name());
}
if (loginInfoDTO != null) {
qrcodeCache.put(scanInfoDTO.getUuid(),loginInfoDTO);
}
log.info("-------扫码成功uuid:{}-------", scanInfoDTO.getUuid());
}
@Override
public void confirm(ScanInfoDTO scanInfoDTO) {
LoginInfoDTO loginInfoDTO = qrcodeCache.getIfPresent(scanInfoDTO.getUuid());
if (loginInfoDTO != null) {
loginInfoDTO.setStatus(LoginStatusEnum.CONFIRMED.name());
loginInfoDTO.setUserId(scanInfoDTO.getUserId());
qrcodeCache.put(scanInfoDTO.getUuid(),loginInfoDTO);
}
log.info("-------确认登录成功uuid:{}-------", scanInfoDTO.getUuid());
}
@Override
public void logOut(Integer id) {
StpUtil.logout(id);
}
@Override
public boolean setPassWord(UserModelDTO userModelDTO) {
UserModel userModel = userDao.selectById(userModelDTO.getId());
if (userModel == null) {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
if (userModel.getPassword()!= null){
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"已设置过密码");
}
if (userModelDTO.getPassword().length()< 6 ){
throw new BusinessException(ErrorCode.PARAMS_ERROR,"密码长度不能小于 6 位");
}
if (!Objects.equals(userModelDTO.getPassword(), userModelDTO.getConfirmPassword())) {
log.error("密码{},确认密码{}",userModelDTO.getPassword(),userModelDTO.getConfirmPassword());
throw new BusinessException(ErrorCode.PARAMS_ERROR,"两次密码输入不一致");
}else{
UserModel saveEntity = BeanUtil.copyProperties(userModelDTO, UserModel.class);
saveEntity.setPassword(BcryptUtils.encryptPassword(userModelDTO.getPassword()));
return userDao.updateById(saveEntity) == 1 ;
}
}
@Override
public Object sendForgetPassWordMail(MailModel mailModel) {
LambdaQueryWrapper<UserModel> lambdaQueryWrapper = new LambdaQueryWrapper<>();
UserModel userModel = userDao.selectOne(lambdaQueryWrapper
.eq(UserModel::getEmail, mailModel.getMailAddress())
.eq(UserModel::getStatus, 0)
.eq(UserModel::getMailVerification, 0));
if (userModel == null) {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
mailService.sendForgetPassWordMail(mailModel.getMailAddress(),userModel.getId());
return true;
}
@Override
public Object resetPassWord(UserModelDTO userModelDTO) {
Integer i = SaTempUtil.parseToken(userModelDTO.getToken(), Integer.class);
UserModel userModel = userDao.selectById(i);
if (userModel == null) {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
if (userModelDTO.getPassword().equals(userModelDTO.getConfirmPassword())) {
UserModel updateEntity = BeanUtil.copyProperties(userModelDTO, UserModel.class);
updateEntity.setPassword(BcryptUtils.encryptPassword(updateEntity.getPassword()));
return userDao.updateById(updateEntity) == 1;
}
return false;
}
@Override
public Boolean updateUserMail(MailModel mailModel) {
String mail = CacheHolder.VERIFICATION_MAIL.getIfPresent(mailModel.getCode());
if (mail != null && mail.isEmpty()) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"验证码过期或验证码错误");
}
LambdaQueryWrapper<UserModel> lambdaQueryWrapper = new LambdaQueryWrapper<>();
UserModel userModel = userDao.selectOne(lambdaQueryWrapper
.eq(UserModel::getEmail, mail)
.eq(UserModel::getStatus, 0));
userModel.setEmail(mailModel.getMailAddress());
mailService.sendVerificationMail(mailModel.getMailAddress(),userModel.getId());
userModel.setMailVerification(1);
return userDao.updateById(userModel) == 1;
}
}

View File

@@ -15,6 +15,8 @@ public enum ErrorCode {
UPDATE_FAILED(1003, "更新失败"),
CONFIG_NAME_DUPLICATE(1004, "配置名称重复"),
SIGN_IN_FAIL(1004, "当天已签到"),
EMAIL_SEND_FREQUENT(1005,"邮件发送太频繁,请 1 分钟后再试"),
QRCODE_EXPIRED(1006,"二维码已过期"),
/* =============== 主播相关 =============== */
ANCHOR_ALREADY_EXISTS(2001, "主播已存在"),
@@ -27,7 +29,11 @@ public enum ErrorCode {
WX_SYSTEM_BUSY(-1, "系统繁忙"),
/* =============== 登录/用户信息 =============== */
WX_GET_USER_INFO_FAILED(50001, "获取用户信息失败,请稍后再试");
WX_GET_USER_INFO_FAILED(50001, "获取用户信息失败,请稍后再试"),
USER_DOES_NOT_EXIST(5002,"用户不存在"),
MAIL_ALREADY_EXIST(5003,"邮箱已存在"),
PASSWORD_ERROR(5004, "用户名或密码错误" ),
TOKEN_INVALID(40400, "Token无效请重新登录");
/**
* 状态码

View File

@@ -0,0 +1,78 @@
package vvpkassistant.config;
import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@Slf4j
public class SaTokenConfigure implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册Sa-Token的拦截器
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
.addPathPatterns("/**")
.excludePathPatterns(getExcludePaths());
}
/**
* 获取需要放行的路径
*/
private String[] getExcludePaths() {
return new String[]{
// Swagger & Knife4j 相关
"/error",
"/doc.html",
"/webjars/**",
"/swagger-resources/**",
"/v2/api-docs",
"/v3/api-docs",
"/v3/api-docs/**",
"/swagger-ui.html",
"/swagger-ui/**",
"/favicon.ico",
// 你的其他放行路径,例如登录接口
"/user/loginWithPhoneNumber",
"/user/registerWithMail",
"/user/loginWithMail",
"/user/inputUserInfo",
"/user/verificationMail",
"/user/activate",
"/user/qrcode",
"/user/check/**",
"/user/scan",
"/user/confirm"
};
}
@Bean
public SaCorsHandleFunction corsHandle() {
return (req, res, sto) -> {
res.
// 允许指定域访问跨域资源
setHeader("Access-Control-Allow-Origin", "*")
// 允许所有请求方式
.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
// 有效时间
.setHeader("Access-Control-Max-Age", "3600")
// 允许的header参数
.setHeader("Access-Control-Allow-Headers", "*");
// 如果是预检请求,则立即返回到前端
SaRouter.match(SaHttpMethod.OPTIONS)
.back();
};
}
}

View File

@@ -1,5 +1,6 @@
package vvpkassistant.exception;
import cn.dev33.satoken.exception.NotLoginException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@@ -28,6 +29,44 @@ public class GlobalExceptionHandler {
log.error("RuntimeException", e);
return ResponseData.error(ErrorCode.SYSTEM_ERROR);
}
// 全局异常拦截拦截项目中的NotLoginException异常
@ExceptionHandler(NotLoginException.class)
public ResponseData<?> handlerNotLoginException(NotLoginException nle)
throws Exception {
// 打印堆栈,以供调试
nle.printStackTrace();
// 判断场景值,定制化异常信息
String message = "";
if(nle.getType().equals(NotLoginException.NOT_TOKEN)) {
message = "未能读取到有效用户令牌";
}
else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) {
message = "令牌无效";
}
else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
message = "令牌已过期";
}
else if(nle.getType().equals(NotLoginException.BE_REPLACED)) {
message = "令牌已被顶下线";
}
else if(nle.getType().equals(NotLoginException.KICK_OUT)) {
message = "令牌已被踢下线";
}
else if(nle.getType().equals(NotLoginException.TOKEN_FREEZE)) {
message = "令牌已被冻结";
}
else if(nle.getType().equals(NotLoginException.NO_PREFIX)) {
message = "未按照指定前缀提交令牌";
}
else {
message = "当前会话未登录";
}
// 返回给前端
return ResponseData.error(ErrorCode.TOKEN_INVALID.getCode(),message);
}
}

View File

@@ -0,0 +1,15 @@
package vvpkassistant.mail.model;
import lombok.Data;
/*
* @author: ziin
* @date: 2025/8/4 15:48
*/
@Data
public class MailModel {
private String mailAddress;
private Integer userId;
private Integer type;
private String code;
}

View File

@@ -0,0 +1,20 @@
package vvpkassistant.mail.service;
import vvpkassistant.mail.model.MailModel;
/*
* @author: ziin
* @date: 2025/8/4 15:42
*/
public interface MailService {
void sendMail(String emailAddress,Integer userId);
void sendVerificationMail(String emailAddress,Integer userId);
Boolean resendMail(MailModel mailModel);
void sendForgetPassWordMail(String mailAddress, Integer id);
Object sendUpdateConfirmMail(MailModel mailModel);
}

View File

@@ -0,0 +1,594 @@
package vvpkassistant.mail.service;
import cn.dev33.satoken.temp.SaTempUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.extra.mail.MailUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import vvpkassistant.Tools.CacheHolder;
import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.User.model.UserModel;
import vvpkassistant.common.ErrorCode;
import vvpkassistant.exception.BusinessException;
import vvpkassistant.mail.model.MailModel;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/*
* @author: ziin
* @date: 2025/8/4 15:42
*/
@Slf4j
@Service
public class MailServiceImpl implements MailService {
@Value("${activateUrl}")
private String activateUrl;
@Value("${verificationMailUrl}")
private String verificationMailUrl;
@Value("${forgetPassWordUrl}")
private String forgetPassWordUrl;
@Resource
private UserDao userDao;
private static final Cache<String, Object> emailSendCache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.build();
@Override
@Async("taskExecutor")
public void sendMail(String emailAddress,Integer userId) {
log.info("Sending email to {}", emailAddress);
String token = SaTempUtil.createToken(userId, 600);
if (emailSendCache.getIfPresent(emailAddress) != null) {
throw new BusinessException(ErrorCode.EMAIL_SEND_FREQUENT);
}
MailUtil.send(emailAddress, "激活你的账号", "<!DOCTYPE html>\n" +
"<html lang=\"en\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
" <title>Account Activation</title>\n" +
" <style>\n" +
" * {\n" +
" margin: 0;\n" +
" padding: 0;\n" +
" box-sizing: border-box;\n" +
" }\n" +
"\n" +
" body {\n" +
" font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n" +
" background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n" +
" min-height: 100vh;\n" +
" display: flex;\n" +
" align-items: center;\n" +
" justify-content: center;\n" +
" padding: 20px;\n" +
" }\n" +
"\n" +
" .container {\n" +
" background: white;\n" +
" border-radius: 12px;\n" +
" box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);\n" +
" padding: 40px;\n" +
" text-align: center;\n" +
" max-width: 400px;\n" +
" width: 100%;\n" +
" }\n" +
"\n" +
" .icon {\n" +
" width: 60px;\n" +
" height: 60px;\n" +
" background: #4CAF50;\n" +
" border-radius: 50%;\n" +
" display: flex;\n" +
" align-items: center;\n" +
" justify-content: center;\n" +
" margin: 0 auto 20px;\n" +
" color: white;\n" +
" font-size: 24px;\n" +
" }\n" +
"\n" +
" h1 {\n" +
" color: #333;\n" +
" font-size: 24px;\n" +
" margin-bottom: 10px;\n" +
" font-weight: 600;\n" +
" }\n" +
"\n" +
" .subtitle {\n" +
" color: #666;\n" +
" font-size: 16px;\n" +
" margin-bottom: 30px;\n" +
" line-height: 1.5;\n" +
" }\n" +
"\n" +
" .activate-button {\n" +
" display: inline-block;\n" +
" background: #4CAF50;\n" +
" border: none;\n" +
" border-radius: 8px;\n" +
" padding: 15px 30px;\n" +
" margin: 20px 0;\n" +
" font-size: 18px;\n" +
" font-weight: bold;\n" +
" color: white;\n" +
" text-decoration: none;\n" +
" cursor: pointer;\n" +
" transition: all 0.3s ease;\n" +
" box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n" +
" }\n" +
" \n" +
" .activate-button:hover {\n" +
" background: #45a049;\n" +
" transform: translateY(-2px);\n" +
" box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);\n" +
" }\n" +
" \n" +
" .activate-button:active {\n" +
" transform: translateY(0);\n" +
" box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n" +
" }\n" +
"\n" +
" .info {\n" +
" background: #e3f2fd;\n" +
" border-left: 4px solid #2196F3;\n" +
" padding: 15px;\n" +
" margin: 20px 0;\n" +
" text-align: left;\n" +
" border-radius: 4px;\n" +
" }\n" +
"\n" +
" .info-title {\n" +
" font-weight: 600;\n" +
" color: #1976D2;\n" +
" margin-bottom: 5px;\n" +
" }\n" +
"\n" +
" .info-text {\n" +
" color: #424242;\n" +
" font-size: 14px;\n" +
" line-height: 1.4;\n" +
" }\n" +
"\n" +
" .footer {\n" +
" margin-top: 30px;\n" +
" padding-top: 20px;\n" +
" border-top: 1px solid #eee;\n" +
" color: #999;\n" +
" font-size: 12px;\n" +
" }\n" +
"\n" +
" @media (max-width: 480px) {\n" +
" .container {\n" +
" padding: 30px 20px;\n" +
" }\n" +
" \n" +
" .verification-code {\n" +
" font-size: 24px;\n" +
" letter-spacing: 2px;\n" +
" }\n" +
" }\n" +
" </style>\n" +
"</head>\n" +
"<body>\n" +
" <div class=\"container\">\n" +
" <div class=\"icon\">✉</div>\n" +
" \n" +
" <h1>Account Activation</h1>\n" +
" <p class=\"subtitle\">Please click the button below to activate your account</p>\n" +
" \n" +
" <a href=\""+ activateUrl+ token + "\" class=\"activate-button\">\n" +
" Activate Account\n" +
" </a>\n" +
" \n" +
" <div class=\"info\">\n" +
" <div class=\"info-title\">Important Notice:</div>\n" +
" <div class=\"info-text\">\n" +
" • This activation link is valid for 24 hours<br>\n" +
" • Please do not share this link with anyone<br>\n" +
" • If you didn't register an account, please ignore this message\n" +
" </div>\n" +
" </div>\n" +
" \n" +
" <div class=\"footer\">\n" +
" This is an automated message. Please do not reply to this email.\n" +
" </div>\n" +
" </div>\n" +
"</body>\n" +
"</html>\n", true);
emailSendCache.put(emailAddress, userId);
}
@Override
public void sendVerificationMail(String emailAddress, Integer userId) {
log.info("Sending email to {}", emailAddress);
String token = SaTempUtil.createToken(userId, 600);
if (emailSendCache.getIfPresent(emailAddress) != null) {
throw new BusinessException(ErrorCode.EMAIL_SEND_FREQUENT);
}
MailUtil.send(emailAddress, "验证你的邮箱", "<!DOCTYPE html>\n" +
"<html lang=\"en\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
" <title>Account Activation</title>\n" +
" <style>\n" +
" * {\n" +
" margin: 0;\n" +
" padding: 0;\n" +
" box-sizing: border-box;\n" +
" }\n" +
"\n" +
" body {\n" +
" font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n" +
" background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n" +
" min-height: 100vh;\n" +
" display: flex;\n" +
" align-items: center;\n" +
" justify-content: center;\n" +
" padding: 20px;\n" +
" }\n" +
"\n" +
" .container {\n" +
" background: white;\n" +
" border-radius: 12px;\n" +
" box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);\n" +
" padding: 40px;\n" +
" text-align: center;\n" +
" max-width: 400px;\n" +
" width: 100%;\n" +
" }\n" +
"\n" +
" .icon {\n" +
" width: 60px;\n" +
" height: 60px;\n" +
" background: #4CAF50;\n" +
" border-radius: 50%;\n" +
" display: flex;\n" +
" align-items: center;\n" +
" justify-content: center;\n" +
" margin: 0 auto 20px;\n" +
" color: white;\n" +
" font-size: 24px;\n" +
" }\n" +
"\n" +
" h1 {\n" +
" color: #333;\n" +
" font-size: 24px;\n" +
" margin-bottom: 10px;\n" +
" font-weight: 600;\n" +
" }\n" +
"\n" +
" .subtitle {\n" +
" color: #666;\n" +
" font-size: 16px;\n" +
" margin-bottom: 30px;\n" +
" line-height: 1.5;\n" +
" }\n" +
"\n" +
" .activate-button {\n" +
" display: inline-block;\n" +
" background: #4CAF50;\n" +
" border: none;\n" +
" border-radius: 8px;\n" +
" padding: 15px 30px;\n" +
" margin: 20px 0;\n" +
" font-size: 18px;\n" +
" font-weight: bold;\n" +
" color: white;\n" +
" text-decoration: none;\n" +
" cursor: pointer;\n" +
" transition: all 0.3s ease;\n" +
" box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n" +
" }\n" +
" \n" +
" .activate-button:hover {\n" +
" background: #45a049;\n" +
" transform: translateY(-2px);\n" +
" box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);\n" +
" }\n" +
" \n" +
" .activate-button:active {\n" +
" transform: translateY(0);\n" +
" box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n" +
" }\n" +
"\n" +
" .info {\n" +
" background: #e3f2fd;\n" +
" border-left: 4px solid #2196F3;\n" +
" padding: 15px;\n" +
" margin: 20px 0;\n" +
" text-align: left;\n" +
" border-radius: 4px;\n" +
" }\n" +
"\n" +
" .info-title {\n" +
" font-weight: 600;\n" +
" color: #1976D2;\n" +
" margin-bottom: 5px;\n" +
" }\n" +
"\n" +
" .info-text {\n" +
" color: #424242;\n" +
" font-size: 14px;\n" +
" line-height: 1.4;\n" +
" }\n" +
"\n" +
" .footer {\n" +
" margin-top: 30px;\n" +
" padding-top: 20px;\n" +
" border-top: 1px solid #eee;\n" +
" color: #999;\n" +
" font-size: 12px;\n" +
" }\n" +
"\n" +
" @media (max-width: 480px) {\n" +
" .container {\n" +
" padding: 30px 20px;\n" +
" }\n" +
" \n" +
" .verification-code {\n" +
" font-size: 24px;\n" +
" letter-spacing: 2px;\n" +
" }\n" +
" }\n" +
" </style>\n" +
"</head>\n" +
"<body>\n" +
" <div class=\"container\">\n" +
" <div class=\"icon\">✉</div>\n" +
" \n" +
" <h1>Verification Mail</h1>\n" +
" <p class=\"subtitle\">Please click the button below to verification your mail</p>\n" +
" \n" +
" <a href=\""+ verificationMailUrl+ token + "\" class=\"activate-button\">\n" +
" Verification\n" +
" </a>\n" +
" \n" +
" <div class=\"info\">\n" +
" <div class=\"info-title\">Important Notice:</div>\n" +
" <div class=\"info-text\">\n" +
" • This activation link is valid for 10 minutes<br>\n" +
" • Please do not share this link with anyone<br>\n" +
" • If you didn't register an account, please ignore this message\n" +
" </div>\n" +
" </div>\n" +
" \n" +
" <div class=\"footer\">\n" +
" This is an automated message. Please do not reply to this email.\n" +
" </div>\n" +
" </div>\n" +
"</body>\n" +
"</html>\n", true);
emailSendCache.put(emailAddress, userId);
}
@Override
public Boolean resendMail(MailModel mailModel) {
Object ifPresent = emailSendCache.getIfPresent(mailModel.getMailAddress());
if (ifPresent == null) {
switch (mailModel.getType()) {
case 1:
sendMail(mailModel.getMailAddress(), mailModel.getUserId());
break;
case 2:
sendVerificationMail(mailModel.getMailAddress(), mailModel.getUserId());
break;
case 3:
sendForgetPassWordMail(mailModel.getMailAddress(), mailModel.getUserId());
break;
}
return true;
}else {
throw new BusinessException(ErrorCode.EMAIL_SEND_FREQUENT);
}
}
@Override
public void sendForgetPassWordMail(String mailAddress, Integer userId) {
if (checkCache(mailAddress)){
String token = SaTempUtil.createToken(userId, 600);
MailUtil.send(mailAddress, "验证你的邮箱", "<!DOCTYPE html>\n" +
"<html lang=\"en\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
" <title>Account Activation</title>\n" +
" <style>\n" +
" * {\n" +
" margin: 0;\n" +
" padding: 0;\n" +
" box-sizing: border-box;\n" +
" }\n" +
"\n" +
" body {\n" +
" font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n" +
" background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n" +
" min-height: 100vh;\n" +
" display: flex;\n" +
" align-items: center;\n" +
" justify-content: center;\n" +
" padding: 20px;\n" +
" }\n" +
"\n" +
" .container {\n" +
" background: white;\n" +
" border-radius: 12px;\n" +
" box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);\n" +
" padding: 40px;\n" +
" text-align: center;\n" +
" max-width: 400px;\n" +
" width: 100%;\n" +
" }\n" +
"\n" +
" .icon {\n" +
" width: 60px;\n" +
" height: 60px;\n" +
" background: #4CAF50;\n" +
" border-radius: 50%;\n" +
" display: flex;\n" +
" align-items: center;\n" +
" justify-content: center;\n" +
" margin: 0 auto 20px;\n" +
" color: white;\n" +
" font-size: 24px;\n" +
" }\n" +
"\n" +
" h1 {\n" +
" color: #333;\n" +
" font-size: 24px;\n" +
" margin-bottom: 10px;\n" +
" font-weight: 600;\n" +
" }\n" +
"\n" +
" .subtitle {\n" +
" color: #666;\n" +
" font-size: 16px;\n" +
" margin-bottom: 30px;\n" +
" line-height: 1.5;\n" +
" }\n" +
"\n" +
" .activate-button {\n" +
" display: inline-block;\n" +
" background: #4CAF50;\n" +
" border: none;\n" +
" border-radius: 8px;\n" +
" padding: 15px 30px;\n" +
" margin: 20px 0;\n" +
" font-size: 18px;\n" +
" font-weight: bold;\n" +
" color: white;\n" +
" text-decoration: none;\n" +
" cursor: pointer;\n" +
" transition: all 0.3s ease;\n" +
" box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n" +
" }\n" +
" \n" +
" .activate-button:hover {\n" +
" background: #45a049;\n" +
" transform: translateY(-2px);\n" +
" box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);\n" +
" }\n" +
" \n" +
" .activate-button:active {\n" +
" transform: translateY(0);\n" +
" box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n" +
" }\n" +
"\n" +
" .info {\n" +
" background: #e3f2fd;\n" +
" border-left: 4px solid #2196F3;\n" +
" padding: 15px;\n" +
" margin: 20px 0;\n" +
" text-align: left;\n" +
" border-radius: 4px;\n" +
" }\n" +
"\n" +
" .info-title {\n" +
" font-weight: 600;\n" +
" color: #1976D2;\n" +
" margin-bottom: 5px;\n" +
" }\n" +
"\n" +
" .info-text {\n" +
" color: #424242;\n" +
" font-size: 14px;\n" +
" line-height: 1.4;\n" +
" }\n" +
"\n" +
" .footer {\n" +
" margin-top: 30px;\n" +
" padding-top: 20px;\n" +
" border-top: 1px solid #eee;\n" +
" color: #999;\n" +
" font-size: 12px;\n" +
" }\n" +
"\n" +
" @media (max-width: 480px) {\n" +
" .container {\n" +
" padding: 30px 20px;\n" +
" }\n" +
" \n" +
" .verification-code {\n" +
" font-size: 24px;\n" +
" letter-spacing: 2px;\n" +
" }\n" +
" }\n" +
" </style>\n" +
"</head>\n" +
"<body>\n" +
" <div class=\"container\">\n" +
" <div class=\"icon\">✉</div>\n" +
" \n" +
" <h1>Verification Mail</h1>\n" +
" <p class=\"subtitle\">Please click the button below to reset your password</p>\n" +
" \n" +
" <a href=\""+ forgetPassWordUrl + token + "\" class=\"activate-button\">\n" +
" Reset PassWord\n" +
" </a>\n" +
" \n" +
" <div class=\"info\">\n" +
" <div class=\"info-title\">Important Notice:</div>\n" +
" <div class=\"info-text\">\n" +
" • This activation link is valid for 10 minutes<br>\n" +
" • Please do not share this link with anyone<br>\n" +
" • If you didn't register an account, please ignore this message\n" +
" </div>\n" +
" </div>\n" +
" \n" +
" <div class=\"footer\">\n" +
" This is an automated message. Please do not reply to this email.\n" +
" </div>\n" +
" </div>\n" +
"</body>\n" +
"</html>\n", true);
emailSendCache.put(mailAddress, userId);
}
}
@Override
public Object sendUpdateConfirmMail(MailModel mailModel) {
LambdaQueryWrapper<UserModel> lambdaQueryWrapper = new LambdaQueryWrapper<>();
UserModel userModel = userDao.selectOne(
lambdaQueryWrapper.eq(UserModel::getEmail, mailModel.getMailAddress())
.eq(UserModel::getStatus, 0)
.eq(UserModel::getMailVerification, 0));
if (userModel == null){
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
if (checkCache(mailModel.getMailAddress())) {
String code = RandomUtil.randomString(6);
emailSendCache.put(mailModel.getMailAddress(), code);
CacheHolder.VERIFICATION_MAIL.put(code, mailModel.getMailAddress());
MailUtil.send(mailModel.getMailAddress(),"your Verification code is :" + code,
"your Verification code is :" + code,false);
log.info("sendMailto:{},Verification code is :{}", mailModel.getMailAddress(),code);
}
return true;
}
public Boolean checkCache(String mailAddress){
Object ifPresent = emailSendCache.getIfPresent(mailAddress);
if (ifPresent == null) {
return true;
}else {
throw new BusinessException(ErrorCode.EMAIL_SEND_FREQUENT);
}
}
}

View File

@@ -15,7 +15,9 @@ import vvpkassistant.pk.mapper.PkRecordDetailDao;
import vvpkassistant.pk.model.PkInfoModel;
import vvpkassistant.pk.model.PkRecord;
import vvpkassistant.pk.model.PkRecordDetail;
import vvpkassistant.pk.service.PKService;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
@@ -24,6 +26,9 @@ import java.util.Map;
@RequestMapping("pk")
public class PkController {
@Resource
private PKService pkService;
@Autowired
private PkInfoDao pkDao;
@@ -41,165 +46,26 @@ public class PkController {
// 创建pk数据
@PostMapping("addPkData")
public ResponseData<Object> addPkData(@RequestBody PkInfoModel pkModel) {
// 初始可邀请状态为0
pkModel.setInviteStatus(0);
pkModel.setPinExpireTime(0);
// 获取主播id
String anchorId = pkModel.getAnchorId();
// 查询当天是否存在该主播发布的pk信息。
Integer pkTime = pkModel.getPkTime();
// 根据设置的pk时间。查询出当天的开始时间和结束时间
Map<String, Long> dayStartAndEndTimestamp = VVTools.getDayStartAndEndTimestamp(pkTime);
Long start = dayStartAndEndTimestamp.get("start");
Long end = dayStartAndEndTimestamp.get("end");
// 查询主播在当天是否有发布过pk信息
List<PkInfoModel> pkInfoModels = pkDao.selectDataWithAnchorIdAndTime(anchorId, start, end);
// 判断该主播在当日是否已发布过pk信息
if (pkInfoModels.size() > 0) {
return ResponseData.error(ResponseInfo.ERROR.getCode(),"该主播当日已有pk信息");
}
int insert = pkDao.insert(pkModel);
return insert == 1 ? ResponseData.success(pkModel) : ResponseData.error(ResponseInfo.ERROR.getCode(),null);
public ResponseData<PkInfoModel> addPkData(@RequestBody PkInfoModel pkModel) {
return ResponseData.success(pkService.addPkAddData(pkModel));
}
// 更新pk信息
@PostMapping("updatePkStatus")
public ResponseData<Object> updatePkStatus(@RequestBody PkRecord recordModel) {
//如果是同意了pk邀请、则更新已发布的主播pk可邀请状态
if (recordModel.getPkStatus() == 1) {
// 查询出记录信息
PkRecord pkRecordById = recordDao.singleRecord(recordModel.getId());
// 更新可邀请状态
PkInfoModel pkInfoModelA = pkDao.selectById(pkRecordById.getPkIdA());
pkInfoModelA.setInviteStatus(1);
PkInfoModel pkInfoModelB = pkDao.selectById(pkRecordById.getPkIdB());
pkInfoModelB.setInviteStatus(1);
pkDao.updateById(pkInfoModelA);
pkDao.updateById(pkInfoModelB);
// 如果有置顶的状态。需要取消置顶
if (pkInfoModelA.getPinExpireTime() > VVTools.currentTimeStamp()) {
long hour = VVTools.calculateHoursFloor(pkInfoModelA.getPinExpireTime(),VVTools.currentTimeStamp());
int coin = Integer.parseInt(FunctionConfigHolder.getValue("置顶扣除积分"));
int totalCoin = (int) (coin * hour);
// 插入记录
CoinRecords coinRecords = new CoinRecords("成功预约pk自动取消置顶", pkInfoModelA.getSenderId(),totalCoin, (int) VVTools.currentTimeStamp(),1);
coinRecordsDao.insert(coinRecords);
// 更新积分
UserModel userModel = userDao.selectById(pkRecordById.getUserIdA());
Integer points = userModel.getPoints();
userModel.setPoints(points + totalCoin);
}
if (pkInfoModelB.getPinExpireTime() > VVTools.currentTimeStamp()) {
long hour = VVTools.calculateHoursFloor(pkInfoModelB.getPinExpireTime(),VVTools.currentTimeStamp());
int coin = Integer.parseInt(FunctionConfigHolder.getValue("置顶扣除积分"));
int totalCoin = (int) (coin * hour);
// 插入记录
CoinRecords coinRecords = new CoinRecords("成功预约pk自动取消置顶", pkInfoModelB.getSenderId(),totalCoin, (int) VVTools.currentTimeStamp(),1);
coinRecordsDao.insert(coinRecords);
// 更新积分
UserModel userModel = userDao.selectById(pkRecordById.getUserIdB());
Integer points = userModel.getPoints();
userModel.setPoints(points + totalCoin);
}
}
// 更新pk邀请记录
int update = recordDao.updateById(recordModel);
return update == 1 ? ResponseData.success("") : ResponseData.error(ResponseInfo.ERROR.getCode(),null);
return ResponseData.success(pkService.updatePkStatus(recordModel));
}
// 创建PK记录
@PostMapping("createPkRecord")
public ResponseData<Object> createPkRecord(@RequestBody PkRecord record) {
// 如果这两个邀约直播之间存在未处理的邀请记录。那就不允许继续发送pk邀请
String anchorIdA = record.getAnchorIdA();
String anchorIdB = record.getAnchorIdB();
Integer dataCount = recordDao.getPendingInvitations(anchorIdA, anchorIdB);
if (dataCount > 0) {
return ResponseData.error(ResponseInfo.ERROR.getCode(),"已存在一条未处理的pk申请不能重复发送");
}
record.setPkStatus(0);
int insert = recordDao.insert(record);
return insert == 1 ? ResponseData.success(record) : ResponseData.error(ResponseInfo.ERROR.getCode(),null);
return ResponseData.success(pkService.createPKRecord(record));
}
// pk列表
@PostMapping("pkList")
public ResponseData<Object> pkList(@RequestBody Map<String,Object> map) {
Integer page = (Integer) map.get("page");
Integer size = (Integer) map.get("size");
Map<String,Object> condition = (Map<String, Object>) map.get("condition");
Map<String, Long> todayTimeStampMap = VVTools.startAndEndTimeStampForToday();
Long start = VVTools.currentTimeStamp();
Long end = todayTimeStampMap.get("end");
List<PkInfoModel> pkModels = pkDao.selectPkInfoByCondition(page * size, size, condition, start, end);
long currentTimeStamp = VVTools.currentTimeStamp();
//如果传了用户id
if (map.containsKey("userId")) {
Long begin = VVTools.currentTimeStamp();
Integer userId = Integer.valueOf(map.get("userId").toString());
// 查询出当前用户大于等于今天的已接受邀请的pk数据
List<PkRecord> pkRecords = recordDao.fetchDataFromTodayWithUserId(userId, begin);
// 遍历查询出的数据。如果文章的id相同。就显示完整的主播名称
for (PkInfoModel pkModel : pkModels) {
pkModel.setDisPlayId(pkModel.getAnchorId());
// 设置是否为置顶
pkModel.setIsPin(pkModel.getPinExpireTime() > currentTimeStamp);
// 如果文章的发布者不是当前登录主播。
if (pkModel.getSenderId().intValue() != userId) {
boolean showId = false;
// 查找是否和当前登录的账号有pk关系。
for (PkRecord pkRecord : pkRecords) {
// 如果当前用户是邀请pk的并且对方已接受pk也可以看到主播id
if (pkRecord.getUserIdB().intValue() == userId &&
pkRecord.getPkIdA().intValue() == pkModel.getId().intValue() &&
pkRecord.getPkStatus() == 1) {
showId = true;
break;
}
}
// 条件不满足则隐藏主播id'
if (!showId) {
pkModel.setDisPlayId(VVTools.replaceChar(pkModel.getAnchorId(), '*'));
}
}
}
}else{
for (PkInfoModel pkModel : pkModels) {
// 设置是否为置顶
pkModel.setIsPin(pkModel.getPinExpireTime() > currentTimeStamp);
pkModel.setDisPlayId(VVTools.replaceChar(pkModel.getAnchorId(), '*'));
}
}
return ResponseData.success(pkModels);
return ResponseData.success(pkService.getPKList(map));
}
// 查询用户发布的大于当前时间的pk数据
@@ -214,42 +80,7 @@ public class PkController {
//pk文章详情
@PostMapping("pkInfoDetail")
public ResponseData<Object> pkInfoDetail(@RequestBody Map<String, Integer> map) {
Integer id = map.get("id");
Integer userId = map.get("userId");
Integer from = map.get("from"); // 1 首页 2 聊天
PkInfoModel pkInfoModel = pkDao.selectById(id);
if (pkInfoModel == null) {
return ResponseData.error(ResponseInfo.ERROR.getCode(),"该信息不存在");
}
if (from == 1) {
if (pkInfoModel.getPkTime() > VVTools.currentTimeStamp()) {
// 判断是否是自己发布的数据 如果不是就隐藏主播id
if (pkInfoModel.getSenderId().equals(userId)) {
pkInfoModel.setDisPlayId(pkInfoModel.getAnchorId());
} else {
// 查询是否存在未完成的pk记录
Integer isHave = pkDao.checkIfUnfinishedPKExistsWithAnchor(userId, pkInfoModel.getAnchorId());
if (isHave > 0) {
pkInfoModel.setDisPlayId(pkInfoModel.getAnchorId());
}else {
pkInfoModel.setDisPlayId(VVTools.replaceChar(pkInfoModel.getAnchorId(), '*'));
}
}
return ResponseData.success(pkInfoModel);
}else {
return ResponseData.error(ResponseInfo.ERROR.getCode(),"当前信息已无效");
}
}else{
Integer isHave = pkDao.checkIfUnfinishedPKExistsWithAnchor(userId, pkInfoModel.getAnchorId());
if (isHave > 0) {
pkInfoModel.setDisPlayId(pkInfoModel.getAnchorId());
}else {
pkInfoModel.setDisPlayId(VVTools.replaceChar(pkInfoModel.getAnchorId(), '*'));
}
return ResponseData.success(pkInfoModel);
}
return ResponseData.success(pkService.pkInfoDetail(map));
}
//删除自己的pk数据 (单个)

View File

@@ -0,0 +1,24 @@
package vvpkassistant.pk.service;
import com.baomidou.mybatisplus.extension.service.IService;
import vvpkassistant.pk.model.PkInfoModel;
import vvpkassistant.pk.model.PkRecord;
import java.util.List;
import java.util.Map;
/*
* @author: ziin
* @date: 2025/8/4 13:53
*/
public interface PKService extends IService<PkInfoModel> {
PkInfoModel addPkAddData(PkInfoModel pkModel);
Boolean updatePkStatus(PkRecord recordModel);
PkRecord createPKRecord(PkRecord record);
List<PkInfoModel> getPKList(Map<String, Object> map);
PkInfoModel pkInfoDetail(Map<String, Integer> map);
}

View File

@@ -0,0 +1,243 @@
package vvpkassistant.pk.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import vvpkassistant.CoinRecords.CoinRecords;
import vvpkassistant.CoinRecords.CoinRecordsDao;
import vvpkassistant.Data.ResponseData;
import vvpkassistant.Data.ResponseInfo;
import vvpkassistant.Tools.VVTools;
import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.User.model.UserModel;
import vvpkassistant.common.ErrorCode;
import vvpkassistant.config.FunctionConfigHolder;
import vvpkassistant.exception.BusinessException;
import vvpkassistant.pk.mapper.PkInfoDao;
import vvpkassistant.pk.mapper.PkRecordDao;
import vvpkassistant.pk.model.PkInfoModel;
import vvpkassistant.pk.model.PkRecord;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
/*
* @author: ziin
* @date: 2025/8/4 13:54
*/
@Service
public class PKServiceImpl extends ServiceImpl<PkInfoDao, PkInfoModel> implements PKService {
@Resource
private PkInfoDao pkInfoDao;
@Resource
private PkRecordDao pkRecordDao;
@Resource
private CoinRecordsDao coinRecordsDao;
@Resource
private UserDao userDao;
@Override
public PkInfoModel addPkAddData(PkInfoModel pkModel) {
// 初始可邀请状态为0
pkModel.setInviteStatus(0);
pkModel.setPinExpireTime(0);
// 获取主播id
String anchorId = pkModel.getAnchorId();
// 查询当天是否存在该主播发布的pk信息。
Integer pkTime = pkModel.getPkTime();
// 根据设置的pk时间。查询出当天的开始时间和结束时间
Map<String, Long> dayStartAndEndTimestamp = VVTools.getDayStartAndEndTimestamp(pkTime);
Long start = dayStartAndEndTimestamp.get("start");
Long end = dayStartAndEndTimestamp.get("end");
// 查询主播在当天是否有发布过pk信息
List<PkInfoModel> pkInfoModels = pkInfoDao.selectDataWithAnchorIdAndTime(anchorId, start, end);
// 判断该主播在当日是否已发布过pk信息
if (!pkInfoModels.isEmpty()) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "该主播当日已有pk信息");
}
if (pkInfoDao.insert(pkModel) == 1) {
return pkModel;
} else
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
@Override
public Boolean updatePkStatus(PkRecord recordModel) {
if (recordModel.getPkStatus() == 1) {
// 查询出记录信息
PkRecord pkRecordById = pkRecordDao.singleRecord(recordModel.getId());
// 更新可邀请状态
PkInfoModel pkInfoModelA = pkInfoDao.selectById(pkRecordById.getPkIdA());
pkInfoModelA.setInviteStatus(1);
PkInfoModel pkInfoModelB = pkInfoDao.selectById(pkRecordById.getPkIdB());
pkInfoModelB.setInviteStatus(1);
pkInfoDao.updateById(pkInfoModelA);
pkInfoDao.updateById(pkInfoModelB);
// 如果有置顶的状态。需要取消置顶
if (pkInfoModelA.getPinExpireTime() > VVTools.currentTimeStamp()) {
long hour = VVTools.calculateHoursFloor(pkInfoModelA.getPinExpireTime(), VVTools.currentTimeStamp());
int coin = Integer.parseInt(FunctionConfigHolder.getValue("置顶扣除积分"));
int totalCoin = (int) (coin * hour);
// 插入记录
CoinRecords coinRecords = new CoinRecords("成功预约pk自动取消置顶", pkInfoModelA.getSenderId(), totalCoin, (int) VVTools.currentTimeStamp(), 1);
coinRecordsDao.insert(coinRecords);
// 更新积分
UserModel userModel = userDao.selectById(pkRecordById.getUserIdA());
Integer points = userModel.getPoints();
userModel.setPoints(points + totalCoin);
}
if (pkInfoModelB.getPinExpireTime() > VVTools.currentTimeStamp()) {
long hour = VVTools.calculateHoursFloor(pkInfoModelB.getPinExpireTime(), VVTools.currentTimeStamp());
int coin = Integer.parseInt(FunctionConfigHolder.getValue("置顶扣除积分"));
int totalCoin = (int) (coin * hour);
// 插入记录
CoinRecords coinRecords = new CoinRecords("成功预约pk自动取消置顶", pkInfoModelB.getSenderId(), totalCoin, (int) VVTools.currentTimeStamp(), 1);
coinRecordsDao.insert(coinRecords);
// 更新积分
UserModel userModel = userDao.selectById(pkRecordById.getUserIdB());
Integer points = userModel.getPoints();
userModel.setPoints(points + totalCoin);
}
}
// 更新pk邀请记录
if (pkRecordDao.updateById(recordModel) == 1) {
return true;
} else {
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
}
@Override
public PkRecord createPKRecord(PkRecord record) {
// 如果这两个邀约直播之间存在未处理的邀请记录。那就不允许继续发送pk邀请
String anchorIdA = record.getAnchorIdA();
String anchorIdB = record.getAnchorIdB();
Integer dataCount = pkRecordDao.getPendingInvitations(anchorIdA, anchorIdB);
if (dataCount > 0) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "已存在一条未处理的pk申请不能重复发送");
}
record.setPkStatus(0);
if (pkRecordDao.insert(record) == 1 ){
return record;
}else
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
@Override
public List<PkInfoModel> getPKList(Map<String, Object> map) {
Integer page = (Integer) map.get("page");
Integer size = (Integer) map.get("size");
Map<String,Object> condition = (Map<String, Object>) map.get("condition");
Map<String, Long> todayTimeStampMap = VVTools.startAndEndTimeStampForToday();
Long start = VVTools.currentTimeStamp();
Long end = todayTimeStampMap.get("end");
List<PkInfoModel> pkModels = pkInfoDao.selectPkInfoByCondition(page * size, size, condition, start, end);
long currentTimeStamp = VVTools.currentTimeStamp();
//如果传了用户id
if (map.containsKey("userId")) {
Long begin = VVTools.currentTimeStamp();
Integer userId = Integer.valueOf(map.get("userId").toString());
// 查询出当前用户大于等于今天的已接受邀请的pk数据
List<PkRecord> pkRecords = pkRecordDao.fetchDataFromTodayWithUserId(userId, begin);
// 遍历查询出的数据。如果文章的id相同。就显示完整的主播名称
for (PkInfoModel pkModel : pkModels) {
pkModel.setDisPlayId(pkModel.getAnchorId());
// 设置是否为置顶
pkModel.setIsPin(pkModel.getPinExpireTime() > currentTimeStamp);
// 如果文章的发布者不是当前登录主播。
if (pkModel.getSenderId().intValue() != userId) {
boolean showId = false;
// 查找是否和当前登录的账号有pk关系。
for (PkRecord pkRecord : pkRecords) {
// 如果当前用户是邀请pk的并且对方已接受pk也可以看到主播id
if (pkRecord.getUserIdB().intValue() == userId &&
pkRecord.getPkIdA().intValue() == pkModel.getId().intValue() &&
pkRecord.getPkStatus() == 1) {
showId = true;
break;
}
}
// 条件不满足则隐藏主播id'
if (!showId) {
pkModel.setDisPlayId(VVTools.replaceChar(pkModel.getAnchorId(), '*'));
}
}
}
}else{
for (PkInfoModel pkModel : pkModels) {
// 设置是否为置顶
pkModel.setIsPin(pkModel.getPinExpireTime() > currentTimeStamp);
pkModel.setDisPlayId(VVTools.replaceChar(pkModel.getAnchorId(), '*'));
}
}
return pkModels;
}
@Override
public PkInfoModel pkInfoDetail(Map<String, Integer> map) {
Integer id = map.get("id");
Integer userId = map.get("userId");
Integer from = map.get("from"); // 1 首页 2 聊天
PkInfoModel pkInfoModel = pkInfoDao.selectById(id);
if (pkInfoModel == null) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "该信息不存在");
}
if (from == 1) {
if (pkInfoModel.getPkTime() > VVTools.currentTimeStamp()) {
// 判断是否是自己发布的数据 如果不是就隐藏主播id
if (pkInfoModel.getSenderId().equals(userId)) {
pkInfoModel.setDisPlayId(pkInfoModel.getAnchorId());
} else {
// 查询是否存在未完成的pk记录
Integer isHave = pkInfoDao.checkIfUnfinishedPKExistsWithAnchor(userId, pkInfoModel.getAnchorId());
if (isHave > 0) {
pkInfoModel.setDisPlayId(pkInfoModel.getAnchorId());
} else {
pkInfoModel.setDisPlayId(VVTools.replaceChar(pkInfoModel.getAnchorId(), '*'));
}
}
return pkInfoModel;
} else {
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"当前信息已无效");
}
} else {
Integer isHave = pkInfoDao.checkIfUnfinishedPKExistsWithAnchor(userId, pkInfoModel.getAnchorId());
if (isHave > 0) {
pkInfoModel.setDisPlayId(pkInfoModel.getAnchorId());
} else {
pkInfoModel.setDisPlayId(VVTools.replaceChar(pkInfoModel.getAnchorId(), '*'));
}
return pkInfoModel;
}
}
}

View File

@@ -4,6 +4,44 @@ server:
spring:
profiles:
active: local
task:
# Spring 执行器配置,对应 TaskExecutionProperties 配置类。对于 Spring 异步任务,会使用该执行器。
execution:
thread-name-prefix: mail-task # 线程池的线程名的前缀。默认为 task- ,建议根据自己应用来设置
pool: # 线程池相关
core-size: 10 # 核心线程数,线程池创建时候初始化的线程数。默认为 8 。
max-size: 20 # 最大线程数,线程池最大的线程数,只有在缓冲队列满了之后,才会申请超过核心线程数的线程。默认为 Integer.MAX_VALUE
keep-alive: 60s # 允许线程的空闲时间,当超过了核心线程之外的线程,在空闲时间到达之后会被销毁。默认为 60 秒
queue-capacity: 200 # 缓冲队列大小,用来缓冲执行任务的队列的大小。默认为 Integer.MAX_VALUE 。
allow-core-thread-timeout: true # 是否允许核心线程超时,即开启线程池的动态增长和缩小。默认为 true 。
shutdown:
await-termination: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true
await-termination-period: 60 # 等待任务完成的最大时长,单位为秒。默认为 0 ,根据自己应用来设置
mybatis-plus:
global-config:
db-config:
id-type: auto
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: token
# token 有效期(单位:秒) 默认30天-1 代表永久有效
timeout: -1
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: 648000
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token
is-share: false
# token 风格默认可取值uuid、simple-uuid、random-32、random-64、random-128、tik
token-style: random-128
# 是否输出操作日志
is-log: true
activateUrl: http://192.168.1.174:8086/user/activate?token=
verificationMailUrl: http://192.168.1.174:8086/user/verification?token=
forgetPassWordUrl: http://192.168.1.174:8086/user//resetPassword/?token=

View File

@@ -0,0 +1,10 @@
# 邮件服务器的SMTP地址可选默认为smtp.<发件人邮箱后缀>
host = smtp.exmail.qq.com
# 邮件服务器的SMTP端口可选默认25
port = 465
# 发件人(必须正确,否则发送失败)
from = niuyuxi@hanxiaokj.cn
# 用户名,默认为发件人邮箱前缀
user = niuyuxi@hanxiaokj.cn
pass = tF35umug9CBCBKqR
sslEnable = true

View File

@@ -4,6 +4,44 @@ server:
spring:
profiles:
active: local
task:
# Spring 执行器配置,对应 TaskExecutionProperties 配置类。对于 Spring 异步任务,会使用该执行器。
execution:
thread-name-prefix: mail-task # 线程池的线程名的前缀。默认为 task- ,建议根据自己应用来设置
pool: # 线程池相关
core-size: 10 # 核心线程数,线程池创建时候初始化的线程数。默认为 8 。
max-size: 20 # 最大线程数,线程池最大的线程数,只有在缓冲队列满了之后,才会申请超过核心线程数的线程。默认为 Integer.MAX_VALUE
keep-alive: 60s # 允许线程的空闲时间,当超过了核心线程之外的线程,在空闲时间到达之后会被销毁。默认为 60 秒
queue-capacity: 200 # 缓冲队列大小,用来缓冲执行任务的队列的大小。默认为 Integer.MAX_VALUE 。
allow-core-thread-timeout: true # 是否允许核心线程超时,即开启线程池的动态增长和缩小。默认为 true 。
shutdown:
await-termination: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true
await-termination-period: 60 # 等待任务完成的最大时长,单位为秒。默认为 0 ,根据自己应用来设置
mybatis-plus:
global-config:
db-config:
id-type: auto
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: token
# token 有效期(单位:秒) 默认30天-1 代表永久有效
timeout: -1
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: 648000
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token
is-share: false
# token 风格默认可取值uuid、simple-uuid、random-32、random-64、random-128、tik
token-style: random-128
# 是否输出操作日志
is-log: true
activateUrl: http://192.168.1.174:8086/user/activate?token=
verificationMailUrl: http://192.168.1.174:8086/user/verification?token=
forgetPassWordUrl: http://192.168.1.174:8086/user//resetPassword/?token=