feat(health):新增用户下单及支付的相关功能

This commit is contained in:
JIAN 2024-09-16 13:13:37 +08:00
parent a0fa46f873
commit 79b7dbcadd
13 changed files with 418 additions and 27 deletions

View File

@ -10,5 +10,10 @@ public class RedisConstants {
/**
* 用户端订单滚动分页查询
*/
public static final String ORDER_PAGE_QUERY = "ORDERS:PAGE_QUERY:PAGE_%s";
}
public static final String ORDER_PAGE_QUERY = "HEALTH:ORDERS:PAGE_QUERY:PAGE_%s";
/**
* 用户端下单订单号生成器
*/
public static final String ORDER_ID_GENERATOR = "HEALTH:ORDERS:GENERATOR";
}

View File

@ -4,6 +4,7 @@ import com.jzo2o.api.trade.enums.PayChannelEnum;
import com.jzo2o.health.model.dto.request.PlaceOrderReqDTO;
import com.jzo2o.health.model.dto.response.OrdersPayResDTO;
import com.jzo2o.health.model.dto.response.PlaceOrderResDTO;
import com.jzo2o.health.service.IOrderCreateService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
@ -19,11 +20,13 @@ import javax.annotation.Resource;
@RequestMapping("/user/orders")
@Api(tags = "用户端 - 下单支付相关接口")
public class OrdersController {
@Resource
private IOrderCreateService orderCreateService;
@ApiOperation("下单接口")
@PostMapping("/place")
public PlaceOrderResDTO place(@RequestBody PlaceOrderReqDTO placeOrderReqDTO) {
return null;
return orderCreateService.placeOrder(placeOrderReqDTO);
}
@PutMapping("/pay/{id}")
@ -33,7 +36,7 @@ public class OrdersController {
@ApiImplicitParam(name = "tradingChannel", value = "支付渠道ALI_PAY、WECHAT_PAY", required = true, dataTypeClass = PayChannelEnum.class),
})
public OrdersPayResDTO pay(@PathVariable("id") Long id, @RequestParam("tradingChannel") PayChannelEnum tradingChannel) {
return null;
return orderCreateService.payOrder(id, tradingChannel);
}
@GetMapping("/pay/{id}/result")
@ -42,6 +45,6 @@ public class OrdersController {
@ApiImplicitParam(name = "id", value = "订单id", required = true, dataTypeClass = Long.class)
})
public OrdersPayResDTO payResult(@PathVariable("id") Long id) {
return null;
return orderCreateService.getPayResult(id);
}
}
}

View File

@ -1,5 +1,7 @@
package com.jzo2o.health.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -16,6 +18,8 @@ public enum OrderPayStatusEnum {
REFUND_SUCCESS(3, "退款成功"),
REFUND_FAIL(4, "退款失败");
@EnumValue
@JsonValue
private final Integer status;
private final String desc;
}
}

View File

@ -1,5 +1,7 @@
package com.jzo2o.health.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -15,6 +17,8 @@ public enum OrderStatusEnum {
CLOSED(300, "已关闭"),
CANCELLED(400, "已取消");
@EnumValue
@JsonValue
private final Integer status;
private final String desc;
@ -32,4 +36,4 @@ public enum OrderStatusEnum {
}
return null;
}
}
}

View File

@ -0,0 +1,70 @@
package com.jzo2o.health.model;
import com.jzo2o.health.enums.OrderPayStatusEnum;
import com.jzo2o.health.enums.OrderStatusEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 通用订单更新DTO
* @author JIAN
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderUpdateStatusDTO {
/**
* 订单id
*/
private Long id;
/**
* 原订单状态
*/
private OrderStatusEnum originStatus;
/**
* 目标订单状态
*/
private OrderStatusEnum targetStatus;
/**
* 支付状态
*/
private OrderPayStatusEnum payStatus;
/**
* 支付时间
*/
private LocalDateTime payTime;
/**
* 支付服务交易单号
*/
private Long tradingOrderNo;
/**
* 第三方支付的交易号
*/
private String transactionId;
/**
* 支付服务退款单号
*/
private Long refundNo;
/**
* 第三方支付的退款单号
*/
private String refundId;
/**
* 支付渠道
*/
private String tradingChannel;
}

View File

@ -4,6 +4,9 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.jzo2o.health.enums.OrderPayStatusEnum;
import com.jzo2o.health.enums.OrderStatusEnum;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
@ -22,6 +25,7 @@ import java.time.LocalDateTime;
* @since 2023-11-02
*/
@Data
@Builder
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("orders")
@ -32,18 +36,18 @@ public class Orders implements Serializable {
/**
* 订单id
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
@TableId(value = "id", type = IdType.NONE)
private Long id;
/**
* 订单状态0未支付100待体检200已体检300已关闭400已取消
*/
private Integer orderStatus;
private OrderStatusEnum orderStatus;
/**
* 支付状态0未支付1已支付2退款中3退款成功4退款失败
*/
private Integer payStatus;
private OrderPayStatusEnum payStatus;
/**
* 套餐id
@ -160,6 +164,4 @@ public class Orders implements Serializable {
* 更新时间
*/
private LocalDateTime updateTime;
}
}

View File

@ -1,16 +1,18 @@
package com.jzo2o.health.model.domain;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* <p>
*
*
* </p>
*
* @author itcast
@ -55,7 +57,7 @@ public class Setmeal implements Serializable {
/**
* 套餐价格
*/
private Float price;
private BigDecimal price;
/**
* 套餐说明
@ -73,4 +75,4 @@ public class Setmeal implements Serializable {
private String img;
}
}

View File

@ -1,6 +1,8 @@
package com.jzo2o.health.model.dto.response;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.jzo2o.health.enums.OrderPayStatusEnum;
import com.jzo2o.health.enums.OrderStatusEnum;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@ -11,7 +13,6 @@ import java.time.LocalDateTime;
/**
* 订单响应
*
* @author itcast
* @create 2023/11/3 19:17
**/
@ -28,14 +29,13 @@ public class OrdersResDTO {
* 订单状态0未支付100待体检200已体检300已关闭400已取消
*/
@ApiModelProperty("订单状态0未支付100待体检200已体检300已关闭400已取消")
private Integer orderStatus;
private OrderStatusEnum orderStatus;
/**
* 支付状态0未支付1已支付2退款中3退款成功4退款失败
*/
@ApiModelProperty("支付状态0未支付1已支付2退款中3退款成功4退款失败")
private Integer payStatus;
private OrderPayStatusEnum payStatus;
/**
* 套餐id
@ -123,4 +123,4 @@ public class OrdersResDTO {
@ApiModelProperty("更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}
}

View File

@ -0,0 +1,35 @@
package com.jzo2o.health.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jzo2o.api.trade.enums.PayChannelEnum;
import com.jzo2o.health.model.domain.Orders;
import com.jzo2o.health.model.dto.request.PlaceOrderReqDTO;
import com.jzo2o.health.model.dto.response.OrdersPayResDTO;
import com.jzo2o.health.model.dto.response.PlaceOrderResDTO;
/**
* 订单创建相关业务层
* @author JIAN
*/
public interface IOrderCreateService extends IService<Orders> {
/**
* 微服务标识
*/
String PRODUCT_APP_ID = "health.orders";
/**
* 用户下单
*/
PlaceOrderResDTO placeOrder(PlaceOrderReqDTO placeOrderReqDTO);
/**
* 用户支付(返回支付二维码)
* @param id 本系统的订单id
*/
OrdersPayResDTO payOrder(Long id, PayChannelEnum tradingChannel);
/**
* 获取指定订单的支付结果
*/
OrdersPayResDTO getPayResult(Long id);
}

View File

@ -0,0 +1,20 @@
package com.jzo2o.health.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jzo2o.health.model.OrderUpdateStatusDTO;
import com.jzo2o.health.model.domain.Orders;
/**
* <p>
* 订单表 服务类
* </p>
*
* @author itcast
* @since 2023-08-02
*/
public interface IOrdersCommonService extends IService<Orders> {
/**
* 更新指定id订单的状态
*/
Boolean updateStatus(OrderUpdateStatusDTO orderUpdateStatusReqDTO);
}

View File

@ -0,0 +1,210 @@
package com.jzo2o.health.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
import com.jzo2o.api.trade.NativePayApi;
import com.jzo2o.api.trade.TradingApi;
import com.jzo2o.api.trade.dto.request.NativePayReqDTO;
import com.jzo2o.api.trade.dto.response.NativePayResDTO;
import com.jzo2o.api.trade.dto.response.TradingResDTO;
import com.jzo2o.api.trade.enums.PayChannelEnum;
import com.jzo2o.api.trade.enums.TradingStateEnum;
import com.jzo2o.common.expcetions.CommonException;
import com.jzo2o.common.expcetions.DBException;
import com.jzo2o.common.expcetions.ForbiddenOperationException;
import com.jzo2o.common.expcetions.ServerErrorException;
import com.jzo2o.common.utils.BeanUtils;
import com.jzo2o.common.utils.DateUtils;
import com.jzo2o.common.utils.ObjectUtils;
import com.jzo2o.health.constant.RedisConstants;
import com.jzo2o.health.enums.OrderPayStatusEnum;
import com.jzo2o.health.enums.OrderStatusEnum;
import com.jzo2o.health.mapper.OrdersMapper;
import com.jzo2o.health.model.OrderUpdateStatusDTO;
import com.jzo2o.health.model.UserThreadLocal;
import com.jzo2o.health.model.domain.Member;
import com.jzo2o.health.model.domain.Orders;
import com.jzo2o.health.model.domain.Setmeal;
import com.jzo2o.health.model.dto.request.PlaceOrderReqDTO;
import com.jzo2o.health.model.dto.response.OrdersPayResDTO;
import com.jzo2o.health.model.dto.response.PlaceOrderResDTO;
import com.jzo2o.health.properties.TradeProperties;
import com.jzo2o.health.service.IMemberService;
import com.jzo2o.health.service.IOrderCreateService;
import com.jzo2o.health.service.IOrdersCommonService;
import com.jzo2o.health.service.ISetmealService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单创建相关业务层
* @author JIAN
*/
@Slf4j
@Service
public class OrderCreateServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements IOrderCreateService {
@Resource
private IMemberService memberService;
@Resource
private ISetmealService setmealService;
@Resource
private IOrdersCommonService ordersCommonService;
@Resource
private TradingApi tradingApi;
@Resource
private NativePayApi nativePayApi;
@Resource
private TradeProperties tradeProperties;
@Resource
private RedisTemplate<String, Long> redisTemplate;
/**
* 生成订单号(2位年+2位月+2位日+13位序号)
*/
private Long generateOrderId() {
Long idNo = redisTemplate.opsForValue().increment(RedisConstants.ORDER_ID_GENERATOR, 1);
String orderId = DateUtils.format(LocalDateTime.now(), "yyMMdd") + String.format("%013d", idNo);
return Long.valueOf(orderId);
}
@Override
public PlaceOrderResDTO placeOrder(PlaceOrderReqDTO placeOrderReqDTO) {
Long currentUserId = UserThreadLocal.currentUserId();
if (ObjectUtils.isEmpty(currentUserId)) {
throw new ForbiddenOperationException("用户信息不存在无法下单");
}
Member member = memberService.getById(currentUserId);
if (ObjectUtils.isEmpty(member)) {
throw new ForbiddenOperationException("用户信息不存在无法下单");
}
Setmeal setmeal = setmealService.getById(placeOrderReqDTO.getSetmealId());
if (ObjectUtils.isEmpty(setmeal)) {
throw new ForbiddenOperationException("套餐信息不存在无法下单");
}
// 生成订单id
Long orderId = this.generateOrderId();
if (ObjectUtils.isEmpty(orderId)) {
throw new ServerErrorException("生成订单id失败无法下单");
}
if (!SqlHelper.retBool(baseMapper.insert(Orders.builder()
.id(orderId)
.orderStatus(OrderStatusEnum.NO_PAY)
.payStatus(OrderPayStatusEnum.NO_PAY)
.setmealId(setmeal.getId())
.setmealName(setmeal.getName())
.setmealPrice(setmeal.getPrice())
.setmealSex(setmeal.getSex())
.setmealAge(setmeal.getAge())
.setmealImg(setmeal.getImg())
.setmealRemark(setmeal.getRemark())
.reservationDate(placeOrderReqDTO.getReservationDate())
.checkupPersonName(placeOrderReqDTO.getCheckupPersonName())
.checkupPersonSex(placeOrderReqDTO.getCheckupPersonSex())
.checkupPersonPhone(placeOrderReqDTO.getCheckupPersonPhone())
.checkupPersonIdcard(placeOrderReqDTO.getCheckupPersonIdcard())
.memberId(member.getId())
.memberPhone(member.getPhone())
.sortBy(DateUtils.toEpochMilli(LocalDateTime.now()))
.build()))) {
throw new DBException("订单表插入失败下单失败");
}
return new PlaceOrderResDTO(orderId);
}
@Override
public OrdersPayResDTO payOrder(Long id, PayChannelEnum tradingChannel) {
Orders orderInfo = baseMapper.selectById(id);
if (ObjectUtils.isEmpty(orderInfo)) {
throw new ForbiddenOperationException("订单不存在无法支付");
}
// 初步防止重复发起支付请求
if (orderInfo.getPayStatus() == OrderPayStatusEnum.PAY_SUCCESS) {
OrdersPayResDTO ordersPayResDTO = BeanUtils.toBean(orderInfo, OrdersPayResDTO.class);
ordersPayResDTO.setProductOrderNo(id);
return ordersPayResDTO;
}
// 下单
String tradingChannelOld = orderInfo.getTradingChannel();
NativePayResDTO nativePayResDTO = nativePayApi.createDownLineTrading(NativePayReqDTO.builder()
.productOrderNo(id)
.productAppId(IOrderCreateService.PRODUCT_APP_ID)
.tradingChannel(tradingChannel)
.tradingAmount(/*orderInfo.getSetmealPrice()*/ new BigDecimal("0.01"))
.memo(orderInfo.getSetmealName())
.changeChannel(ObjectUtils.isNotEmpty(tradingChannelOld)
&& !tradingChannelOld.equals(tradingChannel.name()))
.enterpriseId(tradingChannel == PayChannelEnum.WECHAT_PAY
? tradeProperties.getWechatEnterpriseId()
: tradeProperties.getAliEnterpriseId())
.build());
if (!lambdaUpdate()
.eq(Orders::getId, id)
.set(Orders::getTradingOrderNo, nativePayResDTO.getTradingOrderNo())
.set(Orders::getTradingChannel, nativePayResDTO.getTradingChannel())
.update()) {
log.warn("订单表更新失败(已正常生成支付二维码), 订单id: {}", id);
}
OrdersPayResDTO ordersPayResDTO = BeanUtils.toBean(nativePayResDTO, OrdersPayResDTO.class);
ordersPayResDTO.setPayStatus(OrderPayStatusEnum.NO_PAY.getStatus());
return ordersPayResDTO;
}
@Override
public OrdersPayResDTO getPayResult(Long id) {
Orders orders = baseMapper.selectById(id);
if (ObjectUtils.isEmpty(orders)) {
throw new CommonException("订单不存在");
}
// 公共返回参数
OrdersPayResDTO ordersPayResDTO = BeanUtils.toBean(orders, OrdersPayResDTO.class);
ordersPayResDTO.setProductOrderNo(id);
// 未支付订单更新状态
Long tradingOrderNo = orders.getTradingOrderNo();
if (ObjectUtils.isNotEmpty(tradingOrderNo)
&& orders.getPayStatus() == OrderPayStatusEnum.NO_PAY) {
TradingResDTO tradingResDTO = tradingApi.findTradResultByTradingOrderNo(tradingOrderNo);
// 支付成功
if (ObjectUtils.isNotEmpty(tradingResDTO)
&& tradingResDTO.getTradingState() == TradingStateEnum.YJS) {
if (!ordersCommonService.updateStatus(OrderUpdateStatusDTO.builder()
.id(id)
.originStatus(OrderStatusEnum.NO_PAY)
.targetStatus(OrderStatusEnum.WAITING_CHECKUP)
.payStatus(OrderPayStatusEnum.PAY_SUCCESS)
.tradingOrderNo(tradingResDTO.getTradingOrderNo())
.transactionId(tradingResDTO.getTransactionId())
.tradingChannel(tradingResDTO.getTradingChannel())
.payTime(LocalDateTime.now())
.build())) {
throw new DBException("订单表更新失败");
}
// 支付成功更新最终响应信息的状态
ordersPayResDTO.setPayStatus(OrderPayStatusEnum.PAY_SUCCESS.getStatus());
} else {
// 未支付成功返回二维码
ordersPayResDTO.setQrCode(tradingResDTO.getQrCode());
}
}
return ordersPayResDTO;
}
}

View File

@ -92,8 +92,8 @@ public class OrderManagerServiceImpl extends ServiceImpl<OrdersMapper, Orders> i
payInfo.setThirdOrderId(orders.getTransactionId());
adminOrdersDetailResDTO.setPayInfo(payInfo);
Integer orderStatus = orders.getOrderStatus();
if (OrderStatusEnum.CANCELLED.getStatus().equals(orderStatus)) {
OrderStatusEnum orderStatus = orders.getOrderStatus();
if (OrderStatusEnum.CANCELLED == orderStatus) {
OrdersCancelled cancelInfo = orderCancelService.getById(orders.getId());
// 取消信息
@ -112,7 +112,7 @@ public class OrderManagerServiceImpl extends ServiceImpl<OrdersMapper, Orders> i
.build());
payInfo.setPayStatus(OrderPayStatusEnum.PAY_SUCCESS.getStatus());
} else if (OrderStatusEnum.CLOSED.getStatus().equals(orderStatus)) {
} else if (OrderStatusEnum.CLOSED == orderStatus) {
OrdersCancelled cancelInfo = orderCancelService.getById(orders.getId());
// 取消信息

View File

@ -0,0 +1,36 @@
package com.jzo2o.health.service.impl;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jzo2o.health.mapper.OrdersMapper;
import com.jzo2o.health.model.OrderUpdateStatusDTO;
import com.jzo2o.health.model.domain.Orders;
import com.jzo2o.health.service.IOrdersCommonService;
import org.springframework.stereotype.Service;
/**
* <p>
* 订单表 服务实现类
* </p>
* @author itcast
* @since 2023-08-02
*/
@Service
public class OrdersCommonServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements IOrdersCommonService {
@Override
public Boolean updateStatus(OrderUpdateStatusDTO orderUpdateStatusReqDTO) {
return lambdaUpdate()
.eq(Orders::getId, orderUpdateStatusReqDTO.getId())
.gt(Orders::getMemberId, 0)
.eq(ObjectUtil.isNotNull(orderUpdateStatusReqDTO.getOriginStatus()), Orders::getOrderStatus, orderUpdateStatusReqDTO.getOriginStatus())
.set(Orders::getOrderStatus, orderUpdateStatusReqDTO.getTargetStatus())
.set(ObjectUtil.isNotNull(orderUpdateStatusReqDTO.getPayStatus()), Orders::getPayStatus, orderUpdateStatusReqDTO.getPayStatus())
.set(ObjectUtil.isNotNull(orderUpdateStatusReqDTO.getPayTime()), Orders::getPayTime, orderUpdateStatusReqDTO.getPayTime())
.set(ObjectUtil.isNotNull(orderUpdateStatusReqDTO.getTradingOrderNo()), Orders::getTradingOrderNo, orderUpdateStatusReqDTO.getTradingOrderNo())
.set(ObjectUtil.isNotNull(orderUpdateStatusReqDTO.getTransactionId()), Orders::getTransactionId, orderUpdateStatusReqDTO.getTransactionId())
.set(ObjectUtil.isNotNull(orderUpdateStatusReqDTO.getTradingChannel()), Orders::getTradingChannel, orderUpdateStatusReqDTO.getTradingChannel())
.set(ObjectUtil.isNotNull(orderUpdateStatusReqDTO.getRefundNo()), Orders::getRefundNo, orderUpdateStatusReqDTO.getRefundNo())
.set(ObjectUtil.isNotNull(orderUpdateStatusReqDTO.getRefundId()), Orders::getRefundId, orderUpdateStatusReqDTO.getRefundId())
.update();
}
}