feat:支付

This commit is contained in:
dzc committed 2025-12-09 13:25:12 +08:00
1 parent 30599f2ce3
commit 44b26ffc6b
12 files changed
+253 -27

No files matched your search

@@ -38,6 +38,11 @@
<artifactId>yudao-module-trade-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-pay-api</artifactId>
<version>${revision}</version>
</dependency>
<!-- 业务组件 -->
<dependency>
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.member.controller.admin.opentype;
import cn.iocoder.yudao.module.pay.api.notify.dto.PayOrderNotifyReqDTO;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@@ -10,6 +11,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Operation;
import javax.annotation.security.PermitAll;
import javax.validation.constraints.*;
import javax.validation.*;
import javax.servlet.http.*;
@@ -28,6 +30,7 @@ import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.*;
import cn.iocoder.yudao.module.member.dal.dataobject.opentype.OpenTypeDO;
@@ -42,6 +45,22 @@ public class OpenTypeController {
@Resource
private OpenTypeService openTypeService;
@PostMapping("/update-paid")
@Operation(summary = "更新钱包充值为已充值")
@PermitAll
public CommonResult<Boolean> updateWalletRechargerPaid(@Valid @RequestBody PayOrderNotifyReqDTO notifyReqDTO) {
openTypeService.updateWalletRechargerPaid(Long.valueOf(notifyReqDTO.getMerchantOrderId()), notifyReqDTO.getPayOrderId());
return success(true);
}
@GetMapping("/refund")
@Operation(summary = "发起钱包充值退款")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
public CommonResult<Boolean> refundWalletRecharge(@RequestParam("id") Long id) {
openTypeService.refundWalletRecharge(id, getClientIP());
return success(true);
}
@PostMapping("/create")
@Operation(summary = "创建会员类型")
@PreAuthorize("@ss.hasPermission('member:open-type:create')")
@@ -19,6 +19,7 @@ import javax.validation.Valid;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - 会员类型")
@@ -30,6 +31,8 @@ public class AppOpenTypeController {
@Resource
private OpenTypeService openTypeService;
@GetMapping("/list")
@Operation(summary = "查看全部开通会员列表")
public CommonResult<List<OpenTypeRespVO>> getOpenTypeList(@Valid OpenTypePageReqVO pageReqVO) {
@@ -49,6 +52,7 @@ public class AppOpenTypeController {
@Operation(summary = "购买会员")
public CommonResult<Long> payOpenType(@RequestBody AppPayOpenReqType dto) {
dto.setMemberId(getLoginUserId());
dto.setMemberIp(getClientIP());
return success(openTypeService.payOpenType(dto));
}
@@ -18,4 +18,7 @@ public class AppPayOpenReqType {
@Schema(description = "会员id")
private Long memberId;
@Schema(description = "会员ip")
private String memberIp;
}
@@ -1,12 +1,11 @@
package cn.iocoder.yudao.module.member.dal.dataobject.opentype;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
import lombok.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
@@ -62,4 +61,46 @@ public class OpenTypeUserDO extends BaseDO {
*/
private String status;
/**
* 是否已支付
* true - 已支付
* false - 未支付
*/
private Boolean payStatus;
/**
* 支付订单编号
*/
private Long payOrderId;
/**
* 支付成功的支付渠道
*/
private String payChannelCode;
/**
* 订单支付时间
*/
private LocalDateTime payTime;
/**
* 支付退款单编号
*/
private Long payRefundId;
/**
* 退款支付金额
*/
private Integer refundPayPrice;
/**
* 退款时间
*/
private LocalDateTime refundTime;
/**
* 退款状态
*/
private Integer refundStatus;
}
@@ -27,6 +27,8 @@ public interface OpenTypeUserMapper extends BaseMapperX<OpenTypeUserDO> {
return selectOne(new LambdaQueryWrapperX<OpenTypeUserDO>()
.eq(OpenTypeUserDO::getMemberId, memberId)
.eq(OpenTypeUserDO::getStatus, OpenTypeUserStatusEnum.EFFICIENT.getType())
.eq(OpenTypeUserDO::getPayStatus, true)
.isNotNull(OpenTypeUserDO::getRefundStatus)
.gt(OpenTypeUserDO::getEndTime, LocalDateTime.now())
.orderByDesc(OpenTypeUserDO::getEndTime)
.last("LIMIT 1")
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.member.service.opentype;
import javax.validation.*;
import javax.validation.constraints.NotNull;
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.*;
import cn.iocoder.yudao.module.member.controller.app.opentype.vo.AppPayOpenReqType;
@@ -76,4 +77,14 @@ public interface OpenTypeService {
* 检查过期的vip
*/
int checkVip();
/**
* 支付的回调
*/
void updateWalletRechargerPaid(Long id, Long payOrderId);
/**
* 退款的回调
*/
void refundWalletRecharge(Long id, String ip);
}
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.member.service.opentype;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.member.client.service.ChatApi;
import cn.iocoder.yudao.module.member.client.vo.chat.privatemsg.ChatPrivateTotalMessageTotalReqVO;
@@ -21,6 +22,12 @@ import cn.iocoder.yudao.module.member.service.config.MemberConfigService;
import cn.iocoder.yudao.module.member.service.opentype.BO.OpenTypeExtBO;
import cn.iocoder.yudao.module.member.service.user.MemberRecentTagService;
import cn.iocoder.yudao.module.member.service.user.MemberUserService;
import cn.iocoder.yudao.module.pay.api.order.PayOrderApi;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -37,12 +44,18 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.member.dal.mysql.opentype.OpenTypeMapper;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import static cn.hutool.core.util.ObjectUtil.notEqual;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.addTime;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
/**
* 会员类型 Service 实现类
@@ -66,6 +79,8 @@ public class OpenTypeServiceImpl implements OpenTypeService {
@Resource
private ChatApi chatApi;
@Resource
private PayOrderApi payOrderApi;
/**
@@ -139,12 +154,16 @@ public class OpenTypeServiceImpl implements OpenTypeService {
// 查询开通会员
OpenTypeDO openType = openTypeMapper.selectById(dto.getOpenTypeId());
if (ObjectUtil.isEmpty(openType)) throw exception(MemberErrorCode.OPEN_TYPE_NOT_EXISTS);
// TODO支付待实现
// TODO支付
LocalDateTime now = LocalDateTime.now();
OpenTypeUserDO openTypeUserObj = OpenTypeUserDO.builder().openTypeId(openType.getId()).memberId(dto.getMemberId()).payPrice(openType.getPreferentialPrice()).startTime(now).status(OpenTypeUserStatusEnum.EFFICIENT.getType()).build();
OpenTypeUserDO openTypeUserObj = OpenTypeUserDO.builder()
.openTypeId(openType.getId())
.memberId(dto.getMemberId())
.payPrice(openType.getPreferentialPrice())
.startTime(now)
.status(OpenTypeUserStatusEnum.EFFICIENT.getType())
.payStatus(false)
.build();
// 查看用户有没有开通过并且没有过期的会员
OpenTypeUserDO openTypeUserDO = openTypeUserMapper.selectEnableByUserId(dto.getMemberId());
if (ObjectUtil.isNotEmpty(openTypeUserDO)) {
@@ -155,14 +174,19 @@ public class OpenTypeServiceImpl implements OpenTypeService {
// 新增会员开通表
openTypeUserMapper.insert(openTypeUserObj);
// 新增用户tag
MemberRecentTagDO tagObj = new MemberRecentTagDO();
tagObj.setMemberId(dto.getMemberId());
tagObj.setTagBizType(MemberRecentTagEnum.VIP.getType());
tagObj.setTagBizId(openTypeUserObj.getId());
tagObj.setTagTitle(MemberRecentTagEnum.VIP.getDescription());
memberRecentTagMapper.insert(tagObj);
return openTypeUserObj.getId();
PayOrderCreateReqDTO orderObj = new PayOrderCreateReqDTO();
orderObj.setAppKey("vip");
orderObj.setUserIp(dto.getMemberIp());
orderObj.setMerchantOrderId(String.valueOf(openTypeUserObj.getId()));
orderObj.setSubject("开通会员");
orderObj.setBody("开通会员");
orderObj.setPrice(openTypeUserObj.getPayPrice());
orderObj.setExpireTime(addTime(Duration.ofHours(2L)));
Long payOrderId = payOrderApi.createOrder(orderObj);
// 更新钱包充值记录中支付订单
openTypeUserMapper.updateById(new OpenTypeUserDO().setId(openTypeUserObj.getId()).setPayOrderId(payOrderId));
return payOrderId;
}
/**
@@ -266,6 +290,82 @@ public class OpenTypeServiceImpl implements OpenTypeService {
return updateList.size();
}
/**
* 支付的回调
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void updateWalletRechargerPaid(Long id, Long payOrderId) {
OpenTypeUserDO openTypeUserDO = openTypeUserMapper.selectById(id);
if (ObjectUtil.isEmpty(openTypeUserDO)) {
log.error("[会员开通支付][recharge({}) payOrder({}) 不存在开通会员的订单,请进行处理!]", id, payOrderId);
}
if (openTypeUserDO.getPayStatus()) {
// 特殊:如果订单已支付,且支付单号相同,直接返回,说明重复回调
if (ObjectUtil.equals(openTypeUserDO.getPayOrderId(), payOrderId)) {
log.warn("[会员开通支付][recharge({})已支付,且支付单号相同({}),直接返回]", openTypeUserDO, payOrderId);
return;
}
throw exception(MemberErrorCode.EMPTY, "钱包充值更新支付状态失败,支付单编号不匹配");
}
// 查询订单
PayOrderRespDTO payOrder = validatePayOrderPaid(openTypeUserDO, payOrderId);
// 更新钱包充值的支付状态
LambdaUpdateWrapper<OpenTypeUserDO> updateWrapper = new LambdaUpdateWrapper<OpenTypeUserDO>()
.set(OpenTypeUserDO::getPayChannelCode, payOrder.getChannelCode())
.set(OpenTypeUserDO::getPayStatus, true)
.set(OpenTypeUserDO::getPayTime, LocalDateTime.now())
.eq(OpenTypeUserDO::getId, openTypeUserDO.getOpenTypeId())
.eq(OpenTypeUserDO::getPayStatus, false);
int updateCount = openTypeUserMapper.update(updateWrapper);
if (updateCount == 0) {
throw exception(MemberErrorCode.EMPTY, "钱包充值更新支付状态失败,钱包充值记录不是【未支付】状态");
}
}
/**
* 退款的回调
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void refundWalletRecharge(Long id, String clientIP) {
OpenTypeUserDO openTypeUserDO = openTypeUserMapper.selectById(id);
if (ObjectUtil.isEmpty(openTypeUserDO)) {
log.error("[会员开通退款][recharge({}) clientIP({}) 不存在开通会员的订单,请进行处理!]", id, clientIP);
}
}
/**
* 验证会员支付订单是否成功
*/
private PayOrderRespDTO validatePayOrderPaid(OpenTypeUserDO recharge, Long payOrderId) {
// 1. 校验支付单是否存在
PayOrderRespDTO payOrder = payOrderApi.getOrder(payOrderId);
if (payOrder == null) {
log.error("[validatePayOrderPaid][验证会员支付订单是否成功({}) payOrder({}) 不存在,请进行处理!]", recharge.getId(), payOrderId);
throw exception(PAY_ORDER_NOT_FOUND);
}
// 2.1 校验支付单已支付
if (!PayOrderStatusEnum.isSuccess(payOrder.getStatus())) {
log.error("[validatePayOrderPaid][验证会员支付订单是否成功({}) payOrder({}) 未支付,请进行处理!payOrder 数据是:{}]", recharge.getId(), payOrderId, toJsonString(payOrder));
throw exception(MemberErrorCode.EMPTY, "会员充值更新支付状态失败,支付单状态不是【支付成功】状态");
}
// 2.2 校验支付金额一致
if (notEqual(payOrder.getPrice(), recharge.getPayPrice())) {
log.error("[validatePayOrderPaid][验证会员支付订单是否成功({}) payOrder({}) 支付金额不匹配,请进行处理!钱包 数据是:{},payOrder 数据是:{}]", recharge.getId(), payOrderId, toJsonString(recharge), toJsonString(payOrder));
throw exception(MemberErrorCode.EMPTY, "钱包充值更新支付状态失败,支付单金额不匹配");
}
// 2.3 校验支付订单的商户订单匹配
if (notEqual(payOrder.getMerchantOrderId(), recharge.getId().toString())) {
log.error("[validatePayOrderPaid][验证会员支付订单是否成功({}) 支付单不匹配({}),请进行处理!payOrder 数据是:{}]", recharge.getId(), payOrderId, toJsonString(payOrder));
throw exception(MemberErrorCode.EMPTY, "钱包充值更新支付状态失败,支付单编号不匹配");
}
return payOrder;
}
private void validateOpenTypeExists(Long id) {
if (openTypeMapper.selectById(id) == null) {
throw exception(MemberErrorCode.OPEN_TYPE_NOT_EXISTS);
@@ -197,7 +197,7 @@ public class RecentServiceImpl implements RecentService {
List<MemberRecentOperateEnum> notFindActionList = Arrays.asList(MemberRecentOperateEnum.MEMBER_REPORT);
OperationDO operationDO = null;
if (!notFindActionList.contains(dto.getAction())) {
operationMapper.selectOne(new LambdaQueryWrapperX<OperationDO>()
operationDO = operationMapper.selectOne(new LambdaQueryWrapperX<OperationDO>()
.eq(OperationDO::getMemberId, dto.getMemberId())
.eq(OperationDO::getOperatorMemberId, dto.getOperatorMemberId())
.eq(OperationDO::getBizType, dto.getAction().getType())
@@ -32,6 +32,11 @@ public interface MemberRecentTagService {
*/
boolean createOrUpdateNewPersonTag(Long memberId);
/**
* 创建或者更新vip tag
*/
boolean createOrUpdateVipTag(Long memberId);
/**
* 清理了过期的vip tag
*/
@@ -141,6 +141,43 @@ public class MemberRecentTagServiceImp implements MemberRecentTagService {
}
}
/**
* 创建或者更新vip tag
*/
@Override
public boolean createOrUpdateVipTag(Long memberId) {
// 查询用户
MemberUserDO memberUser = memberUserMapper.selectById(memberId);
if (ObjectUtil.isEmpty(memberUser)) return false;
// 现在是否还是vip
boolean isVip = openTypeService.isUserOpenType(memberId);
MemberRecentTagDO tag = memberRecentTagMapper.selectOne(new LambdaQueryWrapperX<MemberRecentTagDO>()
.eq(MemberRecentTagDO::getMemberId, memberId)
.eq(MemberRecentTagDO::getTagBizType, MemberRecentTagEnum.NEW_PERSON.getType())
);
// 是vip
if (isVip) {
if (ObjectUtil.isEmpty(tag)) {
MemberRecentTagDO obj = new MemberRecentTagDO();
obj.setTagBizId(null);
obj.setMemberId(memberId);
obj.setTagBizType(MemberRecentTagEnum.VIP.getType());
obj.setTagTitle(MemberRecentTagEnum.VIP.getDescription());
memberRecentTagMapper.insert(obj);
return true;
}
return false;
}
// 不是vip
else {
if (ObjectUtil.isNotEmpty(tag)) {
memberRecentTagMapper.deleteById(tag.getId());
return true;
}
return false;
}
}
/**
* 清理了过期的vip tag
*/
@@ -151,19 +188,16 @@ public class MemberRecentTagServiceImp implements MemberRecentTagService {
);
if (ObjectUtil.isEmpty(vipTagList)) return 0;
List<Long> removeTagMemberIdList = new ArrayList<>();
int delCount = 0;
for (MemberRecentTagDO item : vipTagList) {
boolean isVip = openTypeService.isUserOpenType(item.getMemberId());
if (!isVip) {
removeTagMemberIdList.add(item.getMemberId());
boolean isDelete = createOrUpdateVipTag(item.getMemberId());
if (isDelete) {
delCount++;
}
}
int deleteCount = memberRecentTagMapper.delete(new LambdaQueryWrapperX<MemberRecentTagDO>()
.eq(MemberRecentTagDO::getTagBizType, MemberRecentTagEnum.VIP.getType())
.in(MemberRecentTagDO::getMemberId, removeTagMemberIdList)
);
return deleteCount;
return delCount;
}
/**
@@ -180,7 +214,7 @@ public class MemberRecentTagServiceImp implements MemberRecentTagService {
for (MemberRecentTagDO item : newPersonTagList) {
boolean isDelete = createOrUpdateNewPersonTag(item.getMemberId());
if(isDelete){
if (isDelete) {
delCount++;
}
}
@@ -9,6 +9,8 @@
<where>
a.deleted = '0'
and a.status = 1
and a.pay_status = 1
and a.refund_status is null
and a.end_time > now()
<if test="memberIdList != null and memberIdList.size() != 0">
and a.member_id in