版本号 | 更新时间 | 更新内容 | 更新人 |
---|---|---|---|
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 对照值",请勿传入其它值) |
//公钥
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进制转换字典)
https://m.mallcoo.cn/a/p/2/?z=E64EF4F1E6860963|OiSJqE|D_JI|158866654|236.29|Elho1G1e|158622421||||
/// <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
/**
* 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;
}
值 | 描述 |
---|---|
1 | 现金 |
2 | 支付宝 |
3 | 微信 |
4 | 银行卡 |
5 | 平安付 |
6 | 点评 |
注:凡是传入 ”PayType对照值“ 之外的一律无效