|  |  |  | 
|---|
|  |  |  | package com.dy.pipIrrSell.util; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | import com.alibaba.fastjson2.JSON; | 
|---|
|  |  |  | import com.alibaba.fastjson2.JSONArray; | 
|---|
|  |  |  | import com.alibaba.fastjson2.JSONObject; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | import com.dy.common.webUtil.BaseResponse; | 
|---|
|  |  |  | import com.dy.common.webUtil.BaseResponseUtils; | 
|---|
|  |  |  | import com.dy.pipIrrGlobal.cert.WxCertUtil; | 
|---|
|  |  |  | import com.dy.pipIrrGlobal.pojoSe.SeVirtualCard; | 
|---|
|  |  |  | import com.dy.pipIrrGlobal.voSe.VoOrders; | 
|---|
|  |  |  | import com.dy.pipIrrSell.result.SellResultCode; | 
|---|
|  |  |  | import com.dy.pipIrrSell.virtualCard.VirtualCardSv; | 
|---|
|  |  |  | import com.dy.pipIrrSell.wechatpay.PayInfo; | 
|---|
|  |  |  | import com.dy.pipIrrSell.wechatpay.dto.Refund; | 
|---|
|  |  |  | import com.dy.pipIrrSell.wechatpay.dto.RefundRequest; | 
|---|
|  |  |  | import com.dy.pipIrrSell.wechatpay.dto.RefundResponse; | 
|---|
|  |  |  | import com.dy.pipIrrSell.wechatpay.dto.ToRefund; | 
|---|
|  |  |  | import lombok.RequiredArgsConstructor; | 
|---|
|  |  |  | import org.springframework.core.io.ResourceLoader; | 
|---|
|  |  |  | import org.springframework.stereotype.Component; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | import javax.crypto.NoSuchPaddingException; | 
|---|
|  |  |  | import java.io.ByteArrayInputStream; | 
|---|
|  |  |  | import java.io.IOException; | 
|---|
|  |  |  | import java.nio.charset.StandardCharsets; | 
|---|
|  |  |  | import java.nio.file.Files; | 
|---|
|  |  |  | import java.nio.file.Paths; | 
|---|
|  |  |  | import java.security.*; | 
|---|
|  |  |  | import java.security.cert.Certificate; | 
|---|
|  |  |  | import java.security.cert.CertificateException; | 
|---|
|  |  |  | import java.security.cert.CertificateFactory; | 
|---|
|  |  |  | import java.security.spec.InvalidKeySpecException; | 
|---|
|  |  |  | import java.security.spec.PKCS8EncodedKeySpec; | 
|---|
|  |  |  | import java.util.Base64; | 
|---|
|  |  |  | import java.util.HashMap; | 
|---|
|  |  |  | import java.util.Map; | 
|---|
|  |  |  | import java.util.Random; | 
|---|
|  |  |  | import java.util.*; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | /** | 
|---|
|  |  |  | * @author ZhuBaoMin | 
|---|
|  |  |  | 
|---|
|  |  |  | @Component | 
|---|
|  |  |  | @RequiredArgsConstructor | 
|---|
|  |  |  | public class PayHelper { | 
|---|
|  |  |  | private final VirtualCardSv virtualCardSv; | 
|---|
|  |  |  | private final RestTemplateUtil restTemplateUtil; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | 
|---|
|  |  |  | 
|---|
|  |  |  | private String checkSessionUrl = PayInfo.checkSessionUrl; | 
|---|
|  |  |  | private String tokenUrl = PayInfo.tokenUrl; | 
|---|
|  |  |  | private String resetUserSessionKeyUrl = PayInfo.resetUserSessionKeyUrl; | 
|---|
|  |  |  | private String notifyUrl = PayInfo.notifyUrl; | 
|---|
|  |  |  | private String schema = PayInfo.schema; | 
|---|
|  |  |  | private String refundUrl = PayInfo.refundUrl; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | // 平台证书公钥 | 
|---|
|  |  |  | public Map<String, Certificate> CERTIFICATE_MAP = new HashMap(); | 
|---|
|  |  |  | 
|---|
|  |  |  | * @param filename 私钥文件路径 | 
|---|
|  |  |  | * @return 私钥对象 | 
|---|
|  |  |  | * @throws IOException | 
|---|
|  |  |  | */ | 
|---|
|  |  |  |  | 
|---|
|  |  |  | public PrivateKey getPrivateKey(String filename) throws IOException { | 
|---|
|  |  |  | String content = new String(Files.readAllBytes(Paths.get(filename)), "utf-8"); | 
|---|
|  |  |  | try { | 
|---|
|  |  |  | String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "") | 
|---|
|  |  |  | .replace("-----END PRIVATE KEY-----", "") | 
|---|
|  |  |  | .replaceAll("\\s+", ""); | 
|---|
|  |  |  | KeyFactory kf = KeyFactory.getInstance("RSA"); | 
|---|
|  |  |  | return kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))); | 
|---|
|  |  |  | } catch (NoSuchAlgorithmException e) { | 
|---|
|  |  |  | throw new RuntimeException("当前Java环境不支持RSA", e); | 
|---|
|  |  |  | } catch (InvalidKeySpecException e) { | 
|---|
|  |  |  | throw new RuntimeException("无效的密钥格式"); | 
|---|
|  |  |  | } | 
|---|
|  |  |  | } | 
|---|
|  |  |  | */ | 
|---|
|  |  |  |  | 
|---|
|  |  |  | /** | 
|---|
|  |  |  | * 获取商户证书私钥对象 | 
|---|
|  |  |  | * @param bs 私钥文件内容 | 
|---|
|  |  |  | * @return 私钥对象 | 
|---|
|  |  |  | * @throws IOException | 
|---|
|  |  |  | */ | 
|---|
|  |  |  | public PrivateKey getPrivateKey(byte[] bs) throws IOException { | 
|---|
|  |  |  | String content = new String(bs, "utf-8"); | 
|---|
|  |  |  | try { | 
|---|
|  |  |  | String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "") | 
|---|
|  |  |  | .replace("-----END PRIVATE KEY-----", "") | 
|---|
|  |  |  | 
|---|
|  |  |  | * @throws InvalidKeyException | 
|---|
|  |  |  | * @throws SignatureException | 
|---|
|  |  |  | * @throws IOException | 
|---|
|  |  |  | */ | 
|---|
|  |  |  | public String sign(byte[] message, String certFileName) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, IOException { | 
|---|
|  |  |  | Signature sign = Signature.getInstance("SHA256withRSA"); | 
|---|
|  |  |  | sign.initSign(getPrivateKey(certFileName)); | 
|---|
|  |  |  | sign.update(message); | 
|---|
|  |  |  | return Base64.getEncoder().encodeToString(sign.sign()); | 
|---|
|  |  |  | }*/ | 
|---|
|  |  |  | /** | 
|---|
|  |  |  | * 签名 | 
|---|
|  |  |  | * @param message 被签名信息 | 
|---|
|  |  |  | * @param certFileBs 私钥证书文件内容 | 
|---|
|  |  |  | * @return signature签名值,签名信息中的一项,参与生成签名信息 | 
|---|
|  |  |  | * @throws NoSuchAlgorithmException | 
|---|
|  |  |  | * @throws InvalidKeyException | 
|---|
|  |  |  | * @throws SignatureException | 
|---|
|  |  |  | * @throws IOException | 
|---|
|  |  |  | */ | 
|---|
|  |  |  | public String sign(byte[] message, byte[] certFileBs) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, IOException { | 
|---|
|  |  |  | Signature sign = Signature.getInstance("SHA256withRSA"); | 
|---|
|  |  |  | sign.initSign(getPrivateKey(certFileBs)); | 
|---|
|  |  |  | sign.update(message); | 
|---|
|  |  |  | return Base64.getEncoder().encodeToString(sign.sign()); | 
|---|
|  |  |  | } | 
|---|
|  |  |  | 
|---|
|  |  |  | * @return 签名信息,HTTP头中的签名信息 | 
|---|
|  |  |  | * HTTP头:Authorization: 认证类型 签名信息 | 
|---|
|  |  |  | * 认证类型,WECHATPAY2-SHA256-RSA2048 | 
|---|
|  |  |  | */ | 
|---|
|  |  |  | public String getToken(String method, String url, String body, String nonceStr, Long timestamp, String certFileName) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException, SignatureException, InvalidKeyException, NoSuchPaddingException { | 
|---|
|  |  |  | String message = buildMessage_order(method, url, timestamp, nonceStr, body); | 
|---|
|  |  |  | String signature = sign(message.getBytes("utf-8"), certFileName); | 
|---|
|  |  |  |  | 
|---|
|  |  |  | return "mchid=\"" + PayInfo.mchid + "\"," | 
|---|
|  |  |  | + "nonce_str=\"" + nonceStr + "\"," | 
|---|
|  |  |  | + "timestamp=\"" + timestamp + "\"," | 
|---|
|  |  |  | + "serial_no=\"" + PayInfo.serial_no + "\"," | 
|---|
|  |  |  | + "signature=\"" + signature + "\""; | 
|---|
|  |  |  | } | 
|---|
|  |  |  | */ | 
|---|
|  |  |  |  | 
|---|
|  |  |  | /** | 
|---|
|  |  |  | * 获取签名信息 | 
|---|
|  |  |  | * @param method | 
|---|
|  |  |  | * @param url | 
|---|
|  |  |  | * @param body | 
|---|
|  |  |  | * @return 签名信息,HTTP头中的签名信息 | 
|---|
|  |  |  | * HTTP头:Authorization: 认证类型 签名信息 | 
|---|
|  |  |  | * 认证类型,WECHATPAY2-SHA256-RSA2048 | 
|---|
|  |  |  | */ | 
|---|
|  |  |  | public String getToken(String method, String url, String body, String nonceStr, Long timestamp, byte[] certFileBs) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException, SignatureException, InvalidKeyException, NoSuchPaddingException { | 
|---|
|  |  |  | String message = buildMessage_order(method, url, timestamp, nonceStr, body); | 
|---|
|  |  |  | String signature = sign(message.getBytes("utf-8"), certFileBs); | 
|---|
|  |  |  |  | 
|---|
|  |  |  | return "mchid=\"" + PayInfo.mchid + "\"," | 
|---|
|  |  |  | + "nonce_str=\"" + nonceStr + "\"," | 
|---|
|  |  |  | 
|---|
|  |  |  | /** | 
|---|
|  |  |  | * 重新下载证书 | 
|---|
|  |  |  | */ | 
|---|
|  |  |  | public void refreshCertificate() throws GeneralSecurityException, IOException { | 
|---|
|  |  |  | public void refreshCertificate(byte[] keyPemBs) throws GeneralSecurityException, IOException, Exception { | 
|---|
|  |  |  | String method = "GET"; | 
|---|
|  |  |  | String httpUrl = "/v3/certificates"; | 
|---|
|  |  |  | String nonceStr = generateRandomString(); | 
|---|
|  |  |  | Long timestamp = System.currentTimeMillis() / 1000; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | String header = PayInfo.schema + " " + getToken(method, httpUrl, "", nonceStr, timestamp, PayInfo.privateCertFileName); | 
|---|
|  |  |  | String header = PayInfo.schema + " " + getToken(method, httpUrl, "", nonceStr, timestamp, keyPemBs); | 
|---|
|  |  |  |  | 
|---|
|  |  |  | Map<String, String> headers = new HashMap<>(); | 
|---|
|  |  |  | headers.put("Authorization", header); | 
|---|
|  |  |  | headers.put("Accept", "application/json"); | 
|---|
|  |  |  | //headers.put("User-Agent", "https://zh.wikipedia.org/wiki/User_agent"); | 
|---|
|  |  |  |  | 
|---|
|  |  |  | JSONObject job_result = restTemplateUtil.getHeaders(PayInfo.certificates,null, headers); | 
|---|
|  |  |  | JSONObject job_headers = job_result.getJSONObject("headers"); | 
|---|
|  |  |  | 
|---|
|  |  |  | * @throws InvalidKeyException | 
|---|
|  |  |  | * @throws SignatureException | 
|---|
|  |  |  | */ | 
|---|
|  |  |  | public Boolean responseSignVerify(String wechatpaySerial, String signatureStr, String wechatpaySignature) throws GeneralSecurityException, IOException { | 
|---|
|  |  |  | public Boolean responseSignVerify(String wechatpaySerial, String signatureStr, String wechatpaySignature, byte[] keyPemBs) throws GeneralSecurityException, IOException, Exception { | 
|---|
|  |  |  | if(CERTIFICATE_MAP.isEmpty() || !CERTIFICATE_MAP.containsKey(wechatpaySerial)) { | 
|---|
|  |  |  | CERTIFICATE_MAP.clear(); | 
|---|
|  |  |  | refreshCertificate(); | 
|---|
|  |  |  | refreshCertificate(keyPemBs); | 
|---|
|  |  |  | } | 
|---|
|  |  |  | Certificate certificate = (Certificate)CERTIFICATE_MAP.get(wechatpaySerial); | 
|---|
|  |  |  | if(certificate == null) { | 
|---|
|  |  |  | 
|---|
|  |  |  | boolean valid = signature.verify(Base64.getDecoder().decode(wechatpaySignature)); | 
|---|
|  |  |  | return valid; | 
|---|
|  |  |  | } | 
|---|
|  |  |  |  | 
|---|
|  |  |  |  | 
|---|
|  |  |  | /** | 
|---|
|  |  |  | * 获取待退款对象列表 | 
|---|
|  |  |  | *      待退款对象包含订单号和可退款金额 | 
|---|
|  |  |  | *      订单对象包含订单号、充值金额、充值完成时间 | 
|---|
|  |  |  | *      1. 根据虚拟卡号到虚拟卡表中取出该卡余额 | 
|---|
|  |  |  | *      2. 根据虚拟卡号到充值表取出订单对象列表 | 
|---|
|  |  |  | * @param virtualId | 
|---|
|  |  |  | * @param refundAmount | 
|---|
|  |  |  | * @return | 
|---|
|  |  |  | */ | 
|---|
|  |  |  | public List<ToRefund> getToRefunds(Long virtualId, Integer refundAmount) { | 
|---|
|  |  |  | ToRefund toRefund = new ToRefund(); | 
|---|
|  |  |  | List<ToRefund> list = new ArrayList<>(); | 
|---|
|  |  |  | Double money = 0d; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | // 根据虚拟卡号获取当前虚拟卡余额 | 
|---|
|  |  |  | SeVirtualCard seVirtualCard = virtualCardSv.selectVirtuCardById(virtualId); | 
|---|
|  |  |  | if(seVirtualCard != null) { | 
|---|
|  |  |  | money = seVirtualCard.getMoney(); | 
|---|
|  |  |  | } | 
|---|
|  |  |  |  | 
|---|
|  |  |  | // 要退金额大于该卡余额,返回空列表 | 
|---|
|  |  |  | if(refundAmount > money) { | 
|---|
|  |  |  | return list; | 
|---|
|  |  |  | } | 
|---|
|  |  |  |  | 
|---|
|  |  |  | // 根据虚拟卡号获取订单列表(仅限充值成功的) | 
|---|
|  |  |  | List<VoOrders> list_Orders = virtualCardSv.selectOrders(virtualId); | 
|---|
|  |  |  | // 遍历订单列表,获取 | 
|---|
|  |  |  | if(list_Orders != null && list_Orders.size() > 0) { | 
|---|
|  |  |  | JSONArray array_Orders = (JSONArray) JSON.toJSON(list_Orders); | 
|---|
|  |  |  | for(int i = 0; i < array_Orders.size(); i++) { | 
|---|
|  |  |  | JSONObject job_order = array_Orders.getJSONObject(i); | 
|---|
|  |  |  | String orderNumber = job_order.getString("orderNumber"); | 
|---|
|  |  |  | Integer rechargeAmount = job_order.getInteger("rechargeAmount"); | 
|---|
|  |  |  | Date rechargeTime = job_order.getDate("rechargeTime"); | 
|---|
|  |  |  |  | 
|---|
|  |  |  | // 计算充值至今时间差(分钟) | 
|---|
|  |  |  | Long timestamp_Recharge = rechargeTime.getTime() / 1000; | 
|---|
|  |  |  | Long timestamp_Current = System.currentTimeMillis() / 1000; | 
|---|
|  |  |  | Long timeDiff_Minute = (timestamp_Current - timestamp_Recharge)/60; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | // 获取该订单已退款笔数 | 
|---|
|  |  |  | Integer refundCount = 0; | 
|---|
|  |  |  | List<Integer> list_RefundAmount = virtualCardSv.selectRefundAmount(orderNumber); | 
|---|
|  |  |  | if(list_RefundAmount != null && list_RefundAmount.size() > 0) { | 
|---|
|  |  |  | refundCount = list_RefundAmount.size(); | 
|---|
|  |  |  | } | 
|---|
|  |  |  |  | 
|---|
|  |  |  | // 充值至今未超过一年且该订单退款总次数未超过50次 | 
|---|
|  |  |  | if(timeDiff_Minute/(365*24*60) >= 1 && (refundCount + 1) > 50) | 
|---|
|  |  |  | return list; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | /** | 
|---|
|  |  |  | * 1. 如果要退金额小于当前订单的充值金额,要退金额即为应退金额并返回 | 
|---|
|  |  |  | * 2. 如果要推金额大于当前订单充值金额,当前订单充值金额即为应退金额 | 
|---|
|  |  |  | *      a. 生成应退款对象 | 
|---|
|  |  |  | *      b. 计算新的余额 | 
|---|
|  |  |  | *      c. 金蒜新的要退款金额 | 
|---|
|  |  |  | *      d. 如果要退金额大于0,遍历下一个订单 | 
|---|
|  |  |  | */ | 
|---|
|  |  |  | if(refundAmount <= rechargeAmount) { | 
|---|
|  |  |  | toRefund = new ToRefund(); | 
|---|
|  |  |  | toRefund.setOrderNumber(orderNumber); | 
|---|
|  |  |  | toRefund.setRefundAmount(refundAmount); | 
|---|
|  |  |  | list.add(toRefund); | 
|---|
|  |  |  | // 计算新的余额和新的要退金额 | 
|---|
|  |  |  | money = money - refundAmount; | 
|---|
|  |  |  | refundAmount = refundAmount - refundAmount; | 
|---|
|  |  |  | return list; | 
|---|
|  |  |  | }else { | 
|---|
|  |  |  | toRefund = new ToRefund(); | 
|---|
|  |  |  | toRefund.setOrderNumber(orderNumber); | 
|---|
|  |  |  | toRefund.setRefundAmount(rechargeAmount); | 
|---|
|  |  |  | list.add(toRefund); | 
|---|
|  |  |  | // 计算新的余额和新的要退金额 | 
|---|
|  |  |  | money = money - rechargeAmount; | 
|---|
|  |  |  | refundAmount = refundAmount - rechargeAmount; | 
|---|
|  |  |  | if(refundAmount > 0) { | 
|---|
|  |  |  | continue; | 
|---|
|  |  |  | }else { | 
|---|
|  |  |  | return list; | 
|---|
|  |  |  | } | 
|---|
|  |  |  | } | 
|---|
|  |  |  | } | 
|---|
|  |  |  | } | 
|---|
|  |  |  | return list; | 
|---|
|  |  |  | } | 
|---|
|  |  |  |  | 
|---|
|  |  |  | /** | 
|---|
|  |  |  | * 退款申请,调用微信支付退款申请接口 | 
|---|
|  |  |  | * @param po 退款请求对象,包含订单号、退款单号、退款金额 | 
|---|
|  |  |  | * @return | 
|---|
|  |  |  | * @throws NoSuchPaddingException | 
|---|
|  |  |  | * @throws NoSuchAlgorithmException | 
|---|
|  |  |  | * @throws InvalidKeySpecException | 
|---|
|  |  |  | * @throws IOException | 
|---|
|  |  |  | * @throws SignatureException | 
|---|
|  |  |  | * @throws InvalidKeyException | 
|---|
|  |  |  | */ | 
|---|
|  |  |  | public BaseResponse<Boolean> refunds(Refund po, ResourceLoader resourceLoader) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeySpecException, IOException, SignatureException, InvalidKeyException, Exception { | 
|---|
|  |  |  | String tradeNo = po.getTradeNo(); | 
|---|
|  |  |  | String refundNo = po.getRefundNo(); | 
|---|
|  |  |  | Integer refund = po.getRefund(); | 
|---|
|  |  |  |  | 
|---|
|  |  |  | // 生成body,金额单位由元改为分 | 
|---|
|  |  |  | //Integer total = virtualCardSv.getRechargeAmountByOrderNumber(tradeNo); | 
|---|
|  |  |  | Integer total = (int)(virtualCardSv.getRechargeAmountByOrderNumber(tradeNo)*100); | 
|---|
|  |  |  | RefundRequest.Amount amount = new RefundRequest.Amount(); | 
|---|
|  |  |  | amount.setRefund(refund); | 
|---|
|  |  |  | amount.setTotal(total); | 
|---|
|  |  |  | amount.setCurrency("CNY"); | 
|---|
|  |  |  |  | 
|---|
|  |  |  | RefundRequest refundRequest = new RefundRequest(); | 
|---|
|  |  |  | refundRequest.setOut_trade_no(tradeNo); | 
|---|
|  |  |  | refundRequest.setOut_refund_no(refundNo); | 
|---|
|  |  |  | refundRequest.setNotify_url(notifyUrl); | 
|---|
|  |  |  | refundRequest.setAmount(amount); | 
|---|
|  |  |  |  | 
|---|
|  |  |  | // 生成header | 
|---|
|  |  |  | String nonceStr = generateRandomString(); | 
|---|
|  |  |  | Long timestamp = System.currentTimeMillis() / 1000; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | String method = "POST"; | 
|---|
|  |  |  | String httpUrl = "/v3/refund/domestic/refunds"; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | String body = JSONObject.toJSONString(refundRequest); | 
|---|
|  |  |  | String header = schema + " " + getToken(method, httpUrl, body, nonceStr, timestamp, WxCertUtil.getKey_pemBytes(resourceLoader)); | 
|---|
|  |  |  |  | 
|---|
|  |  |  | Map<String, String> headers = new HashMap<>(); | 
|---|
|  |  |  | headers.put("Authorization", header); | 
|---|
|  |  |  | headers.put("Accept", "application/json"); | 
|---|
|  |  |  | headers.put("Content-Type", "application/json"); | 
|---|
|  |  |  |  | 
|---|
|  |  |  | JSONObject job_refundResponse = restTemplateUtil.post(PayInfo.refundUrl, body, headers); | 
|---|
|  |  |  | RefundResponse refundResponse = JSON.parseObject(job_refundResponse.toJSONString(), RefundResponse.class); | 
|---|
|  |  |  |  | 
|---|
|  |  |  | String status = refundResponse.getStatus(); | 
|---|
|  |  |  | if(status != null && status.equals("SUCCESS")) { | 
|---|
|  |  |  | // 退款申请已受理 | 
|---|
|  |  |  | return BaseResponseUtils.buildSuccess(true) ; | 
|---|
|  |  |  | } else if(status != null && status.equals("PROCESSING")) { | 
|---|
|  |  |  | // 退款处理中 | 
|---|
|  |  |  | return BaseResponseUtils.buildFail(SellResultCode.PROCESSING.getMessage()); | 
|---|
|  |  |  | } else { | 
|---|
|  |  |  | // 退款异常 | 
|---|
|  |  |  | return BaseResponseUtils.buildError(SellResultCode.ABNORMAL.getMessage()); | 
|---|
|  |  |  | } | 
|---|
|  |  |  | } | 
|---|
|  |  |  | } | 
|---|