ruoyi-admin/pom.xml
@@ -23,7 +23,10 @@ <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> <!-- 表示依赖不会传递 --> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- swagger3--> <dependency> <groupId>io.springfox</groupId> ruoyi-admin/src/main/java/com/ruoyi/im/ImApiController.java
@@ -352,7 +352,7 @@ userTeamAndPositionOut.setInsureNumber(activeNumber); positions.forEach(f->{ if(userPolicyList.size() >= f.getNumberPeople()){ if(activeNumber >= f.getNumberPeople()){ userTeamAndPositionOut.setPosition(f.getPosition()); userTeamAndPositionOut.setSalary(f.getSalary()); } @@ -381,12 +381,24 @@ * 产品信息列表 */ @GetMapping("/getProduct") public Result getProduct() { public Result getProduct(@RequestParam("account") String account) { UserAccount userAccount = userAccountService.getOne(new LambdaQueryWrapper<>(UserAccount.class).eq(UserAccount::getCloudMessageAccount, account)); if(ObjectUtil.isEmpty(userAccount)){ return Result.error("账号不存在!"); } LambdaQueryWrapper<InsuranceProduct> wrapper = new LambdaQueryWrapper<>(); // 按创建时间倒序排列 wrapper.orderByDesc(InsuranceProduct::getCreatedAt); List<InsuranceProduct> list = insuranceProductService.list(wrapper); list.forEach(f->{ long count = userPolicyService.count(new LambdaQueryWrapper<>(UserPolicy.class) .eq(UserPolicy::getUserId, userAccount.getId()) .eq(UserPolicy::getProductId,f.getId()) .ne(UserPolicy::getApprovalStatus,2) ); if(count > 0){ f.setIsBuy(true); } List<InsuranceFeature> features = insuranceFeatureService.list(new LambdaQueryWrapper<InsuranceFeature>() .eq(InsuranceFeature::getProductId, f.getId())); f.setProductFeature(features); ruoyi-admin/src/main/java/com/ruoyi/im/out/UserAccountOut.java
@@ -9,6 +9,7 @@ import java.math.BigDecimal; import java.util.Date; import java.util.List; @Data public class UserAccountOut { @@ -26,7 +27,8 @@ // 昵称 private String nickname; // 账户余额 private BigDecimal balance = BigDecimal.ZERO; // 邀请码 private String invitationCode; @@ -52,4 +54,7 @@ private Date createTime; //下级用户 private List<UserAccountOut> subordinateList; } ruoyi-admin/src/main/java/com/ruoyi/im/service/impl/UserPolicyServiceImpl.java
@@ -55,13 +55,10 @@ if(!isPhoneValid){ return Result.error("手机号格式不正确!"); } // // 验证身份证 // boolean isIdCardValid = ValidatorUtil.isValidIdCard(dto.getIdCard()); // if(!isIdCardValid){ // return Result.error("身份证格式不正确!"); // } long count = count(new LambdaQueryWrapper<UserPolicy>() .eq(UserPolicy::getUserId, userAccount.getId()) .eq(UserPolicy::getProductId,dto.getProductId()) .eq(UserPolicy::getPolicyStatus, UserPolicy.PolicyStatus.ACTIVE) .and(a-> a.eq(UserPolicy::getApprovalStatus, 0) .or() @@ -75,6 +72,14 @@ if(ObjectUtil.isEmpty(insuranceProduct)){ return Result.error("该产品停止购买或已下架!"); } if(userAccount.getBalance().compareTo(insuranceProduct.getPremium()) < 0){ return Result.error("余额不足!"); } userAccount.setBalance(userAccount.getBalance().subtract(insuranceProduct.getPremium())); userAccountService.updateById(userAccount); UserPolicy userPolicy = new UserPolicy(); userPolicy.setAccount(userAccount.getAccount()); userPolicy.setProductName(insuranceProduct.getProductName()); @@ -99,6 +104,8 @@ userPolicy.setIsLifelong(insuranceProduct.getTerm() == 0 ? 0 : 1); save(userPolicy); return Result.success("购买成功,注意查看资料审核状态!"); } ruoyi-admin/src/main/java/com/ruoyi/im/util/RedisDistributedLock.java
New file @@ -0,0 +1,76 @@ package com.ruoyi.im.util; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; @Component public class RedisDistributedLock { private final RedisTemplate<Object, Object> redisTemplate; private static final String LOCK_PREFIX = "insurance:lock:"; public RedisDistributedLock(RedisTemplate<Object, Object> redisTemplate) { this.redisTemplate = redisTemplate; } /** * 尝试获取锁 * @param lockKey 锁的key * @param expireSeconds 锁过期时间(秒) * @param waitSeconds 等待时间(秒) * @return 是否获取成功 */ public boolean tryLock(String lockKey, long expireSeconds, long waitSeconds) { String key = LOCK_PREFIX + lockKey; long endTime = System.currentTimeMillis() + waitSeconds * 1000; while (System.currentTimeMillis() < endTime) { Boolean success = redisTemplate.opsForValue().setIfAbsent(key, "locked", expireSeconds, TimeUnit.SECONDS); if (Boolean.TRUE.equals(success)) { return true; } try { // 短暂休眠后重试 Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } } return false; } /** * 获取锁(使用默认参数) */ public boolean tryLock(String lockKey) { return tryLock(lockKey, 30L, 10L); } /** * 释放锁 * @param lockKey 锁的key */ public void releaseLock(String lockKey) { String key = LOCK_PREFIX + lockKey; redisTemplate.delete(key); } /** * 生成锁的key(基于用户ID和产品ID) */ public String generateLockKey(String account, Integer productId) { return String.format("purchase:%d:%d", account, productId); } /** * 生成基于用户ID的锁key */ public String generateUserLockKey(Integer userId) { return String.format("purchase:%d", userId); } } ruoyi-admin/src/main/java/com/ruoyi/web/controller/product/UserKycController.java
@@ -122,7 +122,7 @@ */ @GetMapping("/list") public TableDataInfo list(@RequestParam(value = "account",required = false) String account, @RequestParam(value = "state",required = false) Integer state @RequestParam(value = "state",defaultValue = "0") Integer state ) { startPage(); @@ -130,8 +130,10 @@ if(StringUtils.isNotEmpty(account)){ wrapper.eq(UserKyc::getAccount,account); } if(state != null){ if(state != null && state != 3){ wrapper.eq(UserKyc::getState,state); }else if(state == 3){ wrapper.ne(UserKyc::getState,0); } // 按创建时间倒序排列 wrapper.orderByDesc(UserKyc::getCreatedAt); ruoyi-admin/src/main/java/com/ruoyi/web/controller/product/UserPolicyController.java
@@ -10,16 +10,17 @@ import com.ruoyi.common.utils.StringUtils; import com.ruoyi.im.comm.Result; import com.ruoyi.im.service.MedicalInsuranceAccountService; import com.ruoyi.system.domain.InsuranceProduct; import com.ruoyi.system.domain.MedicalInsuranceAccount; import com.ruoyi.system.domain.UserAccount; import com.ruoyi.system.domain.UserPolicy; import com.ruoyi.im.service.impl.InsurancePositionServiceImpl; import com.ruoyi.im.util.RedisDistributedLock; import com.ruoyi.im.util.UserPolicyUtils; import com.ruoyi.system.domain.*; import com.ruoyi.system.domain.dto.UserPolicyDto; import com.ruoyi.im.service.UserPolicyService; import com.ruoyi.system.service.UserAccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.interceptor.TransactionAspectSupport; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; @@ -29,6 +30,8 @@ import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.UUID; import java.util.stream.Collectors; @RestController @RequestMapping("/userPolicy") @@ -43,18 +46,44 @@ @Autowired UserAccountService userAccountService; @Autowired InsurancePositionServiceImpl insurancePositionService; @Autowired private RedisDistributedLock redisDistributedLock; /** * 保险购买申请 */ @PostMapping("/purchaseApplication") public Result purchaseApplication(UserPolicyDto dto) { // 生成锁的key:基于用户ID和产品ID,防止同一用户同时购买同一产品 String lockKey = redisDistributedLock.generateLockKey(dto.getAccount(), dto.getProductId()); boolean lockAcquired = false; try { // 尝试获取分布式锁:等待10秒,锁过期30秒 lockAcquired = redisDistributedLock.tryLock(lockKey, 30L, 10L); if (!lockAcquired) { return Result.error("操作过于频繁,请稍后重试"); } // 执行购买逻辑 return userPolicyService.purchaseApplication(dto); }catch (Exception e){ } catch (Exception e) { e.printStackTrace(); return Result.error("购买失败"); } finally { // 释放锁 if (lockAcquired) { redisDistributedLock.releaseLock(lockKey); } } } /** * 根据用户id查询保单 @@ -77,9 +106,8 @@ */ @GetMapping("/list") public TableDataInfo list(@RequestParam(value = "account",required = false) String account, @RequestParam(value = "approvalStatus",required = false) Integer approvalStatus, @RequestParam(value = "productName",required = false) String productName, @RequestParam(value = "policyStatus",required = false) String policyStatus) { @RequestParam(value = "status",defaultValue = "0") Integer status, @RequestParam(value = "productName",required = false) String productName) { startPage(); LambdaQueryWrapper<UserPolicy> wrapper = new LambdaQueryWrapper<>(); @@ -95,13 +123,10 @@ } // 审批状态 if (approvalStatus != null) { wrapper.eq(UserPolicy::getApprovalStatus, approvalStatus); } // 保单状态 if (StringUtils.isNotEmpty(policyStatus)) { wrapper.eq(UserPolicy::getPolicyStatus, policyStatus); if (status != null && status != 3) { wrapper.ne(UserPolicy::getApprovalStatus, 0); }else{ wrapper.eq(UserPolicy::getApprovalStatus, 0); } // 按创建时间倒序排列 @@ -133,7 +158,7 @@ } //计算到期时间 LocalDate expirationTime = calculateInsuranceEndDate(LocalDate.now(), userPolicy.getTerm()); LocalDate expirationTime = calculateInsuranceEndDateToDay(LocalDate.now(), userPolicy.getTerm()); userPolicy.setApprovalStatus(approvalStatus); userPolicy.setMessage(message); @@ -158,6 +183,50 @@ medicalInsuranceAccount.setCreatedAt(new Date()); medicalInsuranceAccount.setUpdatedAt(new Date()); medicalInsuranceAccountService.save(medicalInsuranceAccount); //判断上级用户职位达成 if(approvalStatus == 1){ //查询当前用户 UserAccount userAccount = userAccountService.getOne(new LambdaQueryWrapper<UserAccount>() .eq(UserAccount::getId, userPolicy.getUserId()) ); //上级 UserAccount superiorUser = userAccountService.getOne(new LambdaQueryWrapper<UserAccount>() .eq(UserAccount::getAccount, userAccount.getInvitationAccount()) ); //查询上级的所有下级 List<UserAccount> userAccountList = userAccountService.list(new LambdaQueryWrapper<UserAccount>() .eq(UserAccount::getInvitationAccount, superiorUser.getAccount()) ); if(userAccountList.size() == 0){ return AjaxResult.success("审批成功"); } List<Integer> idList = userAccountList.stream() .map(UserAccount::getId) .collect(Collectors.toList()); //查询下级的保单 List<UserPolicy> userPolicyList = userPolicyService.list(new LambdaQueryWrapper<>(UserPolicy.class) .in(UserPolicy::getUserId, idList) ); // 手动将当前审批的保单加入到列表中(因为事务隔离可能查不到) userPolicyList.add(userPolicy); if(userPolicyList.size() == 0){ return AjaxResult.success("审批成功"); } //生效保单数量 long activePolicies = UserPolicyUtils.countActivePolicies(userPolicyList); //查询所有职位 List<InsurancePosition> positions = insurancePositionService.list(); positions.forEach(f->{ if(activePolicies >= f.getNumberPeople()){ superiorUser.setPosition(f.getPosition()); superiorUser.setAgreedTime(LocalDate.now()); } }); userAccountService.updateById(superiorUser); return AjaxResult.success("审批成功"); } return AjaxResult.success("审批成功"); }catch (Exception e){ e.printStackTrace(); @@ -170,6 +239,29 @@ /** * 计算保险到期日 * @param startDate 保险开始日期 * @param termDays 保险天数 * @return 保险到期日期 */ public static LocalDate calculateInsuranceEndDateToDay(LocalDate startDate, int termDays) { return calculateDateAfterDays(startDate, termDays); } /** * 计算指定天数后的日期 * @param startDate 开始日期 * @param daysToAdd 要添加的天数 * @return 计算后的日期 */ public static LocalDate calculateDateAfterDays(LocalDate startDate, int daysToAdd) { if (startDate == null) { throw new IllegalArgumentException("开始日期不能为空"); } return startDate.plusDays(daysToAdd); } /** * 计算保险到期日 * @param startDate 保险开始日期 * @param termYears 保险年限 * @return 保险到期日期 */ ruoyi-admin/src/main/java/com/ruoyi/web/controller/product/UserWalletControlkler.java
@@ -49,7 +49,7 @@ }else if(type == 2){ BigDecimal balance = userAccount.getBalance().subtract(money); if(balance.compareTo(BigDecimal.ZERO) < 0){ return AjaxResult.success("扣款金额超过用户余额,操作失败!"); return AjaxResult.error("扣款金额超过用户余额,操作失败!"); } userAccount.setBalance(balance); }else { ruoyi-admin/src/main/java/com/ruoyi/web/controller/user/UserController.java
@@ -23,14 +23,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.*; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.*; import java.util.stream.Collectors; @RestController @RequestMapping("/im/user") @@ -46,12 +42,10 @@ /** * 获取会员列表 */ // @PreAuthorize("@ss.hasPermi('im:user:list')") @GetMapping("/list") public TableDataInfo list(UserAccountVo vo) { // 创建查询条件包装器 LambdaQueryWrapper<UserAccount> queryWrapper = new LambdaQueryWrapper<>(); // 只有当 keyword 不为空时才添加 OR 条件 @@ -71,6 +65,7 @@ queryWrapper .eq(ObjectUtil.isNotEmpty(vo.getAccountType()), UserAccount::getAccountType, vo.getAccountType()) .eq(ObjectUtil.isNotEmpty(vo.getStatus()), UserAccount::getStatus, vo.getStatus()) .eq(ObjectUtil.isNotEmpty(vo.getPosition()), UserAccount::getPosition, vo.getPosition()) .between(ObjectUtil.isAllNotEmpty(vo.getStartTime(), vo.getEndTime()), UserAccount::getCreateTime, vo.getStartTime(), vo.getEndTime()); @@ -81,7 +76,9 @@ PageInfo<UserAccount> pageInfo = new PageInfo<>(list); List<UserAccountOut> toList = ConverterUtil.convertToList(list, UserAccountOut.class); // 转换为输出对象并递归加载下级列表(最多3级) List<UserAccountOut> toList = convertToUserAccountOutWithSubordinates(list, 3); TableDataInfo rspData = new TableDataInfo(); rspData.setCode(HttpStatus.SUCCESS); rspData.setMsg("查询成功"); @@ -91,6 +88,154 @@ } /** * 转换用户账户列表并递归加载下级用户 * @param userAccounts 用户账户列表 * @param maxLevel 最大递归层级 * @return 包含下级列表的用户输出对象列表 */ private List<UserAccountOut> convertToUserAccountOutWithSubordinates(List<UserAccount> userAccounts, int maxLevel) { if (ObjectUtil.isEmpty(userAccounts) || maxLevel <= 0) { return new ArrayList<>(); } // 先转换基础信息 List<UserAccountOut> result = ConverterUtil.convertToList(userAccounts, UserAccountOut.class); // 递归加载下级用户 loadSubordinateUsersRecursive(result, maxLevel); return result; } /** * 递归加载多级下级用户 * @param userAccountOuts 当前层级的用户列表 * @param remainingLevels 剩余递归层级 */ private void loadSubordinateUsersRecursive(List<UserAccountOut> userAccountOuts, int remainingLevels) { if (ObjectUtil.isEmpty(userAccountOuts) || remainingLevels <= 0) { return; } // 加载当前级别的下级用户 loadCurrentLevelSubordinates(userAccountOuts); // 递归加载下级的下级 for (UserAccountOut user : userAccountOuts) { if (ObjectUtil.isNotEmpty(user.getSubordinateList())) { loadSubordinateUsersRecursive(user.getSubordinateList(), remainingLevels - 1); } } } /** * 加载当前层级的直接下级用户 * @param userAccountOuts 当前层级的用户列表 */ private void loadCurrentLevelSubordinates(List<UserAccountOut> userAccountOuts) { if (ObjectUtil.isEmpty(userAccountOuts)) { return; } // 收集所有用户的账号(用于查询下级) List<String> accounts = userAccountOuts.stream() .map(UserAccountOut::getAccount) .filter(ObjectUtil::isNotEmpty) .distinct() .collect(Collectors.toList()); if (accounts.isEmpty()) { // 如果没有账号,为所有用户设置空列表 userAccountOuts.forEach(user -> user.setSubordinateList(new ArrayList<>())); return; } // 批量查询所有直接下级用户 LambdaQueryWrapper<UserAccount> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.in(UserAccount::getInvitationAccount, accounts); List<UserAccount> allSubordinates = userAccountService.list(queryWrapper); // 转换为输出对象 List<UserAccountOut> allSubordinateOuts = ConverterUtil.convertToList(allSubordinates, UserAccountOut.class); // 按邀请人账号分组 Map<String, List<UserAccountOut>> subordinateMap = allSubordinateOuts.stream() .filter(sub -> ObjectUtil.isNotEmpty(sub.getInvitationAccount())) .collect(Collectors.groupingBy(UserAccountOut::getInvitationAccount)); // 为每个用户设置下级列表 for (UserAccountOut user : userAccountOuts) { if (ObjectUtil.isNotEmpty(user.getAccount())) { List<UserAccountOut> subordinates = subordinateMap.get(user.getAccount()); user.setSubordinateList(ObjectUtil.isNotEmpty(subordinates) ? subordinates : new ArrayList<>()); } else { user.setSubordinateList(new ArrayList<>()); } } } /** * 获取用户的下级树形结构 * @param userId 用户ID * @param maxLevel 最大层级深度 * @return 用户下级树形结构 */ @GetMapping("/subordinateTree/{userId}") public AjaxResult getSubordinateTree(@PathVariable Integer userId, @RequestParam(defaultValue = "3") int maxLevel) { try { UserAccount userAccount = userAccountService.getById(userId); if (ObjectUtil.isEmpty(userAccount)) { return AjaxResult.error("用户不存在"); } // 转换当前用户 UserAccountOut userOut = ConverterUtil.convert(userAccount, UserAccountOut.class); // 递归加载下级树 List<UserAccountOut> userList = new ArrayList<>(); userList.add(userOut); loadSubordinateUsersRecursive(userList, maxLevel); return AjaxResult.success(userOut); } catch (Exception e) { log.error("获取用户下级树失败", e); return AjaxResult.error("获取下级树失败"); } } /** * 获取用户的直接下级列表(不分页) */ @GetMapping("/directSubordinates/{userId}") public AjaxResult getDirectSubordinates(@PathVariable Integer userId) { try { UserAccount userAccount = userAccountService.getById(userId); if (ObjectUtil.isEmpty(userAccount)) { return AjaxResult.error("用户不存在"); } // 查询直接下级 LambdaQueryWrapper<UserAccount> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(UserAccount::getInvitationAccount, userAccount.getAccount()); List<UserAccount> subordinates = userAccountService.list(queryWrapper); List<UserAccountOut> result = ConverterUtil.convertToList(subordinates, UserAccountOut.class); return AjaxResult.success(result); } catch (Exception e) { log.error("获取直接下级列表失败", e); return AjaxResult.error("获取下级列表失败"); } } /** * 修改会员 */ // @PreAuthorize("@ss.hasPermi('im:user:updateUserAccount')") ruoyi-system/src/main/java/com/ruoyi/system/domain/InsuranceProduct.java
@@ -64,6 +64,10 @@ @TableField(exist = false) private List<InsuranceFeature> productFeature; // 是否已购买 false:未购买 true: 已购买 @TableField(exist = false) private Boolean isBuy = false; // 投保须知 private String liabilityExemption; ruoyi-system/src/main/java/com/ruoyi/system/domain/UserAccount.java
@@ -5,6 +5,7 @@ import lombok.Data; import java.math.BigDecimal; import java.time.LocalDate; import java.util.Date; @Data @@ -87,4 +88,10 @@ //实名状态:0 认证中 1 已认证 2 未实名 private Integer kycStatus = 2; //职位 private String position = "普通用户"; //达成时间 private LocalDate agreedTime; } ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/UserAccountVo.java
@@ -72,6 +72,9 @@ private Boolean deleted; //职位 private String position; //开始时间 private Date startTime; //结束时间