admin hai 4 semanas
pai
achega
08dd8f2088

+ 24 - 8
client-app/src/main/java/com/sapit/client/app/manage/controller/order/FeedbackManageController.java

@@ -16,6 +16,7 @@ import org.springframework.web.bind.annotation.RestController;
 
 import com.sapit.client.app.manage.service.order.RefundFailedService;
 import com.sapit.client.app.manage.service.order.RefundService;
+import com.sapit.client.app.user.controller.order.WxPayV2Service;
 import com.sapit.client.app.user.service.order.OrderApiService;
 import com.sapit.common.bean.system.WxAmountBean;
 import com.sapit.common.constant.PayType;
@@ -48,6 +49,9 @@ public class FeedbackManageController {
 	private PayChannelProperties payChannelProperties;
 	@Autowired
 	private OrderApiService orderApiService;
+
+	@Autowired
+	private WxPayV2Service wxPayV2Service;
 	/**
 	 * 方法名称:等待微信支付返回结果
 	 * 方法描述:
@@ -70,14 +74,26 @@ public class FeedbackManageController {
 	        br.close();
 	        
 	        System.out.println(sb.toString());
-	        log.info("支付结果:" + sb.toString());
-	
-	        GetWxOrderno wxOrderno = new GetWxOrderno();
-	        @SuppressWarnings("rawtypes")
-			Map map = wxOrderno.doXMLParse(sb.toString());
-	        
-	        String returnCode = (String) map.get("return_code");
-	        String resultCode = (String) map.get("result_code");
+        log.info("支付结果:" + sb.toString());
+
+        Map<String, String> map = wxPayV2Service.parseNotify(sb.toString());
+        if (map == null || map.isEmpty()) {
+        	log.warn("微信支付回调解析失败:" + sb.toString());
+        	String failMsg = "<xml><return_code>FAIL</return_code><return_msg>parse error</return_msg></xml>";
+        	response.getWriter().write(failMsg);
+        	return;
+        }
+
+        // 校验回调签名,防止伪造支付成功通知
+        if (!wxPayV2Service.verifyNotify(map)) {
+        	log.warn("微信支付回调验签失败,拒绝处理:" + sb.toString());
+        	String failMsg = "<xml><return_code>FAIL</return_code><return_msg>sign error</return_msg></xml>";
+        	response.getWriter().write(failMsg);
+        	return;
+        }
+
+        String returnCode = map.get("return_code");
+        String resultCode = map.get("result_code");
 	        // 判断报文的返回值
 	        String rtnmsg = "";
 	        if ("SUCCESS".equals(returnCode)) {

+ 18 - 9
client-app/src/main/java/com/sapit/client/app/user/controller/order/OrderApiCtl.java

@@ -85,7 +85,9 @@ public class OrderApiCtl {
 	 * 作    者:Bard
 	 * 日    期:2021-06-30
 	 */
-	@PostMapping(value = "/createOrder")
+
+	// 支付流程已重构至 OrderApiCtlV2(路径 orderApiV2),旧接口停用
+	// @PostMapping(value = "/createOrder")
     public ResultBean createOrder(Long memberId, Long sourceType, String addressInfo, String requireTime, String goodsstr, Long orderType, String timestamp, String sign, String rmk) {
 		ResultBean bean = new ResultBean();
 		// 参数基本验证
@@ -95,12 +97,14 @@ public class OrderApiCtl {
 			return bean;
 		}
 		// 签名验证
+		//对参数进行拼接然后md5加密,与sign对比,判断是否一致
 		if (!ApiUtil.validate(memberId + timestamp + SystemParam.secretKey, sign)) {
 			bean.getRtnBean().setRtnCode(ReturnCode.SIGN_WRONG);
 			bean.getRtnBean().setRtnMsg(ReturnCode.getName(ReturnCode.SIGN_WRONG));
 			return bean;
 		}
 		// 平台营业状态校验
+		//获取区域id,根据区域id查询系统设置,根据系统设置中的sysStatus判断平台营业状态,是否不为空且等于2
 		Long areaId = commonApiService.getSysAreaId();
 		SettingBean setting = commonApiService.getSysSetting(areaId);
 		if (setting.getSysStatus() != null && setting.getSysStatus().equals(2L)) {
@@ -108,10 +112,12 @@ public class OrderApiCtl {
 			bean.getRtnBean().setRtnMsg("本平台已暂停营业,请待正常营业后,再行下单");
 			return bean;
 		}
-		
+		//创建新的BuOrderDetail类集合
 		List<BuOrderDetail> detaillist = new ArrayList<>();
+		//解析商品字符串,获取商品信息存到list
 		JSONArray list = JSONArray.parseArray(goodsstr);
-		
+
+		//对前端传递的商品信息进行校验,判断商品是否存在,是否下架,是否冻结,是否休息,是否最小购买数量是否符合要求,最后添加到detaillist中
 		Long storeId = null;
 		for (int i = 0; i < list.size(); i++) {
 			JSONObject info = JSON.parseObject(list.get(i).toString());
@@ -174,9 +180,9 @@ public class OrderApiCtl {
 			
 			detaillist.add(detail);
 		}
-		
+		//解析地址信息,获取地址信息存到addressinfo
 		JSONObject addressinfo = JSON.parseObject(addressInfo);
-		// 生成订单
+		// 生成订单,new一个BuOrder类对象order,给order赋值
 		BuOrder order = new BuOrder();
 		order.setMemberId(memberId);
 		if (addressinfo.containsKey("province") && !StringUtils.equals(addressinfo.getString("province"), "null")) {
@@ -214,7 +220,8 @@ public class OrderApiCtl {
 		return orderApiService.createOrder(order, order, detaillist);
     }
 	
-	@PostMapping(value = "/wxPay")
+	// 支付流程已重构至 OrderApiCtlV2(路径 orderApiV2),旧接口停用
+	// @PostMapping(value = "/wxPay")
     public ResultBean wxPay(HttpServletRequest request, String orderCode, Long memberId, String openid, String timestamp, String sign,String rmk) {
 		ResultBean bean = new ResultBean();
 		// 参数基本验证
@@ -259,7 +266,7 @@ public class OrderApiCtl {
 						JSONObject obj = JSON.parseObject(info.get(i).toString());
 						orderMoney = orderMoney + obj.getLong("orderfee");
 					}
-
+					//调用微信支付接口,获取付款单ID
 					Channel channel = new Channel();
 					channel.setMerchantNo(payChannelProperties.getMerchantNo());
 					channel.setKey(payChannelProperties.getKey());
@@ -308,7 +315,8 @@ public class OrderApiCtl {
 		}
     }
 	
-	@PostMapping(value = "/noMoneyPay")
+	// 支付流程已重构至 OrderApiCtlV2(路径 orderApiV2),旧接口停用
+	// @PostMapping(value = "/noMoneyPay")
     public ResultBean noMoneyPay(HttpServletRequest request, String orderCode, Long memberId, String openid, String timestamp, String sign,String rmk) {
 		ResultBean bean = new ResultBean();
 		// 参数基本验证
@@ -353,7 +361,8 @@ public class OrderApiCtl {
 	 * 作    者:Vic
 	 * 日    期:2020-05-14
 	 */
-	@PostMapping(value = "/reWxPay")
+	// 支付流程已重构至 OrderApiCtlV2(路径 orderApiV2),旧接口停用
+	// @PostMapping(value = "/reWxPay")
     public ResultBean reWxPay(HttpServletRequest request, String groupOrderCode, Long memberId, Long wxFlag, String openid, String timestamp, String sign) {
 		ResultBean bean = new ResultBean();
 		// 参数基本验证

+ 367 - 0
client-app/src/main/java/com/sapit/client/app/user/controller/order/OrderApiCtlV2.java

@@ -0,0 +1,367 @@
+package com.sapit.client.app.user.controller.order;
+
+import java.math.BigDecimal;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.lang.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.sapit.client.app.user.service.order.OrderApiService;
+import com.sapit.client.app.user.service.system.CommonApiService;
+import com.sapit.common.bean.mobile.SettingBean;
+import com.sapit.common.bean.mobile.StoreGoodsModelBean;
+import com.sapit.common.bean.system.WxPayBean;
+import com.sapit.common.constant.ReturnCode;
+import com.sapit.common.entity.order.BuOrder;
+import com.sapit.common.entity.order.BuOrderDetail;
+import com.sapit.common.framwork.ResultBean;
+import com.sapit.common.search.order.BuOrderSearchBean;
+import com.sapit.common.util.ApiUtil;
+import com.sapit.common.util.SystemParam;
+
+/**
+ * 用户端 订单 Controller
+ * 
+ */
+@RestController
+@RequestMapping(value = "orderApiV2", produces = "application/json")
+public class OrderApiCtlV2 {
+
+	@Autowired
+	private OrderApiService orderApiService;
+
+	@Autowired
+	private CommonApiService commonApiService;
+
+	@Autowired
+	private RedisLockService redisLockService;
+
+	@Autowired
+	private WxPayV2Service wxPayV2Service;
+
+	/* 订单锁过期时间(秒) */
+	private static final long LOCK_EXPIRE = 10L;
+
+	/**
+	 * 下单处理:校验商品与店铺、计算金额、将订单快照写入 Redis,等待支付
+	 */
+	@PostMapping(value = "/createOrder")
+	public ResultBean createOrder(Long memberId, Long sourceType, String addressInfo, String requireTime,
+			String goodsstr, Long orderType, String timestamp, String sign, String rmk) {
+		ResultBean bean = new ResultBean();
+		// 参数基本验证
+		if (memberId == null || sign == null || timestamp == null) {
+			bean.getRtnBean().setRtnCode(ReturnCode.PARAM_WRONG);
+			bean.getRtnBean().setRtnMsg(ReturnCode.getName(ReturnCode.PARAM_WRONG));
+			return bean;
+		}
+		// 签名验证
+		if (!ApiUtil.validate(memberId + timestamp + SystemParam.secretKey, sign)) {
+			bean.getRtnBean().setRtnCode(ReturnCode.SIGN_WRONG);
+			bean.getRtnBean().setRtnMsg(ReturnCode.getName(ReturnCode.SIGN_WRONG));
+			return bean;
+		}
+		// 平台营业状态校验
+		Long areaId = commonApiService.getSysAreaId();
+		SettingBean setting = commonApiService.getSysSetting(areaId);
+		if (setting.getSysStatus() != null && setting.getSysStatus().equals(2L)) {
+			bean.getRtnBean().setRtnCode(ReturnCode.SYSTEMERR);
+			bean.getRtnBean().setRtnMsg("本平台已暂停营业,请待正常营业后,再行下单");
+			return bean;
+		}
+
+		List<BuOrderDetail> detaillist = new ArrayList<>();
+		JSONArray list = JSONArray.parseArray(goodsstr);
+
+		Long storeId = null;
+		for (int i = 0; i < list.size(); i++) {
+			JSONObject info = list.getJSONObject(i);
+			String modelIdStr = info.getString("modelId");
+
+			StoreGoodsModelBean model = orderApiService
+					.getStoreGoodsModelInfo(Long.valueOf(modelIdStr.replace("t", "")));
+
+			Long buyCount = info.getLong("modelCount");
+
+			if (buyCount <= 0) {
+				bean.getRtnBean().setRtnCode(ReturnCode.FAILED);
+				bean.getRtnBean().setRtnMsg("商品数量有误,请重新选择商品");
+				return bean;
+			}
+			if (model.getStoreStatus().equals(2L)) {
+				bean.getRtnBean().setRtnCode(ReturnCode.FAILED);
+				bean.getRtnBean().setRtnMsg("本店铺已冻结,不能下单");
+				return bean;
+			}
+			if (!model.getBusinessStatus().equals(1L)) {
+				bean.getRtnBean().setRtnCode(ReturnCode.FAILED);
+				bean.getRtnBean().setRtnMsg("本店铺已休息,不能下单");
+				return bean;
+			}
+			if (model.getGoodsStatus() == 0L) {
+				bean.getRtnBean().setRtnCode(ReturnCode.FAILED);
+				bean.getRtnBean().setRtnMsg(model.getGoodsName() + "已经下架,请选购其他商品");
+				return bean;
+			}
+			// 校验最小购买数量
+			if (buyCount < model.getMiniNum()) {
+				bean.getRtnBean().setRtnCode(ReturnCode.FAILED);
+				bean.getRtnBean().setRtnMsg(model.getGoodsName() + "小于当前商品的最小购买数量,请重新选择数量");
+				return bean;
+			}
+
+			BuOrderDetail detail = new BuOrderDetail();
+			detail.setGoodsId(model.getGoodsId());
+			detail.setGoodsName(model.getGoodsName());
+
+			if (modelIdStr.startsWith("t") && !model.getModelCostPrice().equals(model.getModelSalePrice())) {
+				detail.setModelId(modelIdStr);
+				detail.setPrice(model.getModelCostPrice());
+				detail.setSalesPrice(model.getModelSalePrice());
+			} else {
+				detail.setModelId(modelIdStr.replace("t", ""));
+				detail.setPrice(model.getModelCostPrice());
+				detail.setSalesPrice(model.getModelCostPrice());
+			}
+
+			detail.setModelName(model.getModelName());
+			detail.setPropertys(info.getString("paramsStr"));
+			detail.setBuyCount(buyCount);
+			detail.setLimitedNum(model.getLimitedNum());
+			detail.setPackingFee(model.getPackingFee());
+			storeId = model.getStoreId();
+
+			detaillist.add(detail);
+		}
+
+		JSONObject addressinfo = JSON.parseObject(addressInfo);
+		BuOrder order = new BuOrder();
+		order.setMemberId(memberId);
+		if (addressinfo.containsKey("province") && !StringUtils.equals(addressinfo.getString("province"), "null")) {
+			order.setProvince(addressinfo.getLong("province"));
+		}
+		if (addressinfo.containsKey("city") && !StringUtils.equals(addressinfo.getString("city"), "null")) {
+			order.setCity(addressinfo.getLong("city"));
+		}
+		if (addressinfo.containsKey("county") && !StringUtils.equals(addressinfo.getString("county"), "null")) {
+			order.setCounty(addressinfo.getLong("county"));
+		}
+		if (addressinfo.containsKey("roomNumber") && !StringUtils.equals(addressinfo.getString("roomNumber"), "null")) {
+			order.setRoomNumber(addressinfo.getString("roomNumber"));
+		}
+		if (addressinfo.containsKey("address") && !StringUtils.equals(addressinfo.getString("address"), "null")) {
+			order.setOrderAddress(addressinfo.getString("address").replace("+", "+").replace("\"", "“").replace("'", "")
+					.replace("{", "").replace("}", ""));
+		}
+		order.setOrderName(addressinfo.getString("addressName"));
+		order.setOrderTel(addressinfo.getString("addressTel"));
+		order.setOrderType(orderType);
+		order.setSourceType(sourceType);
+		order.setStoreId(storeId);
+		order.setRequireDate(Timestamp.valueOf(requireTime));
+
+		if (order.getCounty() != null) {
+			order.setAddressId(order.getCounty());
+		} else if (order.getCity() != null) {
+			order.setAddressId(order.getCity());
+		} else if (order.getProvince() != null) {
+			order.setAddressId(order.getProvince());
+		}
+
+		order.setCustomerMes(rmk);
+
+		return orderApiService.createOrder(order, order, detaillist);
+	}
+
+	/**
+	 * 微信支付:加锁 -> 判重 -> 取 Redis 订单 -> 统一下单 -> 落库 -> 返回支付参数
+	 */
+	@PostMapping(value = "/wxPay")
+	public ResultBean wxPay(HttpServletRequest request, String orderCode, Long memberId, String openid,
+			String timestamp, String sign, String rmk) {
+		ResultBean bean = new ResultBean();
+		// 参数基本验证
+		if (orderCode == null || memberId == null || sign == null || timestamp == null) {
+			bean.getRtnBean().setRtnCode(ReturnCode.PARAM_WRONG);
+			bean.getRtnBean().setRtnMsg(ReturnCode.getName(ReturnCode.PARAM_WRONG));
+			return bean;
+		}
+		// 签名验证
+		if (!ApiUtil.validate(memberId + timestamp + SystemParam.secretKey, sign)) {
+			bean.getRtnBean().setRtnCode(ReturnCode.SIGN_WRONG);
+			bean.getRtnBean().setRtnMsg(ReturnCode.getName(ReturnCode.SIGN_WRONG));
+			return bean;
+		}
+
+		// 分布式锁:同一 orderCode 仅允许一个请求进入
+		String lockKey = "order:lock" + orderCode;
+		if (!redisLockService.tryLock(lockKey, LOCK_EXPIRE)) {
+			bean.getRtnBean().setRtnCode(ReturnCode.FAILED);
+			bean.getRtnBean().setRtnMsg("订单处理中,请稍后重试");
+			return bean;
+		}
+		try {
+			if (orderApiService.isHaveOrder(orderCode)) {
+				bean.getRtnBean().setRtnCode(ReturnCode.FAILED);
+				bean.getRtnBean().setRtnMsg("订单已经存在");
+				return bean;
+			}
+
+			String orderInfo = orderApiService.getPayedOrderFromRedis(orderCode);
+			if (orderInfo == null || "".equals(orderInfo)) {
+				bean.getRtnBean().setRtnCode(ReturnCode.FAILED);
+				bean.getRtnBean().setRtnMsg("数据不存在");
+				return bean;
+			}
+
+			// 累加订单金额(分)
+			Long orderMoney = 0L;
+			JSONArray info = JSONArray.parseArray(orderInfo);
+			for (int i = 0; i < info.size(); i++) {
+				JSONObject obj = info.getJSONObject(i);
+				orderMoney = orderMoney + obj.getLong("orderfee");
+			}
+
+			// 统一下单获取 prepay_id
+			String prepayId = wxPayV2Service.prepay(orderCode, orderMoney, openid);
+			if (prepayId == null) {
+				bean.getRtnBean().setRtnCode(ReturnCode.SYSTEMERR);
+				bean.getRtnBean().setRtnMsg("下单错误,请稍后尝试!");
+				return bean;
+			}
+
+			// 通过 Redis 快照真正创建订单及明细
+			ResultBean makeOrderResult;
+			try {
+				makeOrderResult = orderApiService.makeOrder(orderInfo, prepayId, rmk);
+			} catch (RuntimeException e) {
+				bean.getRtnBean().setRtnCode(ReturnCode.SYSTEMERR);
+				bean.getRtnBean().setRtnMsg("订单创建失败,请重新下单!");
+				return bean;
+			}
+			if (makeOrderResult.getRtnBean().getRtnCode() != 0) {
+				return makeOrderResult;
+			}
+
+			// 生成小程序端支付数据
+			WxPayBean wxPayBean = wxPayV2Service.buildPayParams(prepayId);
+			bean.setObj(wxPayBean);
+			return bean;
+		} finally {
+			redisLockService.unlock(lockKey);
+		}
+	}
+
+	/**
+	 * 零元支付(免微信支付的订单落地)
+	 */
+	@PostMapping(value = "/noMoneyPay")
+	public ResultBean noMoneyPay(HttpServletRequest request, String orderCode, Long memberId, String openid,
+			String timestamp, String sign, String rmk) {
+		ResultBean bean = new ResultBean();
+		// 参数基本验证
+		if (orderCode == null || memberId == null || sign == null || timestamp == null) {
+			bean.getRtnBean().setRtnCode(ReturnCode.PARAM_WRONG);
+			bean.getRtnBean().setRtnMsg(ReturnCode.getName(ReturnCode.PARAM_WRONG));
+			return bean;
+		}
+		// 签名验证
+		if (!ApiUtil.validate(memberId + timestamp + SystemParam.secretKey, sign)) {
+			bean.getRtnBean().setRtnCode(ReturnCode.SIGN_WRONG);
+			bean.getRtnBean().setRtnMsg(ReturnCode.getName(ReturnCode.SIGN_WRONG));
+			return bean;
+		}
+
+		if (orderApiService.isHaveOrder(orderCode)) {
+			bean.getRtnBean().setRtnCode(ReturnCode.FAILED);
+			bean.getRtnBean().setRtnMsg("订单已经存在");
+			return bean;
+		}
+
+		String orderInfo = orderApiService.getPayedOrderFromRedis(orderCode);
+		if (orderInfo == null || "".equals(orderInfo)) {
+			bean.getRtnBean().setRtnCode(ReturnCode.FAILED);
+			bean.getRtnBean().setRtnMsg("数据不存在");
+			return bean;
+		}
+
+		try {
+			ResultBean makeOrderResult = orderApiService.noMoneyPay(orderInfo, rmk);
+			if (makeOrderResult.getRtnBean().getRtnCode() != 0) {
+				return makeOrderResult;
+			}
+			return bean;
+		} catch (RuntimeException e) {
+			bean.getRtnBean().setRtnCode(ReturnCode.SYSTEMERR);
+			bean.getRtnBean().setRtnMsg("订单创建失败,请重新下单!");
+			return bean;
+		}
+	}
+
+	/**
+	 * 再次微信支付
+	 */
+	@PostMapping(value = "/reWxPay")
+	public ResultBean reWxPay(HttpServletRequest request, String groupOrderCode, Long memberId, Long wxFlag,
+			String openid, String timestamp, String sign) {
+		ResultBean bean = new ResultBean();
+		// 参数基本验证
+		if (groupOrderCode == null || memberId == null || sign == null || timestamp == null) {
+			bean.getRtnBean().setRtnCode(ReturnCode.PARAM_WRONG);
+			bean.getRtnBean().setRtnMsg(ReturnCode.getName(ReturnCode.PARAM_WRONG));
+			return bean;
+		}
+		// 签名验证
+		if (!ApiUtil.validate(memberId + timestamp + SystemParam.secretKey, sign)) {
+			bean.getRtnBean().setRtnCode(ReturnCode.SIGN_WRONG);
+			bean.getRtnBean().setRtnMsg(ReturnCode.getName(ReturnCode.SIGN_WRONG));
+			return bean;
+		}
+
+		BuOrderSearchBean searchBean = new BuOrderSearchBean();
+		searchBean.setOrderCode(groupOrderCode);
+		List<BuOrder> orderList = orderApiService.getOrderListForUserAll(searchBean);
+
+		if (orderList.isEmpty()) {
+			bean.getRtnBean().setRtnCode(ReturnCode.FAILED);
+			bean.getRtnBean().setRtnMsg("订单不存在");
+			return bean;
+		}
+
+		if (orderList.get(0).getPrepayId() != null) {
+			// 已存在 prepay_id,直接重新生成支付参数
+			WxPayBean wxPayBean = wxPayV2Service.buildPayParams(orderList.get(0).getPrepayId());
+			bean.setObj(wxPayBean);
+			return bean;
+		}
+
+		// 累加订单金额并转成分
+		BigDecimal orderMoney = new BigDecimal(0);
+		for (int i = 0; i < orderList.size(); i++) {
+			orderMoney = orderMoney.add(new BigDecimal(orderList.get(i).getOrderFee()));
+		}
+		Long totalfee = orderMoney.multiply(new BigDecimal("100")).longValue();
+
+		String prepayId = wxPayV2Service.prepay(groupOrderCode, totalfee, openid);
+		if (prepayId == null) {
+			bean.getRtnBean().setRtnCode(ReturnCode.SYSTEMERR);
+			bean.getRtnBean().setRtnMsg("微信支付下单错误,请稍后尝试!");
+			return bean;
+		}
+		orderApiService.updateOrderPayedInfo(groupOrderCode, prepayId);
+
+		WxPayBean wxPayBean = wxPayV2Service.buildPayParams(prepayId);
+		bean.setObj(wxPayBean);
+		return bean;
+	}
+}

+ 46 - 0
client-app/src/main/java/com/sapit/client/app/user/controller/order/RedisLockService.java

@@ -0,0 +1,46 @@
+package com.sapit.client.app.user.controller.order;
+
+import java.util.concurrent.TimeUnit;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Service;
+
+/**
+ * 分布式锁服务(基于 Redis SET NX EX 原子操作)
+ * 避免并发场景下同一订单被重复处理
+ */
+@Service
+public class RedisLockService {
+
+	// 锁的值(占位)
+	private static final String LOCK_VALUE = "1";
+
+	@Autowired
+	private StringRedisTemplate redisTemplate;
+
+	/**
+	 * 尝试加锁(原子操作,SET key value NX EX expireSeconds)
+	 *
+	 * @param key           锁键
+	 * @param expireSeconds 锁过期时间(秒)
+	 * @return true=加锁成功,false=已被其他请求持有
+	 */
+	public boolean tryLock(String key, long expireSeconds) {
+		Boolean success = redisTemplate.opsForValue().setIfAbsent(key, LOCK_VALUE, expireSeconds, TimeUnit.SECONDS);
+		return success != null && success;
+	}
+
+	/**
+	 * 释放锁
+	 *
+	 * @param key 锁键
+	 */
+	public void unlock(String key) {
+		try {
+			redisTemplate.delete(key);
+		} catch (Exception e) {
+			// 释放失败不影响主流程,等待锁自动过期即可
+		}
+	}
+}

+ 170 - 0
client-app/src/main/java/com/sapit/client/app/user/controller/order/WxPayV2Service.java

@@ -0,0 +1,170 @@
+package com.sapit.client.app.user.controller.order;
+
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.sapit.common.bean.system.WxPayBean;
+import com.sapit.common.util.DateUtil;
+import com.sapit.common.util.GetWxOrderno;
+import com.sapit.common.util.PayChannelProperties;
+import com.sapit.common.util.RequestHandler;
+import com.sapit.common.util.SystemParam;
+import com.sapit.common.util.TenpayUtil;
+
+/**
+ * 微信支付 V2 服务
+ *
+ * <p>收拢「统一下单获取 prepay_id、拼接小程序支付参数、回调验签、XML 转义」等支付原子能力,
+ * 替代原先散落在 OrderApiCtl 中的 {@code getPrepayId}/{@code getSign} 手写逻辑。</p>
+ *
+ * <p>仅使用 V2 接口(unifiedorder + XML + MD5),不涉及 V3。</p>
+ */
+@Service
+public class WxPayV2Service {
+
+	@Autowired
+	private PayChannelProperties payChannelProperties;
+
+	/**
+	 * 调用微信统一支付下单接口,获取 prepay_id
+	 *
+	 * @param orderCode 商户订单号
+	 * @param totalFee  订单金额(单位:分)
+	 * @param openid    用户 openid
+	 * @return prepay_id,失败返回 null
+	 */
+	public String prepay(String orderCode, Long totalFee, String openid) {
+		String merchantNo = payChannelProperties.getMerchantNo();
+		String key = payChannelProperties.getKey();
+		String nonceStr = generateNonceStr();
+
+		SortedMap<String, String> packageParams = new TreeMap<>();
+		packageParams.put("appid", SystemParam.appidmic);
+		packageParams.put("mch_id", merchantNo);
+		packageParams.put("nonce_str", nonceStr);
+		packageParams.put("body", "美味佳肴");
+		packageParams.put("attach", "pay");
+		packageParams.put("out_trade_no", orderCode);
+		packageParams.put("total_fee", String.valueOf(totalFee));
+		packageParams.put("spbill_create_ip", SystemParam.spbillcreateip);
+		packageParams.put("notify_url", SystemParam.wxnotifyUrl);
+		packageParams.put("trade_type", "JSAPI");
+		packageParams.put("openid", openid);
+		packageParams.put("time_start", DateUtil.getStrToday("yyyyMMddHHmmss"));
+		packageParams.put("time_expire", DateUtil.getTimestampString(DateUtil.addSSToADate(DateUtil.getSqlToday(), 600L), "yyyyMMddHHmmss"));
+
+		String sign = createSign(packageParams, key);
+
+		StringBuilder xml = new StringBuilder("<xml>");
+		for (Map.Entry<String, String> entry : packageParams.entrySet()) {
+			xml.append("<").append(entry.getKey()).append(">")
+				.append(escapeXml(entry.getValue()))
+				.append("</").append(entry.getKey()).append(">");
+		}
+		xml.append("<sign>").append(sign).append("</sign></xml>");
+
+		try {
+			return new GetWxOrderno().getPayNo(SystemParam.CREATEORDERURL, xml.toString());
+		} catch (Exception e) {
+			e.printStackTrace();
+			return null;
+		}
+	}
+
+	/**
+	 * 组装小程序端吊起支付的参数(timeStamp/nonceStr/package/paySign)
+	 *
+	 * @param prepayId prepay_id
+	 * @return 小程序支付参数
+	 */
+	public WxPayBean buildPayParams(String prepayId) {
+		String nonceStr = generateNonceStr();
+
+		WxPayBean wxPayBean = new WxPayBean();
+		wxPayBean.setNonceStr(nonceStr);
+		wxPayBean.setPackages("prepay_id=" + prepayId);
+		wxPayBean.setTimeStamp(String.valueOf(TenpayUtil.getUnixTime(DateUtil.getDate1())));
+
+		SortedMap<String, String> packageParams = new TreeMap<>();
+		packageParams.put("appId", SystemParam.appidmic);
+		packageParams.put("timeStamp", wxPayBean.getTimeStamp());
+		packageParams.put("nonceStr", wxPayBean.getNonceStr());
+		packageParams.put("package", wxPayBean.getPackages());
+		packageParams.put("signType", "MD5");
+
+		wxPayBean.setPaySign(createSign(packageParams, payChannelProperties.getKey()));
+		return wxPayBean;
+	}
+
+	/**
+	 * 校验微信支付回调报文签名
+	 *
+	 * @param notifyMap 回调解析后的参数(已含 sign 字段)
+	 * @return true=签名一致,false=签名不一致(可能被伪造)
+	 */
+	public boolean verifyNotify(Map<String, String> notifyMap) {
+		if (notifyMap == null || notifyMap.isEmpty()) {
+			return false;
+		}
+		String receivedSign = notifyMap.get("sign");
+		if (receivedSign == null || receivedSign.isEmpty()) {
+			return false;
+		}
+		SortedMap<String, String> params = new TreeMap<>(notifyMap);
+		params.remove("sign");
+		String calcSign = createSign(params, payChannelProperties.getKey());
+		return receivedSign.equals(calcSign);
+	}
+
+	/**
+	 * 解析回调 XML,返回字段 Map(复用微信 SDK 的 XML 解析)
+	 *
+	 * @return 字段 Map,解析失败返回 null
+	 */
+	@SuppressWarnings("unchecked")
+	public Map<String, String> parseNotify(String xml) {
+		try {
+			return new GetWxOrderno().doXMLParse(xml);
+		} catch (Exception e) {
+			e.printStackTrace();
+			return null;
+		}
+	}
+
+	/**
+	 * 按微信 V2 规则生成 MD5 签名(参数名 a-z 排序,空值不参与,末尾追加 key)
+	 */
+	private String createSign(SortedMap<String, String> packageParams, String key) {
+		RequestHandler reqHandler = new RequestHandler();
+		reqHandler.init(key);
+		return reqHandler.createSign(packageParams);
+	}
+
+	/**
+	 * 生成随机字符串(8位时间 + 4位随机数)
+	 */
+	private String generateNonceStr() {
+		String currTime = TenpayUtil.getCurrTime();
+		String strTime = currTime.substring(8);
+		String strRandom = TenpayUtil.buildRandom(4) + "";
+		return strTime + strRandom;
+	}
+
+	/**
+	 * XML 特殊字符转义,防止 openid/body 等字段含特殊字符导致下单失败或注入
+	 */
+	private String escapeXml(String value) {
+		if (value == null) {
+			return "";
+		}
+		return value.replace("&", "&amp;")
+			.replace("<", "&lt;")
+			.replace(">", "&gt;")
+			.replace("\"", "&quot;")
+			.replace("'", "&apos;");
+	}
+}

+ 12 - 14
client-app/src/main/java/com/sapit/client/app/user/service/order/OrderApiService.java

@@ -5,7 +5,9 @@ import java.security.SecureRandom;
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Random;
 
 import com.sapit.client.app.persistence.order.*;
@@ -142,14 +144,12 @@ public class OrderApiService {
 	
 	@SuppressWarnings("unchecked")
 	public boolean isHaveOrder(String orderCode) {
-		StringBuilder hql = new StringBuilder();
-		
-		hql.append("SELECT ORDER_ID FROM bu_order where ORDER_CODE = '" + orderCode + "'");
-		List<Object> orderList = orderDao.searchBySql(hql.toString());
-		if (CollectionUtils.isNotEmpty(orderList)) {
-			return true;
-		}
-		return false;
+		// 参数化查询,防止 SQL 注入
+		String sql = "SELECT ORDER_ID FROM bu_order WHERE ORDER_CODE = :orderCode";
+		Map<String, Object> params = new HashMap<>();
+		params.put("orderCode", orderCode);
+		List<Object> orderList = orderDao.searchBySql(null, sql, params);
+		return CollectionUtils.isNotEmpty(orderList);
 	}
 	
 	public String getPayedOrderFromRedis(String orderCode) {
@@ -585,9 +585,8 @@ public class OrderApiService {
 
         } catch (Exception e) {
             e.printStackTrace();
-            bean.getRtnBean().setRtnCode(ReturnCode.FAILED);
-            bean.getRtnBean().setRtnMsg("订单创建失败,请重新下单!");
-            return bean;
+            // 落库/解析异常向上抛出,触发 @Transactional 事务回滚,避免订单落库不一致
+            throw new RuntimeException("订单创建失败,请重新下单!", e);
         }
         bean.setObj(orderMoney);
 		
@@ -725,9 +724,8 @@ public class OrderApiService {
 
         } catch (Exception e) {
             e.printStackTrace();
-            bean.getRtnBean().setRtnCode(ReturnCode.FAILED);
-            bean.getRtnBean().setRtnMsg("订单创建失败,请重新下单!");
-            return bean;
+            // 落库/解析异常向上抛出,触发 @Transactional 事务回滚,避免订单落库不一致
+            throw new RuntimeException("订单创建失败,请重新下单!", e);
         }
         bean.setObj(orderMoney);