@@ -0,0 +1,34 @@ | |||
package cn.com.taiji.core.model.comm.protocol.ats.weiXin; | |||
import cn.com.taiji.core.model.comm.protocol.AbstractSignTypeRequest; | |||
import lombok.Getter; | |||
import lombok.Setter; | |||
import javax.validation.constraints.NotBlank; | |||
/** | |||
* @Author:ChenChao | |||
* @Date:2025/7/11 21:23 | |||
* @Filename:AtsWxDeductResultV2Request | |||
* @description: | |||
*/ | |||
@Getter | |||
@Setter | |||
public class AtsWxDeductResultV2Request extends AbstractSignTypeRequest<AtsWxDeductResultV2Response> { | |||
protected AtsWxDeductResultV2Request() { | |||
super(WeiXinServiceCmd.WEIXINDEDUCTRESULTV2); | |||
} | |||
//=========================配置表获取========================== | |||
@NotBlank(message = "appid不能为空") | |||
private String appId; | |||
@NotBlank(message = "商户号不能为空") | |||
private String mchId; | |||
@NotBlank(message = "商户密钥不能为空") | |||
private String key; | |||
//========================配置表获取========================== | |||
@NotBlank(message = "外部订单号不能为空-自定义") | |||
private String outTradeNo; | |||
} |
@@ -0,0 +1,58 @@ | |||
package cn.com.taiji.core.model.comm.protocol.ats.weiXin; | |||
import cn.com.taiji.core.model.comm.protocol.AbstractSignTypeResponse; | |||
import lombok.Getter; | |||
import lombok.Setter; | |||
/** | |||
* @Author:ChenChao | |||
* @Date:2025/7/11 21:23 | |||
* @Filename:AtsWxDeductResultV2Response | |||
* @description: | |||
*/ | |||
@Getter | |||
@Setter | |||
public class AtsWxDeductResultV2Response extends AbstractSignTypeResponse { | |||
/** | |||
* 微信返回信息(未处理的) | |||
*/ | |||
private String result; | |||
private String appId; | |||
private String mchId; | |||
private String resultCode;//业务结果 SUCCESS/FAIL | |||
private String errCode;//错误代码 | |||
private String errCodeDes;//错误代码描述 | |||
private String deviceInfo;//微信支付分配的终端设备号 | |||
private String openId;//用户在商户appid下的唯一标识 | |||
private String isSubscribe;//是否关注公众账号 Y-关注,N-未关注 | |||
private String tradeType;//交易类型PAP-微信委托代扣支付 | |||
/** | |||
* SUCCESS—支付成功 | |||
* ACCEPT—已接收,等待扣款 | |||
* PAY_FAIL--支付失败(其他原因,如银行返回失败) | |||
*/ | |||
private String tradeState;//交易状态 | |||
private String tradeStateDesc;//交易状态描述 | |||
private Long totalFee;//总金额 单位分 | |||
private String transactionId;//微信支付订单号 | |||
private String outTradeNo;//商户订单号(自定义) | |||
private String attach;//附加数据 | |||
private String timeEnd;//支付完成时间 格式为yyyyMMddHHmmss | |||
} |
@@ -0,0 +1,69 @@ | |||
package cn.com.taiji.core.model.comm.protocol.ats.weiXin; | |||
import cn.com.taiji.core.model.comm.protocol.AbstractSignTypeRequest; | |||
import lombok.Getter; | |||
import lombok.Setter; | |||
import javax.validation.Valid; | |||
import javax.validation.constraints.Min; | |||
import javax.validation.constraints.NotBlank; | |||
import javax.validation.constraints.NotNull; | |||
/** | |||
* @Author:ChenChao | |||
* @Date:2025/7/11 16:07 | |||
* @Filename:AtsWxDeductV2Request | |||
* @description: | |||
*/ | |||
@Getter | |||
@Setter | |||
public class AtsWxDeductV2Request extends AbstractSignTypeRequest<AtsWxDeductV2Response> { | |||
public AtsWxDeductV2Request() { | |||
super(WeiXinServiceCmd.WEIXINDEDUCTV2); | |||
} | |||
//=========================配置表获取========================== | |||
@NotBlank(message = "appid不能为空") | |||
private String appId; | |||
@NotBlank(message = "子商户appid不能为空") | |||
private String subAppId; | |||
@NotBlank(message = "商户号不能为空") | |||
private String mchId; | |||
@NotBlank(message = "子商户号不能为空") | |||
private String subMchId; | |||
@NotBlank(message = "商户密钥不能为空") | |||
private String key; | |||
//========================配置表获取========================== | |||
private String detail;//商品详情 | |||
private String goodsTag;//商品标记 | |||
private String attach;//附加数据 自定义参数 | |||
@NotBlank(message = "外部订单号不能为空-自定义") | |||
private String outTradeNo; | |||
@NotBlank(message = "商品描述不能为空") | |||
private String body; | |||
@NotNull(message = "扣款金额不能为空") | |||
@Min(value = 1, message = "扣款金额不能小于1分") | |||
private Integer totalFee; | |||
@NotBlank(message = "终端IP不能为空") | |||
private String spbillCreateIp;//调用微信支付API的机器IP | |||
@NotBlank(message = "回调地址不能为空") | |||
private String notifyUrl; | |||
/** | |||
* 委托代扣的交易场景值,目前支持 : | |||
* 1.PARKING:车场停车场景 | |||
* 2.PARKING SPACE;车位停车场景 | |||
* 3.GAS 加油场景 | |||
* 4.HIGHWAY 高速场景 | |||
* 5.BRIDGE 路桥场景 | |||
*/ | |||
@NotBlank(message = "交易场景") | |||
private String tradeScene; | |||
@NotNull(message = "场景信息不能为空") | |||
@Valid | |||
private SceneInfo sceneInfo; | |||
} |
@@ -0,0 +1,39 @@ | |||
package cn.com.taiji.core.model.comm.protocol.ats.weiXin; | |||
import cn.com.taiji.core.model.comm.protocol.AbstractSignTypeResponse; | |||
import lombok.Getter; | |||
import lombok.Setter; | |||
/** | |||
* @Author:ChenChao | |||
* @Date:2025/7/11 16:07 | |||
* @Filename:AtsWxDeductV2Response | |||
* @description: | |||
*/ | |||
@Getter | |||
@Setter | |||
public class AtsWxDeductV2Response extends AbstractSignTypeResponse { | |||
/** | |||
* 微信返回信息(未处理的) | |||
*/ | |||
private String result; | |||
private String appId;//公众账号id | |||
private String mchId;//商户号 | |||
private String deviceInfo;//设备号 | |||
/** | |||
* 错误代码 | |||
* 扣款接口请求成功,返回success仅代表扣款申请受理成功,不代表扣款成功。扣款是否成功以支付通知的结果为准。 | |||
*/ | |||
private String resultCode;//业务结果 SUCCESS/FAIL | |||
private String errCode;//错误代码 | |||
private String errCodeDes;//错误代码描述 | |||
} |
@@ -0,0 +1,53 @@ | |||
package cn.com.taiji.core.model.comm.protocol.ats.weiXin; | |||
import com.fasterxml.jackson.annotation.JsonProperty; | |||
import lombok.Data; | |||
import javax.validation.constraints.NotBlank; | |||
/** | |||
* @Author:ChenChao | |||
* @Date:2025/7/11 17:02 | |||
* @Filename:SceneInfo | |||
* @description: v2扣款订单场景信息(目前只支持告诉场景) | |||
*/ | |||
@Data | |||
public class SceneInfo { | |||
@NotBlank(message = "入场时间不能为空") | |||
@JsonProperty(value = "start_time") | |||
private String startTime; | |||
@NotBlank(message = "出场时间不能为空") | |||
@JsonProperty(value = "end_time") | |||
private String endTime; | |||
@NotBlank(message = "车牌号不能为空") | |||
@JsonProperty(value = "plate_number") | |||
private String plateNumber; | |||
/** | |||
* 车辆的类型,可选值:客车、货车 | |||
**/ | |||
@NotBlank(message = "车辆类型不能为空") | |||
@JsonProperty(value = "car_type") | |||
private String carType; | |||
@NotBlank(message = "入口站名称不能为空") | |||
@JsonProperty(value = "entrance_name") | |||
private String entranceName; | |||
@NotBlank(message = "出口站名称不能为空") | |||
@JsonProperty(value = "exit_name") | |||
private String exitName; | |||
/** | |||
* 核载人数 | |||
**/ | |||
@JsonProperty(value = "carrying_capacity") | |||
private String carryingCapacity; | |||
/** | |||
* 核载区间 | |||
**/ | |||
@JsonProperty(value = "carrying_range") | |||
private String carryingRange; | |||
/** | |||
* 通道类型, 目前可选:ETC、MTC | |||
**/ | |||
@NotBlank(message = "通道类型不能为空") | |||
@JsonProperty(value = "channel_type") | |||
private String channelType; | |||
} |
@@ -18,6 +18,8 @@ public enum WeiXinServiceCmd implements SignServiceCommand { | |||
VEHICLESIGNV3("微信V3获取预开通用户ETC扣费签约参数","vehicleSignV3", AtsVehicleSignV3Request.class), | |||
VEHICLESIGNRESULTV3("微信V3ETC签约状态查询","vehicleSignResultV3", AtsVehicleSignV3ResultRequest.class), | |||
GETWECHATOPENDI("微信小程序登录-获取用户openID","getWeChatOpenId", AtsGetWeChatOpenIdRequest.class), | |||
WEIXINDEDUCTV2("微信V2扣款","weiXinDeDuctV2", AtsWxDeductV2Request.class), | |||
WEIXINDEDUCTRESULTV2("微信V2扣款结果查询","weiXinDeDuctResultV2", AtsWxDeductResultV2Request.class), | |||
MPSENDMESSAGE("微信公众号发送订阅信息","mpSendMessage", WxMpSendMessageRequest.class), | |||
MPMESSAGELIST("微信公众号获取模板列表","mpMessageList", WxMpMessageTemplateListRequest.class), | |||
MPISSUBSCRIBED("微信公众号是否订阅","isSubscribed", WxMpIsSubscribedRequest.class), |
@@ -54,6 +54,10 @@ public class WinXinServiceHandler extends AbstractAtsServiceHandler<WeiXinServic | |||
@Autowired | |||
private VehicleSignV3ResultManager vehicleSignV3ResultManager; | |||
@Autowired | |||
private WeixinDeductV2Manager weiXinDeductV2Manager; | |||
@Autowired | |||
private WeixinDeductResultV2Manager weiXinDeductResultV2Manager; | |||
@Autowired | |||
private WxMessageManager wxMessageManager; | |||
@@ -86,6 +90,10 @@ public class WinXinServiceHandler extends AbstractAtsServiceHandler<WeiXinServic | |||
return vehicleSignV3ResultManager.serviceHandle((AtsVehicleSignV3ResultRequest) request); | |||
case GETWECHATOPENDI: | |||
return getWeChatOpenIdManager.serviceHandle((AtsGetWeChatOpenIdRequest) request); | |||
case WEIXINDEDUCTV2: | |||
return weiXinDeductV2Manager.serviceHandle((AtsWxDeductV2Request) request); | |||
case WEIXINDEDUCTRESULTV2: | |||
return weiXinDeductResultV2Manager.serviceHandle((AtsWxDeductResultV2Request) request); | |||
case MPISSUBSCRIBED: | |||
return wxMessageManager.isSubscribed((WxMpIsSubscribedRequest) request); | |||
case MPSENDMESSAGE: |
@@ -0,0 +1,87 @@ | |||
package cn.com.taiji.ats.manager.weixin; | |||
import cn.com.taiji.ats.tools.WxVehicleUtil; | |||
import cn.com.taiji.common.manager.net.http.ServiceHandleException; | |||
import cn.com.taiji.core.model.comm.protocol.ats.weiXin.AtsWxDeductResultV2Request; | |||
import cn.com.taiji.core.model.comm.protocol.ats.weiXin.AtsWxDeductResultV2Response; | |||
import cn.com.taiji.core.model.comm.protocol.valid.GlyServiceError; | |||
import cn.hutool.core.util.RandomUtil; | |||
import cn.hutool.http.HttpRequest; | |||
import com.google.common.collect.Maps; | |||
import org.springframework.stereotype.Service; | |||
import java.util.Map; | |||
/** | |||
* @Author:ChenChao | |||
* @Date:2025/7/11 21:29 | |||
* @Filename:WeixinDeductResultV2Manager | |||
* @description: 微信V2扣款结果查询 | |||
*/ | |||
@Service | |||
public class WeixinDeductResultV2Manager { | |||
public static final String baseUrl = "https://api.mch.weixin.qq.com/transit/queryorder"; | |||
public AtsWxDeductResultV2Response serviceHandle(AtsWxDeductResultV2Request req) throws ServiceHandleException { | |||
String paramXml = null; | |||
try { | |||
paramXml = WxVehicleUtil.generateSignedXml(getParam(req), req.getKey(), "HMACSHA256"); | |||
} catch (Exception e) { | |||
throw GlyServiceError.BUSINESS_VALIDATE_ERR.toHandleException("请款结果查询签名失败"); | |||
} | |||
String responseXml = HttpRequest.post(baseUrl) | |||
.setConnectionTimeout(10000) | |||
.setReadTimeout(10000) | |||
.body(paramXml) | |||
.execute() | |||
.body(); | |||
return getAtsWxDeductResultV2Response(req, responseXml); | |||
} | |||
private static AtsWxDeductResultV2Response getAtsWxDeductResultV2Response(AtsWxDeductResultV2Request req, String responseXml) throws ServiceHandleException { | |||
//返回值验签 | |||
Map<String, String> responseMap = WxVehicleUtil.xmlToMap(responseXml); | |||
if ("FAIL".equals(responseMap.get("return_code"))) { | |||
throw GlyServiceError.BUSINESS_VALIDATE_ERR.toHandleException("微信返回异常:"+responseMap.get("return_msg")); | |||
} | |||
String localSign = null; | |||
try { | |||
localSign = WxVehicleUtil.generateSignature(responseMap, req.getKey(), "HMACSHA256"); | |||
} catch (Exception e) { | |||
throw GlyServiceError.BUSINESS_VALIDATE_ERR.toHandleException("微信返回生成签名异常"); | |||
} | |||
if (!localSign.equals(responseMap.get("sign"))) { | |||
throw GlyServiceError.BUSINESS_VALIDATE_ERR.toHandleException("微信返回验签失败"); | |||
} | |||
AtsWxDeductResultV2Response response = new AtsWxDeductResultV2Response(); | |||
response.setResult(responseXml); | |||
response.setAppId(responseMap.get("appid")); | |||
response.setMchId(responseMap.get("mch_id")); | |||
response.setResultCode(responseMap.get("result_code")); | |||
response.setErrCode(responseMap.get("err_code")); | |||
response.setErrCodeDes(responseMap.get("err_code_des")); | |||
response.setDeviceInfo(responseMap.get("device_info")); | |||
response.setOpenId(responseMap.get("openid")); | |||
response.setIsSubscribe(responseMap.get("is_subscribe")); | |||
response.setTradeType(responseMap.get("trade_type")); | |||
response.setTradeState(responseMap.get("trade_state")); | |||
response.setTradeStateDesc(responseMap.get("trade_state_desc")); | |||
response.setTotalFee(Long.valueOf(responseMap.get("total_fee"))); | |||
response.setTransactionId(responseMap.get("transaction_id")); | |||
response.setOutTradeNo(responseMap.get("out_trade_no")); | |||
response.setAttach(responseMap.get("attach")); | |||
response.setTimeEnd(responseMap.get("time_end")); | |||
return response; | |||
} | |||
private Map<String, String> getParam(AtsWxDeductResultV2Request req){ | |||
Map<String, String> param = Maps.newHashMap(); | |||
param.put("appid", req.getAppId()); | |||
param.put("mch_id", req.getMchId()); | |||
param.put("out_trade_no", req.getOutTradeNo()); | |||
param.put("nonce_str", RandomUtil.randomStringUpper(31)); | |||
param.put("sign_type", "HMAC-SHA256"); | |||
return param; | |||
} | |||
} |
@@ -0,0 +1,97 @@ | |||
package cn.com.taiji.ats.manager.weixin; | |||
import cn.com.taiji.ats.model.wx.PayApplyHighwayVo; | |||
import cn.com.taiji.ats.tools.WxVehicleUtil; | |||
import cn.com.taiji.common.manager.net.http.ServiceHandleException; | |||
import cn.com.taiji.common.pub.json.JsonTools; | |||
import cn.com.taiji.core.model.comm.protocol.ats.weiXin.AtsWxDeductV2Request; | |||
import cn.com.taiji.core.model.comm.protocol.ats.weiXin.AtsWxDeductV2Response; | |||
import cn.com.taiji.core.model.comm.protocol.valid.GlyServiceError; | |||
import cn.hutool.core.util.RandomUtil; | |||
import cn.hutool.http.HttpRequest; | |||
import com.google.common.collect.Maps; | |||
import org.springframework.stereotype.Service; | |||
import java.io.IOException; | |||
import java.util.Map; | |||
/** | |||
* @Author:ChenChao | |||
* @Date:2025/7/11 16:13 | |||
* @Filename:WeixinDeductV2Manager | |||
* @description: 微信V2代扣请款 | |||
*/ | |||
@Service | |||
public class WeixinDeductV2Manager { | |||
private static final String baseUrl = "https://api.mch.weixin.qq.com/vehicle/pay/payapply"; | |||
public AtsWxDeductV2Response serviceHandle(AtsWxDeductV2Request req) throws ServiceHandleException { | |||
String paramXml = ""; | |||
try { | |||
//封装请求参数并签名 | |||
paramXml = WxVehicleUtil.generateSignedXml(getParam(req), req.getKey(), "HMACSHA256"); | |||
} catch (Exception e) { | |||
throw GlyServiceError.BUSINESS_VALIDATE_ERR.toHandleException("请款签名失败"); | |||
} | |||
String responseXml = HttpRequest.post(baseUrl) | |||
.setConnectionTimeout(10000) | |||
.setReadTimeout(10000) | |||
.body(paramXml) | |||
.execute() | |||
.body(); | |||
return getAtsWxDeductV2Response(req, responseXml); | |||
} | |||
//处理响应 | |||
private static AtsWxDeductV2Response getAtsWxDeductV2Response(AtsWxDeductV2Request req, String responseXml) throws ServiceHandleException { | |||
//返回值验签 | |||
Map<String, String> responseMap = WxVehicleUtil.xmlToMap(responseXml); | |||
if ("FAIL".equals(responseMap.get("return_code"))) { | |||
throw GlyServiceError.BUSINESS_VALIDATE_ERR.toHandleException("微信返回异常:"+responseMap.get("return_msg")); | |||
} | |||
String localSign = null; | |||
try { | |||
localSign = WxVehicleUtil.generateSignature(responseMap, req.getKey(), "HMACSHA256"); | |||
} catch (Exception e) { | |||
throw GlyServiceError.BUSINESS_VALIDATE_ERR.toHandleException("微信返回生成签名异常"); | |||
} | |||
if (!localSign.equals(responseMap.get("sign"))) { | |||
throw GlyServiceError.BUSINESS_VALIDATE_ERR.toHandleException("微信返回验签失败"); | |||
} | |||
AtsWxDeductV2Response response = new AtsWxDeductV2Response(); | |||
response.setResult(responseXml); | |||
response.setAppId(responseMap.get("appid")); | |||
response.setMchId(responseMap.get("mch_id")); | |||
response.setDeviceInfo(responseMap.get("device_info")); | |||
response.setResultCode(responseMap.get("result_code")); | |||
response.setErrCode(responseMap.get("err_code")); | |||
response.setErrCodeDes(responseMap.get("err_code_des")); | |||
return response; | |||
} | |||
private Map<String, String> getParam(AtsWxDeductV2Request req) throws IOException { | |||
Map<String, String> param = Maps.newHashMap(); | |||
param.put("appid", req.getAppId()); | |||
param.put("sub_appid", req.getSubAppId()); | |||
param.put("mch_id", req.getMchId()); | |||
param.put("sub_mch_id", req.getSubMchId()); | |||
param.put("nonce_str", RandomUtil.randomStringUpper(31)); | |||
param.put("sign_type", "HMAC-SHA256"); | |||
param.put("body", req.getBody()); | |||
param.put("detail", req.getDetail()); | |||
param.put("attach", req.getAttach()); | |||
param.put("out_trade_no", req.getOutTradeNo()); | |||
param.put("total_fee", String.valueOf(req.getTotalFee())); | |||
param.put("fee_type", "CNY"); | |||
param.put("spbill_create_ip", req.getSpbillCreateIp()); | |||
param.put("goods_tag", req.getGoodsTag()); | |||
param.put("notify_url", req.getNotifyUrl()); | |||
param.put("trade_type", "PAP"); | |||
param.put("version", "2.0"); | |||
param.put("trade_scene", req.getTradeScene()); | |||
param.put("scene_info", JsonTools.toJsonStr(new PayApplyHighwayVo(req.getSceneInfo()))); | |||
return param; | |||
} | |||
} |
@@ -1,7 +1,8 @@ | |||
package cn.com.taiji.ats.model.anXin; | |||
import cn.com.taiji.common.model.BaseModel; | |||
import lombok.Data; | |||
import lombok.Getter; | |||
import lombok.Setter; | |||
import javax.validation.constraints.NotBlank; | |||
@@ -10,7 +11,8 @@ import javax.validation.constraints.NotBlank; | |||
* @ClassName InterfaceRequest | |||
* @Description 公共请求参数实体 | |||
*/ | |||
@Data | |||
@Setter | |||
@Getter | |||
public class InterfaceRequest extends BaseModel { | |||
@NotBlank(message = "业务参数数据不能为空") |
@@ -1,13 +1,15 @@ | |||
package cn.com.taiji.ats.model.anXin; | |||
import cn.com.taiji.common.model.BaseModel; | |||
import lombok.Data; | |||
import lombok.Getter; | |||
import lombok.Setter; | |||
/** | |||
* @ClassName InterfaceRequest | |||
* @Description 公共响应参数实体 | |||
*/ | |||
@Data | |||
@Getter | |||
@Setter | |||
public class InterfaceResponse extends BaseModel { | |||
private String bizContent; |
@@ -0,0 +1,33 @@ | |||
package cn.com.taiji.ats.model.wx; | |||
import cn.com.taiji.common.model.BaseModel; | |||
import cn.com.taiji.core.model.comm.protocol.ats.weiXin.SceneInfo; | |||
import com.fasterxml.jackson.annotation.JsonProperty; | |||
import lombok.Getter; | |||
import lombok.Setter; | |||
import java.io.Serializable; | |||
import java.util.Map; | |||
/*** | |||
* <p> | |||
* 高速场景参数工具类 | |||
* </p> | |||
* @author hou yi | |||
* @date 2022/8/23 18:14 | |||
**/ | |||
@Getter | |||
@Setter | |||
public class PayApplyHighwayVo extends BaseModel { | |||
@JsonProperty(value = "scene_info") | |||
private SceneInfo sceneInfo; | |||
public PayApplyHighwayVo() { | |||
} | |||
public PayApplyHighwayVo(SceneInfo sceneInfo) { | |||
this.sceneInfo = sceneInfo; | |||
} | |||
} |