商户系统支持转账功能;

This commit is contained in:
terrfly 2021-08-13 22:08:02 +08:00
parent 659b75b7a4
commit 5c110d3a71
9 changed files with 459 additions and 2 deletions

View File

@ -548,6 +548,11 @@ insert into t_sys_entitlement values('ENT_MCH_CENTER', '商户中心', 'team', '
insert into t_sys_entitlement values('ENT_MCH_PAY_TEST_PAYWAY_LIST', '页面:获取全部支付方式', 'no-icon', '', '', 'PB', 0, 1, 'ENT_MCH_PAY_TEST', '0', 'MCH', now(), now());
insert into t_sys_entitlement values('ENT_MCH_PAY_TEST_DO', '按钮:支付测试', 'no-icon', '', '', 'PB', 0, 1, 'ENT_MCH_PAY_TEST', '0', 'MCH', now(), now());
insert into t_sys_entitlement values('ENT_MCH_TRANSFER', '转账', 'property-safety', '/doTransfer', 'MchTransferPage', 'ML', 0, 1, 'ENT_MCH_CENTER', '30', 'MCH', now(), now());
insert into t_sys_entitlement values('ENT_MCH_TRANSFER_IF_CODE_LIST', '页面:获取全部代付通道', 'no-icon', '', '', 'PB', 0, 1, 'ENT_MCH_TRANSFER', '0', 'MCH', now(), now());
insert into t_sys_entitlement values('ENT_MCH_TRANSFER_CHANNEL_USER', '按钮:获取渠道用户', 'no-icon', '', '', 'PB', 0, 1, 'ENT_MCH_TRANSFER', '0', 'MCH', now(), now());
insert into t_sys_entitlement values('ENT_MCH_TRANSFER_DO', '按钮:发起转账', 'no-icon', '', '', 'PB', 0, 1, 'ENT_MCH_TRANSFER', '0', 'MCH', now(), now());
-- 【商户系统】 订单管理
insert into t_sys_entitlement values('ENT_ORDER', '订单中心', 'transaction', '', 'RouteView', 'ML', 0, 1, 'ROOT', '20', 'MCH', now(), now());
insert into t_sys_entitlement values('ENT_PAY_ORDER', '订单管理', 'account-book', '/pay', 'PayOrderListPage', 'ML', 0, 1, 'ENT_ORDER', '10', 'MCH', now(), now());

View File

@ -88,4 +88,9 @@ insert into t_sys_entitlement values('ENT_TRANSFER_ORDER_VIEW', '按钮:详情
insert into t_sys_entitlement values('ENT_TRANSFER_ORDER', '转账订单', 'property-safety', '/transfer', 'TransferOrderListPage', 'ML', 0, 1, 'ENT_ORDER', '30', 'MCH', now(), now());
insert into t_sys_entitlement values('ENT_TRANSFER_ORDER_LIST', '页面:转账订单列表', 'no-icon', '', '', 'PB', 0, 1, 'ENT_TRANSFER_ORDER', '0', 'MCH', now(), now());
insert into t_sys_entitlement values('ENT_TRANSFER_ORDER_VIEW', '按钮:详情', 'no-icon', '', '', 'PB', 0, 1, 'ENT_TRANSFER_ORDER', '0', 'MCH', now(), now());
insert into t_sys_entitlement values('ENT_MCH_TRANSFER', '转账', 'property-safety', '/doTransfer', 'MchTransferPage', 'ML', 0, 1, 'ENT_MCH_CENTER', '30', 'MCH', now(), now());
insert into t_sys_entitlement values('ENT_MCH_TRANSFER_IF_CODE_LIST', '页面:获取全部代付通道', 'no-icon', '', '', 'PB', 0, 1, 'ENT_MCH_TRANSFER', '0', 'MCH', now(), now());
insert into t_sys_entitlement values('ENT_MCH_TRANSFER_CHANNEL_USER', '按钮:获取渠道用户', 'no-icon', '', '', 'PB', 0, 1, 'ENT_MCH_TRANSFER', '0', 'MCH', now(), now());
insert into t_sys_entitlement values('ENT_MCH_TRANSFER_DO', '按钮:发起转账', 'no-icon', '', '', 'PB', 0, 1, 'ENT_MCH_TRANSFER', '0', 'MCH', now(), now());
## -- ++++ ++++

View File

@ -0,0 +1,55 @@
/*
* Copyright (c) 2021-2031, 河北计全科技有限公司 (https://www.jeequan.com & jeequan@126.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jeequan.jeepay.mch.ctrl.transfer;
import com.alibaba.fastjson.JSONObject;
import com.jeequan.jeepay.mch.ctrl.CommonCtrl;
import com.jeequan.jeepay.mch.websocket.server.WsChannelUserIdServer;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 获取用户ID - 回调函数
*
* @author terrfly
* @site https://www.jeequan.com
* @date 2021/8/13 17:54
*/
@Controller
@RequestMapping("/api/anon/channelUserIdCallback")
public class ChannelUserIdNotifyController extends CommonCtrl {
@RequestMapping("")
public String channelUserIdCallback() {
try {
//请求参数
JSONObject params = getReqParamJSON();
String extParam = params.getString("extParam");
String channelUserId = params.getString("channelUserId");
String appId = params.getString("appId");
//推送到前端
WsChannelUserIdServer.sendMsgByAppAndCid(appId, extParam, channelUserId);
} catch (Exception e) {
request.setAttribute("errMsg", e.getMessage());
}
return "channelUser/getChannelUserIdPage";
}
}

View File

@ -0,0 +1,149 @@
/*
* Copyright (c) 2021-2031, 河北计全科技有限公司 (https://www.jeequan.com & jeequan@126.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jeequan.jeepay.mch.ctrl.transfer;
import com.alibaba.fastjson.JSONObject;
import com.jeequan.jeepay.JeepayClient;
import com.jeequan.jeepay.core.constants.CS;
import com.jeequan.jeepay.core.entity.MchApp;
import com.jeequan.jeepay.core.entity.MchPayPassage;
import com.jeequan.jeepay.core.entity.PayInterfaceConfig;
import com.jeequan.jeepay.core.entity.PayInterfaceDefine;
import com.jeequan.jeepay.core.exception.BizException;
import com.jeequan.jeepay.core.model.ApiRes;
import com.jeequan.jeepay.core.utils.JeepayKit;
import com.jeequan.jeepay.core.utils.StringKit;
import com.jeequan.jeepay.exception.JeepayException;
import com.jeequan.jeepay.mch.ctrl.CommonCtrl;
import com.jeequan.jeepay.model.PayOrderCreateReqModel;
import com.jeequan.jeepay.model.TransferOrderCreateReqModel;
import com.jeequan.jeepay.model.TransferOrderCreateResModel;
import com.jeequan.jeepay.request.PayOrderCreateRequest;
import com.jeequan.jeepay.request.TransferOrderCreateRequest;
import com.jeequan.jeepay.response.PayOrderCreateResponse;
import com.jeequan.jeepay.response.TransferOrderCreateResponse;
import com.jeequan.jeepay.service.impl.*;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 转账api
*
* @author terrfly
* @site https://www.jeequan.com
* @date 2021/8/13 14:43
*/
@RestController
@RequestMapping("/api/mchTransfers")
public class MchTransferController extends CommonCtrl {
@Autowired private MchAppService mchAppService;
@Autowired private PayInterfaceConfigService payInterfaceConfigService;
@Autowired private PayInterfaceDefineService payInterfaceDefineService;
@Autowired private SysConfigService sysConfigService;
/** 查询商户对应应用下支持的支付通道 **/
@PreAuthorize("hasAuthority('ENT_MCH_TRANSFER_IF_CODE_LIST')")
@GetMapping("/ifCodes/{appId}")
public ApiRes ifCodeList(@PathVariable("appId") String appId) {
List<String> ifCodeList = new ArrayList<>();
payInterfaceConfigService.list(
PayInterfaceConfig.gw().select(PayInterfaceConfig::getIfCode)
.eq(PayInterfaceConfig::getInfoType, CS.INFO_TYPE_MCH_APP)
.eq(PayInterfaceConfig::getInfoId, appId)
.eq(PayInterfaceConfig::getState, CS.PUB_USABLE)
).stream().forEach(r -> ifCodeList.add(r.getIfCode()));
if(ifCodeList.isEmpty()){
return ApiRes.ok(ifCodeList);
}
List<PayInterfaceDefine> result = payInterfaceDefineService.list(PayInterfaceDefine.gw().in(PayInterfaceDefine::getIfCode, ifCodeList));
return ApiRes.ok(result);
}
/** 获取渠道侧用户ID **/
@PreAuthorize("hasAuthority('ENT_MCH_TRANSFER_CHANNEL_USER')")
@GetMapping("/channelUserId")
public ApiRes channelUserId() {
String appId = getValStringRequired("appId");
MchApp mchApp = mchAppService.getById(appId);
if(mchApp == null || mchApp.getState() != CS.PUB_USABLE || !mchApp.getMchNo().equals(getCurrentMchNo())){
throw new BizException("商户应用不存在或不可用");
}
JSONObject param = getReqParamJSON();
param.put("mchNo", getCurrentMchNo());
param.put("appId", appId);
param.put("ifCode", getValStringRequired("ifCode"));
param.put("extParam", getValStringRequired("extParam"));
param.put("reqTime", System.currentTimeMillis() + "");
param.put("version", "1.0");
param.put("signType", "MD5");
param.put("redirectUrl", sysConfigService.getDBApplicationConfig().getMchSiteUrl() + "/api/anon/channelUserIdCallback");
param.put("sign", JeepayKit.getSign(param, mchApp.getAppSecret()));
String url = StringKit.appendUrlQuery(sysConfigService.getDBApplicationConfig().getPaySiteUrl() + "/api/channelUserId/jump", param);
return ApiRes.ok(url);
}
/** 调起下单接口 **/
@PreAuthorize("hasAuthority('ENT_MCH_PAY_TEST_DO')")
@PostMapping("/doTransfer")
public ApiRes doTransfer() {
handleParamAmount("amount");
TransferOrderCreateReqModel model = getObject(TransferOrderCreateReqModel.class);
MchApp mchApp = mchAppService.getById(model.getAppId());
if(mchApp == null || mchApp.getState() != CS.PUB_USABLE || !mchApp.getMchNo().equals(getCurrentMchNo()) ){
throw new BizException("商户应用不存在或不可用");
}
TransferOrderCreateRequest request = new TransferOrderCreateRequest();
model.setMchNo(this.getCurrentMchNo());
model.setAppId(mchApp.getAppId());
model.setCurrency("CNY");
request.setBizModel(model);
JeepayClient jeepayClient = new JeepayClient(sysConfigService.getDBApplicationConfig().getPaySiteUrl(), mchApp.getAppSecret());
try {
TransferOrderCreateResponse response = jeepayClient.execute(request);
if(response.getCode() != 0){
throw new BizException(response.getMsg());
}
return ApiRes.ok(response.get());
} catch (JeepayException e) {
throw new BizException(e.getMessage());
}
}
}

View File

@ -0,0 +1,167 @@
/*
* Copyright (c) 2021-2031, 河北计全科技有限公司 (https://www.jeequan.com & jeequan@126.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jeequan.jeepay.mch.websocket.server;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* WebSocket服务类
* /ws/channelUserId/{appId}/{客戶端自定義ID}
*
* @author terrfly
* @site https://www.jeequan.com
* @date 2021/8/13 18:57
*/
@ServerEndpoint("/api/anon/ws/channelUserId/{appId}/{cid}")
@Component
public class WsChannelUserIdServer {
private final static Logger logger = LoggerFactory.getLogger(WsChannelUserIdServer.class);
//当前在线客户端 数量
private static int onlineClientSize = 0;
// appId WsPayOrderServer 存储关系, ConcurrentHashMap保证线程安全
private static Map<String, Set<WsChannelUserIdServer>> wsAppIdMap = new ConcurrentHashMap<>();
//与某个客户端的连接会话需要通过它来给客户端发送数据
private Session session;
//客户端自定义ID
private String cid = "";
//支付订单号
private String appId = "";
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("appId") String appId, @PathParam("cid") String cid) {
try {
//设置当前属性
this.cid = cid;
this.appId = appId;
this.session = session;
Set<WsChannelUserIdServer> wsServerSet = wsAppIdMap.get(appId);
if(wsServerSet == null) {
wsServerSet = new CopyOnWriteArraySet<>();
}
wsServerSet.add(this);
wsAppIdMap.put(appId, wsServerSet);
addOnlineCount(); //在线数加1
logger.info("cid[{}],appId[{}]连接开启监听!当前在线人数为{}", cid, appId, onlineClientSize);
} catch (Exception e) {
logger.error("ws监听异常cid[{}],appId[{}]", cid, appId, e);
}
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
Set wsSet = wsAppIdMap.get(this.appId);
wsSet.remove(this);
if(wsSet.isEmpty()) {
wsAppIdMap.remove(this.appId);
}
subOnlineCount(); //在线数减1
logger.info("cid[{}],appId[{}]连接关闭!当前在线人数为{}", cid, appId, onlineClientSize);
}
/**
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
logger.error("ws发生错误", error);
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
/**
* 根据订单ID,推送消息
* 捕捉所有的异常避免影响业务
* @param appId
*/
public static void sendMsgByAppAndCid(String appId, String cid, String msg) {
try {
logger.info("推送ws消息到浏览器, appId={}, cid={}, msg={}", appId, cid, msg);
Set<WsChannelUserIdServer> wsSet = wsAppIdMap.get(appId);
if(wsSet == null || wsSet.isEmpty()){
logger.info("appId[{}] 无ws监听客户端", appId);
return ;
}
for (WsChannelUserIdServer item : wsSet) {
if(!cid.equals(item.cid)){
continue;
}
try {
item.sendMessage(msg);
} catch (Exception e) {
logger.info("推送设备消息时异常appId={}, cid={}", appId, item.cid, e);
continue;
}
}
} catch (Exception e) {
logger.info("推送消息时异常appId={}", appId, e);
}
}
public static synchronized int getOnlineClientSize() {
return onlineClientSize;
}
public static synchronized void addOnlineCount() {
onlineClientSize++;
}
public static synchronized void subOnlineCount() {
onlineClientSize--;
}
}

View File

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>提示</title>
<link rel="stylesheet" href="https://cdn.staticfile.org/layui/2.4.3/css/layui.css">
<style>
.mainDiv1 {color:lightseagreen; text-align:center; margin-top: 50px;}
.mainDiv2 {text-align:center; margin-top: 10px;}
.mainDiv3 {text-align:center; margin-top: 200px;}
.mainDivTitle {text-align:center; margin-top: 20px; color:orangered }
</style>
</head>
<body>
<#if errMsg != null >
<div class="mainDiv1 layui-fluid">
<i class="layui-icon" style="font-size:100px; color:orangered">&#x1007;</i>
</div>
<div class="mainDiv2 layui-fluid">
<span style="font-size:16px; ">获取失败</span>
<div class="mainDivTitle layui-fluid">
<span style="font-size:14px">错误提示:${errMsg!''}
</span>
</div>
</div>
<#else>
<div class="mainDiv1 layui-fluid">
<i class="layui-icon" style="font-size:100px;">&#x1005;</i>
</div>
<div class="mainDiv2 layui-fluid">
<span style="font-size:16px">获取成功</span>
</div>
</#if>
<div class="mainDiv3 layui-fluid">
<a class="layui-btn layui-btn-primary closeBtn">关闭页面</a>
</div>
<script src="https://cdn.staticfile.org/layui/2.4.3/layui.min.js"></script>
<script>
layui.use(['jquery'], function(){
layui.$(".closeBtn").click(function(){
var ua = navigator.userAgent.toLowerCase();
if(ua.match(/MicroMessenger/i)=="micromessenger") {
WeixinJSBridge.call('closeWindow');
} else if(ua.indexOf("alipay")!=-1){
AlipayJSBridge.call('closeWebview');
}
else{
window.close();
}
});
});
</script>
</body>
</html>

View File

@ -72,6 +72,7 @@ public class ChannelUserIdController extends AbstractPayOrderController {
JSONObject jsonObject = new JSONObject();
jsonObject.put("mchNo", rq.getMchNo());
jsonObject.put("appId", rq.getAppId());
jsonObject.put("extParam", rq.getExtParam());
jsonObject.put("ifCode", ifCode);
jsonObject.put("redirectUrl", rq.getRedirectUrl());
@ -95,6 +96,7 @@ public class ChannelUserIdController extends AbstractPayOrderController {
String mchNo = callbackData.getString("mchNo");
String appId = callbackData.getString("appId");
String ifCode = callbackData.getString("ifCode");
String extParam = callbackData.getString("extParam");
String redirectUrl = callbackData.getString("redirectUrl");
// 获取接口
@ -111,7 +113,11 @@ public class ChannelUserIdController extends AbstractPayOrderController {
String channelUserId = channelUserService.getChannelUserId(getReqParamJSON(), mchAppConfigContext);
//同步跳转
response.sendRedirect(StringKit.appendUrlQuery(redirectUrl, JsonKit.newJson("channelUserId", channelUserId)));
JSONObject appendParams = new JSONObject();
appendParams.put("appId", appId);
appendParams.put("channelUserId", channelUserId);
appendParams.put("extParam", extParam);
response.sendRedirect(StringKit.appendUrlQuery(redirectUrl, appendParams));
}

View File

@ -33,6 +33,9 @@ public class ChannelUserIdRQ extends AbstractMchAppRQ{
@NotBlank(message="接口代码不能为空")
private String ifCode;
/** 商户扩展参数,将原样返回 **/
private String extParam;
/** 回调地址 **/
@NotBlank(message="回调地址不能为空")
private String redirectUrl;

View File

@ -42,7 +42,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <!-- 项目构建输出编码 -->
<!-- 其他工具包 -->
<jeepay.sdk.java.version>1.1.0</jeepay.sdk.java.version>
<jeepay.sdk.java.version>1.2.0</jeepay.sdk.java.version>
<fastjson.version>1.2.76</fastjson.version> <!-- fastjson -->
<mybatis.plus.starter.version>3.4.2</mybatis.plus.starter.version> <!-- mybatis plus -->
<hutool.util.version>5.6.6</hutool.util.version> <!-- hutool -->