红包功能

This commit is contained in:
fxh 2025-08-06 10:04:41 +08:00
parent 668787f053
commit f640fed356
21 changed files with 744 additions and 45 deletions

View File

@ -181,6 +181,8 @@ tenant:
- sys_version - sys_version
- ums_member_wechat - ums_member_wechat
- sys_tenant_extend - sys_tenant_extend
- red_packet
- red_packet_receive
- commission_template - commission_template
- commission_rate_range - commission_rate_range

View File

@ -15,7 +15,7 @@ public interface TenantConstants {
/** /**
* 超级管理员角色 roleKey * 超级管理员角色 roleKey
*/ */
String SUPER_ADMIN_ROLE_KEY = "superadmin"; String SUPER_ADMIN_ROLE_KEY = "superadmin";
/** /**
* 租户管理员角色 roleKey * 租户管理员角色 roleKey

View File

@ -22,6 +22,10 @@ import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.core.task.VirtualThreadTaskExecutor; import org.springframework.core.task.VirtualThreadTaskExecutor;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
@ -101,6 +105,18 @@ public class RedisConfig {
}; };
} }
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 设置key的序列化方式
template.setKeySerializer(new StringRedisSerializer());
// 设置value的序列化方式
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
/** /**
* 异常处理器 * 异常处理器
*/ */

View File

@ -1,6 +1,5 @@
package com.wzj.soopin.member.domain.po; package com.wzj.soopin.member.domain.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -19,7 +18,7 @@ import java.math.BigDecimal;
@Schema(description="会员账户变动记录") @Schema(description="会员账户变动记录")
@Data @Data
@TableName("ums_account_change_record") @TableName("ums_account_change_record")
@Builder(toBuilder = true) @Builder
public class MemberAccountChangeRecord extends BaseAudit { public class MemberAccountChangeRecord extends BaseAudit {
@Schema(description ="主键") @Schema(description ="主键")

View File

@ -7,6 +7,7 @@ import com.wzj.soopin.member.domain.bo.MemberAccountBO;
import com.wzj.soopin.member.domain.po.MemberAccount; import com.wzj.soopin.member.domain.po.MemberAccount;
import com.wzj.soopin.member.domain.vo.MemberAccountVO; import com.wzj.soopin.member.domain.vo.MemberAccountVO;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
@ -18,5 +19,15 @@ import java.util.List;
*/ */
public interface MemberAccountMapper extends BaseMapper<MemberAccount> { public interface MemberAccountMapper extends BaseMapper<MemberAccount> {
IPage<MemberAccountVO> selectAccountWithMember(Page<?> page, @Param("bo") MemberAccountBO bo); IPage<MemberAccountVO> selectAccountWithMember(Page<?> page, @Param("bo") MemberAccountBO bo);
@Select("SELECT money_balance \n" +
"FROM ums_account \n" +
"WHERE member_id = #{memberId};")
BigDecimal getMoneyBalanceByMemberId(Long memberId);
@Select("UPDATE ums_account \n" +
"SET money_balance = #{afterBalance} \n" +
"WHERE member_id = #{senderId};")
void updateMoneyBalance(Long senderId, BigDecimal afterBalance);
} }

View File

@ -172,5 +172,14 @@
<artifactId>wechatpay-apache-httpclient</artifactId> <artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.7</version> <version>0.4.7</version>
</dependency> </dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -172,7 +172,7 @@ public class OrderController extends BaseController {
} else { } else {
redisKey = "top_trading_products:" + java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd")); redisKey = "top_trading_products:" + java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"));
} }
String jsonData = RedisUtils.getCacheObject(redisKey); String jsonData = RedisUtils.getCacheObject(redisKey);
if (jsonData != null && !jsonData.isEmpty()) { if (jsonData != null && !jsonData.isEmpty()) {
ObjectMapper objectMapper = new ObjectMapper(); ObjectMapper objectMapper = new ObjectMapper();

View File

@ -0,0 +1,68 @@
package com.wzj.soopin.order.controller;
import com.wzj.soopin.order.domain.query.GrabRedPacketRequest;
import com.wzj.soopin.order.domain.query.SendRedPacketRequest;
import com.wzj.soopin.order.service.RedPacketService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.dromara.common.core.domain.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 红包功能控制器
*/
@RestController
@RequestMapping("/api/red-packet")
@Api(tags = "红包功能接口")
public class RedPacketController {
@Autowired
private RedPacketService redPacketService;
/**
* 发红包接口
* @param request 发红包请求参数
* @return 红包创建结果
*/
@PostMapping("/send")
@ApiOperation("发红包")
public R<Map<String, Object>> sendRedPacket(@RequestBody SendRedPacketRequest request) {
Map<String, Object> result = redPacketService.sendRedPacket(request);
return R.ok("红包发送成功", result);
}
/**
* 抢红包接口
* @return 抢红包结果
*/
@PostMapping("/grab")
@ApiOperation("抢红包")
public R<Map<String, Object>> grabRedPacket(@RequestBody GrabRedPacketRequest request) {
Map<String, Object> result = redPacketService.grabRedPacket(request);
return R.ok("红包领取成功", result);
}
// /**
// * 查询红包详情接口
// * @param packetId 红包ID
// * @return 红包详情
// */
// @GetMapping("/detail/{packetId}")
// @ApiOperation("查询红包详情")
// public R<RedPacketDetailVO> getRedPacketDetail(@PathVariable Long packetId) {
// RedPacketDetailVO detail = redPacketService.getRedPacketDetail(packetId);
// return R.ok(detail);
// }
//
// /**
// * 查询用户领取的红包记录
// * @param userId 用户ID
// * @param pageNum 页码
// * @param pageSize 每页条数
// * @return 领取记录列表
// */
}

View File

@ -0,0 +1,68 @@
package com.wzj.soopin.order.domain.entity;
import cn.hutool.core.date.DateTime;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableField;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import lombok.Data;
import io.swagger.v3.oas.annotations.media.Schema;
import org.dromara.common.core.domain.model.BaseAudit;
@Data
@Schema(description = "红包实体")
public class RedPacket extends BaseAudit {
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "红包id")
private Long id;
@TableField("sender_id")
@Schema(description = "发送人id")
private Long senderId;
@TableField("chat_type")
@Schema(description = "聊天类型(1:单聊 2:群聊)")
private Integer chatType;
@TableField("receiver_id")
@Schema(description = "接收者ID(单聊)")
private Long receiverId;
@TableField("group_id")
@Schema(description = "群id")
private Long groupId;
@TableField("packet_type")
@Schema(description = "红包类型(1:普通)")
private Integer packetType;
@TableField("total_amount")
@Schema(description = "总金额")
private BigDecimal totalAmount;
@TableField("total_count")
@Schema(description = "总个数")
private Integer totalCount;
@TableField("remaining_amount")
@Schema(description = "剩余金额")
private BigDecimal remainingAmount;
@TableField("remaining_count")
@Schema(description = "剩余个数")
private Integer remainingCount;
@TableField("status")
@Schema(description = "状态(0:未领取 1:已领取部分 2:已领完 3:已过期 4:已退款)")
private Integer status;
@TableField("expire_time")
@Schema(description = "过期时间")
private LocalDateTime expireTime;
@TableField("remark")
@Schema(description = "备注")
private String remark;
}

View File

@ -0,0 +1,34 @@
package com.wzj.soopin.order.domain.entity;
import cn.hutool.core.date.DateTime;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableField;
import java.math.BigDecimal;
import lombok.Data;
import io.swagger.v3.oas.annotations.media.Schema;
@Data
@Schema(description = "红包领取记录实体")
public class RedPacketReceive {
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键ID")
private Long id;
@TableField("packet_id")
@Schema(description = "红包id")
private Long packetId;
@TableField("receiver_id")
@Schema(description = "领取者id")
private Long receiverId;
@TableField("amount")
@Schema(description = "领取金额")
private BigDecimal amount;
@TableField("recevice_time")
@Schema(description = "领取时间")
private DateTime receviceTime;
}

View File

@ -0,0 +1,28 @@
package com.wzj.soopin.order.domain.form;
import com.wzj.soopin.order.domain.dto.OrderProductListDTO;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Data
public class OrderSubmitForm {
@NotNull
private Long addressId;
private String note;
/** 支付方式 0未支付 1支付宝 2微信 默认微信 */
private Integer payType = 2;
/** 订单来源购物车则为cart */
private String from;
private Long memberCouponId;
@NotEmpty
private List<OrderProductListDTO> skuList;
@Data
public static class SkuParam {
private Long skuId;
private Integer quantity;
}
}

View File

@ -0,0 +1,18 @@
package com.wzj.soopin.order.domain.query;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 抢红包请求参数
*/
@Data
@Schema(description = "抢红包请求参数")
public class GrabRedPacketRequest {
@Schema(description = "红包id")
private Long packetId;
@Schema(description = "用户id")
private Long memberId;
}

View File

@ -0,0 +1,53 @@
package com.wzj.soopin.order.domain.query;
import cn.hutool.core.date.DateTime;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import java.math.BigDecimal;
/**
* 发红包请求参数
*/
@Data
@Schema(description = "发红包请求参数")
public class SendRedPacketRequest {
@NotNull(message = "聊天类型不能为空")
@Range(min = 1, max = 2, message = "聊天类型只能是1(单聊)或2(群聊)")
@Schema(description = "聊天类型(1:单聊 2:群聊)", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer chatType;
@NotNull(message = "发送者ID不能为空")
@Schema(description = "发送者ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long senderId;
@Schema(description = "接收者ID(单聊时必填)")
private Long receiverId;
@Schema(description = "群id(群聊时必填)")
private Long groupId;
@NotNull(message = "红包类型不能为空")
@Range(min = 1, max = 2, message = "红包类型只能是1(普通)或2(拼手气)")
@Schema(description = "红包类型(1:普通 2:拼手气)", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer packetType;
@NotNull(message = "总金额不能为空")
@DecimalMin(value = "0.01", message = "总金额不能小于0.01")
@Schema(description = "总金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal totalAmount;
@NotNull(message = "总个数不能为空")
@Min(value = 1, message = "总个数不能小于1")
@Schema(description = "总个数", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer totalCount;
@Schema(description = "备注(祝福语)")
private String remark;
}

View File

@ -0,0 +1,15 @@
package com.wzj.soopin.order.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.wzj.soopin.order.domain.entity.RedPacket;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface RedPacketMapper extends BaseMapper<RedPacket> {
@Select("SELECT * FROM red_packet WHERE expire_time < NOW() AND status IN (0, 1)")
List<RedPacket> selectExpiredRedPackets();
}

View File

@ -0,0 +1,13 @@
package com.wzj.soopin.order.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.wzj.soopin.order.domain.entity.RedPacketReceive;
import org.apache.ibatis.annotations.Select;
public interface RedPacketReceiveMapper extends BaseMapper<RedPacketReceive> {
@Select("SELECT COUNT(1) FROM red_packet_receive " +
"WHERE packet_id = #{packetId} " +
"AND receiver_id = #{memberId} ")
Integer checkReceived(Long packetId, Long memberId);
}

View File

@ -0,0 +1,13 @@
package com.wzj.soopin.order.service;
import com.wzj.soopin.order.domain.query.GrabRedPacketRequest;
import com.wzj.soopin.order.domain.query.SendRedPacketRequest;
import java.util.Map;
public interface RedPacketService {
Map<String, Object> sendRedPacket(SendRedPacketRequest request);
Map<String, Object> grabRedPacket(GrabRedPacketRequest request);
}

View File

@ -1,7 +1,5 @@
package com.wzj.soopin.order.service.impl; package com.wzj.soopin.order.service.impl;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.CollectionUtil;
@ -10,29 +8,24 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wechat.pay.java.service.partnerpayments.jsapi.model.Transaction;
import com.wzj.soopin.goods.domain.entity.Sku; import com.wzj.soopin.goods.domain.entity.Sku;
import com.wzj.soopin.goods.mapper.SkuMapper; import com.wzj.soopin.goods.mapper.SkuMapper;
import com.wzj.soopin.member.domain.po.Member; import com.wzj.soopin.member.domain.po.Member;
import com.wzj.soopin.member.domain.po.MemberWechat;
import com.wzj.soopin.member.mapper.*; import com.wzj.soopin.member.mapper.*;
import com.wzj.soopin.order.domain.bo.OrderBo; import com.wzj.soopin.order.domain.bo.OrderBo;
import com.wzj.soopin.order.domain.entity.*; import com.wzj.soopin.order.domain.entity.*;
import com.wzj.soopin.order.domain.form.DeliverProductForm; import com.wzj.soopin.order.domain.form.DeliverProductForm;
import com.wzj.soopin.order.domain.form.ManagerOrderQueryForm; import com.wzj.soopin.order.domain.form.ManagerOrderQueryForm;
import com.wzj.soopin.order.domain.form.OrderPayForm;
import com.wzj.soopin.order.domain.query.OrderH5Query; import com.wzj.soopin.order.domain.query.OrderH5Query;
import com.wzj.soopin.order.domain.vo.*; import com.wzj.soopin.order.domain.vo.*;
import com.wzj.soopin.order.mapper.*; import com.wzj.soopin.order.mapper.*;
import com.wzj.soopin.order.service.OrderService; import com.wzj.soopin.order.service.OrderService;
import com.wzj.soopin.order.service.VerificationCodeService; import com.wzj.soopin.order.service.VerificationCodeService;
import com.wzj.soopin.order.wechat.WechatPayData;
import com.wzj.soopin.order.wechat.WechatPayService; import com.wzj.soopin.order.wechat.WechatPayService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R; import org.dromara.common.core.domain.R;
import org.dromara.common.core.domain.event.Constants; import org.dromara.common.core.domain.event.Constants;
import org.dromara.common.core.enums.OrderStatus;
import org.dromara.common.core.utils.PhoneUtils; import org.dromara.common.core.utils.PhoneUtils;
import org.dromara.common.core.utils.SecurityUtils; import org.dromara.common.core.utils.SecurityUtils;
import org.dromara.common.redis.redis.RedisService; import org.dromara.common.redis.redis.RedisService;
@ -46,16 +39,12 @@ import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
/** /**
* 订单表Service业务层处理 * 订单表Service业务层处理
@ -558,10 +547,5 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
} }
} }
} }

View File

@ -0,0 +1,386 @@
package com.wzj.soopin.order.service.impl;
import cn.hutool.core.date.DateTime;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wzj.soopin.member.domain.po.MemberAccountChangeRecord;
import com.wzj.soopin.member.mapper.MemberAccountChangeRecordMapper;
import com.wzj.soopin.member.mapper.MemberAccountMapper;
import com.wzj.soopin.order.domain.entity.RedPacket;
import com.wzj.soopin.order.domain.entity.RedPacketReceive;
import com.wzj.soopin.order.domain.query.GrabRedPacketRequest;
import com.wzj.soopin.order.domain.query.SendRedPacketRequest;
import com.wzj.soopin.order.mapper.RedPacketMapper;
import com.wzj.soopin.order.mapper.RedPacketReceiveMapper;
import com.wzj.soopin.order.service.RedPacketService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Service
@RequiredArgsConstructor
public class RedPacketServiceImpl extends ServiceImpl<RedPacketMapper, RedPacket> implements RedPacketService {
private final RedPacketMapper redPacketMapper;
private final RedPacketReceiveMapper redPacketReceiveMapper;
private final MemberAccountMapper umsAccountMapper;
private final MemberAccountChangeRecordMapper accountChangeRecordMapper;
private final RedisTemplate<String, Object> redisTemplate;
// Redis锁前缀
private static final String LOCK_PREFIX = "red_packet:lock:";
// 红包状态0-未领取 1-已领取部分 2-已领完 3-已过期 4-已退款
private static final int STATUS_UNRECEIVED = 0;
private static final int STATUS_PART_RECEIVED = 1;
private static final int STATUS_ALL_RECEIVED = 2;
private static final int STATUS_EXPIRED = 3;
private static final int STATUS_REFUNDED = 4;
// 聊天类型1-单聊 2-群聊
private static final int CHAT_TYPE_SINGLE = 1;
private static final int CHAT_TYPE_GROUP = 2;
// 金额变动类型100-发红包扣钱 101-领红包加钱 102-红包退款
private static final int CHANGE_TYPE_SEND = 100;
private static final int CHANGE_TYPE_RECEIVE = 101;
private static final int CHANGE_TYPE_REFUND = 102;
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> sendRedPacket(SendRedPacketRequest request) {
// 参数校验
validateSendParams(request);
Long memberId = request.getSenderId();
// 验证发送者余额
BigDecimal balance = umsAccountMapper.getMoneyBalanceByMemberId(memberId);
if (balance == null || balance.compareTo(request.getTotalAmount()) < 0) {
throw new RuntimeException("账户余额不足,无法发送红包");
}
// 创建红包记录
RedPacket redPacket = createRedPacket(request);
redPacketMapper.insert(redPacket);
Long packetId = redPacket.getId();
// 扣减发送者余额并记录变动
deductSenderBalance(request.getSenderId(), redPacket, balance);
// 返回结果
Map<String, Object> result = new HashMap<>();
result.put("packetId", packetId);
result.put("receiverId", redPacket.getReceiverId());
result.put("totalAmount", redPacket.getTotalAmount());
result.put("totalCount", redPacket.getTotalCount());
result.put("createTime", redPacket.getCreateTime());
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> grabRedPacket(GrabRedPacketRequest request) {
Long packetId = request.getPacketId();
Long memberId = request.getMemberId();
// 获取分布式锁
String lockKey = LOCK_PREFIX + packetId;
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
return doReceiveRedPacket(packetId, memberId);
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
} else {
throw new RuntimeException("抢红包太火爆,请稍后再试");
}
}
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> doReceiveRedPacket(Long packetId, Long memberId) {
// 查询红包信息
RedPacket redPacket = redPacketMapper.selectById(packetId);
if (redPacket == null) {
throw new RuntimeException("红包不存在");
}
// 验证红包状态
validateRedPacketStatus(redPacket, memberId);
// 检查是否已领取
Integer received = redPacketReceiveMapper.checkReceived(packetId, memberId);
if (received != null && received > 0) {
throw new RuntimeException("您已领取过该红包");
}
// 计算领取金额
BigDecimal receiveAmount = calculateReceiveAmount(redPacket);
// 更新红包剩余金额和数量
updateRedPacketRemaining(redPacket, receiveAmount);
// 创建领取记录
RedPacketReceive receiveRecord = createReceiveRecord(redPacket, memberId, receiveAmount);
redPacketReceiveMapper.insert(receiveRecord);
// 增加领取者余额并记录变动
addReceiverBalance(memberId, receiveAmount, packetId);
// 更新红包状态
updateRedPacketStatus(redPacket);
Map<String, Object> result = new HashMap<>();
result.put("packetId", packetId);
result.put("receiveAmount", receiveAmount);
result.put("receiveTime", receiveRecord.getReceviceTime());
result.put("isLastOne", redPacket.getRemainingCount() == 0);
return result;
}
/**
* 验证发送红包参数
*/
private void validateSendParams(SendRedPacketRequest request) {
// 验证聊天类型
if (request.getChatType() != CHAT_TYPE_SINGLE && request.getChatType() != CHAT_TYPE_GROUP) {
throw new RuntimeException("聊天类型无效,只能是单聊或群聊");
}
// 单聊必须指定接收者
if (request.getChatType() == CHAT_TYPE_SINGLE && request.getReceiverId() == null) {
throw new RuntimeException("单聊红包必须指定接收者ID");
}
// 群聊必须指定群ID和红包个数
if (request.getChatType() == CHAT_TYPE_GROUP) {
if (request.getGroupId() == null) {
throw new RuntimeException("群聊红包必须指定群ID");
}
if (request.getTotalCount() == null || request.getTotalCount() < 1) {
throw new RuntimeException("群聊红包必须指定有效个数至少1个");
}
}
}
/**
* 创建红包记录
*/
private RedPacket createRedPacket(SendRedPacketRequest request) {
RedPacket redPacket = new RedPacket();
redPacket.setSenderId(request.getSenderId());
redPacket.setChatType(request.getChatType());
redPacket.setReceiverId(request.getReceiverId());
redPacket.setGroupId(request.getGroupId());
// 默认为普通红包
redPacket.setPacketType(1);
redPacket.setTotalAmount(request.getTotalAmount());
redPacket.setTotalCount(request.getTotalCount());
redPacket.setRemainingAmount(request.getTotalAmount());
redPacket.setRemainingCount(request.getTotalCount());
redPacket.setStatus(STATUS_UNRECEIVED);
// 24小时后过期
redPacket.setExpireTime(LocalDateTime.now().plusDays(1));
redPacket.setRemark(request.getRemark() != null && !request.getRemark().isEmpty()
? request.getRemark()
: "恭喜发财,大吉大利!");
return redPacket;
}
/**
* 扣减发送者余额并记录变动
*/
private void deductSenderBalance(Long senderId, RedPacket redPacket, BigDecimal beforeBalance) {
// 计算扣减后的余额
BigDecimal afterBalance = beforeBalance.subtract(redPacket.getTotalAmount());
umsAccountMapper.updateMoneyBalance(senderId, afterBalance);
// 记录金额变动
MemberAccountChangeRecord record = MemberAccountChangeRecord.builder()
.moneyBalance(beforeBalance)
.memberId(senderId)
.beforeBalance(beforeBalance)
.afterBalance(afterBalance)
.changeAmount(redPacket.getTotalAmount())
.changeType(CHANGE_TYPE_SEND)
.changeDesc("发送红包红包ID" + redPacket.getId())
.build();
accountChangeRecordMapper.insert(record);
}
/**
* 验证红包状态
*/
private void validateRedPacketStatus(RedPacket redPacket, Long memberId) {
// 检查是否过期
if (redPacket.getExpireTime().isBefore(LocalDateTime.now())) {
if (redPacket.getRemainingAmount().compareTo(BigDecimal.ZERO) > 0) {
refundRemainingAmount(redPacket);
} else {
redPacket.setStatus(STATUS_EXPIRED);
redPacketMapper.updateById(redPacket);
throw new RuntimeException("红包已过期");
}
}
// 检查是否已领完
if (redPacket.getStatus() == STATUS_ALL_RECEIVED) {
throw new RuntimeException("红包已被领完");
}
// 检查是否是自己发的红包
if (redPacket.getSenderId().equals(memberId)) {
throw new RuntimeException("不能领取自己发送的红包");
}
// 单聊红包只能指定接收者领取
if (redPacket.getChatType() == CHAT_TYPE_SINGLE &&
!redPacket.getReceiverId().equals(memberId)) {
throw new RuntimeException("您无权领取该红包");
}
}
/**
* 退款剩余金额
*/
private void refundRemainingAmount(RedPacket redPacket) {
Long senderId = redPacket.getSenderId();
BigDecimal remainingAmount = redPacket.getRemainingAmount();
// 查询当前余额
BigDecimal beforeBalance = umsAccountMapper.getMoneyBalanceByMemberId(senderId);
if (beforeBalance == null) {
throw new RuntimeException("发送者账户不存在");
}
// 增加余额
BigDecimal afterBalance = beforeBalance.add(remainingAmount);
umsAccountMapper.updateMoneyBalance(senderId, afterBalance);
// 记录金额变动
MemberAccountChangeRecord record = MemberAccountChangeRecord.builder()
.memberId(senderId)
.beforeBalance(beforeBalance)
.afterBalance(afterBalance)
.changeAmount(remainingAmount)
.changeType(CHANGE_TYPE_REFUND)
.changeDesc("红包退款红包ID" + redPacket.getId())
.build();
accountChangeRecordMapper.insert(record);
// 更新红包状态为已退款
redPacket.setStatus(STATUS_REFUNDED);
redPacket.setRemainingAmount(BigDecimal.ZERO);
redPacketMapper.updateById(redPacket);
}
/**
* 计算领取金额
*/
private BigDecimal calculateReceiveAmount(RedPacket redPacket) {
int remainingCount = redPacket.getRemainingCount();
BigDecimal remainingAmount = redPacket.getRemainingAmount();
if (remainingCount == 1) {
// 最后一个红包领取全部剩余金额
return remainingAmount;
}
BigDecimal minAmount = new BigDecimal("0.01");
BigDecimal maxAmount = remainingAmount.subtract(new BigDecimal(remainingCount - 1).multiply(minAmount));
BigDecimal randomAmount = minAmount.add(new BigDecimal(Math.random() * maxAmount.doubleValue())).setScale(2, BigDecimal.ROUND_HALF_UP);
return randomAmount;
}
/**
* 更新红包剩余金额和数量
*/
private void updateRedPacketRemaining(RedPacket redPacket, BigDecimal receiveAmount) {
redPacket.setRemainingAmount(redPacket.getRemainingAmount().subtract(receiveAmount));
redPacket.setRemainingCount(redPacket.getRemainingCount() - 1);
redPacketMapper.updateById(redPacket);
}
/**
* 创建领取记录
*/
private RedPacketReceive createReceiveRecord(RedPacket redPacket, Long memberId, BigDecimal amount) {
RedPacketReceive record = new RedPacketReceive();
record.setPacketId(redPacket.getId());
record.setReceiverId(memberId);
record.setAmount(amount);
record.setReceviceTime(new DateTime());
return record;
}
/**
* 增加领取者余额并记录变动
*/
private void addReceiverBalance(Long memberId, BigDecimal amount, Long packetId) {
// 查询当前余额
BigDecimal beforeBalance = umsAccountMapper.getMoneyBalanceByMemberId(memberId);
if (beforeBalance == null) {
throw new RuntimeException("领取者账户不存在");
}
// 增加余额
BigDecimal afterBalance = beforeBalance.add(amount);
umsAccountMapper.updateMoneyBalance(memberId, afterBalance);
// 记录金额变动
MemberAccountChangeRecord record = MemberAccountChangeRecord.builder()
.memberId(memberId)
.beforeBalance(beforeBalance)
.afterBalance(afterBalance)
.changeAmount(amount)
.changeType(CHANGE_TYPE_RECEIVE)
.changeDesc("领取红包红包ID" + packetId)
.build();
accountChangeRecordMapper.insert(record);
}
/**
* 更新红包状态
*/
private void updateRedPacketStatus(RedPacket redPacket) {
if (redPacket.getRemainingCount() == 0) {
redPacket.setStatus(STATUS_ALL_RECEIVED);
} else {
redPacket.setStatus(STATUS_PART_RECEIVED);
}
redPacketMapper.updateById(redPacket);
}
/**
* 定时任务检查并退款过期的红包
*/
@Scheduled(fixedRate = 1800000) // 每30分钟执行一次
@Transactional(rollbackFor = Exception.class)
public void checkAndRefundExpiredRedPackets() {
// 查询所有未领取或部分领取且已过期的红包
List<RedPacket> expiredRedPackets = redPacketMapper.selectExpiredRedPackets();
for (RedPacket redPacket : expiredRedPackets) {
if (redPacket.getRemainingAmount().compareTo(BigDecimal.ZERO) > 0) {
refundRemainingAmount(redPacket);
} else {
redPacket.setStatus(STATUS_EXPIRED);
redPacketMapper.updateById(redPacket);
}
}
}
}

View File

@ -46,13 +46,6 @@ public class SysTenantController extends BaseController {
private final ISysTenantService tenantService; private final ISysTenantService tenantService;
// /**
// * 查询租户列表
// */
// @GetMapping("/all")
// public TableDataInfo<SysTenantVo> list(SysTenantBo bo, PageQuery pageQuery) {
// return tenantService.queryPageList(bo, pageQuery);
// }
@Tag(name ="查询租户列表") @Tag(name ="查询租户列表")
@SaCheckRole(TenantConstants.SUPER_ADMIN_ROLE_KEY) @SaCheckRole(TenantConstants.SUPER_ADMIN_ROLE_KEY)
@ -76,15 +69,6 @@ public class SysTenantController extends BaseController {
} }
// @SaCheckRole(TenantConstants.SUPER_ADMIN_ROLE_KEY)
// @SaCheckPermission("system:tenant:query")
// @GetMapping("/{id}")
// public R<SysTenantVo> getInfo(@NotNull(message = "主键不能为空")
// @PathVariable Long id) {
// return R.ok(tenantService.queryById(id));
// }
/** /**
* 获取租户详细信息 * 获取租户详细信息
* *
@ -103,7 +87,6 @@ public class SysTenantController extends BaseController {
/** /**
* 新增租户 * 新增租户
*/ */
// @ApiEncrypt
@SaCheckRole(TenantConstants.SUPER_ADMIN_ROLE_KEY) @SaCheckRole(TenantConstants.SUPER_ADMIN_ROLE_KEY)
@SaCheckPermission("system:tenant:add") @SaCheckPermission("system:tenant:add")
@Log(title = "租户管理", businessType = BusinessType.INSERT) @Log(title = "租户管理", businessType = BusinessType.INSERT)
@ -147,13 +130,6 @@ public class SysTenantController extends BaseController {
// @Log(title = "租户管理", businessType = BusinessType.DELETE)
// @DeleteMapping("/{id}")
// public R<Void> remove(@NotEmpty(message = "主键不能为空")
// @PathVariable Long id) {
// return toAjax(tenantService.deleteWithValidByIds(List.of(ids), true));
// }
/** /**
* 删除租户 * 删除租户

View File

@ -147,4 +147,7 @@ public class TenantDTO {
@Schema(description = "邀请人名称") @Schema(description = "邀请人名称")
private String inviteUserName; private String inviteUserName;
@Schema(description = "分配比例模板名称")
private String templateName;
} }

View File

@ -8,13 +8,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
SELECT SELECT
t.*, t.*,
e.*, e.*,
m.nickname AS inviteUserName m.nickname AS inviteUserName,
ct.template_name AS templateName
FROM FROM
sys_tenant t sys_tenant t
LEFT JOIN LEFT JOIN
sys_tenant_extend e ON t.tenant_id = e.tenant_id sys_tenant_extend e ON t.tenant_id = e.tenant_id
LEFT JOIN LEFT JOIN
ums_member m ON e.invite_user_id = m.id ums_member m ON e.invite_user_id = m.id
LEFT JOIN
commission_template ct ON e.split_ratio = ct.id
<where> <where>
<if test="query.tenantId != null and query.tenantId != ''"> <if test="query.tenantId != null and query.tenantId != ''">
AND t.tenant_id LIKE CONCAT('%', #{query.tenantId}, '%') AND t.tenant_id LIKE CONCAT('%', #{query.tenantId}, '%')