Prechádzať zdrojové kódy

VIP会员过审单独需求

zoujiajian 1 deň pred
rodič
commit
a1e131a83e

+ 18 - 0
netflix-dao/src/main/java/com/cyksj/mapper/special/SpecialMemberEntitlementMapper.java

@@ -0,0 +1,18 @@
+package com.cyksj.mapper.special;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.SpecialMemberEntitlement;
+import org.apache.ibatis.annotations.Insert;
+import org.apache.ibatis.annotations.Param;
+
+public interface SpecialMemberEntitlementMapper extends BaseMapper<SpecialMemberEntitlement> {
+
+	@Insert("insert into special_member_entitlement " +
+			"(user_id, expire_time, last_order_id, created_time, update_time) " +
+			"values (#{userId}, date_add(now(), interval #{durationDays} day), #{orderId}, now(), now()) " +
+			"on duplicate key update expire_time = date_add(greatest(expire_time, now()), interval #{durationDays} day), " +
+			"last_order_id = #{orderId}, update_time = now()")
+	int extend(@Param("userId") Long userId,
+			   @Param("durationDays") Integer durationDays,
+			   @Param("orderId") Long orderId);
+}

+ 32 - 0
netflix-dao/src/main/java/com/cyksj/mapper/special/SpecialPayOrderMapper.java

@@ -0,0 +1,32 @@
+package com.cyksj.mapper.special;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.SpecialPayOrder;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
+
+import java.util.Date;
+
+public interface SpecialPayOrderMapper extends BaseMapper<SpecialPayOrder> {
+
+	@Select("select id, order_no as orderNo, user_id as userId, product_code as productCode, " +
+			"subject, amount, duration_days as durationDays, status, shop_id as shopId, " +
+			"alipay_trade_no as alipayTradeNo, buyer_id as buyerId, pay_time as payTime, " +
+			"expire_time as expireTime, fulfill_status as fulfillStatus, fulfill_time as fulfillTime, " +
+			"created_time as createdTime, update_time as updateTime " +
+			"from special_pay_order where order_no = #{orderNo} limit 1 for update")
+	SpecialPayOrder selectByOrderNoForUpdate(@Param("orderNo") String orderNo);
+
+	@Update("update special_pay_order set status = 'PAID', alipay_trade_no = #{tradeNo}, " +
+			"buyer_id = #{buyerId}, pay_time = #{payTime}, update_time = now() " +
+			"where id = #{orderId} and status = 'CREATED'")
+	int markPaid(@Param("orderId") Long orderId,
+				 @Param("tradeNo") String tradeNo,
+				 @Param("buyerId") String buyerId,
+				 @Param("payTime") Date payTime);
+
+	@Update("update special_pay_order set fulfill_status = 'SUCCESS', fulfill_time = now(), " +
+			"update_time = now() where id = #{orderId} and status = 'PAID' and fulfill_status = 'INIT'")
+	int markFulfilled(@Param("orderId") Long orderId);
+}

+ 7 - 0
netflix-dao/src/main/java/com/cyksj/mapper/special/SpecialPayProductMapper.java

@@ -0,0 +1,7 @@
+package com.cyksj.mapper.special;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.SpecialPayProduct;
+
+public interface SpecialPayProductMapper extends BaseMapper<SpecialPayProduct> {
+}

+ 19 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/SpecialMemberEntitlement.java

@@ -0,0 +1,19 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Date;
+
+@Getter
+@Setter
+@TableName("special_member_entitlement")
+public class SpecialMemberEntitlement extends BaseEntity {
+
+	private Long userId;
+
+	private Date expireTime;
+
+	private Long lastOrderId;
+}

+ 56 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/SpecialPayOrder.java

@@ -0,0 +1,56 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Getter
+@Setter
+@TableName("special_pay_order")
+public class SpecialPayOrder extends BaseEntity {
+
+	private String orderNo;
+
+	private Long userId;
+
+	private String productCode;
+
+	private String subject;
+
+	/** Order amount in cents. */
+	private BigDecimal amount;
+
+	private Integer durationDays;
+
+	private Status status;
+
+	private String shopId;
+
+	private String alipayTradeNo;
+
+	private String buyerId;
+
+	private Date payTime;
+
+	private Date expireTime;
+
+	private FulfillStatus fulfillStatus;
+
+	private Date fulfillTime;
+
+	public enum Status {
+		CREATED,
+		PAID,
+		CLOSED,
+		REFUNDED
+	}
+
+	public enum FulfillStatus {
+		INIT,
+		SUCCESS,
+		FAILED
+	}
+}

+ 26 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/SpecialPayProduct.java

@@ -0,0 +1,26 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+@Getter
+@Setter
+@TableName("special_pay_product")
+public class SpecialPayProduct extends BaseEntity {
+
+	private String productCode;
+
+	private String productName;
+
+	/** Price in cents. */
+	private BigDecimal amount;
+
+	private Integer durationDays;
+
+	private Boolean enabled;
+
+	private Integer sortBy;
+}

+ 23 - 0
netflix-dao/src/main/java/com/cyksj/model/request/SpecialPayCreateReq.java

@@ -0,0 +1,23 @@
+package com.cyksj.model.request;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+@Getter
+@Setter
+public class SpecialPayCreateReq {
+
+	@NotBlank
+	private String productCode;
+
+	/** 1: mobile web; 2: desktop web. */
+	@NotNull
+	private Integer payType;
+
+	private String payMode;
+
+	private String returnUrl;
+}

+ 21 - 0
netflix-dao/src/main/java/com/cyksj/model/response/SpecialPayCreateRep.java

@@ -0,0 +1,21 @@
+package com.cyksj.model.response;
+
+import com.cyksj.model.entity.SpecialPayOrder;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Date;
+
+@Getter
+@Setter
+public class SpecialPayCreateRep {
+
+	private String orderNo;
+
+	private SpecialPayOrder.Status status;
+
+	private Date expireTime;
+
+	/** HTML form generated and signed by the Alipay SDK. */
+	private String payBody;
+}

+ 2 - 0
netflix-service/src/main/java/com/cyksj/config/zfb/BaseZfbConfig.java

@@ -60,6 +60,8 @@ public class BaseZfbConfig {
 
 	public static String spotifyUpgradeNotifyUrl = "applets/spotify/prepay/zfb/upgrade/notify";
 
+	public static String specialPayNotifyUrl = "applets/special/member/alipay/notify";
+
 	/**
 	 * qr_pay_mode PC扫码支付的方式。 跳转模式
 	 */

+ 7 - 0
netflix-service/src/main/java/com/cyksj/service/pay/AliPayCommonService.java

@@ -3,6 +3,9 @@ package com.cyksj.service.pay;
 import com.cyksj.model.dto.AliPayParams;
 import com.cyksj.model.request.AlipayOrderDonReq;
 
+import java.math.BigDecimal;
+import java.util.Date;
+
 /*
  *项目名: netflix
  *文件名: AliPayCommonService
@@ -10,6 +13,10 @@ import com.cyksj.model.request.AlipayOrderDonReq;
  *创建时间:2023/6/14 15:19
  */
 public interface AliPayCommonService {
+	AliPayParams builderPayParams(Long orderId, String orderNo, String shopId, BigDecimal money,
+			Date orderCreatedTime, String subject, String productCode, String payMode,
+			String notifyUrl, String returnUrl) throws Exception;
+
 	/**
 	 *
 	 * @param orderId

+ 1 - 0
netflix-service/src/main/java/com/cyksj/service/pay/impl/AliPayCommonServiceImpl.java

@@ -338,6 +338,7 @@ public class AliPayCommonServiceImpl implements AliPayCommonService {
 		return null;
 	}
 
+	@Override
 	public AliPayParams builderPayParams(Long orderId, String orderNo, String shopId, BigDecimal money, Date orderCreatedTime, String subject, String productCode, String payMode, String notifyUrl, String returnUrl) throws Exception {
 		OrderPayBuilderDto orderPayBuilderDto = new OrderPayBuilderDto();
 		orderPayBuilderDto.setOrderId(orderId);

+ 23 - 0
netflix-service/src/main/java/com/cyksj/service/special/SpecialPayService.java

@@ -0,0 +1,23 @@
+package com.cyksj.service.special;
+
+import com.cyksj.model.entity.SpecialPayOrder;
+import com.cyksj.model.entity.SpecialPayProduct;
+import com.cyksj.model.entity.SpecialMemberEntitlement;
+import com.cyksj.model.request.SpecialPayCreateReq;
+import com.cyksj.model.response.SpecialPayCreateRep;
+
+import java.util.List;
+import java.util.Map;
+
+public interface SpecialPayService {
+
+	List<SpecialPayProduct> getEnabledProducts();
+
+	SpecialPayCreateRep createAlipayOrder(Long userId, SpecialPayCreateReq request) throws Exception;
+
+	SpecialPayOrder getUserOrder(Long userId, String orderNo);
+
+	SpecialMemberEntitlement getMemberEntitlement(Long userId);
+
+	void handleAlipayNotify(Map<String, String> params) throws Exception;
+}

+ 230 - 0
netflix-service/src/main/java/com/cyksj/service/special/impl/SpecialPayServiceImpl.java

@@ -0,0 +1,230 @@
+package com.cyksj.service.special.impl;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.StrUtil;
+import com.alipay.api.internal.util.AlipaySignature;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.cyksj.common.EnvCommonService;
+import com.cyksj.common.exception.BusinessRuntimeException;
+import com.cyksj.common.snowflake.Sequence;
+import com.cyksj.common.util.Codec;
+import com.cyksj.common.util.Jsons;
+import com.cyksj.config.zfb.BaseZfbConfig;
+import com.cyksj.config.zfb.ZfbProductCode;
+import com.cyksj.mapper.channel.ShopConfigMapper;
+import com.cyksj.mapper.special.SpecialMemberEntitlementMapper;
+import com.cyksj.mapper.special.SpecialPayOrderMapper;
+import com.cyksj.mapper.special.SpecialPayProductMapper;
+import com.cyksj.model.dto.AliPayParams;
+import com.cyksj.model.entity.ShopConfig;
+import com.cyksj.model.entity.SpecialPayOrder;
+import com.cyksj.model.entity.SpecialPayProduct;
+import com.cyksj.model.entity.SpecialMemberEntitlement;
+import com.cyksj.model.request.SpecialPayCreateReq;
+import com.cyksj.model.request.OrderAttach;
+import com.cyksj.model.response.AliPayTradeStatus;
+import com.cyksj.model.response.SpecialPayCreateRep;
+import com.cyksj.service.pay.AliPayCommonService;
+import com.cyksj.service.special.SpecialPayService;
+import com.cyksj.service.sys.SysConfigService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class SpecialPayServiceImpl implements SpecialPayService {
+
+	private static final Sequence SEQUENCE = new Sequence(0);
+	private static final int ORDER_EXPIRE_MINUTES = 5;
+
+	private final SpecialPayProductMapper productMapper;
+	private final SpecialPayOrderMapper orderMapper;
+	private final SpecialMemberEntitlementMapper entitlementMapper;
+	private final ShopConfigMapper shopConfigMapper;
+	private final SysConfigService sysConfigService;
+	private final AliPayCommonService aliPayCommonService;
+	private final EnvCommonService envCommonService;
+
+	@Override
+	public List<SpecialPayProduct> getEnabledProducts() {
+		return productMapper.selectList(Wrappers.lambdaQuery(SpecialPayProduct.class)
+				.eq(SpecialPayProduct::getEnabled, true)
+				.orderByAsc(SpecialPayProduct::getSortBy, SpecialPayProduct::getId));
+	}
+
+	@Override
+	@Transactional(rollbackFor = Throwable.class)
+	public SpecialPayCreateRep createAlipayOrder(Long userId, SpecialPayCreateReq request) throws Exception {
+		SpecialPayProduct product = productMapper.selectOne(Wrappers.lambdaQuery(SpecialPayProduct.class)
+				.eq(SpecialPayProduct::getProductCode, request.getProductCode())
+				.eq(SpecialPayProduct::getEnabled, true)
+				.last("limit 1"));
+		if (product == null) {
+			throw BusinessRuntimeException.getInstance("支付套餐不存在或已下架");
+		}
+		if (product.getAmount() == null || product.getAmount().compareTo(BigDecimal.ZERO) <= 0
+				|| product.getDurationDays() == null || product.getDurationDays() <= 0) {
+			throw BusinessRuntimeException.getInstance("支付套餐配置异常");
+		}
+		String alipayProductCode = getAlipayProductCode(request.getPayType());
+		Date now = new Date();
+		SpecialPayOrder order = new SpecialPayOrder();
+		order.setOrderNo("SP" + SEQUENCE.nextId());
+		order.setUserId(userId);
+		order.setProductCode(product.getProductCode());
+		order.setSubject(product.getProductName());
+		order.setAmount(product.getAmount());
+		order.setDurationDays(product.getDurationDays());
+		order.setStatus(SpecialPayOrder.Status.CREATED);
+		order.setFulfillStatus(SpecialPayOrder.FulfillStatus.INIT);
+		order.setShopId(sysConfigService.getDefaultShopConfigByType("zfb"));
+		order.setExpireTime(DateUtil.offsetMinute(now, ORDER_EXPIRE_MINUTES));
+		order.setCreatedTime(now);
+		order.setUpdateTime(now);
+		orderMapper.insert(order);
+
+		String notifyUrl = envCommonService.getNotifyHost() + BaseZfbConfig.specialPayNotifyUrl;
+		AliPayParams payParams = aliPayCommonService.builderPayParams(
+				order.getId(), order.getOrderNo(), order.getShopId(), order.getAmount(),
+				order.getCreatedTime(), order.getSubject(), alipayProductCode,
+				request.getPayMode(), notifyUrl, request.getReturnUrl());
+		if (payParams == null || StrUtil.isBlank(payParams.getBody())) {
+			throw BusinessRuntimeException.getInstance("支付宝下单失败");
+		}
+
+		SpecialPayCreateRep response = new SpecialPayCreateRep();
+		response.setOrderNo(order.getOrderNo());
+		response.setStatus(order.getStatus());
+		response.setExpireTime(order.getExpireTime());
+		response.setPayBody(payParams.getBody());
+		return response;
+	}
+
+	@Override
+	public SpecialPayOrder getUserOrder(Long userId, String orderNo) {
+		SpecialPayOrder order = orderMapper.selectOne(Wrappers.lambdaQuery(SpecialPayOrder.class)
+				.eq(SpecialPayOrder::getOrderNo, orderNo)
+				.eq(SpecialPayOrder::getUserId, userId)
+				.last("limit 1"));
+		if (order == null) {
+			throw BusinessRuntimeException.getInstance("订单不存在");
+		}
+		return order;
+	}
+
+	@Override
+	public SpecialMemberEntitlement getMemberEntitlement(Long userId) {
+		return entitlementMapper.selectOne(Wrappers.lambdaQuery(SpecialMemberEntitlement.class)
+				.eq(SpecialMemberEntitlement::getUserId, userId)
+				.last("limit 1"));
+	}
+
+	@Override
+	@Transactional(rollbackFor = Throwable.class)
+	public void handleAlipayNotify(Map<String, String> params) throws Exception {
+		String appId = params.get("app_id");
+		ShopConfig shopConfig = StrUtil.isBlank(appId) ? null : shopConfigMapper.getByAppId(appId);
+		if (shopConfig == null || !AlipaySignature.rsaCheckV1(params, shopConfig.getPublicKey(),
+				BaseZfbConfig.charset, BaseZfbConfig.signType)) {
+			throw BusinessRuntimeException.getInstance("支付宝回调验签失败");
+		}
+
+		String orderNo = params.get("out_trade_no");
+		SpecialPayOrder order = orderMapper.selectByOrderNoForUpdate(orderNo);
+		if (order == null) {
+			throw BusinessRuntimeException.getInstance("特殊支付订单不存在");
+		}
+		if (!StrUtil.equals(order.getShopId(), appId)) {
+			throw BusinessRuntimeException.getInstance("支付宝回调商户不匹配");
+		}
+		String tradeStatus = params.get("trade_status");
+		if (!AliPayTradeStatus.TRADE_SUCCESS.getStatus().equals(tradeStatus)
+				&& !AliPayTradeStatus.TRADE_FINISHED.getStatus().equals(tradeStatus)) {
+			throw BusinessRuntimeException.getInstance("支付宝交易状态未成功");
+		}
+		verifyOrderIdentity(params, order);
+		verifyAmount(params.get("total_amount"), order.getAmount());
+
+		String tradeNo = params.get("trade_no");
+		if (StrUtil.isBlank(tradeNo)) {
+			throw BusinessRuntimeException.getInstance("支付宝交易号为空");
+		}
+		if (order.getStatus() == SpecialPayOrder.Status.PAID) {
+			if (!StrUtil.equals(order.getAlipayTradeNo(), tradeNo)) {
+				throw BusinessRuntimeException.getInstance("支付宝交易号不匹配");
+			}
+			return;
+		}
+		if (order.getStatus() != SpecialPayOrder.Status.CREATED) {
+			throw BusinessRuntimeException.getInstance("当前订单状态不可支付");
+		}
+
+		Date payTime = new Date();
+		if (StrUtil.isNotBlank(params.get("gmt_payment"))) {
+			payTime = DateUtil.parse(params.get("gmt_payment"), "yyyy-MM-dd HH:mm:ss");
+		}
+		String buyerId = Optional.ofNullable(params.get("buyer_id"))
+				.orElse(params.get("buyer_open_id"));
+		if (orderMapper.markPaid(order.getId(), tradeNo, buyerId, payTime) != 1) {
+			throw BusinessRuntimeException.getInstance("订单支付状态更新失败");
+		}
+
+		if (order.getDurationDays() == null || order.getDurationDays() <= 0) {
+			throw BusinessRuntimeException.getInstance("订单套餐配置异常");
+		}
+		entitlementMapper.extend(order.getUserId(), order.getDurationDays(), order.getId());
+		if (orderMapper.markFulfilled(order.getId()) != 1) {
+			throw BusinessRuntimeException.getInstance("会员权益发放失败");
+		}
+		log.info("特殊支付宝订单支付并履约成功, orderNo:{}, tradeNo:{}", orderNo, tradeNo);
+	}
+
+	private String getAlipayProductCode(Integer payType) {
+		if (Integer.valueOf(1).equals(payType)) {
+			return ZfbProductCode.MOBILE_WEB.getProductCode();
+		}
+		if (Integer.valueOf(2).equals(payType)) {
+			return ZfbProductCode.PC_WEB.getProductCode();
+		}
+		throw BusinessRuntimeException.getInstance("不支持的支付宝支付方式");
+	}
+
+	private void verifyOrderIdentity(Map<String, String> params, SpecialPayOrder order) {
+		try {
+			String body = Codec.DoBase64.custom().setData(params.get("body")).decodeAsText();
+			OrderAttach attach = Jsons.parseObject(body, OrderAttach.class);
+			if (attach == null || !order.getId().equals(attach.getO())
+					|| !StrUtil.equals(order.getOrderNo(), params.get("out_trade_no"))) {
+				throw BusinessRuntimeException.getInstance("支付宝回调订单不匹配");
+			}
+		} catch (BusinessRuntimeException e) {
+			throw e;
+		} catch (Exception e) {
+			throw BusinessRuntimeException.getInstance("支付宝回调订单参数异常");
+		}
+	}
+
+	private void verifyAmount(String totalAmount, BigDecimal expectedCents) {
+		try {
+			BigDecimal callbackCents = new BigDecimal(totalAmount)
+					.multiply(BigDecimal.valueOf(100)).setScale(0, RoundingMode.UNNECESSARY);
+			if (expectedCents == null || callbackCents.compareTo(expectedCents) != 0) {
+				throw BusinessRuntimeException.getInstance("支付宝回调金额不匹配");
+			}
+		} catch (BusinessRuntimeException e) {
+			throw e;
+		} catch (Exception e) {
+			throw BusinessRuntimeException.getInstance("支付宝回调金额异常");
+		}
+	}
+}

+ 72 - 0
netflix-web/src/main/java/com/cyksj/web/controller/special/SpecialPayController.java

@@ -0,0 +1,72 @@
+package com.cyksj.web.controller.special;
+
+import com.cyksj.dto.Result;
+import com.cyksj.enums.GatewayResponse;
+import com.cyksj.model.entity.SpecialPayOrder;
+import com.cyksj.model.entity.SpecialPayProduct;
+import com.cyksj.model.entity.SpecialMemberEntitlement;
+import com.cyksj.model.request.SpecialPayCreateReq;
+import com.cyksj.model.response.SpecialPayCreateRep;
+import com.cyksj.service.special.SpecialPayService;
+import com.cyksj.web.util.StpUserUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/applets/special/member")
+public class SpecialPayController {
+
+	private final SpecialPayService specialPayService;
+
+	@GetMapping("/products")
+	public Result<List<SpecialPayProduct>> products() {
+		return GatewayResponse.SUCCESS.newBuilder().toResult(specialPayService.getEnabledProducts());
+	}
+
+	@PostMapping("/orders/alipay")
+	public Result<SpecialPayCreateRep> createAlipayOrder(@RequestBody @Validated SpecialPayCreateReq request) throws Exception {
+		long userId = StpUserUtil.getLoginIdAsLong();
+		return GatewayResponse.SUCCESS.newBuilder()
+				.toResult(specialPayService.createAlipayOrder(userId, request));
+	}
+
+	@GetMapping("/orders/{orderNo}")
+	public Result<SpecialPayOrder> getOrder(@PathVariable String orderNo) {
+		long userId = StpUserUtil.getLoginIdAsLong();
+		return GatewayResponse.SUCCESS.newBuilder().toResult(specialPayService.getUserOrder(userId, orderNo));
+	}
+
+	@GetMapping("/entitlement")
+	public Result<SpecialMemberEntitlement> getEntitlement() {
+		long userId = StpUserUtil.getLoginIdAsLong();
+		return GatewayResponse.SUCCESS.newBuilder().toResult(specialPayService.getMemberEntitlement(userId));
+	}
+
+	@PostMapping("/alipay/notify")
+	public String alipayNotify(HttpServletRequest request) {
+		Map<String, String> params = convertRequestParamsToMap(request);
+		try {
+			specialPayService.handleAlipayNotify(params);
+			return "success";
+		} catch (Exception e) {
+			log.error("特殊支付宝订单回调处理失败, orderNo:{}", params.get("out_trade_no"), e);
+			return "fail";
+		}
+	}
+
+	private Map<String, String> convertRequestParamsToMap(HttpServletRequest request) {
+		Map<String, String> params = new HashMap<>();
+		request.getParameterMap().forEach((name, values) ->
+				params.put(name, values == null ? "" : String.join(",", values)));
+		return params;
+	}
+}