POS设备扫码积分能力接入文档-开放平台

版本号 更新时间 更新内容 更新人
1.0.0 2017-08-11 初始版本 葛炼
1.0.1 2017-08-23 更新“posID”说明 葛炼
1.0.2 2017-08-29 "seralNum"变更为非必填,并更新其对应说明 葛炼
1.0.3 2017-09-25 增加参数“devShopID”,更新部分参数说明,更新示例代码中部分值 葛炼
1.0.4 2018-05-02 增加 java 语言的加解密示例代码 葛炼
1.0.5 2018-07-18 增加参数“payType”,及其对应值说明 葛炼
1.0.6 2019-03-28 更换二维码域名,用于支持 https 葛炼

描述

本文档提供一种使POS设备打印的小票具备扫描二维码积分能力的方法。每次产生线下消费时,POS设备根据本接入文档的方法生成二维码,并打印在购物小票上,用户使用微信/app等手机客户端应用扫描购物小票上的二维码即可完成自助会员积分。下文将详细介绍该二维码的生成方式。

二维码生成方式解析

URL参数:

名称 数据类型 必填 说明
z string 参数名(https://m.mallcoo.cn/a/p/2/?z=)
sign16 string 签名字符串
publicKey string 开放平台公钥(PublicKey)(由猫酷定义并提供)
shopID long 三选一 商户ID(由猫酷定义)(shopID、shopCode和devShopID必须有一个有值)(无值为0,有值为64进制转换
posID string POSID(Pos设备唯一ID)(无长度限制,建议尽量短)(请传收银POS机的真实ID,没有可不传,请勿随意拼接创建值)
money decimal 消费金额(单位:元)(支持小数,元角分按照正常格式,无小数至两位小数都支持)
useTime string 消费时间(时间格式:yyyyMMddHHmmss,做64进制转换)
seralNum string 流水号(无长度限制,建议尽量短)(请传正确的流水号,没有可不传,请勿随意拼接创建值)
shopCode string 三选一 商户编号(shopID、shopCode和devShopID必须有一个有值)【如果对接的商场有第三方CRM系统则此字段必填】
remark string 备注(若有多个值都存放在此参数中,用“,”分割)(无长度限制,建议尽量短)
devShopID string 三选一 商户外部编号(shopID、shopCode和devShopID必须有一个有值)
payType int? 支付方式(仅接受"附3:PayType 对照值",请勿传入其它值)

示例代码(c#):

 //公钥
 string publicKey = "OiSJqE";
 //私钥(由猫酷分配)
 string privateKey = "313b6944c5713a1f";
 //商户ID
 long shopID = 1045064;
 string shopID64 = ConvertUtil.long10ToStr64(shopID);
 //POSID(Pos盒子唯一ID)
 string posID = "158866654";
 //消费金额
 decimal money = 236.29;
 //消费时间 exg:2017-09-25 17:41:10
 string useTime = "20170925174110";
 string useTime64 = ConvertUtil.long10ToStr64(long.Parse(useTime));
 //流水号
 string seralNum = "158622421";
 //商户编号
 string shopCode = "";
 //备注
 string remark = "";
 //商户外部编号
 string devShopID = "";
 //支付方式
 int? payType = null; //注:“int?”表示 可为空的int类型
 //参数(开放平台(加密时顺序从“publicKey”开始))
 string param = string.Format("?z={0}|{1}|{2}|{3}|{4}|{5}|{6}|{7}|{8}|{9}", publicKey, shopID, posID, money, useTime, seralNum, shopCode,remark,devShopID,payType);
 //加密Key
 string key = string.Empty;
 //判断POSID是否为空,并生成加密后的Key
 if(string.IsNullOrWhiteSpace(posID))
 {
     //POSID为空,对私钥进行32位大写的MD5加密
     key = MD5EncryptTo32(privateKey);
 }
 else
 {
     //POSID不为空,对私钥进行16位大写的MD5加密,对POSID进行16位大写的MD5加密
     key = MD5EncryptTo16(privateKey) + MD5EncryptTo16(posID);
 }
 //对数据进行加密
 string sign = DesEncrypt(param,key);
 //生成签名
 sign = MD5EncryptTo16(sign);
 //生成URL(拼接地址栏时,开放平台顺序从“sign16”开始,第二个是“publicKey”,以此类推)
 string url = string.Format("https://m.mallcoo.cn/a/p/2/?z={0}|{1}|{2}|{3}|{4}|{5}|{6}|{7}|{8}|{9}|{10}",sign16, publicKey, shopID64, posID, money, useTime64, seralNum, shopCode,remark,devShopID,payType);

//注:
1. 加密时顺序为“?z=publicKey|shopID|posID|money|useTime|seralNum|shopCode|remark|devShopID|payType”。拼接URL地址栏时,顺序为“?z=sign16|publicKey|shopID64|posID|money|useTime64|searlNum|shopCode|remark|devShopID|payType”
2. 加密时所有列用原始值,加密后用64进制拼接地址栏(64进制在加密后转换)
3. 运行时监视值如下(示例代码参数):
// MD5EncryptTo16(privateKey):CBE670B73A0449DD
// MD5EncryptTo16(posID):2FA082E1902486FE
// key:CBE670B73A0449DD2FA082E1902486FE
// param:?z=OiSJqE|1045064|158866654|236.29|20170925174110|158622421||||
// sign:wkQA4Ffmlh8KycqM/ud8XPZgPOYvR9u0hr+X7pfQxpe8m/Qp+BLpkwIxiwuSSnf24920UIPwd0b2CUqZxCm34Q==
//sign16:E64EF4F1E6860963
//des.Key:CBE670B7  ( Encoding.UTF8.GetString(des.Key))
4. JAVA加密DES时,注意空格和换行问题
5. 扫码时提示“二维码有误请到服务台进行人工积分”错误猜测如下:
// u1). "shopID"、"shopCode"和"devShopID"必须有一个有值(当"shopCode"有值时,"shopID=0")
// u2). "money"必须大于0
// u3). "publicKey"必须要是猫酷数据库中实际在使用且拥有扫码积分权限的
// u4). "useTime"必须要是"yyyyMMddHHmmss"格式
6. 做64进制转换时,请参照示例代码中的“Base64Code”(64进制转换字典)
最终URL:

https://m.mallcoo.cn/a/p/2/?z=E64EF4F1E6860963|OiSJqE|D_JI|158866654|236.29|Elho1G1e|158622421||||

最终二维码:

如果POSID为空则可以不传值但是字段必须有,加密时同理

附1:加密解密及相关算法(C#)

 /// <summary>  
 /// MD5加密(32位)  
 /// </summary>  
 /// <param name="EncryptString">待加密的密文</param>  
 /// <returns>加密的密文</returns>  
 public static string MD5EncryptTo32(string EncryptString)  
 {  
     if (string.IsNullOrEmpty(EncryptString)) { throw (new Exception("密文不得为空")); }  
     MD5 m_ClassMD5 = new MD5CryptoServiceProvider();  
    string m_strEncrypt = "";  
     try
     {
         m_strEncrypt = BitConverter.ToString(m_ClassMD5.ComputeHash(Encoding.Default.GetBytes(EncryptString))).Replace("-", "");  
     }
     catch (ArgumentException ex) { throw ex; }  
     catch (CryptographicException ex) { throw ex; }  
     catch (Exception ex) { throw ex; }  
     finally { m_ClassMD5.Clear(); }  
     return m_strEncrypt;  
 }  

 /// <summary>  
 /// MD5加密(16位)  
 /// </summary>  
 /// <param name="EncryptString">需要加密的字符串</param>  
 /// <returns></returns>
 public static string MD5EncryptTo16(string EncryptString)  
 {  
    if (string.IsNullOrEmpty(EncryptString)) { throw (new Exception("密文不得为空")); }  
    string tt = FormsAuthentication.HashPasswordForStoringInConfigFile(EncryptString, "MD5");  
    if (tt == "" || tt.Length < 30)  
    {  
        return string.Empty;  
    }  
    else  
    {  
        return tt.Substring(8, 16);  
    }  
 }  

 /// <summary>
 /// DES加密
 /// </summary>
 /// <param name="EncryptString">待加密密文</param>
 /// <param name="key">密钥,且必须为8位(若大于8位,则在该方法内部有处理)</param>
 /// <param name="paddingMode">对称算法中使用的填充模式</param>
 /// <param name="mode">对称算法的运算模式</param>
 /// <returns></returns>
 public static string DesEncrypt(string EncryptString, string key, PaddingMode paddingMode = PaddingMode.Zeros, CipherMode mode = CipherMode.ECB)
{
    if (string.IsNullOrEmpty(EncryptString)) { throw (new Exception("密文不得为空")); }
    if (string.IsNullOrEmpty(key)) { throw (new Exception("密钥不得为空")); }
    using (System.Security.Cryptography.DESCryptoServiceProvider des = new System.Security.Cryptography.DESCryptoServiceProvider())
     {
          byte[] inputByteArray = Encoding.UTF8.GetBytes(EncryptString);
          byte[] keyByteArray = new byte[8];
          byte[] inputKeyByteArray = ASCIIEncoding.ASCII.GetBytes(key);
          for (int i = 0; i < 8; i++)
          {
              if (inputKeyByteArray.Length > i)
                  keyByteArray[i] = inputKeyByteArray[i];
              else
                  keyByteArray[i] = 0;
          }
          des.Key = keyByteArray;
          des.IV = keyByteArray;
          des.Mode = mode;
          des.Padding = paddingMode;
          System.IO.MemoryStream ms = new System.IO.MemoryStream();
          using (System.Security.Cryptography.CryptoStream cs = new System.Security.Cryptography.CryptoStream(ms, des.CreateEncryptor(), System.Security.Cryptography.CryptoStreamMode.Write))
          {
              cs.Write(inputByteArray, 0, inputByteArray.Length);
              cs.FlushFinalBlock();
              cs.Close();
          }
          string str = Convert.ToBase64String(ms.ToArray());
          ms.Close();
          return str;
      }
 }

#region 64进制(string)与10进制(long)互相转换
/// <summary>
/// long类型10进制转换成string类型64进制
/// </summary>
/// <param name="xx"></param>
/// <returns></returns>
public static string long10ToStr64(long xx)
{
    string a = "";
    while (xx >= 1)
    {
        int index = Convert.ToInt16(xx - (xx / 64) * 64);
        a = Base64Code[index] + a;
        xx = xx / 64;
    }
    return a;
}

/// <summary>
/// string类型64进制转换成long 10进制
/// </summary>
/// <param name="xx"></param>
/// <returns></returns>
public static long Str64ToLong10(string xx)
{
    long a = 0;
    int power = xx.Length - 1;

    for (int i = 0; i <= power; i++)
    {
        a += _Base64Code[xx[power - i].ToString()] * Convert.ToInt64(Math.Pow(64, i));
    }

    return a;
}

private static Dictionary<int, string> Base64Code = new Dictionary<int, string>() {
{   0  ,"A"}, {   1  ,"B"}, {   2  ,"C"}, {   3  ,"D"}, {   4  ,"E"}, {   5  ,"F"}, {   6  ,"G"}, {   7  ,"H"}, {   8  ,"I"}, 
{   9  ,"J"},{   10  ,"K"}, {   11  ,"L"}, {   12  ,"M"}, {   13  ,"N"}, {   14  ,"O"}, {   15  ,"P"}, {   16  ,"Q"}, 
{   17  ,"R"}, {   18  ,"S"}, {   19  ,"T"},{   20  ,"U"}, {   21  ,"V"}, {   22  ,"W"}, {   23  ,"X"}, {   24  ,"Y"}, 
{   25  ,"Z"}, {   26  ,"a"}, {   27  ,"b"}, {   28  ,"c"}, {   29  ,"d"},{   30  ,"e"}, {   31  ,"f"}, {   32  ,"g"}, 
{   33  ,"h"}, {   34  ,"i"}, {   35  ,"j"}, {   36  ,"k"}, {   37  ,"l"}, {   38  ,"m"}, {   39  ,"n"},{   40  ,"o"}, 
{   41  ,"p"}, {   42  ,"q"}, {   43  ,"r"}, {   44  ,"s"}, {   45  ,"t"}, {   46  ,"u"}, {   47  ,"v"}, {   48  ,"w"}, 
{   49  ,"x"},{   50  ,"y"}, {   51  ,"z"}, {   52  ,"0"}, {   53  ,"1"}, {   54  ,"2"}, {   55  ,"3"}, {   56  ,"4"}, 
{   57  ,"5"}, {   58  ,"6"}, {   59  ,"7"},{   60  ,"8"}, {   61  ,"9"}, {   62  ,"-"}, {   63  ,"_"}, };

private static Dictionary<string, int> _Base64Code
{
    get
    {
        return Enumerable.Range(0, Base64Code.Count()).ToDictionary(i => Base64Code[i], i => i);
    }
}
#endregion

附2:加密解密及相关算法(JAVA)

    /**
     * base64 字典
     */
    private static final char[] LEGAL_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();
    /**
     * DES 加解密模式
     */
    private static final String DES_MODE = "DES/ECB/NoPadding";

    /**
     * data[]进行编码
     * @param data
     * @return
     */
    private static String encode(byte[] data) {
        int start = 0;
        int len = data.length;
        StringBuffer buf = new StringBuffer(data.length * 3 / 2);

        int end = len - 3;
        int i = start;
        int n = 0;

        while (i <= end) {
            int d = ((((int) data[i]) & 0x0ff) << 16)
                    | ((((int) data[i + 1]) & 0x0ff) << 8)
                    | (((int) data[i + 2]) & 0x0ff);

            buf.append(LEGAL_CHARS[(d >> 18) & 63]);
            buf.append(LEGAL_CHARS[(d >> 12) & 63]);
            buf.append(LEGAL_CHARS[(d >> 6) & 63]);
            buf.append(LEGAL_CHARS[d & 63]);

            i += 3;

            if (n++ >= 14) {
                n = 0;
                //buf.append(" ");
            }
        }

        if (i == start + len - 2) {
            int d = ((((int) data[i]) & 0x0ff) << 16)
                    | ((((int) data[i + 1]) & 255) << 8);

            buf.append(LEGAL_CHARS[(d >> 18) & 63]);
            buf.append(LEGAL_CHARS[(d >> 12) & 63]);
            buf.append(LEGAL_CHARS[(d >> 6) & 63]);
            buf.append("=");
        } else if (i == start + len - 1) {
            int d = (((int) data[i]) & 0x0ff) << 16;

            buf.append(LEGAL_CHARS[(d >> 18) & 63]);
            buf.append(LEGAL_CHARS[(d >> 12) & 63]);
            buf.append("==");
        }

        return buf.toString();
    }

    /**
     * 待加密字符串不是 8 的整数倍时,补0
     * @param arg_text 需要处理的待加密字符串
     * @return
     */
    private static byte[] padding(String arg_text){
        byte[] encrypt = arg_text.getBytes();
        //not a multiple of 8
        if(encrypt.length % 8 != 0){
            //create a new array with a size which is a multiple of 8
            byte[] padded = new byte[encrypt.length + 8 - (encrypt.length % 8)];
            //copy the old array into it
            System.arraycopy(encrypt, 0, padded, 0, encrypt.length);
            encrypt = padded;
        }
        return encrypt;
    }

    /**
     * 将自定义的key转换为符合要求的key
     * @param keyRule
     */
    private static byte[] getKey(String keyRule) {
        String str = "";
        if(keyRule .length() > 8){
            str = keyRule.substring(0,8);
        }else if(keyRule.length() == 8){
            str = keyRule;
        }else{
            int l = keyRule.length();
            for(int i = 0; i <(8 - l);i++){
                str += "0";
            }
            str = keyRule + str;
        }
        return str.getBytes();
    }

    /**
     * 加密数据
     * @param encryptString  待加密的字符串
     * @param encryptKey 加密用的key
     * @return
     * @throws Exception
     */
    public static String desEncrypt(String encryptString, String encryptKey) throws Exception {
        SecretKeySpec key = new SecretKeySpec(getKey(encryptKey), "DES");
        Cipher cipher = Cipher.getInstance(DES_MODE);
        cipher.init(Cipher.ENCRYPT_MODE, key);
        byte[] encryptedData = cipher.doFinal(padding(encryptString));
        return encode(encryptedData);
    }

    /***
     * 解密数据
     * @param decryptString 待解密的字符串
     * @param decryptKey 解密用的key
     * @return
     * @throws Exception
     */
    public static String desDecrypt(String decryptString, String decryptKey) throws Exception {
        SecretKeySpec key = new SecretKeySpec(getKey(decryptKey), "DES");
        Cipher cipher = Cipher.getInstance(DES_MODE);
        cipher.init(Cipher.DECRYPT_MODE, key);
        byte[] decryptData = cipher.doFinal(decryptString.getBytes());
        return new String(decryptData);
    }

    /**
     * MD5 加密
     * @param sourceStr
     * @param type
     * @return
     */
    private static String MD5(String sourceStr,int type) {
        String result = "";
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(sourceStr.getBytes());
            byte b[] = md.digest();
            int i;
            StringBuffer buf = new StringBuffer("");
            for (int offset = 0; offset < b.length; offset++) {
                i = b[offset];
                if (i < 0) {
                    i += 256;
                }
                if (i < 16) {
                    buf.append("0");
                }
                buf.append(Integer.toHexString(i));
            }
            if(type == 32) {
                result = buf.toString();
            }else if(type == 16){
                result = buf.toString().substring(8, 24);
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return result;
    }

    public static String md5EncryptTo32(String sourceStr){
        return MD5(sourceStr,32).toUpperCase();
    }

    public static String md5EncryptTo16(String sourceStr){
        return MD5(sourceStr,16).toUpperCase();
    }

    /**
     * 64 进制 转换字典
     */
    private static final char[] ARRAY_64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".toCharArray();

    /**
     * long类型 10 进制转换成string类型64进制
     * @param number
     * @return
     */
    public static String long10ToStr64(long number) {
        // 创建栈
        Stack<Character> stack = new Stack<Character>();
        StringBuilder result = new StringBuilder(0);
        while (number >= 1) {
            // 进栈
            stack.add(ARRAY_64[new Double(number % 64).intValue()]);
            number = number / 64;
        }
        for (; !stack.isEmpty();) {
            // 出栈
            result.append(stack.pop());
        }
        return result.toString();
    }

    /**
     * string类型 64 进制转换成long 10进制
     * @param str
     * @return
     */
    public static double Str64ToLong10(String str) {
        // 倍数
        int multiple = 1;
        double result = 0;
        Character c;
        for (int i = 0; i < str.length(); i++) {
            c = str.charAt(str.length() - i - 1);
            result += decodeChar(c) * multiple;
            multiple = multiple * 64;
        }
        return result;
    }

    private static int decodeChar(Character c) {
        for (int i = 0; i < ARRAY_64.length; i++) {
            if (c == ARRAY_64[i]) {
                return i;
            }
        }
        return -1;
    }

附3:PayType 对照值

描述
1 现金
2 支付宝
3 微信
4 银行卡
5 平安付
6 点评

注:凡是传入 ”PayType对照值“ 之外的一律无效