diff --git a/src/main/java/io/v/nutz/base/service/BaseService.java b/src/main/java/io/v/nutz/base/service/BaseService.java index 0dc5e47..5053205 100644 --- a/src/main/java/io/v/nutz/base/service/BaseService.java +++ b/src/main/java/io/v/nutz/base/service/BaseService.java @@ -911,4 +911,13 @@ public interface BaseService { * @return */ NutMap data(int length, int start, int draw, Cnd cnd, String linkName); + + /** + * 部分更新表 + * + * @param T + * @return + */ + int updateById(T data); + } diff --git a/src/main/java/io/v/nutz/base/service/impl/BaseServiceImpl.java b/src/main/java/io/v/nutz/base/service/impl/BaseServiceImpl.java index 8c8fc32..1ec210c 100644 --- a/src/main/java/io/v/nutz/base/service/impl/BaseServiceImpl.java +++ b/src/main/java/io/v/nutz/base/service/impl/BaseServiceImpl.java @@ -1,5 +1,7 @@ package io.v.nutz.base.service.impl; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; import io.v.nutz.base.page.OffsetPager; import io.v.nutz.base.page.Pagination; import io.v.nutz.base.page.datatable.DataTableColumn; @@ -8,6 +10,10 @@ import io.v.nutz.base.service.BaseService; import org.nutz.dao.*; import org.nutz.dao.entity.Entity; import org.nutz.dao.entity.Record; +import org.nutz.dao.entity.annotation.Column; +import org.nutz.dao.entity.annotation.Id; +import org.nutz.dao.entity.annotation.Prev; +import org.nutz.dao.entity.annotation.Table; import org.nutz.dao.pager.Pager; import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.SqlCallback; @@ -17,13 +23,11 @@ import org.nutz.lang.Strings; import org.nutz.lang.util.NutMap; import org.nutz.service.EntityService; +import java.lang.reflect.Field; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; /** * Created by wizzer on 2016/12/22. @@ -1370,4 +1374,88 @@ public class BaseServiceImpl extends EntityService implements BaseService< re.put("recordsTotal", length); return re; } + + public int updateById(T data) { + try { + if (data == null) { + throw new RuntimeException("data is null"); + } + Class classes = data.getClass(); + Table annotation = (Table) classes.getAnnotation(Table.class); + if (annotation == null) { + throw new RuntimeException("table annotation is null"); + } + String tableName = annotation.value(); + if (StrUtil.isBlank(tableName)) { + throw new RuntimeException("table name is null"); + } + StringBuilder updateSqlStr = new StringBuilder("update "); + // 表名 + updateSqlStr.append(tableName + " set "); + Map updateFieldMap = Collections.synchronizedMap(new HashMap<>()); + // 字段信息 + Field[] fields = classes.getDeclaredFields(); + // 逐渐列名 + String keyColumnName = null; + for (Field field : fields) { + Column column = field.getAnnotation(Column.class); + if (column == null) { + continue; + } + String columnName = column.value(); + if (StrUtil.isBlank(columnName)) { + continue; + } + field.setAccessible(true); + Object columnValue = field.get(data); + if (columnValue == null || StrUtil.isEmpty(columnValue.toString())) { + continue; + } + if (field.isAnnotationPresent(Id.class) || field.isAnnotationPresent(Prev.class)) { + keyColumnName = columnName; + } + updateFieldMap.put(columnName, columnValue); + } + if (StrUtil.isBlank(keyColumnName)) { + throw new RuntimeException("key column name is null"); + } + String keyColumnValue = MapUtil.getStr(updateFieldMap, keyColumnName, ""); + if (StrUtil.isBlank(keyColumnValue)) { + throw new RuntimeException("key column value is null"); + } + + // 遍历生成set语句 + for (String updateFieldName : updateFieldMap.keySet()) { + String updateFieldValue = MapUtil.getStr(updateFieldMap, updateFieldName, ""); + if (StrUtil.isBlank(updateFieldValue)) { + continue; + } + if (updateFieldName.equals(keyColumnName)) { + continue; + } + updateSqlStr.append(updateFieldName + "=@" + updateFieldName + ", "); + } + updateSqlStr = new StringBuilder(updateSqlStr.substring(0, updateSqlStr.lastIndexOf(","))); + updateSqlStr.append(" where " + keyColumnName + "=@" + keyColumnName); + Sql sql = Sqls.create(updateSqlStr.toString()); + + // 设置主键where条件 + sql.params().set(keyColumnName, keyColumnValue); + + // 设置其他参数 + for (String updateFieldName : updateFieldMap.keySet()) { + Object updateFieldValue = updateFieldMap.get(updateFieldName); + if (updateFieldValue == null || StrUtil.isEmpty(updateFieldValue.toString())) { + continue; + } + if (updateFieldName.equals(keyColumnName)) { + continue; + } + sql.params().set(updateFieldName, updateFieldValue); + } + return this.dao().execute(sql).getUpdateCount(); + }catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } } diff --git a/src/main/java/io/v/nutz/pay/OrderPayIndex.java b/src/main/java/io/v/nutz/pay/OrderPayIndex.java new file mode 100644 index 0000000..454faa2 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/OrderPayIndex.java @@ -0,0 +1,19 @@ +package io.v.nutz.pay; + +import io.v.nutz.pay.annotations.Api; +import org.apache.shiro.authz.annotation.RequiresAuthentication; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +@Api(tags = "支付页面") +@At("/order/pay/index") +@Ok("json:full") +public class OrderPayIndex { + + @At("") + @Ok("beetl:/mobile/pay/index.html") + @RequiresAuthentication + public void index() { + + } +} diff --git a/src/main/java/io/v/nutz/pay/PayRunner.java b/src/main/java/io/v/nutz/pay/PayRunner.java new file mode 100644 index 0000000..ac057d9 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/PayRunner.java @@ -0,0 +1,20 @@ +package io.v.nutz.pay; + +import cn.hutool.core.thread.ThreadUtil; +import io.v.nutz.pay.enums.PayChannelEnum; +import io.v.nutz.pay.record.service.IMemberPayRecordService; +import io.v.nutz.pay.task.PayOrderCheckTask; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; + +@IocBean +public class PayRunner { + + @Inject + private IMemberPayRecordService memberPayRecordService; + + public void init() { + // 开启支付订单检查 + ThreadUtil.execAsync(new PayOrderCheckTask(PayChannelEnum.CHINA_BANK, memberPayRecordService, 3)); + } +} diff --git a/src/main/java/io/v/nutz/pay/annotations/Api.java b/src/main/java/io/v/nutz/pay/annotations/Api.java new file mode 100644 index 0000000..6a18415 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/annotations/Api.java @@ -0,0 +1,13 @@ +package io.v.nutz.pay.annotations; + +import java.lang.annotation.*; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface Api { + + String value() default ""; + + String[] tags() default {""}; +} diff --git a/src/main/java/io/v/nutz/pay/annotations/ApiOperation.java b/src/main/java/io/v/nutz/pay/annotations/ApiOperation.java new file mode 100644 index 0000000..12009e3 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/annotations/ApiOperation.java @@ -0,0 +1,13 @@ +package io.v.nutz.pay.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApiOperation { + + String value(); +} diff --git a/src/main/java/io/v/nutz/pay/bank/china/BankChinaWxPayService.java b/src/main/java/io/v/nutz/pay/bank/china/BankChinaWxPayService.java new file mode 100644 index 0000000..177b9d8 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/bank/china/BankChinaWxPayService.java @@ -0,0 +1,371 @@ +package io.v.nutz.pay.bank.china; + + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSONObject; +import io.v.nutz.pay.bank.china.pojo.WxAppPayRequestParams; +import io.v.nutz.pay.bank.china.pojo.WxAppPayResponse; +import io.v.nutz.pay.bank.china.pojo.WxPayOrderQueryResponse; +import io.v.nutz.pay.bank.china.property.ChinaPayProperties; +import io.v.nutz.pay.bank.china.sign.BankPaySign; +import io.v.nutz.pay.cache.ChinaBankOrderWaitPayCacheService; +import io.v.nutz.pay.common.CommonConstants; +import io.v.nutz.pay.enums.*; +import io.v.nutz.pay.util.XmlUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.w3c.dom.Document; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +/** + * 中国银行微信支付 + */ +@Slf4j +@IocBean +public class BankChinaWxPayService { + + @Inject + private ChinaPayProperties payProperties; + + // 微信APP支付请求地址 + private static final String APP_PAY_REQUEST_URL = "/PGWPortal/B2CRecvOrder.do"; + + // 支付交易查询地址 + private static final String APP_ORDER_QUERY_URL = "/PGWPortal/QueryOrderTrans.do"; + + @Inject + private ChinaBankOrderWaitPayCacheService chinaBankOrderWaitPayCacheService; + + /** + * 微信APP支付 + * @param payNo 支付订单号 格式要求为: + * 商户号(merchantNo)后6位+商户按自己规则生成的订单号(仅支持字母或数字); + * eg:221338123456789 + * @param orderAmount 订单金额 + * @param orderTime 订单时间 + * @param orderNote 订单说明 要求如果全中文最多允许60个汉字长度 + * @param orderUrl 订单支付结果通知地址 银行如果配置了商户的URL 则该地址无效 启用order url 则需要提醒银行不配置商户的URL + * @param shopRemark 商品描述 第三方支付(微信)该字段必送 + * 商品描述交易字段格式根据不同的应用场景按照以下格式: + * APP——需传入应用市场上的APP名字-实际商品名称,天天爱消除-游戏充值。 + * @param payDeviceIp 终端IP 第三方支付(微信)该字段必送 + * APP和网页支付提交用户端ip + * @return WxAppPayResponse + */ + public WxAppPayResponse appPay(String payNo, Double orderAmount, Date orderTime, String orderNote, String orderUrl, String shopRemark, String payDeviceIp) { + return this.appPay(payNo, orderAmount, orderTime, orderNote, orderUrl, DateUtil.offsetHour(orderTime, CommonConstants.TWO), WxPayTerminalChnlEnum.PHONE, "WEB", shopRemark, null, payDeviceIp, "no_credit", null, null, null); + } + + + /** + * 微信APP支付 + * @param payNo 支付订单号 格式要求为: + * 商户号(merchantNo)后6位+商户按自己规则生成的订单号(仅支持字母或数字); + * eg:221338123456789 + * @param orderAmount 订单金额 + * @param orderTime 订单时间 + * @param orderNote 订单说明 要求如果全中文最多允许60个汉字长度 + * @param orderUrl 订单支付结果通知地址 银行如果配置了商户的URL 则该地址无效 启用order url 则需要提醒银行不配置商户的URL + * @param overTime 订单超时时间 + * @param chnlEnum 交易终端类型 + * @param deviceInfo 设备号 + * @param shopRemark 商品描述 第三方支付(微信)该字段必送 + * 商品描述交易字段格式根据不同的应用场景按照以下格式: + * APP——需传入应用市场上的APP名字-实际商品名称,天天爱消除-游戏充值。 + * @param attach 附加数据 + * @param payDeviceIp 终端IP 第三方支付(微信)该字段必送 + * APP和网页支付提交用户端ip + * @param limitPay 指定支付方式 第三方支付(微信)该字段选送 + * no_credit-指定不能使用信用卡支付 + * @param identityType 证件类型 第三方支付(微信)该字段选送 + * 证件类型 目前只支持身份证:IDCARD + * 实名支付使用 + * @param identityNumber 证件号 第三方支付(微信)该字段选送 + * 证件号,目前仅支持身份证号 + * 实名支付使用 + * @param identityName 姓名 第三方支付(微信)该字段选送 + * 证件姓名 + * 实名支付使用 + * @return WxAppPayResponse + */ + public WxAppPayResponse appPay(String payNo, Double orderAmount, Date orderTime, String orderNote, String orderUrl, Date overTime, WxPayTerminalChnlEnum chnlEnum, String deviceInfo, String shopRemark, String attach, String payDeviceIp, String limitPay, String identityType, String identityNumber, String identityName) { + // 初始化 加载配置参数 + payProperties.load(); + // 效验配置项参数 + this.checkConfigPayProperties(); + // 子商户公众账号ID + if(StrUtil.isBlank(payProperties.getSubAppid())) { + throw new RuntimeException("缺少子商户公众账号ID subAppid"); + } + + WxAppPayRequestParams payParams = new WxAppPayRequestParams(); + // 商户号 + payParams.setMerchantNo(payProperties.getMerchantNo()); + // 支付类型 + payParams.setPayType(WxPayTypeEnum.ONLINE_SHOPPING.getCode()); + // 商户订单号 + payParams.setOrderNo(payNo); + // 订单币种 + payParams.setCurCode(WxPayCurCodeEnum.RMB.getCode()); + // 订单金额 + payParams.setOrderAmount(orderAmount.toString()); + // 订单时间 + payParams.setOrderTime(Long.valueOf(DateUtil.format(orderTime, "yyyyMMddHHmmss"))); + // 订单说明 + payParams.setOrderNote(orderNote); + // 订单结果通知地址 + payParams.setOrderUrl(orderUrl); + // 订单超时 + if(overTime != null) { + payParams.setOrderTimeoutDate(Long.valueOf(DateUtil.format(overTime, "yyyyMMddHHmmss"))); + } + // 交易终端类型 + payParams.setTerminalChnl(chnlEnum.getCode()); + // 交易类型 + payParams.setTradeType(WxPayTradeTypeEnum.APP_WX.getCode()); + // 子商户公众账号ID + payParams.setSubAppid(payProperties.getSubAppid()); + // 设备号 + payParams.setDeviceInfo(deviceInfo); + // 商品描述 + payParams.setBody(shopRemark); + // 附加数据 + payParams.setAttach(attach); + // 终端ip + payParams.setSpbillCreateIp(payDeviceIp); + // 指定支付方式 + payParams.setLimitPay(limitPay); + // 证件类型 + payParams.setIdentityType(identityType); + // 证件号 + payParams.setIdentityNumber(identityNumber); + // 姓名 + payParams.setIdentityName(identityName); + + // 生成签名数据 + String businessSignData = payParams.getPaySignParamsStr(); + String signData = BankPaySign.getInstance().getSinData(payProperties.getSignKeyFile(), payProperties.getSignkeyPassword(), payProperties.getSignkeyPassword(), businessSignData); + log.info("中国银行微信APP订单支付签名:signData={}", signData); + payParams.setSignData(signData); + log.info("中国银行微信APP订单支付参数:{}", payParams.toString()); + + List formParams = payParams.formParams(); + log.info("中国银行微信APP订单支付发送参数:{}", JSONObject.toJSONString(formParams)); + try { + HttpClient httpClient = new DefaultHttpClient(); + HttpEntity entity = new UrlEncodedFormEntity(formParams, "UTF-8"); + String action = payProperties.getDomain() + APP_PAY_REQUEST_URL; + log.info("中国银行微信APP订单支付请求地址:{}", action); + HttpPost post = new HttpPost(action); + post.setEntity(entity); + HttpResponse postRes = httpClient.execute(post); + HttpEntity entityResult = postRes.getEntity(); + String result = EntityUtils.toString(entityResult); + log.info("---------- BankChinaWxPayService.appPay receive message ----------"); + log.info("[result]=[" + result + "]"); + // 确定响应体数据不能为空 + if(StrUtil.isBlank(result)) { + throw new RuntimeException("BankChinaWxPayService.appPay receive message result is empty"); + } + + // 解析xml响应数据 + Document resultDocument = XmlUtil.createDocument(result); + // 支付请求处理状态 + String hdlSts = XmlUtil.getNodeText(resultDocument, "hdlSts"); + if(StrUtil.isBlank(hdlSts) || hdlSts.equals(WxPayHdlStsEnum.FAIL.getCode()) + || hdlSts.equals(WxPayHdlStsEnum.UNKNOWN.getCode())) { + String rtnMsg = XmlUtil.getNodeText(resultDocument, "rtnMsg", ""); + throw new RuntimeException("中国银行微信APP支付拉起订单失败," + rtnMsg); + } + + // 业务体报文块存在标识 + String bdFlg = XmlUtil.getNodeText(resultDocument, "bdFlg", ""); + if(StrUtil.isBlank(bdFlg) || !"0".equals(bdFlg)) { + throw new RuntimeException("中国银行微信APP支付拉起订单失败: 无响应信息包体"); + } + + // 解析响应信息返回给业务层 + return analysisWxAppPayResponseBodyXml(resultDocument); + } catch (Exception e) { + log.error("中国银行微信APP支付发起失败:{}", e.getMessage()); + throw new RuntimeException("中国银行微信APP支付发起失败:" + e.getMessage()); + } + } + + // 解析响应信息返回给业务层 + private WxAppPayResponse analysisWxAppPayResponseBodyXml(Document document) throws Exception { + // 银行订单流水号 银行的订单流水号(银行产生的订单唯一标识) + String orderSeq = XmlUtil.getNodeText(document, "orderSeq"); + // 下单状态 1:成功 2:失败 + String tranStatus = XmlUtil.getNodeText(document, "tranStatus"); + // 交易类型 APP:APP支付 + String tradeType = XmlUtil.getNodeText(document, "tradeType"); + // 错误码 + String tranCode = XmlUtil.getNodeText(document, "tranCode"); + // 错误描述 + String tranMsg = XmlUtil.getNodeText(document, "tranMsg"); + // 预支付交易会话标识 银联生成的预支付会话标识,用于后续接口调用中使用,该值有效期为2小时 + String prepayId = XmlUtil.getNodeText(document, "prepayId"); + // 应用ID 微信开放平台审核通过的应用APPID,为特约商户申请的应用APPID + String wcPayDataAppId = XmlUtil.getNodeText(document, "wcPayDataAppId"); + // 从业机构号 微信支付分配的从业机构号 + String wcPayDataPartnerId = XmlUtil.getNodeText(document, "wcPayDataPartnerId"); + // 预支付交易会话ID 微信返回的支付交易会话ID + String wcPayDataPrepayId = XmlUtil.getNodeText(document, "wcPayDataPrepayId"); + // 时间戳 当前的时间 + String wcPayDataTimeStamp = XmlUtil.getNodeText(document, "wcPayDataTimeStamp"); + // 随机字符串,不长于32位。推荐随机数生成算法 + String wcPayDataNonceStr = XmlUtil.getNodeText(document, "wcPayDataNonceStr"); + // 订单详情扩展字符串 + String wcPayDataPackage = XmlUtil.getNodeText(document, "wcPayDataPackage"); + // 签名方式 + String wcPayDataSignType = XmlUtil.getNodeText(document, "wcPayDataSignType"); + // 签名 + String wcPayDataPaySign = XmlUtil.getNodeText(document, "wcPayDataPaySign"); + return new WxAppPayResponse().setOrderSeq(orderSeq) + .setTranStatus(tranStatus) + .setTradeType(tradeType) + .setTranCode(tranCode) + .setTranMsg(tranMsg) + .setPrepayId(prepayId) + .setWcPayDataAppId(wcPayDataAppId) + .setWcPayDataPartnerId(wcPayDataPartnerId) + .setWcPayDataPrepayId(wcPayDataPrepayId) + .setWcPayDataTimeStamp(wcPayDataTimeStamp) + .setWcPayDataNonceStr(wcPayDataNonceStr) + .setWcPayDataPackage(wcPayDataPackage) + .setWcPayDataSignType(wcPayDataSignType) + .setWcPayDataPaySign(wcPayDataPaySign); + } + + private void checkConfigPayProperties() { + // 效验商户号 + if(StrUtil.isBlank(payProperties.getMerchantNo())) { + throw new RuntimeException("缺少商户号信息"); + } + + // 效验域名 + if(StrUtil.isBlank(payProperties.getDomain())) { + throw new RuntimeException("缺少API接口调用域名"); + } + + // 签名证书 文件地址 + if(StrUtil.isBlank(payProperties.getSignKeyFile())) { + throw new RuntimeException("缺少签名证书文件地址"); + } + + // 签名证书密码 + if(StrUtil.isBlank(payProperties.getSignkeyPassword())) { + throw new RuntimeException("缺少签名证书密钥"); + } + } + + /** + * 查询银行支付订单信息 + * @param payNo 商户支付订单号 + */ + public WxPayOrderQueryResponse queryOrder(String payNo) { + payProperties.load(); + + // 生成签名数据 + String businessSignData = payProperties.getMerchantNo() + ":" + payNo; + String signData = BankPaySign.getInstance().getSinData(payProperties.getSignKeyFile(), payProperties.getSignkeyPassword(), payProperties.getSignkeyPassword(), businessSignData); + log.info("中国银行订单支付交易查询签名:signData={}", signData); + List formParams = Collections.synchronizedList(new ArrayList()); + formParams.add(new BasicNameValuePair("merchantNo", payProperties.getMerchantNo())); + formParams.add(new BasicNameValuePair("orderNo", payNo)); + formParams.add(new BasicNameValuePair("signData", signData)); + log.info("中国银行订单支付交易查询发送参数:{}", JSONObject.toJSONString(formParams)); + try{ + HttpClient httpClient = new DefaultHttpClient(); + HttpEntity entity = new UrlEncodedFormEntity(formParams, "UTF-8"); + String action = payProperties.getDomain() + APP_ORDER_QUERY_URL; + log.info("中国银行订单支付交易查询请求地址:{}", action); + HttpPost post = new HttpPost(action); + post.setEntity(entity); + HttpResponse postRes = httpClient.execute(post); + HttpEntity entityResult = postRes.getEntity(); + String result = EntityUtils.toString(entityResult); + log.info("---------- BankChinaWxPayService.queryOrder receive message ----------"); + log.info("[result]=[" + result + "]"); + // 确定响应体数据不能为空 + if(StrUtil.isBlank(result)) { + throw new RuntimeException("BankChinaWxPayService.queryOrder receive message result is empty"); + } + + // 解析xml响应数据 + Document resultDocument = XmlUtil.createDocument(result); + // 支付请求处理状态 + String hdlSts = XmlUtil.getNodeText(resultDocument, "hdlSts"); + if(StrUtil.isBlank(hdlSts) || hdlSts.equals(WxPayHdlStsEnum.FAIL.getCode()) + || hdlSts.equals(WxPayHdlStsEnum.UNKNOWN.getCode())) { + String rtnMsg = XmlUtil.getNodeText(resultDocument, "rtnMsg", ""); + if(hdlSts.equals(WxPayHdlStsEnum.FAIL.getCode()) + || hdlSts.equals(WxPayHdlStsEnum.UNKNOWN.getCode())) { + chinaBankOrderWaitPayCacheService.delItem(payNo, 60 * 60 * 24); + } + throw new RuntimeException("中国银行订单支付交易查询失败," + rtnMsg); + } + + // 业务体报文块存在标识 + String bdFlg = XmlUtil.getNodeText(resultDocument, "bdFlg", ""); + if(StrUtil.isBlank(bdFlg) || !"0".equals(bdFlg)) { + throw new RuntimeException("中国银行订单支付交易查询失败: 无响应信息包体"); + } + // 解析响应信息返回给业务层 + return analysisOrderQueryResponseBodyXml(resultDocument); + } catch (Exception e) { + log.error("中国银行订单支付交易查询发起失败:{}", e.getMessage()); + throw new RuntimeException("中国银行订单支付交易查询发起失败:" + e.getMessage()); + } + } + + private WxPayOrderQueryResponse analysisOrderQueryResponseBodyXml(Document document) throws Exception { + // 网关交易流水号 + String tranSeq = XmlUtil.getNodeText(document, "tranSeq"); + // 商户号 + String merchantNo = XmlUtil.getNodeText(document, "merchantNo"); + // 商户订单号 + String orderNo = XmlUtil.getNodeText(document, "orderNo"); + // 商户流水号 + String custTranId = XmlUtil.getNodeText(document, "custTranId"); + // 交易类型 + String tranCode = XmlUtil.getNodeText(document, "tranCode"); + // 交易时间 + String tranTime = XmlUtil.getNodeText(document, "tranTime"); + // 交易金额 + String tranAmount = XmlUtil.getNodeText(document, "tranAmount"); + // 交易状态 + String tranStatus = XmlUtil.getNodeText(document, "tranStatus"); + // 银联订单号 + String unionPaySeq = XmlUtil.getNodeText(document, "unionPaySeq"); + // 二维码超期标识 + String qrOvertimeFlag = XmlUtil.getNodeText(document, "qrOvertimeFlag"); + return new WxPayOrderQueryResponse().setTranSeq(tranSeq) + .setMerchantNo(merchantNo) + .setOrderNo(orderNo) + .setCustTranId(custTranId) + .setTranCode(tranCode) + .setTranTime(tranTime) + .setTranAmount(tranAmount) + .setTranStatus(tranStatus) + .setUnionPaySeq(unionPaySeq) + .setQrOvertimeFlag(qrOvertimeFlag); + } +} diff --git a/src/main/java/io/v/nutz/pay/bank/china/pojo/CommonRequestParams.java b/src/main/java/io/v/nutz/pay/bank/china/pojo/CommonRequestParams.java new file mode 100644 index 0000000..937e766 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/bank/china/pojo/CommonRequestParams.java @@ -0,0 +1,39 @@ +package io.v.nutz.pay.bank.china.pojo; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class CommonRequestParams implements Serializable { + + // 商户号 + private String merchantNo; + + // 商户类型 + private String payType; + + // 订单号 + private String orderNo; + + // 订单币种 + private String curCode; + + // 订单金额 + private String orderAmount; + + // 订单时间 + private Number orderTime; + + // 订单说明 + private String orderNote; + + // 订单支付结果接收通知URL + private String orderUrl; + + // 订单超时时间 + private Number orderTimeoutDate; + + // 商户签名数据 + private String signData; +} diff --git a/src/main/java/io/v/nutz/pay/bank/china/pojo/WxAppPayRequestParams.java b/src/main/java/io/v/nutz/pay/bank/china/pojo/WxAppPayRequestParams.java new file mode 100644 index 0000000..151890c --- /dev/null +++ b/src/main/java/io/v/nutz/pay/bank/china/pojo/WxAppPayRequestParams.java @@ -0,0 +1,156 @@ +package io.v.nutz.pay.bank.china.pojo; + +import cn.hutool.core.util.StrUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.apache.http.NameValuePair; +import org.apache.http.message.BasicNameValuePair; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class WxAppPayRequestParams extends CommonRequestParams implements Serializable { + + // 交易终端类型 08: 手机 + private String terminalChnl; + + // 交易类型 APP_WX:APP支付(微信) + private String tradeType; + + // 子商户公众账号ID 第三方支付(微信)该字段必送 微信分配的子商户公众账号ID + private String subAppid; + + // 设备号 第三方支付(微信)该字段必送 终端设备号(门店号或收银设备ID),公众号内支付请传"WEB" + private String deviceInfo; + + // 商品描述 第三方支付(微信)该字段必送 商品描述交易字段格式根据不同的应用场景按照以下格式: APP——需传入应用市场上的APP名字-实际商品名称,天天爱消除-游戏充值。 + private String body; + + // 附加数据 第三方支付(微信)该字段选送 附加数据,该字段主要用于商户携带订单的自定义数据 + private String attach; + + // 终端ip 第三方支付(微信)该字段必送 APP和网页支付提交用户端ip + private String spbillCreateIp; + + // 指定支付方式 第三方支付(微信)该字段选送 no_credit-指定不能使用信用卡支付 + private String limitPay; + + // 证件类型 第三方支付(微信)该字段选送 证件类型 目前只支持身份证:IDCARD 实名支付使用 + private String identityType; + + // 证件号 第三方支付(微信)该字段选送 证件号,目前仅支持身份证号 实名支付使用 + private String identityNumber; + + // 姓名 第三方支付(微信)该字段选送 证件姓名 实名支付使用 + private String identityName; + + // 此方法用于生成签名时所需的业务数据 + public String getPaySignParamsStr() { + StringBuffer sb = new StringBuffer(); +// sb.append(this.getMerchantNo()).append("|"); +// sb.append(this.getPayType()).append("|"); + sb.append(this.getOrderNo()).append("|"); + sb.append(this.getOrderTime()).append("|"); + sb.append(this.getCurCode()).append("|"); + sb.append(this.getOrderAmount()).append("|"); + sb.append(this.getMerchantNo()).append("|"); +// sb.append(this.getOrderNote()).append("|"); +// sb.append(this.getOrderUrl()).append("|"); +// if(this.getOrderTimeoutDate() != null) { +// sb.append(this.getOrderTimeoutDate()).append("|"); +// } +// sb.append(this.getTerminalChnl()).append("|"); +// sb.append(this.getTradeType()).append("|"); +// sb.append(this.getSubAppid()).append("|"); +// sb.append(this.getDeviceInfo()).append("|"); +// sb.append(this.getBody()).append("|"); +// if(StrUtil.isNotBlank(this.getAttach())) { +// sb.append(this.getAttach()).append("|"); +// } +// sb.append(this.getSpbillCreateIp()).append("|"); +// if(StrUtil.isNotBlank(this.getLimitPay())) { +// sb.append(this.getLimitPay()).append("|"); +// } +// if(StrUtil.isNotBlank(this.getIdentityType())) { +// sb.append(this.getIdentityType()).append("|"); +// } +// if(StrUtil.isNotBlank(this.getIdentityNumber())) { +// sb.append(this.getIdentityNumber()).append("|"); +// } +// if(StrUtil.isNotBlank(this.getIdentityName())) { +// sb.append(this.getIdentityName()).append("|"); +// } + return sb.toString(); + } + + public List formParams() { + List params = Collections.synchronizedList(new ArrayList()); + params.add(new BasicNameValuePair("merchantNo", this.getMerchantNo())); + params.add(new BasicNameValuePair("payType", this.getPayType())); + params.add(new BasicNameValuePair("orderNo", this.getOrderNo())); + params.add(new BasicNameValuePair("curCode", this.getCurCode())); + params.add(new BasicNameValuePair("orderAmount", this.getOrderAmount())); + params.add(new BasicNameValuePair("orderTime", this.getOrderTime().toString())); + params.add(new BasicNameValuePair("orderNote", this.getOrderNote())); + params.add(new BasicNameValuePair("orderUrl", this.getOrderUrl())); + if(this.getOrderTimeoutDate() != null) { + params.add(new BasicNameValuePair("orderTimeoutDate", this.getOrderTimeoutDate().toString())); + } + params.add(new BasicNameValuePair("terminalChnl", this.getTerminalChnl())); + params.add(new BasicNameValuePair("tradeType", this.getTradeType())); + params.add(new BasicNameValuePair("subAppid", this.getSubAppid())); + params.add(new BasicNameValuePair("deviceInfo", this.getDeviceInfo())); + params.add(new BasicNameValuePair("body", this.getBody())); + if(StrUtil.isNotBlank(this.getAttach())) { + params.add(new BasicNameValuePair("attach", this.getAttach())); + } + params.add(new BasicNameValuePair("spbillCreateIp", this.getSpbillCreateIp())); + if(StrUtil.isNotBlank(this.getLimitPay())) { + params.add(new BasicNameValuePair("limitPay", this.getLimitPay())); + } + if(StrUtil.isNotBlank(this.getIdentityType())) { + params.add(new BasicNameValuePair("identityType", this.getIdentityType())); + } + if(StrUtil.isNotBlank(this.getIdentityNumber())) { + params.add(new BasicNameValuePair("identityNumber", this.getIdentityNumber())); + } + if(StrUtil.isNotBlank(this.getIdentityName())) { + params.add(new BasicNameValuePair("identityName", this.getIdentityName())); + } + params.add(new BasicNameValuePair("signData", this.getSignData())); + return params; + } + + @Override + public String toString() { + return "WxAppPayRequestParams{" + + "merchantNo='" + this.getMerchantNo() + '\'' + + "payType='" + this.getPayType() + '\'' + + "orderNo='" + this.getOrderNo() + '\'' + + "curCode='" + this.getCurCode() + '\'' + + "orderAmount='" + this.getOrderAmount() + '\'' + + "orderTime='" + this.getOrderTime() + '\'' + + "orderNote='" + this.getOrderNote() + '\'' + + "orderUrl='" + this.getOrderUrl() + '\'' + + "orderTimeoutDate='" + this.getOrderTimeoutDate() + '\'' + + "terminalChnl='" + this.getTerminalChnl() + '\'' + + "signData='" + this.getSignData() + '\'' + + "tradeType='" + this.getTradeType() + '\'' + + "subAppid='" + this.getSubAppid() + '\'' + + "deviceInfo='" + this.getDeviceInfo() + '\'' + + "body='" + this.getBody() + '\'' + + "attach='" + this.getAttach() + '\'' + + "spbillCreateIp='" + this.getSpbillCreateIp() + '\'' + + "limitPay='" + this.getLimitPay() + '\'' + + "identityType='" + this.getIdentityType() + '\'' + + "identityNumber='" + this.getIdentityNumber() + '\'' + + "identityName='" + this.getIdentityName() + '\'' + + '}'; + } +} diff --git a/src/main/java/io/v/nutz/pay/bank/china/pojo/WxAppPayResponse.java b/src/main/java/io/v/nutz/pay/bank/china/pojo/WxAppPayResponse.java new file mode 100644 index 0000000..2faeee6 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/bank/china/pojo/WxAppPayResponse.java @@ -0,0 +1,55 @@ +package io.v.nutz.pay.bank.china.pojo; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class WxAppPayResponse implements Serializable { + + // 银行订单流水号 银行的订单流水号(银行产生的订单唯一标识) + private String orderSeq; + + // 下单状态 1:成功 2:失败 + private String tranStatus; + + // 交易类型 APP:APP支付 + private String tradeType; + + // 错误码 + private String tranCode; + + // 错误描述 + private String tranMsg; + + // 预支付交易会话标识 银联生成的预支付会话标识,用于后续接口调用中使用,该值有效期为2小时 + private String prepayId; + + // 应用ID 微信开放平台审核通过的应用APPID,为特约商户申请的应用APPID + private String wcPayDataAppId; + + // 从业机构号 微信支付分配的从业机构号 + private String wcPayDataPartnerId; + + // 预支付交易会话ID 微信返回的支付交易会话ID + private String wcPayDataPrepayId; + + // 时间戳 当前的时间 + private String wcPayDataTimeStamp; + + // 随机字符串,不长于32位。推荐随机数生成算法 + private String wcPayDataNonceStr; + + // 订单详情扩展字符串 + private String wcPayDataPackage; + + // 签名方式 + private String wcPayDataSignType; + + // 签名 + private String wcPayDataPaySign; +} diff --git a/src/main/java/io/v/nutz/pay/bank/china/pojo/WxPayOrderQueryResponse.java b/src/main/java/io/v/nutz/pay/bank/china/pojo/WxPayOrderQueryResponse.java new file mode 100644 index 0000000..66e5dc1 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/bank/china/pojo/WxPayOrderQueryResponse.java @@ -0,0 +1,43 @@ +package io.v.nutz.pay.bank.china.pojo; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class WxPayOrderQueryResponse implements Serializable { + + // 网关交易流水号 + private String tranSeq; + + // 商户号 + private String merchantNo; + + // 商户订单号 + private String orderNo; + + // 商户流水号 + private String custTranId; + + // 交易类型 + private String tranCode; + + // 交易时间 + private String tranTime; + + // 交易金额 + private String tranAmount; + + // 交易状态0:待处理,1:成功,2:失败,3:未明 + private String tranStatus; + + // 银联订单号 + private String unionPaySeq; + + // 二维码超期标识 Y:超期 N:未超期 + private String qrOvertimeFlag; +} diff --git a/src/main/java/io/v/nutz/pay/bank/china/property/ChinaPayProperties.java b/src/main/java/io/v/nutz/pay/bank/china/property/ChinaPayProperties.java new file mode 100644 index 0000000..904ce45 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/bank/china/property/ChinaPayProperties.java @@ -0,0 +1,82 @@ +package io.v.nutz.pay.bank.china.property; + +import cn.hutool.core.util.StrUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; +import org.nutz.ioc.impl.PropertiesProxy; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; + +@Data +@Slf4j +@IocBean +@Accessors(chain = true) +@NoArgsConstructor +public class ChinaPayProperties { + + @Inject + private PropertiesProxy conf; + + // 支付网关PGWPortal地址 + private String pgwPortalUrl; + + // 上传下载BocnetExpress地址 + private String bocnetExpressUrl; + + // 签名证书 文件地址 + private String signKeyFile; + + // 签名证书密码 + private String signkeyPassword; + + // 验签的中行根证书 + private String verifyCerFile; + + // 下载文件保存路径 + private String downloadDir; + + // 上传的文件路径 + private String uploadFile; + + // 请求域名 + private String domain; + + // 商户号 + private String merchantNo; + + // 子商户公众账号ID + private String subAppid; + + // 通知域名 + private String notifyDomain; + + // 加载配置参数 + public ChinaPayProperties load() { + pgwPortalUrl = conf.get("bank.china.pgw.portal.url", ""); + bocnetExpressUrl = conf.get("bank.china.bocnet.express.url", ""); + signKeyFile = conf.get("bank.china.sign.key.file", ""); + signkeyPassword = conf.get("bank.china.sign.key.password", ""); + verifyCerFile = conf.get("bank.china.verify.cer.file", ""); + downloadDir = conf.get("bank.china.download.dir", ""); + uploadFile = conf.get("bank.china.upload.file", ""); + domain = conf.get("bank.china.domain", ""); + merchantNo = conf.get("bank.china.merchant.no", ""); + subAppid = conf.get("bank.china.sub.appid", ""); + notifyDomain = conf.get("bank.china.notify.domain", ""); + log.info("中国银行支付读取配置信息:pgwPortalUrl-{}, bocnetExpressUrl-{}, signKeyFile-{}, signkeyPassword-{}, verifyCerFile-{}, downloadDir-{}, uploadFile-{}, domain-{}, merchantNo-{}, subAppid-{}, notifyDomain-{}", pgwPortalUrl, bocnetExpressUrl, signKeyFile, signkeyPassword, verifyCerFile, downloadDir, uploadFile, domain, merchantNo, subAppid, notifyDomain); + return this; + } + + public String getMerchantNoAfterChar(int length) { + if(StrUtil.isBlank(this.merchantNo)) { + return ""; + } + if(this.merchantNo.length() < length) { + throw new RuntimeException(merchantNo + " is too short"); + } + String afterChar = this.merchantNo.substring(this.merchantNo.length() - length, this.merchantNo.length()); + return afterChar; + } +} diff --git a/src/main/java/io/v/nutz/pay/bank/china/sign/BankPaySign.java b/src/main/java/io/v/nutz/pay/bank/china/sign/BankPaySign.java new file mode 100644 index 0000000..aa1eff7 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/bank/china/sign/BankPaySign.java @@ -0,0 +1,58 @@ +package io.v.nutz.pay.bank.china.sign; + +import cn.hutool.core.util.StrUtil; +import io.v.nutz.pay.util.PKCS7Tool; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.Base64; + +/** + * 中国银行支付签名 + */ +@Slf4j +public class BankPaySign { + + + public static BankPaySign getInstance() { + return new BankPaySign(); + } + + /** + * 生成签名数据 + * @param signKeyStorePath 证书库路径 + * @param signKeyStorePassword 证书库口令 + * @param signKeyPassword 签名私钥口令,与证书库口令相同 + * @param businessParams 业务参数 参数格式 param|param|param + * @return signData + */ + public String getSinData(String signKeyStorePath, String signKeyStorePassword, String signKeyPassword, String businessParams) { + if(StrUtil.isBlank(signKeyStorePath)) { + throw new RuntimeException("缺少证书库路径"); + } + if(StrUtil.isBlank(signKeyStorePassword)) { + throw new RuntimeException("缺少证书库口令"); + } + if(StrUtil.isBlank(signKeyPassword)) { + throw new RuntimeException("缺少签名私钥口令"); + } + if(businessParams == null) { + businessParams = ""; + } + log.info("签名信息:signKeyStorePath-{}, signKeyStorePassword-{}, signKeyPassword-{}, businessParams-{}", signKeyStorePath, signKeyStorePassword, signKeyPassword, businessParams); + + try { + PKCS7Tool tool = PKCS7Tool.instance(); + byte[] businessDataBytes = businessParams.getBytes("UTF-8"); + String signData = tool.signature(signKeyStorePath, signKeyPassword, businessDataBytes); + log.info("签名数据:signData-{}", signData); +// Boolean signDataBoolean = tool.verifySignature(signKeyStorePath, signKeyPassword, businessDataBytes, Base64.getDecoder().decode(signData)); +// log.info("签名数据生成状态: signDataBoolean-{}", signDataBoolean); + return signData; + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e.getMessage()); + } + } +} diff --git a/src/main/java/io/v/nutz/pay/cache/ChinaBankOrderPayNotifyCacheService.java b/src/main/java/io/v/nutz/pay/cache/ChinaBankOrderPayNotifyCacheService.java new file mode 100644 index 0000000..7db945c --- /dev/null +++ b/src/main/java/io/v/nutz/pay/cache/ChinaBankOrderPayNotifyCacheService.java @@ -0,0 +1,23 @@ +package io.v.nutz.pay.cache; + +import org.nutz.integration.jedis.RedisService; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; + +@IocBean +public class ChinaBankOrderPayNotifyCacheService { + + private static final String SSK_MEMBER_PAY_CHINA_BANK_NOTIFY = "SSK:MEMBER:PAY:CHINA:BANK:NOTIFY:"; + + @Inject + private RedisService redisService; + + + public boolean exists(String key) { + return redisService.exists(SSK_MEMBER_PAY_CHINA_BANK_NOTIFY + key); + } + + public void setex(String key, int seconds, String value) { + redisService.setex(key, seconds, value); + } +} diff --git a/src/main/java/io/v/nutz/pay/cache/ChinaBankOrderWaitPayCacheService.java b/src/main/java/io/v/nutz/pay/cache/ChinaBankOrderWaitPayCacheService.java new file mode 100644 index 0000000..f7d90f1 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/cache/ChinaBankOrderWaitPayCacheService.java @@ -0,0 +1,81 @@ +package io.v.nutz.pay.cache; + +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import org.nutz.integration.jedis.RedisService; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@IocBean +public class ChinaBankOrderWaitPayCacheService { + + private static final String REDIS_KEY = "SSK:CACHE:MEMBER:CHINA:WAIT:PAY:ORDER:QUERY"; + + @Inject + private RedisService redisService; + + public void set(String value) { + set(value, 0); + } + + public void set(String value, int seconds) { + if(seconds <= 0) { + redisService.set(REDIS_KEY, value); + } else { + redisService.setex(REDIS_KEY, seconds, value); + } + } + + public void setItem(String itemValue) { + this.setItem(itemValue, 0); + } + + public void setItem(String itemValue, int seconds) { + String keyValue = ""; + if(redisService.exists(REDIS_KEY)) { + keyValue = redisService.get(REDIS_KEY); + } + List list = Collections.synchronizedList(new ArrayList<>()); + if(StrUtil.isNotBlank(keyValue)) { + list = JSON.parseArray(keyValue, String.class); + } + if(list.contains(itemValue)) { + return; + } + list.add(itemValue); + this.set(JSON.toJSONString(list), seconds); + } + + public String get() { + return redisService.get(REDIS_KEY); + } + + public void del() { + redisService.del(REDIS_KEY); + } + + public void delItem(String itemValue) { + this.delItem(itemValue, 0); + } + + public void delItem(String itemValue, int seconds) { + String keyValue = ""; + if(redisService.exists(REDIS_KEY)) { + keyValue = redisService.get(REDIS_KEY); + } else { + return; + } + List list = JSON.parseArray(keyValue, String.class); + list = list.stream().filter(item -> !item.equals(itemValue)).collect(Collectors.toList()); + if(list.size() > 0) { + this.set(JSON.toJSONString(list), seconds); + } else { + this.del(); + } + } +} diff --git a/src/main/java/io/v/nutz/pay/common/CommonConstants.java b/src/main/java/io/v/nutz/pay/common/CommonConstants.java new file mode 100644 index 0000000..120e9f6 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/common/CommonConstants.java @@ -0,0 +1,16 @@ +package io.v.nutz.pay.common; + +public interface CommonConstants { + + public static final Integer ZERO = 0; + public static final Integer ONE = 1; + public static final Integer TWO = 2; + public static final Integer THREE = 3; + public static final Integer FOUR = 4; + public static final Integer FIVE = 5; + public static final Integer SIX = 6; + public static final Integer SEVEN = 7; + public static final Integer EIGHT = 8; + public static final Integer NINE = 9; + public static final Integer TEN = 10; +} diff --git a/src/main/java/io/v/nutz/pay/enums/ChannelTypeEnum.java b/src/main/java/io/v/nutz/pay/enums/ChannelTypeEnum.java new file mode 100644 index 0000000..55e743c --- /dev/null +++ b/src/main/java/io/v/nutz/pay/enums/ChannelTypeEnum.java @@ -0,0 +1,36 @@ +package io.v.nutz.pay.enums; + +public enum ChannelTypeEnum { + + // 渠道类型:1.微信APP + WX_APP(1, "微信APP"); + + private Integer code; + public Integer getCode() { + return code; + } + + private String name; + public String getName() { + return name; + } + + public static ChannelTypeEnum getByCode(Integer code) { + if(code == null) { + return null; + } else { + ChannelTypeEnum[] values = ChannelTypeEnum.values(); + for(ChannelTypeEnum value : values) { + if(value.getCode().equals(code)) { + return value; + } + } + } + return null; + } + + ChannelTypeEnum(Integer code, String name) { + this.code = code; + this.name = name; + } +} diff --git a/src/main/java/io/v/nutz/pay/enums/MemberPayBusinessTypeEnum.java b/src/main/java/io/v/nutz/pay/enums/MemberPayBusinessTypeEnum.java new file mode 100644 index 0000000..e7c47a7 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/enums/MemberPayBusinessTypeEnum.java @@ -0,0 +1,36 @@ +package io.v.nutz.pay.enums; + +public enum MemberPayBusinessTypeEnum { + + // 业务类型:1.会员工会缴费 + UNION_PAYMENT(1, "会员工会缴费"); + + private Integer code; + public Integer getCode() { + return code; + } + + private String name; + public String getName() { + return name; + } + + public static MemberPayBusinessTypeEnum getByCode(Integer code) { + if(code == null) { + return null; + } else { + MemberPayBusinessTypeEnum[] values = MemberPayBusinessTypeEnum.values(); + for(MemberPayBusinessTypeEnum value : values) { + if(value.getCode().equals(code)) { + return value; + } + } + } + return null; + } + + MemberPayBusinessTypeEnum(Integer code, String name) { + this.code = code; + this.name = name; + } +} diff --git a/src/main/java/io/v/nutz/pay/enums/MemberPayStatusEnum.java b/src/main/java/io/v/nutz/pay/enums/MemberPayStatusEnum.java new file mode 100644 index 0000000..efd2d83 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/enums/MemberPayStatusEnum.java @@ -0,0 +1,39 @@ +package io.v.nutz.pay.enums; + +public enum MemberPayStatusEnum { + + // 支付状态:0.待支付 1.支付成功 2.支付失败 3.已退款 + WAIT(0, "待支付"), + SUCCESS(1, "支付成功"), + FAIL(2, "支付失败"), + REFUND(3, "已退款"); + + private Integer code; + public Integer getCode() { + return code; + } + + private String name; + public String getName() { + return name; + } + + public static MemberPayStatusEnum getByCode(Integer code) { + if(code == null) { + return null; + } else { + MemberPayStatusEnum[] values = MemberPayStatusEnum.values(); + for(MemberPayStatusEnum value : values) { + if(value.getCode().equals(code)) { + return value; + } + } + } + return null; + } + + MemberPayStatusEnum(Integer code, String name) { + this.code = code; + this.name = name; + } +} diff --git a/src/main/java/io/v/nutz/pay/enums/PayChannelEnum.java b/src/main/java/io/v/nutz/pay/enums/PayChannelEnum.java new file mode 100644 index 0000000..dd35a67 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/enums/PayChannelEnum.java @@ -0,0 +1,36 @@ +package io.v.nutz.pay.enums; + +public enum PayChannelEnum { + + // 支付渠道:1.中国银行 + CHINA_BANK(1, "中国银行"); + + private Integer code; + public Integer getCode() { + return code; + } + + private String name; + public String getName() { + return name; + } + + public static PayChannelEnum getByCode(Integer code) { + if(code == null) { + return null; + } else { + PayChannelEnum[] values = PayChannelEnum.values(); + for(PayChannelEnum value : values) { + if(value.getCode().equals(code)) { + return value; + } + } + } + return null; + } + + PayChannelEnum(Integer code, String name) { + this.code = code; + this.name = name; + } +} diff --git a/src/main/java/io/v/nutz/pay/enums/PaymentOrderStatusEnum.java b/src/main/java/io/v/nutz/pay/enums/PaymentOrderStatusEnum.java new file mode 100644 index 0000000..c4c236b --- /dev/null +++ b/src/main/java/io/v/nutz/pay/enums/PaymentOrderStatusEnum.java @@ -0,0 +1,26 @@ +package io.v.nutz.pay.enums; + +public enum PaymentOrderStatusEnum { + + // 订单状态: 0.待支付 1.部分支付 2.已支付 3.支付失败 4.已退款 + WAIT(0, "待支付"), + PART_PAY(1, "部分支付"), + PAID(2, "已支付"), + FAIL(3, "支付失败"), + REFUND(4, "已退款"); + + private Integer code; + public Integer getCode() { + return code; + } + + private String name; + public String getName() { + return name; + } + + PaymentOrderStatusEnum(Integer code, String name) { + this.code = code; + this.name = name; + } +} diff --git a/src/main/java/io/v/nutz/pay/enums/WxPayCurCodeEnum.java b/src/main/java/io/v/nutz/pay/enums/WxPayCurCodeEnum.java new file mode 100644 index 0000000..a675f88 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/enums/WxPayCurCodeEnum.java @@ -0,0 +1,21 @@ +package io.v.nutz.pay.enums; + +public enum WxPayCurCodeEnum { + + RMB("001", "人民币"); + + private String code; + public String getCode() { + return code; + } + + private String name; + public String getName() { + return name; + } + + WxPayCurCodeEnum(String code, String name) { + this.code = code; + this.name = name; + } +} diff --git a/src/main/java/io/v/nutz/pay/enums/WxPayHdlStsEnum.java b/src/main/java/io/v/nutz/pay/enums/WxPayHdlStsEnum.java new file mode 100644 index 0000000..e33e3e1 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/enums/WxPayHdlStsEnum.java @@ -0,0 +1,24 @@ +package io.v.nutz.pay.enums; + +public enum WxPayHdlStsEnum { + + SUCCESS("A", "成功"), + FAIL("B","失败"), + UNKNOWN("K", "未明"); + + + private String code; + public String getCode() { + return code; + } + + private String name; + public String getName() { + return name; + } + + WxPayHdlStsEnum(String code, String name) { + this.code = code; + this.name = name; + } +} diff --git a/src/main/java/io/v/nutz/pay/enums/WxPayTerminalChnlEnum.java b/src/main/java/io/v/nutz/pay/enums/WxPayTerminalChnlEnum.java new file mode 100644 index 0000000..257c3d0 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/enums/WxPayTerminalChnlEnum.java @@ -0,0 +1,21 @@ +package io.v.nutz.pay.enums; + +public enum WxPayTerminalChnlEnum { + + PHONE("08", "手机"); + + private String code; + public String getCode() { + return code; + } + + private String name; + public String getName() { + return name; + } + + WxPayTerminalChnlEnum(String code, String name) { + this.code = code; + this.name = name; + } +} diff --git a/src/main/java/io/v/nutz/pay/enums/WxPayTradeTypeEnum.java b/src/main/java/io/v/nutz/pay/enums/WxPayTradeTypeEnum.java new file mode 100644 index 0000000..6052c58 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/enums/WxPayTradeTypeEnum.java @@ -0,0 +1,21 @@ +package io.v.nutz.pay.enums; + +public enum WxPayTradeTypeEnum { + + APP_WX("APP_WX", "APP支付(微信)"); + + private String code; + public String getCode() { + return code; + } + + private String name; + public String getName() { + return name; + } + + WxPayTradeTypeEnum(String code, String name) { + this.code = code; + this.name = name; + } +} diff --git a/src/main/java/io/v/nutz/pay/enums/WxPayTypeEnum.java b/src/main/java/io/v/nutz/pay/enums/WxPayTypeEnum.java new file mode 100644 index 0000000..b3bc642 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/enums/WxPayTypeEnum.java @@ -0,0 +1,21 @@ +package io.v.nutz.pay.enums; + +public enum WxPayTypeEnum { + + ONLINE_SHOPPING("1", "网上购物"); + + private String code; + public String getCode() { + return code; + } + + private String name; + public String getName() { + return name; + } + + WxPayTypeEnum(String code, String name) { + this.code = code; + this.name = name; + } +} diff --git a/src/main/java/io/v/nutz/pay/form/PayRecordForm.java b/src/main/java/io/v/nutz/pay/form/PayRecordForm.java new file mode 100644 index 0000000..2dc9590 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/form/PayRecordForm.java @@ -0,0 +1,22 @@ +package io.v.nutz.pay.form; + +import lombok.Data; +import org.nutz.dao.entity.annotation.Comment; + +import java.io.Serializable; + +@Data +public class PayRecordForm implements Serializable { + + @Comment("业务类型:1.会员工会缴费") + private Integer businessType; + + @Comment("业务ID") + private String businessId; + + @Comment("支付渠道:1.中国银行") + private Integer payChannel; + + @Comment("渠道类型:1.微信APP") + private Integer channelType; +} diff --git a/src/main/java/io/v/nutz/pay/form/PaymentOrderForm.java b/src/main/java/io/v/nutz/pay/form/PaymentOrderForm.java new file mode 100644 index 0000000..2aaceae --- /dev/null +++ b/src/main/java/io/v/nutz/pay/form/PaymentOrderForm.java @@ -0,0 +1,13 @@ +package io.v.nutz.pay.form; + +import lombok.Data; +import org.nutz.dao.entity.annotation.Comment; + +import java.io.Serializable; + +@Data +public class PaymentOrderForm implements Serializable { + + @Comment("订单金额") + private Double money; +} diff --git a/src/main/java/io/v/nutz/pay/notify/ChinaBankPayResultNotify.java b/src/main/java/io/v/nutz/pay/notify/ChinaBankPayResultNotify.java new file mode 100644 index 0000000..cb215d1 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/notify/ChinaBankPayResultNotify.java @@ -0,0 +1,87 @@ +package io.v.nutz.pay.notify; + +import cn.wizzer.framework.base.Result; +import io.v.nutz.pay.annotations.Api; +import io.v.nutz.pay.annotations.ApiOperation; +import io.v.nutz.pay.cache.ChinaBankOrderPayNotifyCacheService; +import io.v.nutz.pay.cache.ChinaBankOrderWaitPayCacheService; +import io.v.nutz.pay.notify.enums.PayNotifyOrderStatusEnum; +import io.v.nutz.pay.notify.enums.PayNotifyReturnActFlagEnum; +import io.v.nutz.pay.record.service.IMemberPayRecordService; +import lombok.extern.slf4j.Slf4j; +import org.nutz.integration.jedis.RedisService; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 中国银行支付通知 controller + */ +@Slf4j +@IocBean +@Api("中国银行支付通知") +@At("/notify/china/bank") +public class ChinaBankPayResultNotify { + + @Inject + private IMemberPayRecordService memberPayRecordService; + + @Inject + private ChinaBankOrderWaitPayCacheService chinaBankOrderWaitPayCacheService; + + @Inject + private ChinaBankOrderPayNotifyCacheService chinaBankOrderPayNotifyCacheService; + + + @At("/pay") + @Ok("json") + @ApiOperation("中国银行支付通知") + public Result payResultNotify(HttpServletRequest req, HttpServletResponse resp) { + // 商户订单号 + String orderNo = req.getParameter("orderNo"); + // 银行订单流水号 + String orderSeq = req.getParameter("orderSeq"); + // 银行订单状态 0:未处理 1:支付 2:撤销 3:退货 4:未明 5:失败 6:下单 + String orderStatus = req.getParameter("orderStatus"); + // 银行交易流水号 + String bankTranSeq = req.getParameter("bankTranSeq"); + // 中行签名数据 + String signData = req.getParameter("signData"); + // 支付金额 + String payAmount = req.getParameter("payAmount"); + // 操作类型 1:支付结果通知 2:电话订单结果通知 3:退款结果通知 4:订单查询结果 + String returnActFlag = req.getParameter("returnActFlag"); + log.info("收到中行银行支付结果通知: orderNo->{}, orderSeq->{}, orderStatus->{}, bankTranSeq->{}, payAmount->{}, signData->{}", orderNo, orderSeq, orderStatus, bankTranSeq, payAmount, signData); + log.info("收到中行银行支付结果通知: returnActFlag->{}", returnActFlag); + + try{ + // 支付结果通知 + if(PayNotifyReturnActFlagEnum.PAY_RESULT_NOTIFY.getCode().equals(returnActFlag)) { + // 设置30秒内同一个商户订单 不可重复接收到通知 + if(chinaBankOrderPayNotifyCacheService.exists(orderNo)) { + log.error("商户订单号重复支付结果通知:orderNo->{}", orderNo); + return Result.error("商户订单号重复通知"); + } + chinaBankOrderPayNotifyCacheService.setex(orderNo, 30, orderNo); + + // 支付成功 + if (PayNotifyOrderStatusEnum.PAY.getCode().equals(orderStatus)) { + memberPayRecordService.paySuccess(orderNo, Double.valueOf(payAmount)); + + // 支付失败 + } else if (PayNotifyOrderStatusEnum.FAIL.getCode().equals(orderStatus)) { + memberPayRecordService.payFail(orderNo, "用户支付失败"); + } + chinaBankOrderWaitPayCacheService.delItem(orderNo); + } + }catch (Exception e){ + e.printStackTrace(); + return Result.error(); + } + return Result.success(); + } +} diff --git a/src/main/java/io/v/nutz/pay/notify/enums/PayNotifyOrderStatusEnum.java b/src/main/java/io/v/nutz/pay/notify/enums/PayNotifyOrderStatusEnum.java new file mode 100644 index 0000000..7f98ca6 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/notify/enums/PayNotifyOrderStatusEnum.java @@ -0,0 +1,43 @@ +package io.v.nutz.pay.notify.enums; + +import io.v.nutz.pay.enums.PayChannelEnum; + +public enum PayNotifyOrderStatusEnum { + // 订单状态 0:未处理 1:支付 2:撤销 3:退货 4:未明 5:失败 6:下单 + NOT_HANDLE("0", "未处理"), + PAY("1", "支付"), + REVOKE("2", "撤销"), + RETURN("3", "退货"), + UNKNOWN("4", "未明"), + FAIL("5", "失败"), + ORDER("6", "下单"); + + private String code; + public String getCode() { + return code; + } + + private String name; + public String getName() { + return name; + } + + public static PayNotifyOrderStatusEnum getByCode(String code) { + if(code == null) { + return null; + } else { + PayNotifyOrderStatusEnum[] values = PayNotifyOrderStatusEnum.values(); + for(PayNotifyOrderStatusEnum value : values) { + if(value.getCode().equals(code)) { + return value; + } + } + } + return null; + } + + PayNotifyOrderStatusEnum(String code, String name) { + this.code = code; + this.name = name; + } +} diff --git a/src/main/java/io/v/nutz/pay/notify/enums/PayNotifyReturnActFlagEnum.java b/src/main/java/io/v/nutz/pay/notify/enums/PayNotifyReturnActFlagEnum.java new file mode 100644 index 0000000..83ffc7a --- /dev/null +++ b/src/main/java/io/v/nutz/pay/notify/enums/PayNotifyReturnActFlagEnum.java @@ -0,0 +1,39 @@ +package io.v.nutz.pay.notify.enums; + +public enum PayNotifyReturnActFlagEnum { + + // 操作类型 1:支付结果通知 2:电话订单结果通知 3:退款结果通知 4:订单查询结果 + PAY_RESULT_NOTIFY("1", "支付结果通知"), + PAY_PHONE_NOTIFY("2", "电话订单结果通知"), + PAY_REFUND_NOTIFY("3", "退款结果通知"), + ORDER_QUERY_NOTIFY("4", "订单查询结果"); + + private String code; + public String getCode() { + return code; + } + + private String name; + public String getName() { + return name; + } + + public static PayNotifyReturnActFlagEnum getByCode(String code) { + if(code == null) { + return null; + } else { + PayNotifyReturnActFlagEnum[] values = PayNotifyReturnActFlagEnum.values(); + for(PayNotifyReturnActFlagEnum value : values) { + if(value.getCode().equals(code)) { + return value; + } + } + } + return null; + } + + PayNotifyReturnActFlagEnum(String code, String name) { + this.code = code; + this.name = name; + } +} diff --git a/src/main/java/io/v/nutz/pay/order/controller/MemberPaymentOrderController.java b/src/main/java/io/v/nutz/pay/order/controller/MemberPaymentOrderController.java new file mode 100644 index 0000000..44d4914 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/order/controller/MemberPaymentOrderController.java @@ -0,0 +1,55 @@ +package io.v.nutz.pay.order.controller; + +import cn.wizzer.framework.base.Result; +import io.v.nutz.pay.annotations.Api; +import io.v.nutz.pay.annotations.ApiOperation; +import io.v.nutz.pay.form.PaymentOrderForm; +import io.v.nutz.pay.order.module.MemberPaymentOrder; +import io.v.nutz.pay.order.service.IMemberPaymentOrderService; +import io.v.nutz.pay.pojo.LoginUser; +import io.v.nutz.pay.util.AuthUtil; +import org.nutz.dao.entity.annotation.Comment; +import org.nutz.integration.jedis.RedisService; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; +import org.springframework.util.Assert; + +/** + * 会员缴费订单 controller + */ +@IocBean +@Api("会员缴费订单") +@At("/member/payment/order") +public class MemberPaymentOrderController { + + @Inject + private IMemberPaymentOrderService memberPaymentOrderService; + + @Inject + private RedisService redisService; + + + private final static String SSK_MEMBER_PAYMENT_ORDER = "SSK:MEMBER:PAYMENT:ORDER:PAY:"; + + + @At("/add") + @Ok("json") + @ApiOperation("生成会员缴费订单记录") + public Result add(PaymentOrderForm form) { + Assert.isTrue(form.getMoney() > 0, "缺少订单金额"); + LoginUser loginUser = AuthUtil.instance().getLoginUser(); + String memberId = loginUser.getId(); + if(memberId == null) { + throw new RuntimeException("用户登录失效,请重新登录"); + } + // 设置三秒内用户不可重复下单 防止产生错误数据 + if(redisService.exists(SSK_MEMBER_PAYMENT_ORDER + memberId)) { + return Result.error("请三秒之后再次下单"); + } + redisService.setex(SSK_MEMBER_PAYMENT_ORDER.concat(memberId), 3, memberId); + MemberPaymentOrder order = memberPaymentOrderService.createRecord(memberId, form.getMoney()); + return Result.success(order); + } +} diff --git a/src/main/java/io/v/nutz/pay/order/module/MemberPaymentOrder.java b/src/main/java/io/v/nutz/pay/order/module/MemberPaymentOrder.java new file mode 100644 index 0000000..7c0e17a --- /dev/null +++ b/src/main/java/io/v/nutz/pay/order/module/MemberPaymentOrder.java @@ -0,0 +1,76 @@ +package io.v.nutz.pay.order.module; + +import lombok.Data; +import org.nutz.dao.entity.annotation.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +@Data +@Comment("会员缴费订单") +@Table(value = "member_payment_order") +public class MemberPaymentOrder implements Serializable { + + @Comment("主键ID") + @Column(value = "id") + @ColDefine(type = ColType.VARCHAR, width = 32) + @Prev(els = {@EL("uuid()")}) + private String id; + + @Comment("订单编号") + @Column(value = "order_code") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String orderCode; + + @Comment("会员ID") + @Column(value = "member_id") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String memberId; + + @Comment("金额") + @Column(value = "money") + @ColDefine(type = ColType.DOUBLE, width = 32) + private Double money; + + @Comment("支付金额") + @Column(value = "pay_money") + @ColDefine(type = ColType.DOUBLE, width = 32) + private Double payMoney; + + @Comment("退款金额") + @Column(value = "refund_money") + @ColDefine(type = ColType.DOUBLE, width = 32) + private Double refundMoney; + + @Comment("订单状态: 0.待支付 1.部分支付 2.已支付 3.支付失败 4.已退款") + @Column(value = "status") + @ColDefine(type = ColType.INT) + private Integer status; + + @Comment("订单状态说明") + @Column(value = "status_reason") + @ColDefine(type = ColType.VARCHAR, width = 500) + private String statusReason; + + @Comment("最后支付时间") + @Column(value = "last_pay_time") + @ColDefine(type = ColType.TIMESTAMP) + private Date lastPayTime; + + @Comment("最后退款时间") + @Column(value = "last_refund_time") + @ColDefine(type = ColType.TIMESTAMP) + private Date lastRefundTime; + + @Comment("创建时间") + @Column(value = "create_time") + @ColDefine(type = ColType.TIMESTAMP) + private Date createTime; + + @Comment("过期时间") + @Column(value = "expired_time") + @ColDefine(type = ColType.TIMESTAMP) + private Date expiredTime; +} diff --git a/src/main/java/io/v/nutz/pay/order/service/IMemberPaymentOrderService.java b/src/main/java/io/v/nutz/pay/order/service/IMemberPaymentOrderService.java new file mode 100644 index 0000000..f791135 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/order/service/IMemberPaymentOrderService.java @@ -0,0 +1,15 @@ +package io.v.nutz.pay.order.service; + +import io.v.nutz.base.service.BaseService; +import io.v.nutz.pay.order.module.MemberPaymentOrder; + +public interface IMemberPaymentOrderService extends BaseService { + + MemberPaymentOrder getById(String id); + + public MemberPaymentOrder createRecord(String memberId, Double money); + + void paySuccess(String id, Double money); + + void payFail(String id); +} diff --git a/src/main/java/io/v/nutz/pay/order/service/impl/MemberPaymentOrderServiceImpl.java b/src/main/java/io/v/nutz/pay/order/service/impl/MemberPaymentOrderServiceImpl.java new file mode 100644 index 0000000..8544ce4 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/order/service/impl/MemberPaymentOrderServiceImpl.java @@ -0,0 +1,76 @@ +package io.v.nutz.pay.order.service.impl; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import io.v.nutz.base.service.impl.BaseServiceImpl; +import io.v.nutz.pay.enums.PaymentOrderStatusEnum; +import io.v.nutz.pay.order.module.MemberPaymentOrder; +import io.v.nutz.pay.order.service.IMemberPaymentOrderService; +import io.v.nutz.pay.util.BillNoUtil; +import org.nutz.dao.Dao; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.ioc.loader.annotation.IocBean; + +import java.util.List; + +@IocBean(args = {"refer:dao"}) +public class MemberPaymentOrderServiceImpl extends BaseServiceImpl implements IMemberPaymentOrderService { + + public MemberPaymentOrderServiceImpl(Dao dao) { + super(dao); + } + + @Override + public MemberPaymentOrder getById(String id) { + Sql sql = Sqls.create("select * from member_payment_order where id = @id limit 0,1"); + sql.params().set("id", id); + List orders = this.listEntity(sql); + if(orders != null && orders.size() > 0) { + return orders.get(0); + } else { + return null; + } + } + + @Override + public MemberPaymentOrder createRecord(String memberId, Double money) { + if(StrUtil.isBlank(memberId)) { + throw new RuntimeException("当前用户信息为空"); + } + if(money == null || money <= 0.0) { + throw new RuntimeException("缺少支付金额"); + } + MemberPaymentOrder order = new MemberPaymentOrder(); + order.setMemberId(memberId); + order.setOrderCode(BillNoUtil.getSimpleNo("MPC-")); + order.setMoney(money); + order.setStatus(PaymentOrderStatusEnum.WAIT.getCode()); + order.setCreateTime(DateUtil.date()); + this.insert(order); + return order; + } + + @Override + public void paySuccess(String id, Double money) { + MemberPaymentOrder order = this.getById(id); + order.setLastPayTime(DateUtil.date()); + order.setStatus(PaymentOrderStatusEnum.PAID.getCode()); + Double payMoney = order.getMoney(); + if(payMoney != null && payMoney > 0.0) { + payMoney = payMoney + money; + } else { + payMoney = money; + } + order.setPayMoney(payMoney); + this.updateById(order); + } + + @Override + public void payFail(String id) { + MemberPaymentOrder order = this.getById(id); + order.setLastPayTime(DateUtil.date()); + order.setStatus(PaymentOrderStatusEnum.FAIL.getCode()); + this.updateById(order); + } +} diff --git a/src/main/java/io/v/nutz/pay/pojo/LoginUser.java b/src/main/java/io/v/nutz/pay/pojo/LoginUser.java new file mode 100644 index 0000000..551bdb1 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/pojo/LoginUser.java @@ -0,0 +1,19 @@ +package io.v.nutz.pay.pojo; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.nutz.dao.entity.annotation.Comment; + +import java.io.Serializable; + +@Data +@NoArgsConstructor +@Comment("登录用户信息") +@Accessors(chain = true) +public class LoginUser implements Serializable { + + private String id; + + private String username; +} diff --git a/src/main/java/io/v/nutz/pay/record/controller/MemberPayRecordController.java b/src/main/java/io/v/nutz/pay/record/controller/MemberPayRecordController.java new file mode 100644 index 0000000..b04c4b2 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/record/controller/MemberPayRecordController.java @@ -0,0 +1,110 @@ +package io.v.nutz.pay.record.controller; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.wizzer.framework.base.Result; +import io.v.nutz.pay.annotations.Api; +import io.v.nutz.pay.annotations.ApiOperation; +import io.v.nutz.pay.cache.ChinaBankOrderWaitPayCacheService; +import io.v.nutz.pay.enums.ChannelTypeEnum; +import io.v.nutz.pay.enums.MemberPayBusinessTypeEnum; +import io.v.nutz.pay.enums.PayChannelEnum; +import io.v.nutz.pay.form.PayRecordForm; +import io.v.nutz.pay.pojo.LoginUser; +import io.v.nutz.pay.record.module.MemberPayRecord; +import io.v.nutz.pay.record.service.IMemberPayRecordService; +import io.v.nutz.pay.util.AuthUtil; +import io.v.nutz.pay.util.ServletUtil; +import org.nutz.integration.jedis.RedisService; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +import javax.servlet.http.HttpServletRequest; + +/** + * 会员用户支付记录 controller + */ +@IocBean +@Api("会员用户支付记录") +@At("/member/pay/record") +public class MemberPayRecordController { + + @Inject + private IMemberPayRecordService memberPayRecordService; + + @Inject + private RedisService redisService; + + private final static String SSK_MEMBER_PAY_ORDER_RECORD = "SSK:MEMBER:PAY:ORDER:RECORD:"; + + @Inject + private ChinaBankOrderWaitPayCacheService chinaBankOrderWaitPayCacheService; + + + @Ok("json") + @At("/get/by/code") + @ApiOperation("通过支付商品交易单号查询支付记录") + public Result getByCode(String goodsTradeCode) { + return Result.success(memberPayRecordService.getByGoodsTradeCode(goodsTradeCode)); + } + + @At("/pay") + @Ok("json") + @ApiOperation("生成会员缴费订单记录") + public Result pay(PayRecordForm form, HttpServletRequest request) { + Assert.notNull(form.getBusinessType(), "缺少业务类型"); + Assert.isTrue(StrUtil.isNotBlank(form.getBusinessId()), "缺少业务ID"); + Assert.notNull(form.getPayChannel(), "请选择支付方式"); + Assert.notNull(form.getChannelType(), "请选择支付方式"); + Assert.notNull(MemberPayBusinessTypeEnum.getByCode(form.getBusinessType()), "不支持该业务支付"); + Assert.notNull(PayChannelEnum.getByCode(form.getPayChannel()), "选择的支付方式暂时未开通"); + Assert.notNull(ChannelTypeEnum.getByCode(form.getChannelType()), "选择的支付方式暂时未开通"); + LoginUser loginUser = AuthUtil.instance().getLoginUser(); + String memberId = loginUser.getId(); + if(memberId == null) { + throw new RuntimeException("用户登录失效,请重新登录"); + } + // 设置三秒内用户不可重复下单 防止产生错误数据 + if(redisService.exists(SSK_MEMBER_PAY_ORDER_RECORD + memberId + ":" + form.getBusinessId())) { + return Result.error("请三秒之后再次支付"); + } + redisService.setex(SSK_MEMBER_PAY_ORDER_RECORD + memberId + ":" + form.getBusinessId(), 3, memberId); + + // 支付设备IP + String deviceIp = ServletUtil.getIp(request); + + try { + // 生成支付记录 + MemberPayRecord payRecord = memberPayRecordService.createRecord(form.getBusinessType(), form.getBusinessId(), form.getPayChannel(), form.getChannelType(), deviceIp); + return Result.success(payRecord); + }catch (Exception e) { + e.printStackTrace(); + return Result.error(e.getMessage()); + } + } + + + @At("/check/pay/code") + @Ok("json") + public Result checkPayCode(String payCode) { + if(StrUtil.isBlank(payCode)) { + return Result.success(); + } + + chinaBankOrderWaitPayCacheService.setItem(payCode, 60 * 60 * 24); + return Result.success(); + } + + @At("/del/check/pay/code") + @Ok("json") + public Result delCheckPayCode(String payCode) { + if(StrUtil.isBlank(payCode)) { + return Result.success(); + } + + chinaBankOrderWaitPayCacheService.delItem(payCode, 60 * 60 * 24); + return Result.success(); + } +} diff --git a/src/main/java/io/v/nutz/pay/record/module/MemberPayRecord.java b/src/main/java/io/v/nutz/pay/record/module/MemberPayRecord.java new file mode 100644 index 0000000..565a9c5 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/record/module/MemberPayRecord.java @@ -0,0 +1,116 @@ +package io.v.nutz.pay.record.module; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import org.nutz.dao.entity.annotation.*; + +import java.io.Serializable; +import java.util.Date; + +@Data +@Comment("会员缴费订单") +@Table(value = "member_pay_record") +public class MemberPayRecord implements Serializable { + + @Comment("主键ID") + @Column(value = "id") + @ColDefine(type = ColType.VARCHAR, width = 32) + @Prev(els = {@EL("uuid()")}) + private String id; + + @Comment("支付单号") + @Column(value = "pay_code") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String payCode; + + @Comment("业务类型:1.会员工会缴费") + @Column(value = "business_type") + @ColDefine(type = ColType.INT) + private Integer businessType; + + @Comment("业务ID") + @Column(value = "business_id") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String businessId; + + @Comment("业务订单号") + @Column(value = "business_order_code") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String businessOrderCode; + + @Comment("会员ID") + @Column(value = "member_id") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String memberId; + + @Comment("支付金额") + @Column(value = "pay_money") + @ColDefine(type = ColType.DOUBLE) + private Double payMoney; + + @Comment("支付状态:0.待支付 1.支付成功 2.支付失败 3.已退款") + @Column(value = "status") + @ColDefine(type = ColType.INT) + private Integer status; + + @Comment("状态原因") + @Column(value = "status_reason") + @ColDefine(type = ColType.VARCHAR, width = 500) + private String statusReason; + + @Comment("商品交易单号") + @Column(value = "goods_trade_code") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String goodsTradeCode; + + @Comment("交易单号") + @Column(value = "trade_no") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String tradeNo; + + @Comment("支付渠道:1.中国银行") + @Column(value = "pay_channel") + @ColDefine(type = ColType.INT) + private Integer payChannel; + + @Comment("渠道类型:1.微信APP") + @Column(value = "channel_type") + @ColDefine(type = ColType.INT) + private Integer channelType; + + @Comment("支付时间") + @Column(value = "pay_time") + @ColDefine(type = ColType.TIMESTAMP) + private Date payTime; + + @Comment("退款金额") + @Column(value = "refund_money") + @ColDefine(type = ColType.DOUBLE, width = 32) + private Double refundMoney; + + @Comment("最后退款时间") + @Column(value = "last_refund_time") + @ColDefine(type = ColType.TIMESTAMP) + private Date lastRefundTime; + + @Comment("创建时间") + @Column(value = "create_time") + @ColDefine(type = ColType.TIMESTAMP) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @Comment("请求结果") + @Column(value = "request_result") + @ColDefine(type = ColType.TIMESTAMP) + private String requestResult; + + @Comment("支付摘要") + @Column(value = "pay_remark") + @ColDefine(type = ColType.VARCHAR, width = 500) + private String payRemark; + + @Comment("设备IP") + @Column(value = "device_ip") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String deviceIp; +} diff --git a/src/main/java/io/v/nutz/pay/record/service/IMemberPayRecordService.java b/src/main/java/io/v/nutz/pay/record/service/IMemberPayRecordService.java new file mode 100644 index 0000000..3728e53 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/record/service/IMemberPayRecordService.java @@ -0,0 +1,25 @@ +package io.v.nutz.pay.record.service; + +import io.v.nutz.base.service.BaseService; +import io.v.nutz.pay.record.module.MemberPayRecord; + +import java.util.List; + +public interface IMemberPayRecordService extends BaseService { + + MemberPayRecord getByCode(String code); + + MemberPayRecord getById(String id); + + MemberPayRecord getByGoodsTradeCode(String goodsTradeCode); + + List getByGoodsTradeCode(Object... goodsTradeOrders); + + MemberPayRecord createRecord(Integer businessType, String businessId, Integer payChannel, Integer channelType, String deviceIp); + + void paySuccess(String goodsTradeCode, Double oriPayAmount); + + void payFail(String goodsTradeCode, String failReason); + + void checkChinaPayOrderStatus(); +} diff --git a/src/main/java/io/v/nutz/pay/record/service/impl/MemberPayRecordServiceImpl.java b/src/main/java/io/v/nutz/pay/record/service/impl/MemberPayRecordServiceImpl.java new file mode 100644 index 0000000..84a89b7 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/record/service/impl/MemberPayRecordServiceImpl.java @@ -0,0 +1,270 @@ +package io.v.nutz.pay.record.service.impl; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import io.v.nutz.base.service.impl.BaseServiceImpl; +import io.v.nutz.pay.bank.china.BankChinaWxPayService; +import io.v.nutz.pay.bank.china.pojo.WxPayOrderQueryResponse; +import io.v.nutz.pay.bank.china.property.ChinaPayProperties; +import io.v.nutz.pay.cache.ChinaBankOrderPayNotifyCacheService; +import io.v.nutz.pay.cache.ChinaBankOrderWaitPayCacheService; +import io.v.nutz.pay.common.CommonConstants; +import io.v.nutz.pay.enums.ChannelTypeEnum; +import io.v.nutz.pay.enums.MemberPayBusinessTypeEnum; +import io.v.nutz.pay.enums.MemberPayStatusEnum; +import io.v.nutz.pay.enums.PayChannelEnum; +import io.v.nutz.pay.order.module.MemberPaymentOrder; +import io.v.nutz.pay.order.service.IMemberPaymentOrderService; +import io.v.nutz.pay.record.module.MemberPayRecord; +import io.v.nutz.pay.record.service.IMemberPayRecordService; +import io.v.nutz.pay.util.BillNoUtil; +import lombok.extern.slf4j.Slf4j; +import org.nutz.dao.Chain; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +@Slf4j +@IocBean(args = {"refer:dao"}) +public class MemberPayRecordServiceImpl extends BaseServiceImpl implements IMemberPayRecordService { + + @Inject + private ChinaPayProperties chinaPayProperties; + + @Inject + private IMemberPaymentOrderService memberPaymentOrderService; + + @Inject + private BankChinaWxPayService bankChinaWxPayService; + + @Inject + private ChinaBankOrderWaitPayCacheService chinaBankOrderWaitPayCacheService; + + @Inject + private ChinaBankOrderPayNotifyCacheService chinaBankOrderPayNotifyCacheService; + + public MemberPayRecordServiceImpl(Dao dao) { + super(dao); + } + + @Override + public MemberPayRecord getByCode(String code) { + Sql sql = Sqls.create("select * from member_pay_record where pay_code = @pay_code limit 0,1"); + sql.setParam("pay_code", code); + List records = this.listEntity(sql); + if(records != null && records.size() > 0) { + return records.get(0); + } else { + return null; + } + } + + @Override + public MemberPayRecord getById(String id) { + Sql sql = Sqls.create("select * from member_pay_record where id = @id limit 0,1"); + sql.params().set("id", id); + List records = this.listEntity(sql); + if(records != null && records.size() > 0) { + return records.get(0); + } else { + return null; + } + } + + @Override + public MemberPayRecord getByGoodsTradeCode(String goodsTradeCode) { + Sql sql = Sqls.create("select * from member_pay_record where goods_trade_code = @goods_trade_code limit 0,1"); + sql.params().set("goods_trade_code", goodsTradeCode); + List records = this.listEntity(sql); + if(records != null && records.size() > 0) { + return records.get(0); + } else { + return null; + } + } + + @Override + public List getByGoodsTradeCode(Object... goodsTradeOrders) { + Sql sql = Sqls.create("select * from member_pay_record where $goodsTradeOrders"); + StringBuilder where = new StringBuilder(); + for(int i = 0; i < goodsTradeOrders.length; i++) { + where.append("goods_trade_code='" + goodsTradeOrders[i].toString() + "' or "); + } + String whereStr = where.toString().substring(0, where.toString().lastIndexOf("or")); + sql.vars().set("goodsTradeOrders", whereStr); + List records = this.listEntity(sql); + return records; + } + + @Override + public MemberPayRecord createRecord(Integer businessType, String businessId, Integer payChannel, Integer channelType, String deviceIp) { + // 初始化配置参数 + chinaPayProperties.load(); + + MemberPayRecord record = new MemberPayRecord(); + record.setBusinessId(businessId); + record.setBusinessType(businessType); + record.setPayChannel(payChannel); + record.setChannelType(channelType); + record.setCreateTime(DateUtil.date()); + record.setStatus(MemberPayStatusEnum.WAIT.getCode()); + record.setPayCode(BillNoUtil.getSimpleNo("MP-")); + record.setDeviceIp(deviceIp); + + // 中国银行支付中商品交易单号需要特殊处理 该单号用于发起支付-> 商户订单号 + if(PayChannelEnum.CHINA_BANK.getCode().equals(payChannel)) { + record.setGoodsTradeCode(chinaPayProperties.getMerchantNoAfterChar(6).concat(BillNoUtil.getTimeNo(24))); + } else { + record.setGoodsTradeCode(record.getPayCode()); + } + + // 会员工会缴费 + if(MemberPayBusinessTypeEnum.UNION_PAYMENT.getCode().equals(businessType)) { + // 会员工会缴费订单信息 + MemberPaymentOrder paymentOrder = memberPaymentOrderService.getById(businessId); + if(paymentOrder == null) { + throw new RuntimeException("未获取到会员工会缴费订单信息"); + } + record.setBusinessOrderCode(paymentOrder.getOrderCode()); + record.setPayMoney(paymentOrder.getMoney()); + record.setMemberId(paymentOrder.getMemberId()); + + } else { + throw new RuntimeException("不支持的支付业务"); + } + this.insert(record); + return this.sendPay(record); + } + + @Override + public void paySuccess(String goodsTradeCode, Double oriPayAmount) { + MemberPayRecord record = this.getByGoodsTradeCode(goodsTradeCode); + if(record == null) { + throw new RuntimeException("商品交易单号不存在"); + } + record.setStatus(MemberPayStatusEnum.SUCCESS.getCode()); + record.setPayTime(DateUtil.date()); + this.updateById(record); + + // 会员工会缴费 + if(MemberPayBusinessTypeEnum.UNION_PAYMENT.getCode().equals(record.getBusinessType())) { + // 支付成功 + memberPaymentOrderService.paySuccess(record.getBusinessId(), oriPayAmount); + } else { + throw new RuntimeException("不支持的业务支付通知"); + } + } + + @Override + public void payFail(String goodsTradeCode, String failReason) { + MemberPayRecord record = this.getByGoodsTradeCode(goodsTradeCode); + if(record == null) { + throw new RuntimeException("商品交易单号不存在"); + } + record.setStatus(MemberPayStatusEnum.FAIL.getCode()); + record.setPayTime(DateUtil.date()); + record.setStatusReason(failReason); + this.updateById(record); + + // 会员工会缴费 + if(MemberPayBusinessTypeEnum.UNION_PAYMENT.getCode().equals(record.getBusinessType())) { + // 支付成功 + memberPaymentOrderService.payFail(record.getBusinessId()); + } else { + throw new RuntimeException("不支持的业务支付通知"); + } + } + + @Override + public void checkChinaPayOrderStatus() { + try { + // 初始化配置参数 + chinaPayProperties.load(); + + String goodsTradeOrder = chinaBankOrderWaitPayCacheService.get(); + if (StrUtil.isBlank(goodsTradeOrder)) { + log.info("未检测中国银行支付订单:null"); + return; + } + List goodsTradeOrders = JSON.parseArray(goodsTradeOrder, String.class); + log.info("检测中国银行支付订单:{}", goodsTradeOrders.size()); + if (goodsTradeOrders.size() == 0) { + return; + } + List payRecords = this.getByGoodsTradeCode(goodsTradeOrders.toArray()); + if (payRecords == null || payRecords.isEmpty()) { + return; + } + for (MemberPayRecord record : payRecords) { + if (!record.getStatus().equals(MemberPayStatusEnum.WAIT.getCode())) { + continue; + } + if (!record.getPayChannel().equals(PayChannelEnum.CHINA_BANK.getCode())) { + continue; + } + WxPayOrderQueryResponse response = bankChinaWxPayService.queryOrder(record.getGoodsTradeCode()); + if(chinaBankOrderPayNotifyCacheService.exists(record.getGoodsTradeCode())) { + continue; + } + chinaBankOrderPayNotifyCacheService.setex(record.getGoodsTradeCode(), 30, record.getGoodsTradeCode()); + if(response.getTranStatus() != null + &&(response.getTranStatus().equals(CommonConstants.ONE.toString()) || response.getTranStatus().equals(CommonConstants.TWO.toString()))) { + // 支付成功 + if(response.getTranStatus().equals(CommonConstants.ONE.toString())) { + this.paySuccess(response.getOrderNo(), Double.valueOf(response.getTranAmount())); + + // 支付失败 + } else { + this.payFail(response.getOrderNo(), "支付失败"); + } + chinaBankOrderWaitPayCacheService.delItem(record.getGoodsTradeCode()); + } + } + } catch (Exception e) { + e.printStackTrace(); + log.error("中国银行支付检测待支付订单异常:{}", e.getMessage()); + } + } + + private MemberPayRecord sendPay(MemberPayRecord record) { + try { + Object requestResult = null; + // 中国银行支付渠道 + if (PayChannelEnum.CHINA_BANK.getCode().equals(record.getPayChannel())) { + + // 微信APP支付 + if (ChannelTypeEnum.WX_APP.getCode().equals(record.getChannelType())) { + // 获取中国银行微信支付拉起支付订单信息 + requestResult = bankChinaWxPayService.appPay( + record.getGoodsTradeCode(), + record.getPayMoney(), + record.getCreateTime(), + record.getPayRemark(), + chinaPayProperties.getNotifyDomain() + "/notify/china/bank/pay", + record.getPayRemark(), + record.getDeviceIp()); + } + + } else { + throw new RuntimeException("不支持的支付方式"); + } + if (requestResult != null) { + record.setRequestResult(JSONObject.toJSONString(requestResult)); + this.updateById(record); + } + } catch (Exception e) { + this.payFail(record.getGoodsTradeCode(), e.getMessage()); + throw new RuntimeException(e.getMessage()); + } + return record; + } +} diff --git a/src/main/java/io/v/nutz/pay/task/PayOrderCheckTask.java b/src/main/java/io/v/nutz/pay/task/PayOrderCheckTask.java new file mode 100644 index 0000000..b6adcc5 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/task/PayOrderCheckTask.java @@ -0,0 +1,49 @@ +package io.v.nutz.pay.task; + +import io.v.nutz.pay.bank.china.BankChinaWxPayService; +import io.v.nutz.pay.bank.china.property.ChinaPayProperties; +import io.v.nutz.pay.enums.PayChannelEnum; +import io.v.nutz.pay.record.service.IMemberPayRecordService; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class PayOrderCheckTask implements Runnable { + + private PayChannelEnum payChannelEnum; + + private IMemberPayRecordService memberPayRecordService; + + private long seconds = 5; // 每隔5秒执行一次 + + public PayOrderCheckTask(PayChannelEnum payChannelEnum, IMemberPayRecordService memberPayRecordService, long seconds) { + this.memberPayRecordService = memberPayRecordService; + this.payChannelEnum = payChannelEnum; + this.seconds = seconds; + } + + @Override + public void run() { + // 启动中国银行支付订单检测 + if(payChannelEnum != null && payChannelEnum.getCode().equals(PayChannelEnum.CHINA_BANK.getCode())) { + log.info("业务支付中国银行支付订单检测线程启动成功"); + this.startChinaPay(); + } + log.info("业务支付方式订单检测线程启动全部成功"); + } + + // 中国银行支付 + public void startChinaPay() { + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5); + Runnable task = () -> { + // 检查中国银行支付订单状态 + memberPayRecordService.checkChinaPayOrderStatus(); + }; + + // 安排首次执行延迟0秒后,之后每隔5秒执行一次 + scheduler.scheduleAtFixedRate(task, 0, seconds, TimeUnit.SECONDS); + } +} diff --git a/src/main/java/io/v/nutz/pay/util/AuthUtil.java b/src/main/java/io/v/nutz/pay/util/AuthUtil.java new file mode 100644 index 0000000..21ddc3e --- /dev/null +++ b/src/main/java/io/v/nutz/pay/util/AuthUtil.java @@ -0,0 +1,28 @@ +package io.v.nutz.pay.util; + +import io.v.nutz.pay.pojo.LoginUser; +import io.v.nutz.sys.models.Sys_user; +import io.v.nutz.web.commons.utils.ShiroUtil; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.subject.Subject; + +public class AuthUtil { + + public static AuthUtil instance() { + return new AuthUtil(); + } + + public LoginUser getLoginUser() { + + return new LoginUser().setId(ShiroUtil.getUserId()) + .setUsername(ShiroUtil.getPlatformUsername()); + } + + public String getUserId() { + return getLoginUser().getId(); + } + + public String getUsername() { + return getLoginUser().getUsername(); + } +} diff --git a/src/main/java/io/v/nutz/pay/util/BillNoUtil.java b/src/main/java/io/v/nutz/pay/util/BillNoUtil.java new file mode 100644 index 0000000..27c2073 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/util/BillNoUtil.java @@ -0,0 +1,41 @@ +package io.v.nutz.pay.util; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.RandomUtil; + +import java.util.Date; +import java.util.Random; + +public class BillNoUtil { + /** + * 雪花算法获取订单ID + * @param pre 单号ID前缀,自定义即可 + * @return + */ + public static String getSimpleNo(String pre) { + return pre + IdUtil.getSnowflake().nextId(); + } + + public static String getDateNo(String pre, Integer length) { + String no = "000000000" + RandomUtil.randomNumbers(length); + no = no.substring(no.length() - length, no.length()); + return pre + DateUtil.format(new Date(), "yyMMddHH") + no; + } + + public static String getTimeNo(Integer length) { + String dateStart = DateUtil.format(new Date(), "yyyyMMddHHmmss"); + Integer roundLength = length - dateStart.length(); + String roundStr = generate24Digit(roundLength); + return dateStart + roundStr; + } + + public static String generate24Digit(Integer length) { + Random random = new Random(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + sb.append(random.nextInt(10)); // 生成0-9的随机数字 + } + return sb.toString(); + } +} diff --git a/src/main/java/io/v/nutz/pay/util/PKCS7Tool.java b/src/main/java/io/v/nutz/pay/util/PKCS7Tool.java new file mode 100644 index 0000000..68be1b7 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/util/PKCS7Tool.java @@ -0,0 +1,139 @@ +package io.v.nutz.pay.util; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.security.*; +import java.security.cert.Certificate; +import java.util.Base64; +import java.util.Enumeration; + +@Slf4j +public class PKCS7Tool { + + // 加密算法 + private static String KEY_ALGORITHM = "SHA256withRSA"; + + public static PKCS7Tool instance() { + return new PKCS7Tool(); + } + + + + /** + * Pkcs7Detach 签名 + * @param signFilePath 签名证书 + * @param password 证书秘钥 + * @param content 内容 + * @return + */ + public String signature(String signFilePath, String password, byte[] content){ + if (StringUtils.isEmpty(signFilePath)) { + throw new RuntimeException("缺少证书"); + } + if (StringUtils.isEmpty(password)) { + throw new RuntimeException("缺少证书密码"); + } + log.info("生成签名信息:signFilePath-->{}, password-->{}", signFilePath, password); + try { + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + + //创建密钥对 + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + + File file = new File(signFilePath); + if(!file.exists()) { + throw new RuntimeException("证书路径不存在"); + } + FileInputStream fis = new FileInputStream(file); + keyStore.load(fis, password.toCharArray()); + fis.close(); + + //获取别名 + Enumeration enumas = keyStore.aliases(); + String alias = null; + while (enumas.hasMoreElements()) { + alias = (String) enumas.nextElement(); + } + + //准备密钥对 + Key key = keyStore.getKey(alias, password.toCharArray()); + Signature sig = Signature.getInstance(KEY_ALGORITHM); + // 使用私钥初始化 Signature 对象 + sig.initSign((PrivateKey) key); + sig.update(content); + // 生成签名 + byte[] sign = sig.sign(); + log.info("Pkcs7Detach 签名成功,加密后的密文为:{}", Base64.getEncoder().encodeToString(sign)); + return Base64.getEncoder().encodeToString(sign); + } catch (Exception e) { + e.printStackTrace(); + log.error("Pkcs7Detach 签名异常:"+e.getMessage()); + } + return null; + } + + /** + * 签名验证 + * @param verifySignFilePath 验证签名证书 + * @param password 证书秘钥 + * @param data data是你的原始数据 + * @param signature signature是之前生成的PKCS#7格式的签名 + * @return boolean + */ + public boolean verifySignature(String verifySignFilePath, String password, byte[] data, byte[] signature) { + if (StringUtils.isEmpty(verifySignFilePath)) { + throw new RuntimeException("缺少证书"); + } + if (StringUtils.isEmpty(password)) { + throw new RuntimeException("缺少证书密码"); + } + log.info("验证签名信息:verifySignFilePath-->{}, password-->{}", verifySignFilePath, password); + try { + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + + //创建密钥对 + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + + File file = new File(verifySignFilePath); + FileInputStream fis = new FileInputStream(file); + keyStore.load(fis, password.toCharArray()); + fis.close(); + + //获取别名 + Enumeration enumas = keyStore.aliases(); + String alias = null; + while (enumas.hasMoreElements()) { + alias = (String) enumas.nextElement(); + } + + //准备密钥对 + Key key = keyStore.getKey(alias, password.toCharArray()); + PublicKey publicKey=null; + if (key instanceof PrivateKey) { + //获取用户证书 + Certificate cert = keyStore.getCertificate(alias); + publicKey = cert.getPublicKey(); + } + Signature sig = Signature.getInstance(KEY_ALGORITHM); + // 使用私钥初始化 Signature 对象 + sig.initSign((PrivateKey) key); + sig.update(data); + // 使用公钥验证签名 + sig.initVerify(publicKey); + sig.update(data); + boolean isVerified = sig.verify(signature); + log.info("Pkcs7Detach 验签结果:",isVerified); + if (isVerified) { + log.info("Pkcs7Detach 验签成功,返回为:",isVerified); + } + return isVerified; + } catch (Exception e) { + e.printStackTrace(); + log.error("Pkcs7Detach 验证失败:{}",e.getMessage()); + } + return false; + } +} diff --git a/src/main/java/io/v/nutz/pay/util/ServletUtil.java b/src/main/java/io/v/nutz/pay/util/ServletUtil.java new file mode 100644 index 0000000..017b354 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/util/ServletUtil.java @@ -0,0 +1,43 @@ +package io.v.nutz.pay.util; + +import javax.servlet.http.HttpServletRequest; + +public class ServletUtil { + + public static String getIp(HttpServletRequest request) { + if (request == null) { + return null; + } else { + String ip = request.getHeader("x-forwarded-for"); + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("X-Real-IP"); + } + + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("http_client_ip"); + } + + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + + if (ip != null && ip.indexOf(",") != -1) { + ip = ip.substring(ip.lastIndexOf(",") + 1, ip.length()).trim(); + } + + return ip; + } + } +} diff --git a/src/main/java/io/v/nutz/pay/util/XmlUtil.java b/src/main/java/io/v/nutz/pay/util/XmlUtil.java new file mode 100644 index 0000000..af996e9 --- /dev/null +++ b/src/main/java/io/v/nutz/pay/util/XmlUtil.java @@ -0,0 +1,206 @@ +package io.v.nutz.pay.util; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.*; +import org.w3c.dom.*; +import org.xml.sax.InputSource; + +public class XmlUtil +{ + + public XmlUtil() + { + } + + public static Schema createSchema(File file) + throws Exception + { + SchemaFactory schemafactory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema"); + return schemafactory.newSchema(file); + } + + public static void validateViaDOM(Document document, Schema schema) + throws Exception + { + DOMSource domsource = new DOMSource(document); + Validator validator = schema.newValidator(); + validator.validate(domsource); + } + + public static void validateViaDOM(String s, Schema schema) + throws Exception + { + DOMSource domsource = new DOMSource(createDocument(s)); + Validator validator = schema.newValidator(); + validator.validate(domsource); + } + + public static void validateViaSAX(String s, Schema schema) + throws Exception + { + SAXSource saxsource = new SAXSource(new InputSource(new InputStreamReader(new ByteArrayInputStream(s.getBytes("UTF-8"))))); + Validator validator = schema.newValidator(); + validator.validate(saxsource); + } + + public static void validateViaStreamSource(String s, Schema schema) + throws Exception + { + StreamSource streamsource = new StreamSource(new ByteArrayInputStream(s.getBytes("UTF-8"))); + Validator validator = schema.newValidator(); + validator.validate(streamsource); + } + + public static Document createDocument(String s) + throws Exception + { + DocumentBuilder documentbuilder = createDocumentBuilder(); + return documentbuilder.parse(new ByteArrayInputStream(s.getBytes("UTF-8"))); + } + + public static Document createDocument(String s, String s1) + throws Exception + { + DocumentBuilder documentbuilder = createDocumentBuilder(); + return documentbuilder.parse(new ByteArrayInputStream(s.getBytes(s1))); + } + + public static Document createDocument(File file) + throws Exception + { + DocumentBuilder documentbuilder = createDocumentBuilder(); + return documentbuilder.parse(file); + } + + public static DocumentBuilder createDocumentBuilder() + throws Exception + { + DocumentBuilderFactory documentbuilderfactory = DocumentBuilderFactory.newInstance(); + return documentbuilderfactory.newDocumentBuilder(); + } + + public static String getNodeText(Document document, String s) + throws Exception + { + return getNodeText(document, s, 0); + } + + public static String getNodeText(Document document, String s, String defaultValue) + throws Exception + { + return getNodeText(document, s, 0) == null? defaultValue : getNodeText(document, s, 0); + } + + public static String getNodeText(Document document, String s, int i) + throws Exception + { + NodeList nodelist = document.getElementsByTagName(s); + if(nodelist == null || i >= nodelist.getLength()) + return null; + else + return nodelist.item(i).getTextContent(); + } + + public static String getChildNodeText(Node node, String s) + { + NodeList nodelist = node.getChildNodes(); + int i = nodelist.getLength(); + for(int j = 0; j < i; j++) + { + Node node1 = nodelist.item(j); + if(node1.getNodeType() == 1 && node1.getNodeName() == s) + return node1.getTextContent(); + } + + return null; + } + + public static List getNodeList(Document document, String s) + throws Exception + { + NodeList nodelist = document.getElementsByTagName(s); + ArrayList arraylist = new ArrayList(); + if(nodelist == null) + return null; + for(int i = 0; i < nodelist.getLength(); i++) + arraylist.add(nodelist.item(i).getTextContent()); + + return arraylist; + } + + public static String getNodeAttributeValue(Document document, String s, String s1) + throws Exception + { + NodeList nodelist = document.getElementsByTagName(s); + if(nodelist != null && nodelist.getLength() > 0) + { + Element element = (Element)nodelist.item(0); + return element.getAttribute(s1); + } else + { + return null; + } + } + + public static String getNodeText(Node node, String s) + throws Exception + { + NodeList nodelist = node.getChildNodes(); + int i = nodelist.getLength(); + for(int j = 0; j < i; j++) + { + Node node1 = nodelist.item(j); + if(node1.getNodeType() == 1 && node1.getNodeName().equals(s)) + return node1.getTextContent(); + } + + return null; + } + + public static String createPrettyFormat(Document document) + throws Exception + { + return createPrettyFormat(document, "UTF-8"); + } + + public static String createPrettyFormat(Document document, String s) + throws Exception + { + DOMSource domsource = new DOMSource(document); + TransformerFactory transformerfactory = TransformerFactory.newInstance(); + Transformer transformer = transformerfactory.newTransformer(); + transformer.setOutputProperty("indent", "yes"); + transformer.setOutputProperty("encoding", s); + StringWriter stringwriter = new StringWriter(); + transformer.transform(domsource, new StreamResult(stringwriter)); + return stringwriter.toString(); + } + + public static String createCompactFormat(Document document) + throws Exception + { + return createCompactFormat(document, "UTF-8"); + } + + public static String createCompactFormat(Document document, String s) + throws Exception + { + DOMSource domsource = new DOMSource(document); + TransformerFactory transformerfactory = TransformerFactory.newInstance(); + Transformer transformer = transformerfactory.newTransformer(); + transformer.setOutputProperty("encoding", s); + StringWriter stringwriter = new StringWriter(); + transformer.transform(domsource, new StreamResult(stringwriter)); + return stringwriter.toString(); + } +} diff --git a/src/main/resources/views/mobile/pay/index.html b/src/main/resources/views/mobile/pay/index.html new file mode 100644 index 0000000..a88d131 --- /dev/null +++ b/src/main/resources/views/mobile/pay/index.html @@ -0,0 +1,508 @@ + + + + +
+ +
+
订单号:{{ urlParams.orderNo }}
+
订单有效期 + + {{ expiredTime.hours }}: + {{ expiredTime.minutes }}: + {{ expiredTime.seconds }}s +
+
¥{{ urlParams.money }}
+
{{ urlParams.remark || '您正在为当前业务缴费' }}
+
+
+
+
+
支付方式
+
+
+
+
+
+ +
+
+ + + +
+
+
+
+
+
+
+
+
+ 确认支付 +
+
+ 取消支付 +
+
+
+
+ + \ No newline at end of file