|
|
@@ -0,0 +1,417 @@
|
|
|
+package com.kxs.common.cash.service.lg;
|
|
|
+
|
|
|
+import cn.hutool.core.bean.BeanUtil;
|
|
|
+import cn.hutool.core.util.NumberUtil;
|
|
|
+import cn.hutool.core.util.RandomUtil;
|
|
|
+import cn.hutool.core.util.StrUtil;
|
|
|
+import cn.hutool.http.HttpRequest;
|
|
|
+import cn.hutool.json.JSONObject;
|
|
|
+import cn.hutool.json.JSONUtil;
|
|
|
+import com.google.common.base.Joiner;
|
|
|
+import com.kxs.common.cash.handlers.CashMessageHandler;
|
|
|
+import com.kxs.common.cash.model.common.CashRequest;
|
|
|
+import com.kxs.common.cash.model.common.CommCashReq;
|
|
|
+import com.kxs.common.cash.model.common.CommCashRes;
|
|
|
+import com.kxs.common.cash.model.common.CommQueryReq;
|
|
|
+import com.kxs.common.cash.model.lg.*;
|
|
|
+import com.kxs.common.cash.model.params.IsvParams;
|
|
|
+import com.kxs.common.cash.model.params.lg.LgIsvParams;
|
|
|
+import com.kxs.common.cash.model.yhs.YhsFileDataRequest;
|
|
|
+import com.kxs.common.cash.service.CashService;
|
|
|
+import com.kxs.common.cash.service.ConfigContextService;
|
|
|
+import com.kxs.common.cash.utils.LgRSAEncryptUtils;
|
|
|
+import com.kxs.common.cash.utils.LgRSASignatureUtils;
|
|
|
+import com.kxs.common.core.exception.GlobalCustomerException;
|
|
|
+import jakarta.servlet.http.HttpServletRequest;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.apache.commons.codec.digest.DigestUtils;
|
|
|
+import org.apache.commons.compress.utils.IOUtils;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.util.StreamUtils;
|
|
|
+
|
|
|
+import java.io.ByteArrayOutputStream;
|
|
|
+import java.net.URL;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.TreeMap;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 乐工提现实现
|
|
|
+ *
|
|
|
+ * @author 没秃顶的码农
|
|
|
+ * @date 2025-03-21
|
|
|
+ */
|
|
|
+@Service
|
|
|
+@RequiredArgsConstructor
|
|
|
+@Slf4j
|
|
|
+public class LgCashService implements CashService {
|
|
|
+
|
|
|
+ private final ConfigContextService configContextService;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Boolean cash(CommCashReq req) {
|
|
|
+
|
|
|
+ if(StrUtil.isBlank(req.getIfCode())){
|
|
|
+ throw new GlobalCustomerException("ifCode不能为空");
|
|
|
+ }
|
|
|
+ IsvParams commIsvParams = configContextService.getParams(req.getIfCode());
|
|
|
+ if (commIsvParams instanceof LgIsvParams isvParams) {
|
|
|
+
|
|
|
+ try {
|
|
|
+ LgCashSubmitReq cashRequest = new LgCashSubmitReq();
|
|
|
+ //公共字段
|
|
|
+ cashRequest.setProjectCode(isvParams.getProjectCode());
|
|
|
+ cashRequest.setNotifyUrl(isvParams.getNotifyUrl());
|
|
|
+ cashRequest.setOutOrderNo(req.getOutOrderNo());
|
|
|
+ cashRequest.setName(req.getAcctName());
|
|
|
+ cashRequest.setIdCard(req.getAcctIdCard());
|
|
|
+ cashRequest.setAccountNo(req.getAcctNo());
|
|
|
+ cashRequest.setAmount(NumberUtil.div(req.getAmount(), 100));
|
|
|
+ cashRequest.setRemark(req.getOutOrderNo());
|
|
|
+
|
|
|
+ String response = send(isvParams, cashRequest, "settlement/settleApi/pay");
|
|
|
+ JSONUtil.toBean(response, LgCashSubmitRes.class);
|
|
|
+ return Boolean.TRUE;
|
|
|
+
|
|
|
+ }catch (Exception e) {
|
|
|
+ log.error("乐工发起提现任务失败{}", JSONUtil.toJsonStr(req), e);
|
|
|
+ throw new GlobalCustomerException(e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return Boolean.FALSE;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Object create(CashRequest req, YhsFileDataRequest fileDataReq) {
|
|
|
+
|
|
|
+ if(StrUtil.isBlank(req.getIfCode())){
|
|
|
+ throw new GlobalCustomerException("ifCode不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ IsvParams commIsvParams = configContextService.getParams(req.getIfCode());
|
|
|
+ if (commIsvParams instanceof LgIsvParams isvParams && req instanceof LgRegisterReq createRequest) {
|
|
|
+
|
|
|
+ try {
|
|
|
+
|
|
|
+ createRequest.setCompanyCode(isvParams.getCompanyCode());
|
|
|
+ String response = send(isvParams, createRequest, "member/freelancerSpecialApi/register");
|
|
|
+ LgRegisterRes commRes = JSONUtil.toBean(response, LgRegisterRes.class);
|
|
|
+ if("2".equals(commRes.getValidateStatus())){
|
|
|
+ throw new GlobalCustomerException(commRes.getRemark());
|
|
|
+ }
|
|
|
+
|
|
|
+ //上传身份证
|
|
|
+
|
|
|
+ String data = upload(isvParams, fileDataReq.getFrontCertImg());
|
|
|
+ JSONObject entries = JSONUtil.parseObj(data);
|
|
|
+ String frontId = entries.getStr("fileId");
|
|
|
+
|
|
|
+ data = upload(isvParams, fileDataReq.getBackCertImg());
|
|
|
+ entries = JSONUtil.parseObj(data);
|
|
|
+ String backId = entries.getStr("fileId");
|
|
|
+
|
|
|
+ LgIdCardAuthReq idCardAuthReq = new LgIdCardAuthReq();
|
|
|
+ idCardAuthReq.setFreelancerId(commRes.getFreelancerId());
|
|
|
+ idCardAuthReq.setFrontImgId(frontId);
|
|
|
+ idCardAuthReq.setBackImgId(backId);
|
|
|
+ send(isvParams, idCardAuthReq, "member/freelancerApi/idCardAuth");
|
|
|
+
|
|
|
+ return commRes.getFreelancerId();
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("创客{}上传身份证乐工失败", createRequest.getMobilePhone(), e);
|
|
|
+ throw new GlobalCustomerException(e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ throw new GlobalCustomerException("未查询到通道配置参数");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 绑定银行卡
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public Object queryInfo(CashRequest req) {
|
|
|
+
|
|
|
+ if(StrUtil.isBlank(req.getIfCode())){
|
|
|
+ throw new GlobalCustomerException("ifCode不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ IsvParams commIsvParams = configContextService.getParams(req.getIfCode());
|
|
|
+ if (commIsvParams instanceof LgIsvParams isvParams && req instanceof LgAddBankCardReq bankCardReq) {
|
|
|
+
|
|
|
+ try {
|
|
|
+
|
|
|
+ bankCardReq.setCompanyCode(isvParams.getCompanyCode());
|
|
|
+ return send(isvParams, bankCardReq, "member/freelancerApi/addBankCard");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("创客{}绑定银行卡失败", bankCardReq.getIdCardNo(), e);
|
|
|
+ throw new GlobalCustomerException(e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ throw new GlobalCustomerException("未查询到通道配置参数");
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Boolean query(CommQueryReq req) {
|
|
|
+
|
|
|
+ if(StrUtil.isBlank(req.getIfCode())){
|
|
|
+ throw new GlobalCustomerException("ifCode不能为空");
|
|
|
+ }
|
|
|
+ IsvParams commIsvParams = configContextService.getParams(req.getIfCode());
|
|
|
+ if (commIsvParams instanceof LgIsvParams isvParams) {
|
|
|
+
|
|
|
+ try {
|
|
|
+ LgQueryInfoReq queryReq = new LgQueryInfoReq();
|
|
|
+ queryReq.setOutOrderNo(req.getOutOrderNo());
|
|
|
+
|
|
|
+ String response = send(isvParams, queryReq, "settlement/settleApi/query");
|
|
|
+ LgCashSubmitRes commRes = JSONUtil.toBean(response, LgCashSubmitRes.class);
|
|
|
+ log.info("乐工查询订单信息{}", response);
|
|
|
+
|
|
|
+ //获取处理的handler方法
|
|
|
+ CashMessageHandler cashMessageHandler = getCashMessageHandler(req.getIfCode());
|
|
|
+
|
|
|
+ CommCashRes commCashRes = new CommCashRes();
|
|
|
+ commCashRes.setIfCode(req.getIfCode());
|
|
|
+ commCashRes.setOutOrderNo(commRes.getOutOrderNo());
|
|
|
+ switch (commRes.getStatus()) {
|
|
|
+ case "P" -> {
|
|
|
+ commCashRes.setStatus(2);
|
|
|
+ commCashRes.setRemark("处理中");
|
|
|
+ }
|
|
|
+ case "S" -> {
|
|
|
+ commCashRes.setStatus(1);
|
|
|
+ commCashRes.setRemark("交易成功");
|
|
|
+ }
|
|
|
+ default -> {
|
|
|
+ commCashRes.setStatus(0);
|
|
|
+ commCashRes.setRemark(StrUtil.isNotBlank(commRes.getDesc()) ? commRes.getDesc() : "交易失败");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ commCashRes.setAmount(commRes.getAmount());
|
|
|
+ commCashRes.setTaxAmount(commRes.getInComeTax());
|
|
|
+
|
|
|
+ cashMessageHandler.handle(commCashRes);
|
|
|
+
|
|
|
+ return Boolean.TRUE;
|
|
|
+ }catch (Exception e) {
|
|
|
+ log.error("乐工查询订单任务失败{}", JSONUtil.toJsonStr(req), e);
|
|
|
+ throw new GlobalCustomerException(e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return Boolean.FALSE;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String cashBack(HttpServletRequest request, String ifCode) {
|
|
|
+
|
|
|
+ IsvParams commIsvParams = configContextService.getParams(ifCode);
|
|
|
+ if (commIsvParams instanceof LgIsvParams isvParams) {
|
|
|
+
|
|
|
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
|
|
+
|
|
|
+ StreamUtils.copy(request.getInputStream(), baos);
|
|
|
+ String bodyStr = baos.toString();
|
|
|
+ log.info("乐工{}回调接口:{}", ifCode, bodyStr);
|
|
|
+
|
|
|
+ LgApiBaseRes baseRes = JSONUtil.toBean(bodyStr, LgApiBaseRes.class);
|
|
|
+
|
|
|
+ //验证签名
|
|
|
+ if (!checkSign(isvParams, bodyStr)) {
|
|
|
+ throw new GlobalCustomerException("签名错误");
|
|
|
+ }
|
|
|
+ String decrypt = LgRSAEncryptUtils.decrypt(baseRes.getBody().getData(), isvParams.getClientPrivateKey());
|
|
|
+ log.info("乐工{}回调解密接口:{}", ifCode, decrypt);
|
|
|
+
|
|
|
+ LgCashSubmitRes commRes = JSONUtil.toBean(decrypt, LgCashSubmitRes.class);
|
|
|
+ //获取处理的handler方法
|
|
|
+ CashMessageHandler cashMessageHandler = getCashMessageHandler(ifCode);
|
|
|
+
|
|
|
+ CommCashRes commCashRes = new CommCashRes();
|
|
|
+ commCashRes.setIfCode(ifCode);
|
|
|
+ commCashRes.setOutOrderNo(commRes.getOutOrderNo());
|
|
|
+ switch (commRes.getStatus()) {
|
|
|
+ case "P" -> {
|
|
|
+ commCashRes.setStatus(2);
|
|
|
+ commCashRes.setRemark("处理中");
|
|
|
+ }
|
|
|
+ case "S" -> {
|
|
|
+ commCashRes.setStatus(1);
|
|
|
+ commCashRes.setRemark("交易成功");
|
|
|
+ }
|
|
|
+ default -> {
|
|
|
+ commCashRes.setStatus(0);
|
|
|
+ commCashRes.setRemark(StrUtil.isNotBlank(commRes.getDesc()) ? commRes.getDesc() : "交易失败");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ commCashRes.setAmount(NumberUtil.mul(commRes.getAmount(), 100));
|
|
|
+ commCashRes.setTaxAmount(NumberUtil.mul(commRes.getInComeTax(), 100));
|
|
|
+
|
|
|
+ return cashMessageHandler.handle(commCashRes);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("乐工回调接口异常", e);
|
|
|
+ throw new GlobalCustomerException(e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return "SUCCESS";
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ private String send(LgIsvParams isvParams, CashRequest req, String url) {
|
|
|
+
|
|
|
+ req.setIfCode(null);
|
|
|
+ LgCommHead header = new LgCommHead();
|
|
|
+ header.setNonce(RandomUtil.randomString(32));
|
|
|
+ header.setOrgCode(isvParams.getOrgCode());
|
|
|
+ header.setRequestId(RandomUtil.randomString(32));
|
|
|
+ header.setRequestTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));
|
|
|
+ header.setTenantCode(isvParams.getTenantCode());
|
|
|
+ LgApiReqBody body = new LgApiReqBody();
|
|
|
+ // 加密
|
|
|
+ String jsonStr = JSONUtil.toJsonStr(req);
|
|
|
+ body.setData(LgRSAEncryptUtils.encrypt(jsonStr, isvParams.getServerPublicKey()));
|
|
|
+ // 加签
|
|
|
+ String signContent = createSign(header, body);
|
|
|
+ String sign = LgRSASignatureUtils.sign(isvParams.getClientPrivateKey(), signContent);
|
|
|
+ header.setSign(sign);
|
|
|
+ LgApiBaseReq baseReq = new LgApiBaseReq();
|
|
|
+ baseReq.setHead(header);
|
|
|
+ baseReq.setBody(body);
|
|
|
+ log.info("乐工请求参数{},url:{}", JSONUtil.toJsonStr(req), url);
|
|
|
+ String response = HttpRequest.post(isvParams.getUrl() + url)
|
|
|
+ .body(JSONUtil.toJsonStr(baseReq))
|
|
|
+ .timeout(50000)
|
|
|
+ .execute().body();
|
|
|
+
|
|
|
+ // 解密
|
|
|
+ LgApiBaseRes baseRes = JSONUtil.toBean(response, LgApiBaseRes.class);
|
|
|
+ if(!"00000".equals(baseRes.getBody().getCode())){
|
|
|
+ log.error("乐工请求错误返回参数{},url:{}", response, url);
|
|
|
+ throw new GlobalCustomerException(baseRes.getBody().getMsg());
|
|
|
+ }
|
|
|
+ //验证签名
|
|
|
+ if (!checkSign(isvParams, response)) {
|
|
|
+ throw new GlobalCustomerException("签名错误");
|
|
|
+ }
|
|
|
+ String data = baseRes.getBody().getData();
|
|
|
+ String decrypt = LgRSAEncryptUtils.decrypt(data, isvParams.getClientPrivateKey());
|
|
|
+ log.info("乐工返回参数{},url:{}", decrypt, url);
|
|
|
+ return decrypt;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String upload(LgIsvParams isvParams, String fileUrl) throws Exception {
|
|
|
+
|
|
|
+ LgCommHead header = new LgCommHead();
|
|
|
+ header.setNonce(RandomUtil.randomString(32));
|
|
|
+ header.setOrgCode(isvParams.getOrgCode());
|
|
|
+ header.setRequestId(RandomUtil.randomString(32));
|
|
|
+ header.setRequestTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));
|
|
|
+ header.setTenantCode(isvParams.getTenantCode());
|
|
|
+
|
|
|
+
|
|
|
+ URL url = new URL(fileUrl);
|
|
|
+ byte[] byteArray = IOUtils.toByteArray(url.openStream());
|
|
|
+
|
|
|
+
|
|
|
+ LgFileUploadReq uploadReq = new LgFileUploadReq();
|
|
|
+ String fileType = StrUtil.subAfter(fileUrl, StrUtil.DOT, true);
|
|
|
+ uploadReq.setFileName("frontCertImg." + fileType);
|
|
|
+ uploadReq.setFileHash(DigestUtils.md5Hex(byteArray));
|
|
|
+ uploadReq.setFileType(1);
|
|
|
+
|
|
|
+ LgApiReqBody body = new LgApiReqBody();
|
|
|
+ // 加密
|
|
|
+ String jsonStr = JSONUtil.toJsonStr(uploadReq);
|
|
|
+ body.setData(LgRSAEncryptUtils.encrypt(jsonStr, isvParams.getServerPublicKey()));
|
|
|
+ // 加签
|
|
|
+ String signContent = createSign(header, body);
|
|
|
+ String sign = LgRSASignatureUtils.sign(isvParams.getClientPrivateKey(), signContent);
|
|
|
+ header.setSign(sign);
|
|
|
+
|
|
|
+ String response = HttpRequest.post(isvParams.getUrl() + "sys/file/upload/file")
|
|
|
+ .form("file", byteArray, uploadReq.getFileName())
|
|
|
+ .form("head", JSONUtil.toJsonStr(header))
|
|
|
+ .form("body", JSONUtil.toJsonStr(body))
|
|
|
+ .timeout(100000)
|
|
|
+ .execute().body();
|
|
|
+
|
|
|
+ // 解密
|
|
|
+ LgApiBaseRes baseRes = JSONUtil.toBean(response, LgApiBaseRes.class);
|
|
|
+ if(!"00000".equals(baseRes.getBody().getCode())){
|
|
|
+ throw new GlobalCustomerException(baseRes.getBody().getMsg());
|
|
|
+ }
|
|
|
+ String data = baseRes.getBody().getData();
|
|
|
+ String decrypt = LgRSAEncryptUtils.decrypt(data, isvParams.getClientPrivateKey());
|
|
|
+ return decrypt;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 所有参数按照字段名的 ascii 码从小到大排序后使用 QueryString 的格式(即
|
|
|
+ * key1=value1&key2=value2…)拼接而成
|
|
|
+ */
|
|
|
+ private String createSign(Object head, Object body) {
|
|
|
+
|
|
|
+ Map<String, Object> headMap = BeanUtil.beanToMap(head);
|
|
|
+ Map<String, Object> bodyMap = BeanUtil.beanToMap(body);
|
|
|
+ TreeMap<String, Object> mergedMap = new TreeMap<>();
|
|
|
+
|
|
|
+ // 剔除空值字段
|
|
|
+ for (String key : headMap.keySet()) {
|
|
|
+ if ("sign".equals(key)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Object value = headMap.get(key);
|
|
|
+ if (value != null && StrUtil.isNotBlank(value.toString())) {
|
|
|
+ mergedMap.put(key, value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (String key : bodyMap.keySet()) {
|
|
|
+ if (bodyMap.get(key) != null && StrUtil.isNotBlank(bodyMap.get(key).toString())) {
|
|
|
+ mergedMap.put(key, bodyMap.get(key));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return Joiner.on("&").withKeyValueSeparator("=").join(mergedMap);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 校验SIGN
|
|
|
+ */
|
|
|
+ private boolean checkSign(LgIsvParams isvParams, String res) {
|
|
|
+
|
|
|
+ JSONObject entries = JSONUtil.parseObj(res);
|
|
|
+ JSONObject headMap = entries.getJSONObject("head");
|
|
|
+ JSONObject bodyMap = entries.getJSONObject("body");
|
|
|
+ TreeMap<String, Object> mergedMap = new TreeMap<>();
|
|
|
+
|
|
|
+ // 剔除空值字段
|
|
|
+ for (String key : headMap.keySet()) {
|
|
|
+ if ("sign".equals(key)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Object value = headMap.get(key);
|
|
|
+ if (value != null && StrUtil.isNotBlank(value.toString())) {
|
|
|
+ mergedMap.put(key, value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ for (String key : bodyMap.keySet()) {
|
|
|
+ if (bodyMap.get(key) != null && StrUtil.isNotBlank(bodyMap.get(key).toString())) {
|
|
|
+ mergedMap.put(key, bodyMap.get(key));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ String join = Joiner.on("&").withKeyValueSeparator("=").join(mergedMap);
|
|
|
+
|
|
|
+ return LgRSASignatureUtils.verify(isvParams.getServerPublicKey(), headMap.getStr("sign"), join);
|
|
|
+ }
|
|
|
+
|
|
|
+}
|