package com.lightinit.hsdataportal.controller.open;

import com.lightinit.hsdataportal.Unionsdk.*;
import com.lightinit.hsdataportal.common.AlipayConfig;
import com.lightinit.hsdataportal.dictionary.DicOrderState;
import com.lightinit.hsdataportal.entity.Order;
import com.lightinit.hsdataportal.model.ResultState;
import com.lightinit.hsdataportal.service.IAlipayService;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Map.Entry;

/**
 * @Author: sunxiang
 * @Date: 2019/3/11 15:42
 * @Description:
 **/
@Controller
@RequestMapping(value = "/unionPay/")
public class UnionPayController {

    public static final Logger logger= LoggerFactory.getLogger(AlipayConfig.class);
    @Autowired
    private IAlipayService alipayService;
    private static SimpleDateFormat format=new SimpleDateFormat("yyyyMMddHHmmss");
    public UnionPayController(){
        //System.out.println("初始化");
        SDKConfig.getConfig().loadPropertiesFromSrc(); //从classpath加载acp_sdk.properties文件
    }

    @RequestMapping(value = "/pay.action/{orderNumber}")
    public void pay(HttpServletRequest req, HttpServletResponse resp,@PathVariable String orderNumber) throws IOException {
        resp.setContentType("text/html; charset="+ DemoBase.encoding);
        Order order=alipayService.queryUserOrderByOrderNumber(orderNumber);
        if(order==null){//订单号不存在
            return;
        }
        String merId = DemoBase.merId;//卖家得商户号，需要申请。配置文件读取
        String txnAmt = Double.valueOf(order.getOrderAmount().multiply(new BigDecimal(100)).toString()).intValue()+"";//交易金额：单位为分,不能带小数点
        String orderId = order.getOrderNumber();//订单号:8-32位数字字母,不能含“-”或“_”
        String txnTime =format.format(new Date());//订单发送时间:YYYYMMDDhhmmss格式,必须取当前时间
        if(StringUtils.isEmpty(merId) || StringUtils.isEmpty(txnAmt)){//防止数据库中交易金额不存在
            return;
        }
        Map<String, String> requestData = new HashMap<String, String>();

        /***银联全渠道系统，产品参数，除了encoding自行选择外其他不需修改***/
        requestData.put("version", DemoBase.version);   			  //版本号，全渠道默认值
        requestData.put("encoding", DemoBase.encoding); 			  //字符集编码，可以使用UTF-8,GBK两种方式
        requestData.put("signMethod", SDKConfig.getConfig().getSignMethod()); //签名方法
        requestData.put("txnType", "01");               			  //交易类型 ，01：消费
        requestData.put("txnSubType", "01");            			  //交易子类型， 01：自助消费
        requestData.put("bizType", "000201");           			  //业务类型，B2C网关支付，手机wap支付
        requestData.put("channelType", "07");           			  //渠道类型，这个字段区分B2C网关支付和手机wap支付；07：PC,平板  08：手机

        /***商户接入参数***/
        requestData.put("merId", merId);    	          			  //商户号码，请改成自己申请的正式商户号或者open上注册得来的777测试商户号
        requestData.put("accessType", "0");             			  //接入类型，0：直连商户
        requestData.put("orderId",orderId);                         //商户订单号，8-40位数字字母，不能含“-”或“_”，可以自行定制规则
        requestData.put("txnTime", txnTime);                        //订单发送时间，取系统时间，格式为YYYYMMDDhhmmss，必须取当前时间，否则会报txnTime无效
        requestData.put("currencyCode", "156");         			  //交易币种（境内商户一般是156 人民币）
        requestData.put("txnAmt", txnAmt);             			      //交易金额，单位分，不要带小数点
        //requestData.put("reqReserved", "透传字段");        	    //请求方保留域，如需使用请启用即可；透传字段（可以实现商户自定义参数的追踪）本交易的后台通知,对本交易的交易状态查询交易、对账文件中均会原样返回，商户可以按需上传，长度为1-1024个字节。出现&={}[]符号时可能导致查询接口应答报文解析失败，建议尽量只传字母数字并使用|分割，或者可以最外层做一次base64编码(base64编码之后出现的等号不会导致解析失败可以不用管)。

        requestData.put("riskRateInfo", "{commodityName=付费商品}");//商品名称

        //前台通知地址 （需设置为外网能访问 http https均可），支付成功后的页面 点击“返回商户”按钮的时候将异步通知报文post到该地址
        //如果想要实现过几秒中自动跳转回商户页面权限，需联系银联业务申请开通自动返回商户权限
        //异步通知参数详见open.unionpay.com帮助中心 下载  产品接口规范  网关支付产品接口规范 消费交易 商户通知
        requestData.put("frontUrl", DemoBase.frontUrl);

        //后台通知地址（需设置为【外网】能访问 http https均可），支付成功后银联会自动将异步通知报文post到商户上送的该地址，失败的交易银联不会发送后台通知
        //后台通知参数详见open.unionpay.com帮助中心 下载  产品接口规范  网关支付产品接口规范 消费交易 商户通知
        //注意:1.需设置为外网能访问，否则收不到通知    2.http https均可  3.收单后台通知后需要10秒内返回http200或302状态码
        //    4.如果银联通知服务器发送通知后10秒内未收到返回状态码或者应答码非http200，那么银联会间隔一段时间再次发送。总共发送5次，每次的间隔时间为0,1,2,4分钟。
        //    5.后台通知地址如果上送了带有？的参数，例如：http://abc/web?a=b&c=d 在后台通知处理程序验证签名之前需要编写逻辑将这些字段去掉再验签，否则将会验签失败
        requestData.put("backUrl", DemoBase.backUrl);

        // 订单超时时间。
        // 超过此时间后，除网银交易外，其他交易银联系统会拒绝受理，提示超时。 跳转银行网银交易如果超时后交易成功，会自动退款，大约5个工作日金额返还到持卡人账户。
        // 此时间建议取支付时的北京时间加15分钟。
        // 超过超时时间调查询接口应答origRespCode不是A6或者00的就可以判断为失败。
        requestData.put("payTimeout",format.format(new Date().getTime() + 15 * 60 * 1000));

        //////////////////////////////////////////////////
        //
        //       报文中特殊用法请查看 PCwap网关跳转支付特殊用法.txt
        //
        //////////////////////////////////////////////////

        /**请求参数设置完毕，以下对请求参数进行签名并生成html表单，将表单写入浏览器跳转打开银联页面**/
        Map<String, String> submitFromData = AcpService.sign(requestData,DemoBase.encoding);  //报文中certId,signature的值是在signData方法中获取并自动赋值的，只要证书配置正确即可。

        String requestFrontUrl = SDKConfig.getConfig().getFrontRequestUrl();  //获取请求银联的前台地址：对应属性文件acp_sdk.properties文件中的acpsdk.frontTransUrl
        String html = AcpService.createAutoFormHtml(requestFrontUrl, submitFromData,DemoBase.encoding);   //生成自动跳转的Html表单

        //LogUtil.writeLog("打印请求HTML，此为请求报文，为联调排查问题的依据："+html);
        //将生成的html写到浏览器中完成自动跳转打开银联支付页面；这里调用signData之后，将html写到浏览器跳转到银联页面之前均不能对html中的表单项的名称和值进行修改，如果修改会导致验签不通过
        resp.getWriter().write(html);
    }



    /*
     *@Description 银联返回同步参数，可以返回自己网站的页面
     *@Param
     *@Date 2018/10/27
     *@return
     **/
    @RequestMapping(value="/success.action",method = RequestMethod.POST)
    public ModelAndView successPage(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
        ModelAndView modelAndView = new ModelAndView("open/pay/feedback");//支付完跳转的页面
        //LogUtil.writeLog("FrontRcvResponse前台接收报文返回开始");

        String encoding = req.getParameter(SDKConstants.param_encoding);
        //LogUtil.writeLog("返回报文中encoding=[" + encoding + "]");
        Map<String, String> respParam = getAllRequestParam(req);
        // 打印请求报文
        //LogUtil.printRequestLog(respParam);
        Map<String, String> valideData = null;
        if (null != respParam && !respParam.isEmpty()) {
            Iterator<Entry<String, String>> it = respParam.entrySet()
                    .iterator();
            valideData = new HashMap<String, String>(respParam.size());
            while (it.hasNext()) {
                Entry<String, String> e = it.next();
                String key = (String) e.getKey();
                String value = (String) e.getValue();
                value = new String(value.getBytes(encoding), encoding);
                valideData.put(key, value);
            }
        }
        if (!AcpService.validate(valideData, encoding)) {
            //LogUtil.writeLog("验证签名结果[失败].");
            modelAndView.addObject("message","验证签名结果,交易失败，请联系商家。");
        } else {
            //LogUtil.writeLog("验证签名结果[成功].");
            String orderId = valideData.get("orderId");
            System.out.println(orderId); //其他字段也可用类似方式获取
            String respCode = valideData.get("respCode");
            modelAndView.addObject("message","支付成功: "+orderId);
            //判断respCode=00、A6后，对涉及资金类的交易，请再发起查询接口查询，确定交易成功后更新数据库。
            //sx:暂时没有实现，用回调函数来确定交易是否成功
        }
        //LogUtil.writeLog("FrontRcvResponse前台接收报文返回结束");
        return modelAndView;
    }

    /*
     *@Description 银联异步通知交易完成情况通知
     *@Param
     *@Date 2018/10/27
     *@return
     **/
    @RequestMapping(value="/notice.action",method = RequestMethod.POST)
    public void tradeNotice(HttpServletRequest req,HttpServletResponse resp) throws IOException {
        //后台通知以标准的HTTP协议的POST方法向商户的后台通知URL发送，超时时间为10秒。
        //由于网络等原因，商户可能会收到重复的后台通知，商户应能正确识别并处理。
        //商户返回码为200或302时，银联判定为通知成功，其他返回码为通知失败。
        // 如10秒内未收到应答，银联判定为通知失败。
        //第一次通知失败后，银联会重发，最多发送五次，每次的间隔时间为0,1,2,4分钟。
        //仅成功交易会发送后台通知，失败交易不会发送。
        //LogUtil.writeLog("BackRcvResponse接收后台通知开始");
        String encoding = req.getParameter(SDKConstants.param_encoding);
        // 获取银联通知服务器发送的后台通知参数
        Map<String, String> reqParam = getAllRequestParam(req);
        //LogUtil.printRequestLog(reqParam);
        //重要！验证签名前不要修改reqParam中的键值对的内容，否则会验签不过
        if (!AcpService.validate(reqParam, encoding)) {
            //LogUtil.writeLog("验证签名结果[失败].");
            //验签失败，需解决验签问题
        } else {
            //LogUtil.writeLog("验证签名结果[成功].");
            //【注：为了安全验签成功才应该写商户的成功处理逻辑】交易成功，更新商户订单状态
            String orderId =reqParam.get("orderId"); //获取后台通知的数据，其他字段也可用类似方式获取
            String respCode = reqParam.get("respCode");
            //判断respCode=00、A6后，对涉及资金类的交易，请再发起查询接口查询，确定交易成功后更新数据库。
            if(respCode.equals("00") ||respCode.equals("A6")){//sx:这里对交易判断是否成功
                //更新数据库数据
                Order order = alipayService.queryUserOrderByOrderNumber(orderId);
                if(order!=null && !DicOrderState.PAY_SUCCESS.equals(order.getOrderState())){
                    ResultState<String> resultState = alipayService.updateUserOderByOrderNumber(order.getId(), orderId);//orderId:订单号
                    if(resultState.getStateCode()!=200){
                        logger.info(resultState.getMsg());
                    }
                }
            }
        }
        //LogUtil.writeLog("BackRcvResponse接收后台通知结束");
        //返回给银联服务器http 200  状态码
        resp.getWriter().print("ok");
        resp.getWriter().flush();
        resp.getWriter().close();
        //LogUtil.writeLog("返回状态吗到银联");
    }


    /**
     * 获取请求参数中所有的信息
     * 当商户上送frontUrl或backUrl地址中带有参数信息的时候，
     * 这种方式会将url地址中的参数读到map中，会导多出来这些信息从而致验签失败，这个时候可以自行修改过滤掉url中的参数或者使用getAllRequestParamStream方法。
     * @param request
     * @return
     */
    public static Map<String, String> getAllRequestParam(
            final HttpServletRequest request) {
        Map<String, String> res = new HashMap<String, String>();
        Enumeration<?> temp = request.getParameterNames();
        if (null != temp) {
            while (temp.hasMoreElements()) {
                String en = (String) temp.nextElement();
                String value = request.getParameter(en);
                res.put(en, value);
                // 在报文上送时，如果字段的值为空，则不上送<下面的处理为在获取所有参数数据时，判断若值为空，则删除这个字段>
                if (res.get(en) == null || "".equals(res.get(en))) {
                    // System.out.println("======为空的字段名===="+en);
                    res.remove(en);
                }
            }
        }
        return res;
    }

    /**
     * 获取请求参数中所有的信息。
     * 非struts可以改用此方法获取，好处是可以过滤掉request.getParameter方法过滤不掉的url中的参数。
     * struts可能对某些content-type会提前读取参数导致从inputstream读不到信息，所以可能用不了这个方法。理论应该可以调整struts配置使不影响，但请自己去研究。
     * 调用本方法之前不能调用req.getParameter("key");这种方法，否则会导致request取不到输入流。
     * @param request
     * @return
     */
    public static Map<String, String> getAllRequestParamStream(
            final HttpServletRequest request) {
        Map<String, String> res = new HashMap<String, String>();
        try {
            String notifyStr = new String(IOUtils.toByteArray(request.getInputStream()),DemoBase.encoding);
            //LogUtil.writeLog("收到通知报文：" + notifyStr);
            String[] kvs= notifyStr.split("&");
            for(String kv : kvs){
                String[] tmp = kv.split("=");
                if(tmp.length >= 2){
                    String key = tmp[0];
                    String value = URLDecoder.decode(tmp[1],DemoBase.encoding);
                    res.put(key, value);
                }
            }
        } catch (UnsupportedEncodingException e) {
            //LogUtil.writeLog("getAllRequestParamStream.UnsupportedEncodingException error: " + e.getClass() + ":" + e.getMessage());
        } catch (IOException e) {
            //LogUtil.writeLog("getAllRequestParamStream.IOException error: " + e.getClass() + ":" + e.getMessage());
        }
        return res;
    }
}

