Compare commits

..

5 Commits

Author SHA1 Message Date
d24365234c 修复套餐购买的积分扣减与租户时长更新异常 2026-03-27 16:03:00 +08:00
a7a093c48b 删除无用代码 2026-03-27 13:04:14 +08:00
37ebde7b9d feat(item): 新增 PkItem 实体及 CRUD 全套代码
新增 PkItem.java 实体、Mapper、Service、Controller 与 XML,并配置 MyBatisCodeHelper 生成规则,同时忽略 /src/test/ 目录
2026-03-27 11:53:35 +08:00
79d541e4df feat(pk): 支持平局双方加分逻辑
- PkResultPointsDTO 新增 draw 字段标识平局
- 服务层识别平局后读取配置 PK平局增加积分 并给双方加相同积分
- 新增 grantDrawPoints 方法处理平局场景
- 补充单元测试验证平局加分及失败场景
2026-03-27 09:40:58 +08:00
3fdd75a5df 按照PK记录为PK双方添加积分 2026-03-26 16:36:36 +08:00
30 changed files with 1382 additions and 364 deletions

1
.gitignore vendored
View File

@@ -28,3 +28,4 @@ replay_pid*
/target/
/AGENTS.md
/.xcodemap/
/src/test/

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -4,8 +4,87 @@
<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="generatedClassPathList">
<list>
<option value="$PROJECT_DIR$/src/main/java/vvpkassistant/User/model" />
</list>
</option>
<option name="javaMapperPackage" value="vvpkassistant.iterm_recoder" />
<option name="javaMapperPath" value="$PROJECT_DIR$/src/main/java" />
<option name="javaModelPackage" value="vvpkassistant.iterm_recoder" />
<option name="javaModelPath" value="$PROJECT_DIR$/src/main/java" />
<option name="lastDatabaseCrudChooseModuleName" value="vvPkAssistant" />
<option name="moduleNameToPackageAndPathMap">
<map>
<entry key="vvPkAssistant">
<value>
<UserPackageAndPathInfoByModule>
<option name="javaMapperPackage" value="vvpkassistant.iterm_recoder" />
<option name="javaMapperPath" value="$PROJECT_DIR$/src/main/java" />
<option name="javaModelPacakge" value="vvpkassistant.iterm_recoder" />
<option name="javaModelPath" value="$PROJECT_DIR$/src/main/java" />
<option name="javaServiceInterfacePath" value="$PROJECT_DIR$/src/main/java" />
<option name="javaServicePath" value="$PROJECT_DIR$/src/main/java" />
<option name="xmlPackage" value="mapper" />
<option name="xmlPath" value="$PROJECT_DIR$/src/main/resources" />
</UserPackageAndPathInfoByModule>
</value>
</entry>
</map>
</option>
<option name="mybatisPlusIdType" value="AUTO" />
<option name="tableGenerateConfigs">
<map>
<entry key="ruoyi-vue-pro:pk_item">
<value>
<TableGenerateConfig>
<option name="generatedKey" value="" />
<option name="javaModelName" value="PkItem" />
<option name="moduleName" value="vvPkAssistant" />
<option name="mybatisplusIdType" value="INPUT" />
<option name="sequenceColumn" value="" />
<option name="sequenceId" value="" />
<option name="useActualColumnName" value="false" />
</TableGenerateConfig>
</value>
</entry>
<entry key="ruoyi-vue-pro:pk_item_recoder">
<value>
<TableGenerateConfig>
<option name="generatedKey" value="" />
<option name="javaModelName" value="PkItemRecoder" />
<option name="moduleName" value="vvPkAssistant" />
<option name="mybatisplusIdType" value="AUTO" />
<option name="sequenceColumn" value="" />
<option name="sequenceId" value="" />
<option name="useActualColumnName" value="false" />
</TableGenerateConfig>
</value>
</entry>
<entry key="ruoyi-vue-pro:system_tenant">
<value>
<TableGenerateConfig>
<option name="generatedKey" value="id" />
<option name="javaModelName" value="SystemTenant" />
<option name="moduleName" value="vvPkAssistant" />
<option name="mybatisplusIdType" value="AUTO" />
<option name="sequenceColumn" value="" />
<option name="sequenceId" value="" />
<option name="useActualColumnName" value="false" />
</TableGenerateConfig>
</value>
</entry>
<entry key="ruoyi-vue-pro:system_users">
<value>
<TableGenerateConfig>
<option name="generatedKey" value="id" />
<option name="javaModelName" value="SystemUsers" />
<option name="sequenceColumn" value="" />
<option name="sequenceId" value="" />
<option name="useActualColumnName" value="false" />
</TableGenerateConfig>
</value>
</entry>
<entry key="vv_assistant:user">
<value>
<TableGenerateConfig>
@@ -19,6 +98,8 @@
</entry>
</map>
</option>
<option name="xmlMapperPackage" value="mapper" />
<option name="xmlMapperPath" value="$PROJECT_DIR$/src/main/resources" />
</ProjectProfile>
</option>
</component>

View File

@@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
@Data
@TableName("system_users")
public class UserModel {
@@ -15,4 +17,99 @@ public class UserModel {
private Integer status; // 用户状态 0 正常 其他业务逻辑待定
private String userChatId; // 聊天使用的id使用微信的openid作为标识
private Integer points; // 用户积分
/**
* 用户账号
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 备注
*/
private String remark;
/**
* 部门ID
*/
private Long deptId;
/**
* 岗位编号数组
*/
private String postIds;
/**
* 用户邮箱
*/
private String email;
/**
* 用户性别
*/
private Byte sex;
/**
* 头像地址
*/
private String avatar;
/**
* 最后登录IP
*/
private String loginIp;
/**
* 最后登录时间
*/
private Date loginDate;
/**
* 创建者
*/
private String creator;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新者
*/
private String updater;
/**
* 更新时间
*/
private Date updateTime;
/**
* 是否删除
*/
private Boolean deleted;
/**
* 租户编号
*/
private Long tenantId;
/**
* 能否登录主播爬虫客户端
*/
private Byte crawl;
/**
* 能否登录大哥爬虫客户端
*/
private Byte bigBrother;
/**
* 能否登录网页 AI
*/
private Byte webAi;
}

View File

@@ -14,34 +14,5 @@ import vvpkassistant.mail.model.MailModel;
* @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);
//
// LoginInfoDTO 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);
//
// Boolean checkUserName(UserModelDTO userModelDTO);
}

View File

@@ -56,324 +56,4 @@ public class UserServiceImpl extends ServiceImpl<UserDao, UserModel> implements
.expireAfterWrite(2, TimeUnit.MINUTES)
.build();
//
//
// @Override
// public UserModelVO loginWithMail(UserModelDTO model) {
// if (model.getUserNameOrEmail().isEmpty()){
// throw new BusinessException(ErrorCode.PARAMS_ERROR,"用户名或邮箱不能为空");
// }
//
// LambdaQueryWrapper<UserModel> lambdaQueryWrapper = new LambdaQueryWrapper<>();
// lambdaQueryWrapper.eq(UserModel::getEmail,model.getUserNameOrEmail())
// .or()
// .eq(UserModel::getUserName,model.getUserNameOrEmail())
// .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<UserModel> usernameWrapper = new LambdaQueryWrapper<>();
//
// lambdaQueryWrapper.eq(UserModel::getEmail,userModelDTO.getEmail());
// UserModel userModel = userDao.selectOne(lambdaQueryWrapper);
//
//
// UserModel usernameModel = userDao.selectOne(usernameWrapper
// .eq(UserModel::getUserName, userModelDTO.getUserName()));
//
// if (userModel != null) {
// throw new BusinessException(ErrorCode.MAIL_ALREADY_EXIST);
// }
// if (usernameModel != null) {
// throw new BusinessException(ErrorCode.USERNAME_ALREADY_EXIST);
// }
// if (userModelDTO.getPassword().length() < 6 ){
// throw new BusinessException(ErrorCode.PARAMS_ERROR,"密码长度不能小于 6 位");
// }
// if (userModelDTO.getUserName().isEmpty()){
// throw new BusinessException(ErrorCode.PARAMS_ERROR,"用户名不能为空");
// }
//
// 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);
// StpUtil.login(userModelVO.getId());
// userModelVO.setToken(StpUtil.getTokenValue());
// userModelVO.setHavaPassword(true);
// userModelVO.setNewAccount(true);
// userModelVO.setChatInfo(wxChatParam);
// log.info("用户{}注册,邮箱{},手机号{}",userModelVO.getId(),userModelVO.getEmail(),userModelVO.getPhoneNumber());
// 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);
// }
// if (userModel.getStatus() == 0){
// throw new BusinessException(ErrorCode.USER_HAS_ACTIVATED);
// }
// 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.getId());
// StpUtil.login(userModel.getId());
// UserModelVO userModelVO = BeanUtil.copyProperties(userModel, UserModelVO.class);
// userModelVO.setToken(StpUtil.getTokenValue());
// userModelVO.setChatInfo(wxChatParam);
// return userModelVO;
// }
// return null;
// }
//
// @Override
// public LoginInfoDTO scanQrcode(ScanInfoDTO scanInfoDTO) {
// LoginInfoDTO loginInfoDTO = qrcodeCache.getIfPresent(scanInfoDTO.getUuid());
// if (loginInfoDTO != null) {
// loginInfoDTO.setStatus(LoginStatusEnum.SCANNED.name());
// qrcodeCache.put(scanInfoDTO.getUuid(),loginInfoDTO);
// }
// log.info("-------扫码成功uuid:{}-------", scanInfoDTO.getUuid());
// return loginInfoDTO;
// }
//
// @Override
// public void confirm(ScanInfoDTO scanInfoDTO) {
// LoginInfoDTO loginInfoDTO = qrcodeCache.getIfPresent(scanInfoDTO.getUuid());
// if (loginInfoDTO != null) {
// loginInfoDTO.setStatus(LoginStatusEnum.CONFIRMED.name());
// loginInfoDTO.setId(scanInfoDTO.getId());
// 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.setPassword(BcryptUtils.encryptPassword(userModelDTO.getPassword()));
// return userDao.updateById(userModel) == 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> duplicateMailUserWrapper = new LambdaQueryWrapper<>();
//
// LambdaQueryWrapper<UserModel> lambdaQueryWrapper = new LambdaQueryWrapper<>();
//
// UserModel duplicateMailUser = userDao.selectOne(duplicateMailUserWrapper
// .eq(UserModel::getEmail, mailModel.getMailAddress()));
// if (duplicateMailUser != null) {
// throw new BusinessException(ErrorCode.SYSTEM_ERROR,"邮箱地址已被使用");
// }
//
// UserModel userModel = userDao.selectOne(lambdaQueryWrapper
// .eq(UserModel::getEmail, mail)
// .eq(UserModel::getMailVerification, 0 ));
//
// if (userModel == null) {
// throw new BusinessException(ErrorCode.USER_MAIL_NOT_VERIFICATION);
// }
// userModel.setEmail(mailModel.getMailAddress());
// mailService.sendVerificationMail(mailModel.getMailAddress(),userModel.getId());
// userModel.setMailVerification(1);
// return userDao.updateById(userModel) == 1;
// }
//
// @Override
// public Boolean checkUserName(UserModelDTO userModelDTO) {
// LambdaQueryWrapper<UserModel> lambdaQueryWrapper = new LambdaQueryWrapper<>();
// UserModel userModel = userDao.selectOne(lambdaQueryWrapper
// .eq(UserModel::getUserName, userModelDTO.getUserName()));
// return userModel == null;
// }
}

View File

@@ -1,5 +1,6 @@
package vvpkassistant.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import vvpkassistant.FunctionConfig.mapper.FunctionConfigMapper;
@@ -11,9 +12,10 @@ import java.util.concurrent.CopyOnWriteArrayList;
@Component
@RequiredArgsConstructor
@Slf4j
public class FunctionConfigHolder {
// 线程安全的全局配置容器
public static final List<FunctionConfigModel> CONFIGS = new CopyOnWriteArrayList<>();
public static List<FunctionConfigModel> CONFIGS = new CopyOnWriteArrayList<>();
@Autowired
private FunctionConfigMapper configMapper;
@@ -26,7 +28,7 @@ public class FunctionConfigHolder {
List<FunctionConfigModel> dbConfigs = configMapper.selectList(null);
CONFIGS.clear();
CONFIGS.addAll(dbConfigs);
System.out.println("已加载 "+CONFIGS.size()+" 条功能配置");
log.info("已加载 {} 条功能配置", CONFIGS.size());
}
/**

View File

@@ -62,7 +62,8 @@ public class SaTokenConfigure implements WebMvcConfigurer {
"/systemMessage/list",
"/pk/pkListForPython",
"/pk/insertPkDetail",
"/pk/updatePkRecordInfo"
"/pk/updatePkRecordInfo",
"/pk/grantPkResultPoints"
};
}

View File

@@ -1,11 +1,9 @@
package vvpkassistant.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import vvpkassistant.CoinRecords.CoinRecordsDao;
import vvpkassistant.Data.ResponseData;
import vvpkassistant.Data.ResponseInfo;
import vvpkassistant.Tools.VVTools;
import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.pk.mapper.PkInfoDao;
import vvpkassistant.pk.mapper.PkRecordDao;
import vvpkassistant.pk.mapper.PkRecordDetailDao;
@@ -15,10 +13,12 @@ import vvpkassistant.pk.model.DTO.PkInfoDetailDTO;
import vvpkassistant.pk.model.DTO.PkListRequestDTO;
import vvpkassistant.pk.model.DTO.PkListUninvitedDTO;
import vvpkassistant.pk.model.DTO.PkQueryMyCanUseDTO;
import vvpkassistant.pk.model.DTO.PkResultPointsDTO;
import vvpkassistant.pk.model.PkInfoModel;
import vvpkassistant.pk.model.PkRecord;
import vvpkassistant.pk.model.PkRecordDetail;
import vvpkassistant.pk.service.PKService;
import vvpkassistant.pk.service.PkResultPointService;
import javax.annotation.Resource;
import java.util.List;
@@ -32,6 +32,9 @@ public class PkController {
@Resource
private PKService pkService;
@Resource
private PkResultPointService pkResultPointService;
@Autowired
private PkInfoDao pkDao;
@@ -41,12 +44,6 @@ public class PkController {
@Autowired
private PkRecordDetailDao detailDao;
@Autowired
private UserDao userDao;
@Autowired
private CoinRecordsDao coinRecordsDao;
// 创建pk数据
@PostMapping("addPkData")
public ResponseData<PkInfoModel> addPkData(@RequestBody PkInfoModel pkModel) {
@@ -155,6 +152,12 @@ public class PkController {
return i == 1 ? ResponseData.success("") : ResponseData.error(ResponseInfo.ERROR.getCode(),null);
}
// 根据PK结果为用户增加积分
@PostMapping("grantPkResultPoints")
public ResponseData<Object> grantPkResultPoints(@RequestBody PkResultPointsDTO request) {
return ResponseData.success(pkResultPointService.grantPkResultPoints(request));
}
// 插入pk明细表数据
@PostMapping("insertPkDetail")
public ResponseData<Object> insert(@RequestBody PkRecordDetail detail) {

View File

@@ -0,0 +1,45 @@
package vvpkassistant.controller;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.web.bind.annotation.GetMapping;
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 vvpkassistant.Data.ResponseData;
import vvpkassistant.common.ErrorCode;
import vvpkassistant.exception.BusinessException;
import vvpkassistant.item.model.PkItem;
import vvpkassistant.item.model.DTO.PkItemPurchaseDTO;
import vvpkassistant.item.service.PkItemPurchaseService;
import vvpkassistant.item.service.PkItemService;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("pkItem")
public class PkItemController {
@Resource
private PkItemService pkItemService;
@Resource
private PkItemPurchaseService pkItemPurchaseService;
@GetMapping("list")
public ResponseData<List<PkItem>> list() {
return ResponseData.success(pkItemService.selectItemList());
}
@PostMapping("buy")
public ResponseData<Object> buy(@RequestBody PkItemPurchaseDTO request) {
if (request == null || request.getItemId() == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "itemId不能为空");
}
long userId = StpUtil.getLoginIdAsLong();
return ResponseData.success(pkItemPurchaseService.purchase(userId, request.getItemId()));
}
}

View File

@@ -0,0 +1,27 @@
package vvpkassistant.item;
import org.apache.ibatis.annotations.Mapper;
import vvpkassistant.item.model.PkItem;
import java.util.List;
/*
* @author: ziin
* @date: 2026/3/27 10:13
*/
@Mapper
public interface PkItemMapper {
int deleteByPrimaryKey(Long id);
int insert(PkItem record);
int insertSelective(PkItem record);
PkItem selectByPrimaryKey(Long id);
List<PkItem> selectItemList();
int updateByPrimaryKeySelective(PkItem record);
int updateByPrimaryKey(PkItem record);
}

View File

@@ -0,0 +1,9 @@
package vvpkassistant.item.model.DTO;
import lombok.Data;
@Data
public class PkItemPurchaseDTO {
private Long itemId;
}

View File

@@ -0,0 +1,78 @@
package vvpkassistant.item.model;
import lombok.Data;
import java.util.Date;
/*
* @author: ziin
* @date: 2026/3/27 10:13
*/
@Data
public class PkItem {
/**
* 主键Id
*/
private Long id;
/**
* 套餐名称
*/
private String itemName;
/**
* 套餐价格
*/
private Integer itemPrice;
/**
* 套餐描述
*/
private String itemDesc;
/**
* 功能
*/
private String itemFunction;
/**
* 时长
*/
private Integer itemDuration;
/**
* 是否上架
*/
private Integer itemStatus;
/**
* 备注
*/
private String remark;
/**
* 创建者
*/
private String creator;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新者
*/
private String updater;
/**
* 更新时间
*/
private Date updateTime;
/**
* 是否删除
*/
private Boolean deleted;
}

View File

@@ -0,0 +1,6 @@
package vvpkassistant.item.service;
public interface PkItemPurchaseService {
String purchase(Long userId, Long itemId);
}

View File

@@ -0,0 +1,284 @@
package vvpkassistant.item.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import vvpkassistant.CoinRecords.CoinRecords;
import vvpkassistant.CoinRecords.CoinRecordsDao;
import vvpkassistant.Tools.EpochSecondProvider;
import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.User.model.UserModel;
import vvpkassistant.common.ErrorCode;
import vvpkassistant.exception.BusinessException;
import vvpkassistant.item.PkItemMapper;
import vvpkassistant.item.model.PkItem;
import vvpkassistant.iterm_recoder.mapper.PkItemRecoderMapper;
import vvpkassistant.iterm_recoder.model.PkItemRecoder;
import vvpkassistant.tenant.mapper.SystemTenantMapper;
import vvpkassistant.tenant.model.SystemTenant;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Service
public class PkItemPurchaseServiceImpl implements PkItemPurchaseService {
private static final byte ENABLED = 1;
private static final int COIN_RECORD_DEDUCT = 0;
private static final long SECONDS_PER_HOUR = 3600L;
private static final ZoneId SHANGHAI_ZONE_ID = ZoneId.of("Asia/Shanghai");
private final PkItemMapper pkItemMapper;
private final UserDao userDao;
private final SystemTenantMapper systemTenantMapper;
private final CoinRecordsDao coinRecordsDao;
private final PkItemRecoderMapper pkItemRecoderMapper;
private final EpochSecondProvider epochSecondProvider;
public PkItemPurchaseServiceImpl(
PkItemMapper pkItemMapper,
UserDao userDao,
SystemTenantMapper systemTenantMapper,
CoinRecordsDao coinRecordsDao,
PkItemRecoderMapper pkItemRecoderMapper,
EpochSecondProvider epochSecondProvider
) {
this.pkItemMapper = pkItemMapper;
this.userDao = userDao;
this.systemTenantMapper = systemTenantMapper;
this.coinRecordsDao = coinRecordsDao;
this.pkItemRecoderMapper = pkItemRecoderMapper;
this.epochSecondProvider = epochSecondProvider;
}
@Override
@Transactional(rollbackFor = Exception.class)
public String purchase(Long userId, Long itemId) {
validateRequest(userId, itemId);
PkItem item = loadAvailableItem(itemId);
UserModel user = loadUser(userId);
ItemFunction function = resolveFunction(item.getItemFunction());
long now = epochSecondProvider.nowEpochSecond();
ensureTenantBound(user);
ensureNotPurchasedToday(userId, itemId, now);
deductPoints(userId, item.getItemPrice());
enableUserFunction(user.getId(), function);
extendTenantExpireTime(user.getTenantId(), function, item.getItemDuration(), now);
insertConsumeRecord(userId, item, now);
insertPurchaseRecord(userId, itemId, now);
return "购买成功:" + item.getItemName();
}
private static void validateRequest(Long userId, Long itemId) {
if (userId == null || itemId == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数不能为空");
}
}
private PkItem loadAvailableItem(Long itemId) {
PkItem item = pkItemMapper.selectByPrimaryKey(itemId);
if (item == null || Boolean.TRUE.equals(item.getDeleted()) || item.getItemStatus() == null || item.getItemStatus() != 0) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "商品不存在或已下架");
}
if (item.getItemPrice() == null || item.getItemPrice() < 0) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "商品价格配置错误");
}
if (item.getItemDuration() == null || item.getItemDuration() <= 0) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "商品时长配置错误");
}
return item;
}
private UserModel loadUser(Long userId) {
UserModel user = userDao.selectById(userId);
if (user == null) {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
return user;
}
private static void ensureTenantBound(UserModel user) {
if (user.getTenantId() == null) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "用户未绑定租户");
}
}
private void ensureNotPurchasedToday(Long userId, Long itemId, long now) {
Map<String, Long> dayRange = todayRangeInShanghai(now);
Date startTime = toDate(dayRange.get("start"));
Date endTime = toDate(dayRange.get("end"));
long count = pkItemRecoderMapper.countTodayPurchase(userId, String.valueOf(itemId), startTime, endTime);
if (count > 0) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "该商品今日已购买");
}
}
private void deductPoints(Long userId, Integer itemPrice) {
int intUserId = toIntId(userId);
if (userDao.decreasePointsIfEnough(intUserId, itemPrice) == 1) {
return;
}
if (userDao.selectById(userId) == null) {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
throw new BusinessException(ErrorCode.SYSTEM_ERROR, String.format("积分不足,需要%d积分", itemPrice));
}
private void enableUserFunction(Integer userId, ItemFunction function) {
UserModel updateModel = new UserModel();
updateModel.setId(userId);
switch (function) {
case CRAWL:
updateModel.setCrawl(ENABLED);
break;
case WEB_AI:
updateModel.setWebAi(ENABLED);
break;
case BIG_BROTHER:
updateModel.setBigBrother(ENABLED);
break;
default:
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "暂不支持的商品功能");
}
if (userDao.updateById(updateModel) != 1) {
throw new BusinessException(ErrorCode.UPDATE_FAILED, "更新用户功能失败");
}
}
private void extendTenantExpireTime(Long tenantId, ItemFunction function, Integer durationHours, long now) {
SystemTenant tenant = systemTenantMapper.selectById(tenantId);
if (tenant == null || Boolean.TRUE.equals(tenant.getDeleted())) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "租户不存在");
}
Date expireTime = calculateExpireTime(getExpireTime(tenant, function), durationHours, now);
SystemTenant updateModel = new SystemTenant();
updateModel.setId(tenantId);
applyExpireTime(updateModel, function, expireTime);
if (systemTenantMapper.updateById(updateModel) != 1) {
throw new BusinessException(ErrorCode.UPDATE_FAILED, "更新租户功能时长失败");
}
}
private void insertConsumeRecord(Long userId, PkItem item, long now) {
CoinRecords coinRecords = new CoinRecords(
"购买商品:" + item.getItemName(),
toIntId(userId),
item.getItemPrice(),
toIntEpoch(now),
COIN_RECORD_DEDUCT
);
if (coinRecordsDao.insert(coinRecords) != 1) {
throw new BusinessException(ErrorCode.ADD_FAILED, "写入消费记录失败");
}
}
private void insertPurchaseRecord(Long userId, Long itemId, long now) {
Date nowDate = toDate(now);
PkItemRecoder recoder = new PkItemRecoder();
recoder.setUserId(userId);
recoder.setItemId(String.valueOf(itemId));
recoder.setCreator(String.valueOf(userId));
recoder.setCreateTime(nowDate);
recoder.setUpdater(String.valueOf(userId));
recoder.setUpdateTime(nowDate);
recoder.setDeleted(Boolean.FALSE);
if (pkItemRecoderMapper.insert(recoder) != 1) {
throw new BusinessException(ErrorCode.ADD_FAILED, "写入购买记录失败");
}
}
private static Date calculateExpireTime(Date currentExpireTime, Integer durationHours, long now) {
long durationSeconds = Math.multiplyExact(durationHours.longValue(), SECONDS_PER_HOUR);
long baseEpochSecond = resolveBaseEpochSecond(currentExpireTime, now);
return toDate(Math.addExact(baseEpochSecond, durationSeconds));
}
private static long resolveBaseEpochSecond(Date currentExpireTime, long now) {
if (currentExpireTime == null) {
return now;
}
return Math.max(currentExpireTime.toInstant().getEpochSecond(), now);
}
private static Date getExpireTime(SystemTenant tenant, ItemFunction function) {
switch (function) {
case CRAWL:
return tenant.getCrawlExpireTime();
case WEB_AI:
return tenant.getAiExpireTime();
case BIG_BROTHER:
return tenant.getBrotherExpireTime();
default:
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "暂不支持的商品功能");
}
}
private static void applyExpireTime(SystemTenant tenant, ItemFunction function, Date expireTime) {
switch (function) {
case CRAWL:
tenant.setCrawlExpireTime(expireTime);
return;
case WEB_AI:
tenant.setAiExpireTime(expireTime);
return;
case BIG_BROTHER:
tenant.setBrotherExpireTime(expireTime);
return;
default:
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "暂不支持的商品功能");
}
}
private static ItemFunction resolveFunction(String itemFunction) {
if ("crawl".equalsIgnoreCase(itemFunction)) {
return ItemFunction.CRAWL;
}
if ("webAi".equalsIgnoreCase(itemFunction) || "web_ai".equalsIgnoreCase(itemFunction) || "ai".equalsIgnoreCase(itemFunction)) {
return ItemFunction.WEB_AI;
}
if ("bigBrother".equalsIgnoreCase(itemFunction) || "big_brother".equalsIgnoreCase(itemFunction) || "brother".equalsIgnoreCase(itemFunction)) {
return ItemFunction.BIG_BROTHER;
}
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "未知的商品功能类型:" + itemFunction);
}
private static Date toDate(long epochSecond) {
return Date.from(Instant.ofEpochSecond(epochSecond));
}
private static Map<String, Long> todayRangeInShanghai(long epochSecond) {
LocalDate targetDate = Instant.ofEpochSecond(epochSecond)
.atZone(SHANGHAI_ZONE_ID)
.toLocalDate();
long start = targetDate.atStartOfDay(SHANGHAI_ZONE_ID).toEpochSecond();
long end = targetDate.plusDays(1).atStartOfDay(SHANGHAI_ZONE_ID).toEpochSecond() - 1;
Map<String, Long> dayRange = new HashMap<>();
dayRange.put("start", start);
dayRange.put("end", end);
return dayRange;
}
private static int toIntId(Long id) {
if (id > Integer.MAX_VALUE) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "用户编号超出范围");
}
return id.intValue();
}
private static int toIntEpoch(long epochSecond) {
if (epochSecond > Integer.MAX_VALUE) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "时间超出范围");
}
return (int) epochSecond;
}
private enum ItemFunction {
CRAWL,
WEB_AI,
BIG_BROTHER
}
}

View File

@@ -0,0 +1,13 @@
package vvpkassistant.item.service;
import vvpkassistant.item.model.PkItem;
import java.util.List;
/*
* @author: ziin
* @date: 2026/3/27 10:13
*/
public interface PkItemService {
List<PkItem> selectItemList();
}

View File

@@ -0,0 +1,24 @@
package vvpkassistant.item.service;
import org.springframework.stereotype.Service;
import vvpkassistant.item.PkItemMapper;
import vvpkassistant.item.model.PkItem;
import javax.annotation.Resource;
import java.util.List;
/*
* @author: ziin
* @date: 2026/3/27 10:13
*/
@Service
public class PkItemServiceImpl implements PkItemService {
@Resource
private PkItemMapper pkItemMapper;
@Override
public List<PkItem> selectItemList() {
return pkItemMapper.selectItemList();
}
}

View File

@@ -0,0 +1,21 @@
package vvpkassistant.iterm_recoder.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import vvpkassistant.iterm_recoder.model.PkItemRecoder;
import java.util.Date;
@Mapper
public interface PkItemRecoderMapper extends BaseMapper<PkItemRecoder> {
@Select("select count(1) from pk_item_recoder where user_Id = #{userId} and item_id = #{itemId} and deleted = 0 and create_time between #{startTime} and #{endTime}")
long countTodayPurchase(
@Param("userId") Long userId,
@Param("itemId") String itemId,
@Param("startTime") Date startTime,
@Param("endTime") Date endTime
);
}

View File

@@ -0,0 +1,33 @@
package vvpkassistant.iterm_recoder.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
@Data
@TableName("pk_item_recoder")
public class PkItemRecoder {
@TableId(value = "Id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("user_id")
private Long userId;
@TableField("item_id")
private String itemId;
private String creator;
private Date createTime;
private String updater;
private Date updateTime;
private Boolean deleted;
}

View File

@@ -0,0 +1,10 @@
package vvpkassistant.pk.model.DTO;
import lombok.Data;
@Data
public class PkResultPointsDTO {
private Integer winnerUserId;
private Integer loserUserId;
private Boolean draw;
}

View File

@@ -4,8 +4,6 @@ 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;
@@ -242,4 +240,5 @@ public class PKServiceImpl extends ServiceImpl<PkInfoDao, PkInfoModel> implement
return pkInfoModel;
}
}
}

View File

@@ -0,0 +1,7 @@
package vvpkassistant.pk.service;
import vvpkassistant.pk.model.DTO.PkResultPointsDTO;
public interface PkResultPointService {
String grantPkResultPoints(PkResultPointsDTO request);
}

View File

@@ -0,0 +1,100 @@
package vvpkassistant.pk.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import vvpkassistant.CoinRecords.CoinRecords;
import vvpkassistant.CoinRecords.CoinRecordsDao;
import vvpkassistant.Tools.EpochSecondProvider;
import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.common.ErrorCode;
import vvpkassistant.config.FunctionConfigProvider;
import vvpkassistant.exception.BusinessException;
import vvpkassistant.pk.model.DTO.PkResultPointsDTO;
@Service
public class PkResultPointServiceImpl implements PkResultPointService {
private static final String PK_WIN_COIN_CONFIG_NAME = "PK胜利增加积分";
private static final String PK_LOSE_COIN_CONFIG_NAME = "PK失败增加积分";
private static final String PK_DRAW_COIN_CONFIG_NAME = "PK平局增加积分";
private static final int COIN_RECORD_ADD = 1;
private final UserDao userDao;
private final CoinRecordsDao coinRecordsDao;
private final FunctionConfigProvider functionConfigProvider;
private final EpochSecondProvider epochSecondProvider;
public PkResultPointServiceImpl(
UserDao userDao,
CoinRecordsDao coinRecordsDao,
FunctionConfigProvider functionConfigProvider,
EpochSecondProvider epochSecondProvider
) {
this.userDao = userDao;
this.coinRecordsDao = coinRecordsDao;
this.functionConfigProvider = functionConfigProvider;
this.epochSecondProvider = epochSecondProvider;
}
@Override
@Transactional(rollbackFor = Exception.class)
public String grantPkResultPoints(PkResultPointsDTO request) {
validateRequest(request);
int now = (int) epochSecondProvider.nowEpochSecond();
if (isDraw(request)) {
return grantDrawPoints(request, now);
}
int winPoints = parsePositivePoints(PK_WIN_COIN_CONFIG_NAME);
int losePoints = parsePositivePoints(PK_LOSE_COIN_CONFIG_NAME);
grantPoints(request.getWinnerUserId(), winPoints, PK_WIN_COIN_CONFIG_NAME, now);
grantPoints(request.getLoserUserId(), losePoints, PK_LOSE_COIN_CONFIG_NAME, now);
return String.format("操作成功,胜利方增加%d积分失败方增加%d积分", winPoints, losePoints);
}
private String grantDrawPoints(PkResultPointsDTO request, int now) {
int drawPoints = parsePositivePoints(PK_DRAW_COIN_CONFIG_NAME);
grantPoints(request.getWinnerUserId(), drawPoints, PK_DRAW_COIN_CONFIG_NAME, now);
grantPoints(request.getLoserUserId(), drawPoints, PK_DRAW_COIN_CONFIG_NAME, now);
return String.format("操作成功,平局双方各增加%d积分", drawPoints);
}
private static void validateRequest(PkResultPointsDTO request) {
if (request == null || request.getWinnerUserId() == null || request.getLoserUserId() == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数不能为空");
}
if (request.getWinnerUserId().equals(request.getLoserUserId())) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "胜利者和失败者不能是同一用户");
}
}
private static boolean isDraw(PkResultPointsDTO request) {
return Boolean.TRUE.equals(request.getDraw());
}
private int parsePositivePoints(String configName) {
String value = functionConfigProvider.getValue(configName);
if (value == null) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "未配置" + configName);
}
try {
int points = Integer.parseInt(value);
if (points < 0) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, configName + "配置错误");
}
return points;
} catch (NumberFormatException e) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, configName + "配置错误");
}
}
private void grantPoints(int userId, int points, String configName, int now) {
if (userDao.increasePoints(userId, points) != 1) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "用户不存在");
}
CoinRecords coinRecords = new CoinRecords(configName, userId, points, now, COIN_RECORD_ADD);
if (coinRecordsDao.insert(coinRecords) != 1) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "积分记录写入失败");
}
}
}

View File

@@ -0,0 +1,14 @@
package vvpkassistant.tenant.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import vvpkassistant.tenant.model.SystemTenant;
/*
* @author: ziin
* @date: 2026/3/27 13:17
*/
@Mapper
public interface SystemTenantMapper extends BaseMapper<SystemTenant> {
}

View File

@@ -0,0 +1,137 @@
package vvpkassistant.tenant.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/*
* @author: ziin
* @date: 2026/3/27 13:17
*/
/**
* 租户表
*/
@Data
@TableName("system_tenant")
public class SystemTenant {
/**
* 租户编号
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 租户名
*/
private String name;
/**
* 联系人的用户编号
*/
private Long contactUserId;
/**
* 联系人
*/
private String contactName;
/**
* 联系手机
*/
private String contactMobile;
/**
* 租户状态0正常 1停用
*/
private Byte status;
/**
* 绑定域名
*/
private String website;
/**
* 租户套餐编号
*/
private Long packageId;
/**
* 过期时间
*/
private Date expireTime;
/**
* 账号数量
*/
private Integer accountCount;
/**
* 创建者
*/
private String creator;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新者
*/
private String updater;
/**
* 更新时间
*/
private Date updateTime;
/**
* 是否删除
*/
private Boolean deleted;
/**
* 备注
*/
private String remark;
/**
* 爬虫到期时间
*/
private Date crawlExpireTime;
/**
* AI 到期时间
*/
private Date aiExpireTime;
/**
* 大哥过期时间
*/
private Date brotherExpireTime;
/**
* 上级租户 Id
*/
private Long parentId;
/**
* 租户类型:代理,用户
*/
private String tenantType;
/**
* 代理级别
*/
private Integer tenantLevel;
/**
* 初始用户
*/
private String initialUser;
}

View File

@@ -0,0 +1,206 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="vvpkassistant.item.PkItemMapper">
<resultMap id="BaseResultMap" type="vvpkassistant.item.model.PkItem">
<!--@mbg.generated-->
<!--@Table pk_item-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="item_name" jdbcType="VARCHAR" property="itemName" />
<result column="item_price" jdbcType="INTEGER" property="itemPrice" />
<result column="item_desc" jdbcType="VARCHAR" property="itemDesc" />
<result column="item_function" jdbcType="VARCHAR" property="itemFunction" />
<result column="item_duration" jdbcType="INTEGER" property="itemDuration" />
<result column="item_status" jdbcType="INTEGER" property="itemStatus" />
<result column="remark" jdbcType="VARCHAR" property="remark" />
<result column="creator" jdbcType="VARCHAR" property="creator" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="updater" jdbcType="VARCHAR" property="updater" />
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
<result column="deleted" jdbcType="BIT" property="deleted" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, item_name, item_price, item_desc, item_function, item_duration, item_status,
remark, creator, create_time, updater, update_time, deleted
</sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
<!--@mbg.generated-->
select
<include refid="Base_Column_List" />
from pk_item
where id = #{id,jdbcType=BIGINT}
</select>
<select id="selectItemList" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from pk_item
where deleted = 0
and
item_status = 0
order by id desc
</select>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
<!--@mbg.generated-->
delete from pk_item
where id = #{id,jdbcType=BIGINT}
</delete>
<insert id="insert" parameterType="vvpkassistant.item.model.PkItem">
<!--@mbg.generated-->
insert into pk_item (id, item_name, item_price,
item_desc, item_function, item_duration,
item_status, remark, creator,
create_time, updater, update_time,
deleted)
values (#{id,jdbcType=BIGINT}, #{itemName,jdbcType=VARCHAR}, #{itemPrice,jdbcType=INTEGER},
#{itemDesc,jdbcType=VARCHAR}, #{itemFunction,jdbcType=VARCHAR}, #{itemDuration,jdbcType=INTEGER},
#{itemStatus,jdbcType=INTEGER}, #{remark,jdbcType=VARCHAR}, #{creator,jdbcType=VARCHAR},
#{createTime,jdbcType=TIMESTAMP}, #{updater,jdbcType=VARCHAR}, #{updateTime,jdbcType=TIMESTAMP},
#{deleted,jdbcType=BIT})
</insert>
<insert id="insertSelective" parameterType="vvpkassistant.item.model.PkItem">
<!--@mbg.generated-->
insert into pk_item
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">
id,
</if>
<if test="itemName != null">
item_name,
</if>
<if test="itemPrice != null">
item_price,
</if>
<if test="itemDesc != null">
item_desc,
</if>
<if test="itemFunction != null">
item_function,
</if>
<if test="itemDuration != null">
item_duration,
</if>
<if test="itemStatus != null">
item_status,
</if>
<if test="remark != null">
remark,
</if>
<if test="creator != null">
creator,
</if>
<if test="createTime != null">
create_time,
</if>
<if test="updater != null">
updater,
</if>
<if test="updateTime != null">
update_time,
</if>
<if test="deleted != null">
deleted,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
#{id,jdbcType=BIGINT},
</if>
<if test="itemName != null">
#{itemName,jdbcType=VARCHAR},
</if>
<if test="itemPrice != null">
#{itemPrice,jdbcType=INTEGER},
</if>
<if test="itemDesc != null">
#{itemDesc,jdbcType=VARCHAR},
</if>
<if test="itemFunction != null">
#{itemFunction,jdbcType=VARCHAR},
</if>
<if test="itemDuration != null">
#{itemDuration,jdbcType=INTEGER},
</if>
<if test="itemStatus != null">
#{itemStatus,jdbcType=INTEGER},
</if>
<if test="remark != null">
#{remark,jdbcType=VARCHAR},
</if>
<if test="creator != null">
#{creator,jdbcType=VARCHAR},
</if>
<if test="createTime != null">
#{createTime,jdbcType=TIMESTAMP},
</if>
<if test="updater != null">
#{updater,jdbcType=VARCHAR},
</if>
<if test="updateTime != null">
#{updateTime,jdbcType=TIMESTAMP},
</if>
<if test="deleted != null">
#{deleted,jdbcType=BIT},
</if>
</trim>
</insert>
<update id="updateByPrimaryKeySelective" parameterType="vvpkassistant.item.model.PkItem">
<!--@mbg.generated-->
update pk_item
<set>
<if test="itemName != null">
item_name = #{itemName,jdbcType=VARCHAR},
</if>
<if test="itemPrice != null">
item_price = #{itemPrice,jdbcType=INTEGER},
</if>
<if test="itemDesc != null">
item_desc = #{itemDesc,jdbcType=VARCHAR},
</if>
<if test="itemFunction != null">
item_function = #{itemFunction,jdbcType=VARCHAR},
</if>
<if test="itemDuration != null">
item_duration = #{itemDuration,jdbcType=INTEGER},
</if>
<if test="itemStatus != null">
item_status = #{itemStatus,jdbcType=INTEGER},
</if>
<if test="remark != null">
remark = #{remark,jdbcType=VARCHAR},
</if>
<if test="creator != null">
creator = #{creator,jdbcType=VARCHAR},
</if>
<if test="createTime != null">
create_time = #{createTime,jdbcType=TIMESTAMP},
</if>
<if test="updater != null">
updater = #{updater,jdbcType=VARCHAR},
</if>
<if test="updateTime != null">
update_time = #{updateTime,jdbcType=TIMESTAMP},
</if>
<if test="deleted != null">
deleted = #{deleted,jdbcType=BIT},
</if>
</set>
where id = #{id,jdbcType=BIGINT}
</update>
<update id="updateByPrimaryKey" parameterType="vvpkassistant.item.model.PkItem">
<!--@mbg.generated-->
update pk_item
set item_name = #{itemName,jdbcType=VARCHAR},
item_price = #{itemPrice,jdbcType=INTEGER},
item_desc = #{itemDesc,jdbcType=VARCHAR},
item_function = #{itemFunction,jdbcType=VARCHAR},
item_duration = #{itemDuration,jdbcType=INTEGER},
item_status = #{itemStatus,jdbcType=INTEGER},
remark = #{remark,jdbcType=VARCHAR},
creator = #{creator,jdbcType=VARCHAR},
create_time = #{createTime,jdbcType=TIMESTAMP},
updater = #{updater,jdbcType=VARCHAR},
update_time = #{updateTime,jdbcType=TIMESTAMP},
deleted = #{deleted,jdbcType=BIT}
where id = #{id,jdbcType=BIGINT}
</update>
</mapper>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="vvpkassistant.iterm_recoder.mapper.PkItemRecoderMapper">
<resultMap id="BaseResultMap" type="vvpkassistant.iterm_recoder.model.PkItemRecoder">
<!--@mbg.generated-->
<!--@Table pk_item_recoder-->
<id column="Id" jdbcType="BIGINT" property="id" />
<result column="user_id" jdbcType="BIGINT" property="userId" />
<result column="item_id" jdbcType="VARCHAR" property="itemId" />
<result column="creator" jdbcType="VARCHAR" property="creator" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="updater" jdbcType="VARCHAR" property="updater" />
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
<result column="deleted" jdbcType="BIT" property="deleted" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
Id, user_Id, item_id, creator, create_time, updater, update_time, deleted
</sql>
</mapper>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="vvpkassistant.tenant.mapper.SystemTenantMapper">
<resultMap id="BaseResultMap" type="vvpkassistant.tenant.model.SystemTenant">
<!--@mbg.generated-->
<!--@Table system_tenant-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="contact_user_id" jdbcType="BIGINT" property="contactUserId" />
<result column="contact_name" jdbcType="VARCHAR" property="contactName" />
<result column="contact_mobile" jdbcType="VARCHAR" property="contactMobile" />
<result column="status" jdbcType="TINYINT" property="status" />
<result column="website" jdbcType="VARCHAR" property="website" />
<result column="package_id" jdbcType="BIGINT" property="packageId" />
<result column="expire_time" jdbcType="TIMESTAMP" property="expireTime" />
<result column="account_count" jdbcType="INTEGER" property="accountCount" />
<result column="creator" jdbcType="VARCHAR" property="creator" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="updater" jdbcType="VARCHAR" property="updater" />
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
<result column="deleted" jdbcType="BIT" property="deleted" />
<result column="remark" jdbcType="VARCHAR" property="remark" />
<result column="crawl_expire_time" jdbcType="TIMESTAMP" property="crawlExpireTime" />
<result column="ai_expire_time" jdbcType="TIMESTAMP" property="aiExpireTime" />
<result column="brother_expire_time" jdbcType="TIMESTAMP" property="brotherExpireTime" />
<result column="parent_Id" jdbcType="BIGINT" property="parentId" />
<result column="tenant_type" jdbcType="VARCHAR" property="tenantType" />
<result column="tenant_level" jdbcType="INTEGER" property="tenantLevel" />
<result column="initial_user" jdbcType="VARCHAR" property="initialUser" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, `name`, contact_user_id, contact_name, contact_mobile, `status`, website, package_id,
expire_time, account_count, creator, create_time, updater, update_time, deleted,
remark, crawl_expire_time, ai_expire_time, brother_expire_time, parent_Id, tenant_type,
tenant_level, initial_user
</sql>
</mapper>

View File

@@ -0,0 +1,110 @@
package vvpkassistant.pk.service;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import vvpkassistant.CoinRecords.CoinRecords;
import vvpkassistant.CoinRecords.CoinRecordsDao;
import vvpkassistant.Tools.EpochSecondProvider;
import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.common.ErrorCode;
import vvpkassistant.config.FunctionConfigProvider;
import vvpkassistant.exception.BusinessException;
import vvpkassistant.pk.model.DTO.PkResultPointsDTO;
import static org.mockito.Mockito.*;
class PkResultPointServiceImplTests {
@Test
void shouldGrantWinAndLosePointsTogether() {
UserDao userDao = mock(UserDao.class);
CoinRecordsDao coinRecordsDao = mock(CoinRecordsDao.class);
FunctionConfigProvider functionConfigProvider = mock(FunctionConfigProvider.class);
EpochSecondProvider epochSecondProvider = () -> 1_234L;
when(functionConfigProvider.getValue("PK胜利增加积分")).thenReturn("8");
when(functionConfigProvider.getValue("PK失败增加积分")).thenReturn("3");
when(userDao.increasePoints(1001, 8)).thenReturn(1);
when(userDao.increasePoints(1002, 3)).thenReturn(1);
when(coinRecordsDao.insert(any())).thenReturn(1);
PkResultPointServiceImpl service = new PkResultPointServiceImpl(
userDao, coinRecordsDao, functionConfigProvider, epochSecondProvider
);
PkResultPointsDTO request = new PkResultPointsDTO();
request.setWinnerUserId(1001);
request.setLoserUserId(1002);
String result = service.grantPkResultPoints(request);
Assertions.assertEquals("操作成功胜利方增加8积分失败方增加3积分", result);
ArgumentCaptor<CoinRecords> captor = ArgumentCaptor.forClass(CoinRecords.class);
verify(coinRecordsDao, times(2)).insert(captor.capture());
Assertions.assertEquals("PK胜利增加积分", captor.getAllValues().get(0).getInfo());
Assertions.assertEquals(Integer.valueOf(1001), captor.getAllValues().get(0).getUserId());
Assertions.assertEquals(Integer.valueOf(8), captor.getAllValues().get(0).getNumber());
Assertions.assertEquals("PK失败增加积分", captor.getAllValues().get(1).getInfo());
Assertions.assertEquals(Integer.valueOf(1002), captor.getAllValues().get(1).getUserId());
Assertions.assertEquals(Integer.valueOf(3), captor.getAllValues().get(1).getNumber());
Assertions.assertEquals(Integer.valueOf(1234), captor.getAllValues().get(0).getTime());
Assertions.assertEquals(Integer.valueOf(1234), captor.getAllValues().get(1).getTime());
}
@Test
void shouldGrantDrawPointsToBothUsers() {
UserDao userDao = mock(UserDao.class);
CoinRecordsDao coinRecordsDao = mock(CoinRecordsDao.class);
FunctionConfigProvider functionConfigProvider = mock(FunctionConfigProvider.class);
EpochSecondProvider epochSecondProvider = () -> 1_234L;
when(functionConfigProvider.getValue("PK平局增加积分")).thenReturn("5");
when(userDao.increasePoints(1001, 5)).thenReturn(1);
when(userDao.increasePoints(1002, 5)).thenReturn(1);
when(coinRecordsDao.insert(any())).thenReturn(1);
PkResultPointServiceImpl service = new PkResultPointServiceImpl(
userDao, coinRecordsDao, functionConfigProvider, epochSecondProvider
);
PkResultPointsDTO request = new PkResultPointsDTO();
request.setWinnerUserId(1001);
request.setLoserUserId(1002);
request.setDraw(true);
String result = service.grantPkResultPoints(request);
Assertions.assertEquals("操作成功平局双方各增加5积分", result);
ArgumentCaptor<CoinRecords> captor = ArgumentCaptor.forClass(CoinRecords.class);
verify(coinRecordsDao, times(2)).insert(captor.capture());
Assertions.assertEquals("PK平局增加积分", captor.getAllValues().get(0).getInfo());
Assertions.assertEquals(Integer.valueOf(1001), captor.getAllValues().get(0).getUserId());
Assertions.assertEquals(Integer.valueOf(5), captor.getAllValues().get(0).getNumber());
Assertions.assertEquals("PK平局增加积分", captor.getAllValues().get(1).getInfo());
Assertions.assertEquals(Integer.valueOf(1002), captor.getAllValues().get(1).getUserId());
Assertions.assertEquals(Integer.valueOf(5), captor.getAllValues().get(1).getNumber());
Assertions.assertEquals(Integer.valueOf(1234), captor.getAllValues().get(0).getTime());
Assertions.assertEquals(Integer.valueOf(1234), captor.getAllValues().get(1).getTime());
}
@Test
void shouldFailWhenWinnerAndLoserAreSameUser() {
UserDao userDao = mock(UserDao.class);
CoinRecordsDao coinRecordsDao = mock(CoinRecordsDao.class);
FunctionConfigProvider functionConfigProvider = mock(FunctionConfigProvider.class);
EpochSecondProvider epochSecondProvider = () -> 1_234L;
PkResultPointServiceImpl service = new PkResultPointServiceImpl(
userDao, coinRecordsDao, functionConfigProvider, epochSecondProvider
);
PkResultPointsDTO request = new PkResultPointsDTO();
request.setWinnerUserId(1001);
request.setLoserUserId(1001);
BusinessException ex = Assertions.assertThrows(BusinessException.class,
() -> service.grantPkResultPoints(request));
Assertions.assertEquals(ErrorCode.PARAMS_ERROR.getCode(), ex.getCode());
verifyNoInteractions(functionConfigProvider, coinRecordsDao);
verify(userDao, never()).increasePoints(anyInt(), anyInt());
}
}