第四阶段开发完成
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
package com.yupi.project.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.yupi.project.annotation.AuthCheck;
|
||||
import com.yupi.project.common.BaseResponse;
|
||||
import com.yupi.project.common.DeleteRequest;
|
||||
import com.yupi.project.common.ErrorCode;
|
||||
import com.yupi.project.common.ResultUtils;
|
||||
import com.yupi.project.constant.UserConstant;
|
||||
import com.yupi.project.exception.BusinessException;
|
||||
import com.yupi.project.exception.ThrowUtils;
|
||||
import com.yupi.project.model.dto.activationcode.ActivationCodeQueryRequest;
|
||||
import com.yupi.project.model.dto.activationcode.GenerateCodesRequest;
|
||||
import com.yupi.project.model.dto.activationcode.RedeemCodeRequest;
|
||||
import com.yupi.project.model.dto.common.IdRequest;
|
||||
import com.yupi.project.model.entity.ActivationCode;
|
||||
import com.yupi.project.model.entity.User;
|
||||
import com.yupi.project.model.vo.ActivationCodeVO;
|
||||
import com.yupi.project.service.ActivationCodeService;
|
||||
import com.yupi.project.service.UserService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/activation-code")
|
||||
@Slf4j
|
||||
public class ActivationCodeController {
|
||||
|
||||
@Resource
|
||||
private ActivationCodeService activationCodeService;
|
||||
|
||||
@Resource
|
||||
private UserService userService;
|
||||
|
||||
// region 用户操作
|
||||
|
||||
/**
|
||||
* 用户兑换激活码
|
||||
*
|
||||
* @param redeemCodeRequest 激活码请求
|
||||
* @param request HTTP请求
|
||||
* @return 是否成功
|
||||
*/
|
||||
@PostMapping("/redeem")
|
||||
public BaseResponse<Boolean> redeemActivationCode(@RequestBody RedeemCodeRequest redeemCodeRequest, HttpServletRequest request) {
|
||||
if (redeemCodeRequest == null || redeemCodeRequest.getCode() == null || redeemCodeRequest.getCode().trim().isEmpty()) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "激活码不能为空");
|
||||
}
|
||||
User loginUser = userService.getCurrentUser(request);
|
||||
if (loginUser == null) {
|
||||
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR, "用户未登录或登录已失效");
|
||||
}
|
||||
boolean result = activationCodeService.redeemCode(loginUser.getId(), redeemCodeRequest.getCode());
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region 管理员操作
|
||||
|
||||
/**
|
||||
* 管理员批量生成激活码
|
||||
*
|
||||
* @param generateCodesRequest 生成请求
|
||||
* @return 生成的激活码列表 (只返回code,避免信息过多,或根据需要返回VO)
|
||||
*/
|
||||
@PostMapping("/admin/generate")
|
||||
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||
public BaseResponse<List<ActivationCodeVO>> generateActivationCodes(@RequestBody GenerateCodesRequest generateCodesRequest) {
|
||||
if (generateCodesRequest == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
if (generateCodesRequest.getCount() == null || generateCodesRequest.getCount() <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "生成数量必须大于0");
|
||||
}
|
||||
if (generateCodesRequest.getValue() == null || generateCodesRequest.getValue().signum() <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "面值必须大于0");
|
||||
}
|
||||
|
||||
try {
|
||||
List<ActivationCode> codes = activationCodeService.generateCodes(
|
||||
generateCodesRequest.getCount(),
|
||||
generateCodesRequest.getValue(),
|
||||
generateCodesRequest.getExpireTime(),
|
||||
generateCodesRequest.getBatchId()
|
||||
);
|
||||
List<ActivationCodeVO> voList = codes.stream()
|
||||
.map(code -> {
|
||||
ActivationCodeVO vo = new ActivationCodeVO();
|
||||
BeanUtils.copyProperties(code, vo);
|
||||
return vo;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
return ResultUtils.success(voList);
|
||||
} catch (Exception e) {
|
||||
log.error("Error generating activation codes", e);
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员分页查询激活码
|
||||
*
|
||||
* @param activationCodeQueryRequest 查询条件
|
||||
* @return 分页的激活码视图对象
|
||||
*/
|
||||
@PostMapping("/admin/list/page")
|
||||
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||
public BaseResponse<Page<ActivationCodeVO>> listActivationCodesByPage(@RequestBody ActivationCodeQueryRequest activationCodeQueryRequest) {
|
||||
if (activationCodeQueryRequest == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
long current = activationCodeQueryRequest.getCurrent();
|
||||
long size = activationCodeQueryRequest.getPageSize();
|
||||
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
|
||||
|
||||
Page<ActivationCode> activationCodePage = activationCodeService.page(new Page<>(current, size),
|
||||
activationCodeService.getQueryWrapper(activationCodeQueryRequest));
|
||||
|
||||
Page<ActivationCodeVO> activationCodeVOPage = new Page<>(activationCodePage.getCurrent(), activationCodePage.getSize(), activationCodePage.getTotal());
|
||||
List<ActivationCodeVO> activationCodeVOList = activationCodePage.getRecords().stream().map(activationCode -> {
|
||||
ActivationCodeVO activationCodeVO = new ActivationCodeVO();
|
||||
BeanUtils.copyProperties(activationCode, activationCodeVO);
|
||||
if (activationCode.getIsUsed() == 1 && activationCode.getUserId() != null) {
|
||||
User user = userService.getById(activationCode.getUserId());
|
||||
if (user != null) {
|
||||
activationCodeVO.setUserName(user.getUsername());
|
||||
}
|
||||
}
|
||||
return activationCodeVO;
|
||||
}).collect(Collectors.toList());
|
||||
activationCodeVOPage.setRecords(activationCodeVOList);
|
||||
|
||||
return ResultUtils.success(activationCodeVOPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员删除激活码
|
||||
*
|
||||
* @param idRequest 包含激活码ID的请求体
|
||||
* @param request HTTP请求 (用于权限校验等)
|
||||
* @return 是否成功
|
||||
*/
|
||||
@PostMapping("/admin/delete")
|
||||
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||
public BaseResponse<Boolean> deleteActivationCode(@RequestBody IdRequest idRequest, HttpServletRequest request) {
|
||||
if (idRequest == null || idRequest.getId() == null || idRequest.getId() <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "激活码ID不能为空");
|
||||
}
|
||||
boolean result = activationCodeService.deleteCode(idRequest.getId());
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.yupi.project.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yupi.project.model.entity.ActivationCode;
|
||||
|
||||
/**
|
||||
* @description 针对表【activation_code(激活码表)】的数据库操作Mapper
|
||||
* @createDate 2024-08-04 10:00:00
|
||||
* @Entity com.yupi.project.model.entity.ActivationCode
|
||||
*/
|
||||
public interface ActivationCodeMapper extends BaseMapper<ActivationCode> {
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.yupi.project.model.dto.activationcode;
|
||||
|
||||
import com.yupi.project.common.PageRequest;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 激活码查询请求
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
public class ActivationCodeQueryRequest extends PageRequest implements Serializable {
|
||||
|
||||
/**
|
||||
* 激活码 (模糊匹配)
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 是否已使用 (0 - 未使用, 1 - 已使用)
|
||||
*/
|
||||
private Integer isUsed;
|
||||
|
||||
/**
|
||||
* 批次号
|
||||
*/
|
||||
private String batchId;
|
||||
|
||||
/**
|
||||
* 使用者用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 面值下限 (用于范围查询)
|
||||
*/
|
||||
private BigDecimal valueMin;
|
||||
|
||||
/**
|
||||
* 面值上限 (用于范围查询)
|
||||
*/
|
||||
private BigDecimal valueMax;
|
||||
|
||||
/**
|
||||
* 过期时间开始 (用于范围查询)
|
||||
*/
|
||||
private LocalDateTime expireTimeStart;
|
||||
|
||||
/**
|
||||
* 过期时间结束 (用于范围查询)
|
||||
*/
|
||||
private LocalDateTime expireTimeEnd;
|
||||
|
||||
/**
|
||||
* 创建时间开始 (用于范围查询)
|
||||
*/
|
||||
private LocalDateTime createTimeStart;
|
||||
|
||||
/**
|
||||
* 创建时间结束 (用于范围查询)
|
||||
*/
|
||||
private LocalDateTime createTimeEnd;
|
||||
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.yupi.project.model.dto.activationcode;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 管理员生成激活码请求
|
||||
*/
|
||||
@Data
|
||||
public class GenerateCodesRequest implements Serializable {
|
||||
|
||||
/**
|
||||
* 生成数量
|
||||
*/
|
||||
private Integer count;
|
||||
|
||||
/**
|
||||
* 激活码面值
|
||||
*/
|
||||
private BigDecimal value;
|
||||
|
||||
/**
|
||||
* 过期时间 (可选, null 表示永不过期)
|
||||
*/
|
||||
private LocalDateTime expireTime;
|
||||
|
||||
/**
|
||||
* 批次号 (可选, 用于追踪)
|
||||
*/
|
||||
private String batchId;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.yupi.project.model.dto.activationcode;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 用户兑换激活码请求
|
||||
*/
|
||||
@Data
|
||||
public class RedeemCodeRequest implements Serializable {
|
||||
|
||||
/**
|
||||
* 激活码
|
||||
*/
|
||||
private String code;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.yupi.project.model.dto.common;
|
||||
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 通用ID请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class IdRequest implements Serializable {
|
||||
private Long id;
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.yupi.project.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 激活码表实体类
|
||||
*/
|
||||
@TableName(value = "activation_code")
|
||||
@Data
|
||||
public class ActivationCode implements Serializable {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 激活码字符串,确保全局唯一
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 激活码对应的面值(金额)
|
||||
*/
|
||||
private BigDecimal value;
|
||||
|
||||
/**
|
||||
* 是否已使用:0-未使用,1-已使用
|
||||
*/
|
||||
private Integer isUsed;
|
||||
|
||||
/**
|
||||
* 使用者用户ID (如果已使用)
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 激活码使用时间 (如果已使用)
|
||||
*/
|
||||
private LocalDateTime useTime;
|
||||
|
||||
/**
|
||||
* 激活码过期时间 (NULL表示永不过期)
|
||||
*/
|
||||
private LocalDateTime expireTime;
|
||||
|
||||
/**
|
||||
* 生成批次号,方便管理员追踪管理
|
||||
*/
|
||||
private String batchId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 逻辑删除标志:0-未删除,1-已删除
|
||||
*/
|
||||
@TableLogic
|
||||
private Integer isDelete;
|
||||
|
||||
@TableField(exist = false)
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.yupi.project.model.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 激活码视图对象
|
||||
*/
|
||||
@Data
|
||||
public class ActivationCodeVO implements Serializable {
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 激活码
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 面值
|
||||
*/
|
||||
private BigDecimal value;
|
||||
|
||||
/**
|
||||
* 是否已使用 (0 - 未使用, 1 - 已使用)
|
||||
*/
|
||||
private Integer isUsed;
|
||||
|
||||
/**
|
||||
* 使用者用户ID (如果已使用)
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 使用者用户名 (如果已使用, 需要额外查询填充)
|
||||
*/
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 使用时间 (如果已使用)
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime useTime;
|
||||
|
||||
/**
|
||||
* 过期时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime expireTime;
|
||||
|
||||
/**
|
||||
* 批次号
|
||||
*/
|
||||
private String batchId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.yupi.project.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.yupi.project.model.entity.ActivationCode;
|
||||
import com.yupi.project.model.dto.activationcode.ActivationCodeQueryRequest;
|
||||
// TODO: Import Page, ActivationCodeVO, ActivationCodeQueryRequest when created
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 激活码服务接口
|
||||
*/
|
||||
public interface ActivationCodeService extends IService<ActivationCode> {
|
||||
|
||||
/**
|
||||
* (管理员) 批量生成激活码。
|
||||
*
|
||||
* @param count 生成数量
|
||||
* @param value 单个激活码的面值
|
||||
* @param expireTime 过期时间 (null 表示永不过期)
|
||||
* @param batchId 批次ID (可由调用者传入或在Service中生成)
|
||||
* @return 生成的激活码列表
|
||||
* @throws Exception 如果生成过程中发生错误
|
||||
*/
|
||||
List<ActivationCode> generateCodes(int count, BigDecimal value, LocalDateTime expireTime, String batchId) throws Exception;
|
||||
|
||||
/**
|
||||
* (用户) 兑换激活码。
|
||||
*
|
||||
* @param currentUserId 当前登录用户的ID
|
||||
* @param code 激活码字符串
|
||||
* @return true 如果兑换成功,并增加了用户余额
|
||||
* @throws com.yupi.project.exception.BusinessException 如果激活码无效、已使用、已过期或兑换失败
|
||||
*/
|
||||
boolean redeemCode(Long currentUserId, String code);
|
||||
|
||||
/**
|
||||
* 构建查询条件
|
||||
*
|
||||
* @param activationCodeQueryRequest 查询请求
|
||||
* @return QueryWrapper
|
||||
*/
|
||||
QueryWrapper<ActivationCode> getQueryWrapper(ActivationCodeQueryRequest activationCodeQueryRequest);
|
||||
|
||||
/**
|
||||
* 根据ID删除激活码 (逻辑删除)
|
||||
*
|
||||
* @param codeId 激活码ID
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
boolean deleteCode(Long codeId);
|
||||
|
||||
// TODO: Add method for listing codes (admin)
|
||||
// Page<ActivationCodeVO> listCodes(ActivationCodeQueryRequest queryRequest);
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package com.yupi.project.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.yupi.project.common.ErrorCode;
|
||||
import com.yupi.project.exception.BusinessException;
|
||||
import com.yupi.project.mapper.ActivationCodeMapper;
|
||||
import com.yupi.project.model.dto.activationcode.ActivationCodeQueryRequest;
|
||||
import com.yupi.project.model.entity.ActivationCode;
|
||||
import com.yupi.project.service.ActivationCodeService;
|
||||
import com.yupi.project.service.UserService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ActivationCodeServiceImpl extends ServiceImpl<ActivationCodeMapper, ActivationCode> implements ActivationCodeService {
|
||||
|
||||
@Resource
|
||||
private UserService userService;
|
||||
|
||||
// 通常不直接注入Mapper,而是通过继承的ServiceImpl的方法操作,但如果需要复杂查询可以注入
|
||||
// @Resource
|
||||
// private ActivationCodeMapper activationCodeMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<ActivationCode> generateCodes(int count, BigDecimal value, LocalDateTime expireTime, String batchId) throws Exception {
|
||||
if (count <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "生成数量必须大于0");
|
||||
}
|
||||
if (value == null || value.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "激活码面值必须大于0");
|
||||
}
|
||||
|
||||
List<ActivationCode> generatedCodes = new ArrayList<>();
|
||||
String effectiveBatchId = (batchId == null || batchId.isEmpty()) ? UUID.randomUUID().toString() : batchId;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
ActivationCode activationCode = new ActivationCode();
|
||||
// 生成唯一code的简单策略,实际项目中可能需要更复杂的防碰撞机制或预生成库
|
||||
String uniqueCode = UUID.randomUUID().toString().replace("-", "").substring(0, 16).toUpperCase();
|
||||
// TODO: 考虑code冲突的可能性,虽然UUID冲突概率极低,但严格来说需要循环检查或数据库唯一约束处理
|
||||
// 在高并发下,先生成再批量插入,并处理唯一约束异常可能是更好的方式
|
||||
|
||||
activationCode.setCode(uniqueCode);
|
||||
activationCode.setValue(value);
|
||||
activationCode.setExpireTime(expireTime);
|
||||
activationCode.setBatchId(effectiveBatchId);
|
||||
activationCode.setIsUsed(0); // 0 for not used
|
||||
activationCode.setIsDelete(0); // 0 for not deleted
|
||||
// createTime and updateTime will be handled by DB default or MyBatis Plus fill strategy
|
||||
generatedCodes.add(activationCode);
|
||||
}
|
||||
|
||||
boolean saveBatchResult = this.saveBatch(generatedCodes);
|
||||
if (!saveBatchResult) {
|
||||
log.error("Failed to batch save activation codes for batchId: {}", effectiveBatchId);
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "批量生成激活码失败");
|
||||
}
|
||||
log.info("Successfully generated {} activation codes with value {}, batchId: {}.", count, value, effectiveBatchId);
|
||||
return generatedCodes;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean redeemCode(Long currentUserId, String code) {
|
||||
if (currentUserId == null || currentUserId <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "无效的用户ID");
|
||||
}
|
||||
if (code == null || code.trim().isEmpty()) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "激活码不能为空");
|
||||
}
|
||||
|
||||
// 1. 查询激活码
|
||||
QueryWrapper<ActivationCode> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("code", code.trim());
|
||||
// is_delete = 0 会由 @TableLogic 自动处理
|
||||
ActivationCode activationCode = this.getOne(queryWrapper);
|
||||
|
||||
if (activationCode == null) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "无效的激活码");
|
||||
}
|
||||
|
||||
// 2. 检查是否已使用
|
||||
if (activationCode.getIsUsed() == 1) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "此激活码已被使用");
|
||||
}
|
||||
|
||||
// 3. 检查是否过期
|
||||
if (activationCode.getExpireTime() != null && LocalDateTime.now().isAfter(activationCode.getExpireTime())) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "此激活码已过期");
|
||||
}
|
||||
|
||||
// 4. 增加用户余额
|
||||
boolean increaseSuccess = userService.increaseBalance(currentUserId, activationCode.getValue());
|
||||
if (!increaseSuccess) {
|
||||
// increaseBalance 内部应该会抛出异常如果失败(如用户不存在),或者返回false
|
||||
log.error("Failed to increase balance for user {} while redeeming code {}", currentUserId, code);
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "余额充值失败,请稍后再试");
|
||||
}
|
||||
|
||||
// 5. 更新激活码状态
|
||||
activationCode.setIsUsed(1);
|
||||
activationCode.setUserId(currentUserId);
|
||||
activationCode.setUseTime(LocalDateTime.now());
|
||||
// updateTime 会自动更新
|
||||
|
||||
boolean updateResult = this.updateById(activationCode);
|
||||
if (!updateResult) {
|
||||
log.error("Failed to update activation code {} status after redeeming by user {}. Potential inconsistency!", code, currentUserId);
|
||||
// 由于余额已增加,但激活码状态更新失败,这是一个危险状态。事务应回滚。
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "更新激活码状态失败,请联系客服");
|
||||
}
|
||||
|
||||
log.info("User {} successfully redeemed activation code {}. Value: {}", currentUserId, code, activationCode.getValue());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryWrapper<ActivationCode> getQueryWrapper(ActivationCodeQueryRequest activationCodeQueryRequest) {
|
||||
QueryWrapper<ActivationCode> queryWrapper = new QueryWrapper<>();
|
||||
if (activationCodeQueryRequest == null) {
|
||||
return queryWrapper;
|
||||
}
|
||||
|
||||
String code = activationCodeQueryRequest.getCode();
|
||||
Integer isUsed = activationCodeQueryRequest.getIsUsed();
|
||||
String batchId = activationCodeQueryRequest.getBatchId();
|
||||
Long userId = activationCodeQueryRequest.getUserId();
|
||||
BigDecimal valueMin = activationCodeQueryRequest.getValueMin();
|
||||
BigDecimal valueMax = activationCodeQueryRequest.getValueMax();
|
||||
LocalDateTime expireTimeStart = activationCodeQueryRequest.getExpireTimeStart();
|
||||
LocalDateTime expireTimeEnd = activationCodeQueryRequest.getExpireTimeEnd();
|
||||
LocalDateTime createTimeStart = activationCodeQueryRequest.getCreateTimeStart();
|
||||
LocalDateTime createTimeEnd = activationCodeQueryRequest.getCreateTimeEnd();
|
||||
String sortField = activationCodeQueryRequest.getSortField();
|
||||
String sortOrder = activationCodeQueryRequest.getSortOrder();
|
||||
|
||||
// 拼接查询条件
|
||||
if (StringUtils.isNotBlank(code)) {
|
||||
queryWrapper.like("code", code);
|
||||
}
|
||||
if (isUsed != null) {
|
||||
queryWrapper.eq("isUsed", isUsed);
|
||||
}
|
||||
if (StringUtils.isNotBlank(batchId)) {
|
||||
queryWrapper.eq("batchId", batchId);
|
||||
}
|
||||
if (userId != null && userId > 0) {
|
||||
queryWrapper.eq("userId", userId);
|
||||
}
|
||||
if (valueMin != null) {
|
||||
queryWrapper.ge("value", valueMin);
|
||||
}
|
||||
if (valueMax != null) {
|
||||
queryWrapper.le("value", valueMax);
|
||||
}
|
||||
if (expireTimeStart != null) {
|
||||
queryWrapper.ge("expireTime", expireTimeStart);
|
||||
}
|
||||
if (expireTimeEnd != null) {
|
||||
queryWrapper.le("expireTime", expireTimeEnd);
|
||||
}
|
||||
if (createTimeStart != null) {
|
||||
queryWrapper.ge("createTime", createTimeStart);
|
||||
}
|
||||
if (createTimeEnd != null) {
|
||||
queryWrapper.le("createTime", createTimeEnd);
|
||||
}
|
||||
// 默认按创建时间降序
|
||||
if (StringUtils.isNotBlank(sortField)) {
|
||||
queryWrapper.orderBy(true, "ascend".equalsIgnoreCase(sortOrder), sortField);
|
||||
} else {
|
||||
queryWrapper.orderByDesc("create_time");
|
||||
}
|
||||
|
||||
return queryWrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean deleteCode(Long codeId) {
|
||||
if (codeId == null || codeId <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "无效的激活码ID");
|
||||
}
|
||||
// 检查激活码是否存在,这步可选,因为removeById如果找不到也不会报错,但返回false
|
||||
ActivationCode existingCode = this.getById(codeId);
|
||||
if (existingCode == null || existingCode.getIsDelete() == 1) { // 已经是逻辑删除状态
|
||||
// 可以选择抛出未找到错误,或者认为已经是"已删除"状态,返回true
|
||||
// throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "激活码不存在或已被删除");
|
||||
log.warn("Attempted to delete a non-existent or already deleted activation code with ID: {}", codeId);
|
||||
return true; // 或者 false,取决于业务定义,这里认为已经是目标状态所以返回true
|
||||
}
|
||||
|
||||
// 使用MyBatis Plus的逻辑删除
|
||||
boolean result = this.removeById(codeId);
|
||||
if (!result) {
|
||||
// 这种情况理论上不应该发生,除非并发删除了或者 getById 和 removeById 之间状态变了
|
||||
log.error("Failed to logically delete activation code with ID: {}. removeById returned false.", codeId);
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "删除激活码失败");
|
||||
}
|
||||
log.info("Activation code with ID: {} logically deleted.", codeId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -140,25 +140,51 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
|
||||
public User getCurrentUser(HttpServletRequest request) {
|
||||
// 优先从 SecurityContextHolder 获取认证信息
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication != null && authentication.isAuthenticated() && !(authentication.getPrincipal() instanceof String && authentication.getPrincipal().equals("anonymousUser"))) {
|
||||
if (authentication != null && authentication.isAuthenticated() && !(authentication.getPrincipal() instanceof String && "anonymousUser".equals(authentication.getPrincipal()))) {
|
||||
Object principal = authentication.getPrincipal();
|
||||
if (principal instanceof User) {
|
||||
return (User) principal; // principal 已经是 safetyUser
|
||||
User userFromContext = (User) principal;
|
||||
// 根据 Context 中的用户ID,从数据库重新获取最新的用户信息
|
||||
User latestUser = this.getById(userFromContext.getId());
|
||||
if (latestUser != null && latestUser.getIsDeleted() == 0) { // 确保用户未被删除
|
||||
// 返回脱敏后的最新用户信息
|
||||
return getSafetyUser(latestUser);
|
||||
} else {
|
||||
// 如果根据ID查不到用户了(例如被删除了),则认为未登录或异常
|
||||
// 清除可能无效的认证信息
|
||||
SecurityContextHolder.clearContext();
|
||||
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR, "用户状态异常或已失效,请重新登录");
|
||||
}
|
||||
} else if (principal instanceof org.springframework.security.core.userdetails.User) {
|
||||
// 如果 principal 是 Spring Security 的 User (不太可能在这里,因为我们设置的是 safetyUser)
|
||||
// 需要转换或重新查询
|
||||
// For now, assume it's our User object based on login logic
|
||||
// 如果是Spring Security的User对象,通常包含username
|
||||
org.springframework.security.core.userdetails.User springUser = (org.springframework.security.core.userdetails.User) principal;
|
||||
User userByUsername = this.getOne(new QueryWrapper<User>().eq("username", springUser.getUsername()));
|
||||
if (userByUsername != null && userByUsername.getIsDeleted() == 0) {
|
||||
return getSafetyUser(userByUsername);
|
||||
} else {
|
||||
SecurityContextHolder.clearContext();
|
||||
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR, "用户状态异常或已失效,请重新登录");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果 SecurityContextHolder 中没有,尝试从 session (旧逻辑,作为后备或移除)
|
||||
|
||||
// 如果 SecurityContextHolder 中没有有效信息,尝试从 session (旧逻辑,可作为后备)
|
||||
// 注意:如果完全依赖 Spring Security,这部分可以考虑移除或调整
|
||||
Object userObj = request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);
|
||||
if (userObj instanceof User) {
|
||||
// 最好在这里也验证一下数据库中的用户状态,或者确保session中的信息足够可信
|
||||
return (User) userObj;
|
||||
User userFromSession = (User) userObj;
|
||||
User latestUser = this.getById(userFromSession.getId());
|
||||
if (latestUser != null && latestUser.getIsDeleted() == 0) {
|
||||
// 如果session中的用户有效,也最好将其信息同步到SecurityContext以保持一致性 (可选)
|
||||
// recreateAuthenticationInSecurityContext(latestUser, request); // 辅助方法,如果需要
|
||||
return getSafetyUser(latestUser);
|
||||
}
|
||||
// Session 中的用户信息无效
|
||||
request.getSession().removeAttribute(UserConstant.USER_LOGIN_STATE);
|
||||
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR, "会话已过期或无效,请重新登录");
|
||||
}
|
||||
|
||||
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
|
||||
|
||||
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR, "用户未登录");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user