diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 1438502be..961a60190 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -181,6 +181,8 @@ tenant: - sys_version - ums_member_wechat - sys_tenant_extend + - red_packet + - red_packet_receive - commission_template - commission_rate_range diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/TenantConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/TenantConstants.java index 33ce0cf6c..d7fdf7db3 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/TenantConstants.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/TenantConstants.java @@ -15,7 +15,7 @@ public interface TenantConstants { /** * 超级管理员角色 roleKey */ - String SUPER_ADMIN_ROLE_KEY = "superadmin"; + String SUPER_ADMIN_ROLE_KEY = "superadmin"; /** * 租户管理员角色 roleKey diff --git a/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/RedisConfig.java b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/RedisConfig.java index 7ba94751a..bd251d57e 100644 --- a/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/RedisConfig.java +++ b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/RedisConfig.java @@ -22,6 +22,10 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; 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.format.DateTimeFormatter; @@ -101,6 +105,18 @@ public class RedisConfig { }; } + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory); + + // 设置key的序列化方式 + template.setKeySerializer(new StringRedisSerializer()); + // 设置value的序列化方式 + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + + return template; + } /** * 异常处理器 */ diff --git a/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/domain/po/MemberAccountChangeRecord.java b/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/domain/po/MemberAccountChangeRecord.java index 8efaf1657..0129d7cd4 100644 --- a/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/domain/po/MemberAccountChangeRecord.java +++ b/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/domain/po/MemberAccountChangeRecord.java @@ -1,6 +1,5 @@ package com.wzj.soopin.member.domain.po; -import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; @@ -19,7 +18,7 @@ import java.math.BigDecimal; @Schema(description="会员账户变动记录") @Data @TableName("ums_account_change_record") -@Builder(toBuilder = true) +@Builder public class MemberAccountChangeRecord extends BaseAudit { @Schema(description ="主键") diff --git a/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/mapper/MemberAccountMapper.java b/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/mapper/MemberAccountMapper.java index d994f7f47..104909c02 100644 --- a/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/mapper/MemberAccountMapper.java +++ b/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/mapper/MemberAccountMapper.java @@ -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.vo.MemberAccountVO; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; import java.math.BigDecimal; import java.util.List; @@ -18,5 +19,15 @@ import java.util.List; */ public interface MemberAccountMapper extends BaseMapper { IPage 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); } diff --git a/ruoyi-modules/ruoyi-order/pom.xml b/ruoyi-modules/ruoyi-order/pom.xml index 1b54b3969..163235cd3 100644 --- a/ruoyi-modules/ruoyi-order/pom.xml +++ b/ruoyi-modules/ruoyi-order/pom.xml @@ -172,5 +172,14 @@ wechatpay-apache-httpclient 0.4.7 + + org.mockito + mockito-core + + + + org.springframework.boot + spring-boot-starter-validation + diff --git a/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/controller/OrderController.java b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/controller/OrderController.java index 5abe5d444..c160c0328 100644 --- a/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/controller/OrderController.java +++ b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/controller/OrderController.java @@ -172,7 +172,7 @@ public class OrderController extends BaseController { } else { redisKey = "top_trading_products:" + java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd")); } - + String jsonData = RedisUtils.getCacheObject(redisKey); if (jsonData != null && !jsonData.isEmpty()) { ObjectMapper objectMapper = new ObjectMapper(); diff --git a/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/controller/RedPacketController.java b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/controller/RedPacketController.java new file mode 100644 index 000000000..a52b21617 --- /dev/null +++ b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/controller/RedPacketController.java @@ -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> sendRedPacket(@RequestBody SendRedPacketRequest request) { + Map result = redPacketService.sendRedPacket(request); + return R.ok("红包发送成功", result); + } + + /** + * 抢红包接口 + * @return 抢红包结果 + */ + @PostMapping("/grab") + @ApiOperation("抢红包") + public R> grabRedPacket(@RequestBody GrabRedPacketRequest request) { + Map result = redPacketService.grabRedPacket(request); + return R.ok("红包领取成功", result); + } + +// /** +// * 查询红包详情接口 +// * @param packetId 红包ID +// * @return 红包详情 +// */ +// @GetMapping("/detail/{packetId}") +// @ApiOperation("查询红包详情") +// public R getRedPacketDetail(@PathVariable Long packetId) { +// RedPacketDetailVO detail = redPacketService.getRedPacketDetail(packetId); +// return R.ok(detail); +// } +// +// /** +// * 查询用户领取的红包记录 +// * @param userId 用户ID +// * @param pageNum 页码 +// * @param pageSize 每页条数 +// * @return 领取记录列表 +// */ + +} diff --git a/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/domain/entity/RedPacket.java b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/domain/entity/RedPacket.java new file mode 100644 index 000000000..ac8cef722 --- /dev/null +++ b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/domain/entity/RedPacket.java @@ -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; +} diff --git a/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/domain/entity/RedPacketReceive.java b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/domain/entity/RedPacketReceive.java new file mode 100644 index 000000000..626e71754 --- /dev/null +++ b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/domain/entity/RedPacketReceive.java @@ -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; + +} diff --git a/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/domain/form/OrderSubmitForm.java b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/domain/form/OrderSubmitForm.java new file mode 100644 index 000000000..1046f6a91 --- /dev/null +++ b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/domain/form/OrderSubmitForm.java @@ -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 skuList; + @Data + public static class SkuParam { + private Long skuId; + private Integer quantity; + } +} diff --git a/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/domain/query/GrabRedPacketRequest.java b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/domain/query/GrabRedPacketRequest.java new file mode 100644 index 000000000..f513362bc --- /dev/null +++ b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/domain/query/GrabRedPacketRequest.java @@ -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; +} diff --git a/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/domain/query/SendRedPacketRequest.java b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/domain/query/SendRedPacketRequest.java new file mode 100644 index 000000000..b6c2d6f0f --- /dev/null +++ b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/domain/query/SendRedPacketRequest.java @@ -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; +} diff --git a/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/mapper/RedPacketMapper.java b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/mapper/RedPacketMapper.java new file mode 100644 index 000000000..eb142a98a --- /dev/null +++ b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/mapper/RedPacketMapper.java @@ -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 { + + @Select("SELECT * FROM red_packet WHERE expire_time < NOW() AND status IN (0, 1)") + List selectExpiredRedPackets(); +} diff --git a/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/mapper/RedPacketReceiveMapper.java b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/mapper/RedPacketReceiveMapper.java new file mode 100644 index 000000000..4dbaf0f87 --- /dev/null +++ b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/mapper/RedPacketReceiveMapper.java @@ -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 { + + @Select("SELECT COUNT(1) FROM red_packet_receive " + + "WHERE packet_id = #{packetId} " + + "AND receiver_id = #{memberId} ") + Integer checkReceived(Long packetId, Long memberId); +} diff --git a/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/service/RedPacketService.java b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/service/RedPacketService.java new file mode 100644 index 000000000..8cb7e7027 --- /dev/null +++ b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/service/RedPacketService.java @@ -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 sendRedPacket(SendRedPacketRequest request); + + Map grabRedPacket(GrabRedPacketRequest request); +} diff --git a/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/service/impl/OrderServiceImpl.java b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/service/impl/OrderServiceImpl.java index 3d7079b95..6a71dca6e 100644 --- a/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/service/impl/OrderServiceImpl.java +++ b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/service/impl/OrderServiceImpl.java @@ -1,7 +1,5 @@ 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 cn.hutool.core.collection.CollUtil; 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.metadata.IPage; 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.mapper.SkuMapper; 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.order.domain.bo.OrderBo; import com.wzj.soopin.order.domain.entity.*; import com.wzj.soopin.order.domain.form.DeliverProductForm; 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.vo.*; import com.wzj.soopin.order.mapper.*; import com.wzj.soopin.order.service.OrderService; import com.wzj.soopin.order.service.VerificationCodeService; -import com.wzj.soopin.order.wechat.WechatPayData; import com.wzj.soopin.order.wechat.WechatPayService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.domain.R; 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.SecurityUtils; 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.data.domain.PageImpl; import org.springframework.data.domain.Pageable; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.math.BigDecimal; import java.time.LocalDateTime; -import java.time.ZoneId; import java.util.*; import java.util.stream.Collectors; -import java.util.stream.Stream; /** * 订单表Service业务层处理 @@ -558,10 +547,5 @@ public class OrderServiceImpl extends ServiceImpl implements } } - - - - - } diff --git a/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/service/impl/RedPacketServiceImpl.java b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/service/impl/RedPacketServiceImpl.java new file mode 100644 index 000000000..d0000a8a6 --- /dev/null +++ b/ruoyi-modules/ruoyi-order/src/main/java/com/wzj/soopin/order/service/impl/RedPacketServiceImpl.java @@ -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 implements RedPacketService { + + private final RedPacketMapper redPacketMapper; + private final RedPacketReceiveMapper redPacketReceiveMapper; + private final MemberAccountMapper umsAccountMapper; + private final MemberAccountChangeRecordMapper accountChangeRecordMapper; + private final RedisTemplate 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 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 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 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 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 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 expiredRedPackets = redPacketMapper.selectExpiredRedPackets(); + + for (RedPacket redPacket : expiredRedPackets) { + if (redPacket.getRemainingAmount().compareTo(BigDecimal.ZERO) > 0) { + refundRemainingAmount(redPacket); + } else { + redPacket.setStatus(STATUS_EXPIRED); + redPacketMapper.updateById(redPacket); + } + } + } + +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/SysTenantController.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/SysTenantController.java index 25909967b..e4ca006e9 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/SysTenantController.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/SysTenantController.java @@ -46,13 +46,6 @@ public class SysTenantController extends BaseController { private final ISysTenantService tenantService; -// /** -// * 查询租户列表 -// */ -// @GetMapping("/all") -// public TableDataInfo list(SysTenantBo bo, PageQuery pageQuery) { -// return tenantService.queryPageList(bo, pageQuery); -// } @Tag(name ="查询租户列表") @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 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) @SaCheckPermission("system:tenant:add") @Log(title = "租户管理", businessType = BusinessType.INSERT) @@ -147,13 +130,6 @@ public class SysTenantController extends BaseController { -// @Log(title = "租户管理", businessType = BusinessType.DELETE) -// @DeleteMapping("/{id}") -// public R remove(@NotEmpty(message = "主键不能为空") -// @PathVariable Long id) { -// return toAjax(tenantService.deleteWithValidByIds(List.of(ids), true)); -// } - /** * 删除租户 diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/dto/TenantDTO.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/dto/TenantDTO.java index a51bfb9ce..aa14adc11 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/dto/TenantDTO.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/dto/TenantDTO.java @@ -147,4 +147,7 @@ public class TenantDTO { @Schema(description = "邀请人名称") private String inviteUserName; + + @Schema(description = "分配比例模板名称") + private String templateName; } diff --git a/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysTenantMapper.xml b/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysTenantMapper.xml index c118cbdfd..ba0eca3ba 100644 --- a/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysTenantMapper.xml +++ b/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysTenantMapper.xml @@ -8,13 +8,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" SELECT t.*, e.*, - m.nickname AS inviteUserName + m.nickname AS inviteUserName, + ct.template_name AS templateName FROM sys_tenant t LEFT JOIN sys_tenant_extend e ON t.tenant_id = e.tenant_id LEFT JOIN ums_member m ON e.invite_user_id = m.id + LEFT JOIN + commission_template ct ON e.split_ratio = ct.id AND t.tenant_id LIKE CONCAT('%', #{query.tenantId}, '%')