Python 微信卡券投放

所需资料

  1. 开发者ID(AppID)
  2. 开发者密码(AppSecret)
  3. 卡券ID

创建卡券

1
见附件

投放卡券(验证可行tornado示例)

Python代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
class WechatAuthHandler(BaseHandler):        
def get(self):
try:
appid = settings.WECHAT_APPID
app_secret = settings.WECHAT_SECRET

# 获取access_token(每天2000次数,过期时间7200秒,放入redis缓存)
access_token = self.get_access_token(appid, app_secret)

# 获取通用jssdk需要的ticket(每天2000次数,过期时间7200秒,放入redis缓存)
jsapi = self.get_ticket(access_token['access_token'], "jsapi")

# 生成jssdk签名,用于基础jssdk的各种功能调用
# 签名生成规则如下:参与签名的字段包括noncestr(随机字符串), 有效的jsapi_ticket, timestamp(时间戳), url(当前网页的URL,不包含#及其后面部分) 。
# 对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1。
# 这里需要注意的是所有参数名均为小写字符。对string1作sha1加密,字段名和字段值都采用原始值,不进行URL 转义

jsapi_ticket = jsapi['ticket']
jsapi_timestamp = self.get_timestamp()
jsapi_current_url = self.get_request_url()
jsapi_noncestr = self.get_noncestr('jssdk')
before_string = "jsapi_ticket=%s&noncestr=%s&timestamp=%s&url=%s" % (jsapi_ticket, jsapi_noncestr, jsapi_timestamp, jsapi_current_url)

# 签名
sha = hashlib.sha1(before_string.encode('utf8'))
jsapi_signature = sha.hexdigest()

# 拼接数据
jssdk_data = {
"appId": appid,
"timestamp": str(jsapi_timestamp),
"nonceStr": jsapi_noncestr,
"signature": jsapi_signature,
}

# 生成卡券信息,这些参数用于卡券的功能调用
card_noncestr = self.get_noncestr('card')
card_timestamp = self.get_timestamp()
card_id = "pLKpXwKR7TbJEPdHEsnNYLbBc8JE" # 卡券ID

# 获取卡券专用ticket(每天2000次数,过期时间7200秒,放入redis缓存)
card_ticket = self.get_ticket(access_token['access_token'], "wx_card")
cardapi_ticket = card_ticket['ticket']

# 将 api_ticket、timestamp、card_id、code、openid、nonce_str的value值进行字符串的字典序排序。
sorted_param_list = sorted([cardapi_ticket, card_timestamp, card_id, card_noncestr])

# 将所有参数字符串拼接成一个字符串进行sha1加密
card_string = "".join([str(x) for x in sorted_param_list])
sha_card = hashlib.sha1(card_string.encode('utf8'))
card_signature = sha_card.hexdigest()

# 拼接数据
card_data = {
"timestamp": card_timestamp,
"nonce_str": card_noncestr,
"signature": card_signature
}
data = {'jssdk':jssdk_data, 'cardId':card_id, 'cardExt':card_data}
self.render("wechat.html", data=data)
# self.write_json(data=data, status=status)
except Exception, e:
print e, "出错,调转到结束页面"

def get_access_token(self, appid, app_secret):
# 获取access_token,由于接口有限制,所以加上缓存
wx_access_token_url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s" % (appid, app_secret)
access_token_string = redis_utils.redis_get("WECHAT_ACCESS_TOKEN")
if not access_token_string:
access_token_string = requests.get(url=wx_access_token_url, timeout=15).content.decode('utf8')
redis_utils.redis_set("WECHAT_ACCESS_TOKEN", access_token_string, 7000) # 过期时间设置为7000,比微信的提前200
return json.loads(access_token_string)

def get_ticket(self, access_token, ticket_type):
# 根据ticket_type不同,调用不同的微信接口,由于接口有限制,所以加上缓存
REDIS_KEY = "WECHAT_TICKET"+str.upper(ticket_type)
wx_ticket_url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=%s" % (access_token, ticket_type)
ticket_string = redis_utils.redis_get(REDIS_KEY)
if not ticket_string:
ticket_string = requests.get(url=wx_ticket_url, timeout=15).content.decode('utf8')
redis_utils.redis_set(REDIS_KEY, ticket_string, 7000) # 过期时间设置为7000,比微信的提前200
return json.loads(ticket_string)

def get_timestamp(self):
# 获取时间戳
return int(time.time())

def get_noncestr(self, name):
# 产生随机字符串,可能会用于校验,所以缓存120s
random_str = str(uuid.uuid1())
redis_utils.redis_set(name, random_str, 120)
return random_str

def get_request_url(self):
# 根据规则只取#之前的部分
return (self.request.protocol+"://"+self.request.host+self.request.uri).split("#")[0]

html代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>问卷网CEM-卡券领取测试</title>
<link href="https://cdn.bootcss.com/bootstrap/4.1.0/css/bootstrap.css" rel="stylesheet">
<script src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>
<script>
wx.config({
debug: false,
appId: "{{ data['jssdk']['appId'] }}",
timestamp: "{{ data['jssdk']['timestamp'] }}",
nonceStr: "{{ data['jssdk']['nonceStr'] }}",
signature: "{{ data['jssdk']['signature'] }}",
jsApiList: [
'addCard'
]
});
wx.ready(function(){
//添加卡券
document.querySelector('#addCard').onclick = function () {
wx.addCard({
cardList: [
{
cardId: "{{ data['cardId'] }}",
cardExt: '{"timestamp":"{{ data['cardExt']['timestamp'] }}", "nonce_str": "{{ data['cardExt']['nonce_str'] }}" , "signature":"{{ data['cardExt']['signature'] }}"}'
}
],
success: function (res) {
alert('已添加卡券:' + JSON.stringify(res.cardList));
}
});
};

document.querySelector('#select_all').onclick = function () {
wx.chooseCard({
shopId: '', // 门店Id
cardType: '', // 卡券类型
cardId: '', // 卡券Id
timestamp: '{{ data['cardExt']['timestamp'] }}', // 卡券签名时间戳
nonceStr: '{{ data['cardExt']['nonce_str'] }}', // 卡券签名随机串
signType: 'SHA1', // 签名方式,默认'SHA1'
cardSign: '{{ data['cardExt']['signature'] }}', // 卡券签名
success: function (res) {
var cardList= res.cardList; // 用户选中的卡券列表信息
alert(cardList)
}
});
};
});
</script>
</head>
<body>
<center><button type="button" class="btn btn-primary btn-lg btn-block" style="margin-top:10%;" id="addCard">点我添加卡券</button></center>
<center><button type="button" class="btn btn-primary btn-lg btn-block" style="margin-top:10%;" id="select_all">查看所有卡券</button></center>

</body>
</html>

核销卡券

1
见附件

微信登录(未验证flask示例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@app.route("/")
def index():
appid = "wx*************"
redirect_uri = "http%3A%2F%2Fcoupon.abc.com%2Fauthorization"
csrf_state = str(random.randint(0, 10000)) + ''.join(random.sample('abcdefghijk', 5))
session["csfr_token"] = csrf_state
wx_code_url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_userinfo&state=%s#wechat_redirect" % (appid, redirect_uri, csrf_state)
return redirect(wx_code_url)

@app.route("/authorization")
def authorization():
page_status = "1"
wx_code = request.args.get("code") # Flask 获取 get请求的值用request.args,获取post请求用request.form,获取前端ajax的json数据用request.data,这里的code是微信回调的时候加上去的
csrf_state = request.args.get("state") # 这个state是微信回调回来的时候带上去的,是之前我们自己生成的随机字符串
if session["csfr_token"] != csrf_state: # 随机字符串的校验,校验失败就跳转首页
return redirect("http://coupon.abc.com")
else:
appid = "wx****************"
app_secret = "*********************"
wx_login_token_url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code" % (appid, app_secret, wx_code)
try:
wx_login_token = requests.get(url=wx_login_token_url, timeout=15).json() # 获取一个token

session['openid'] = wx_login_token['openid'] # 微信返回的openid,同时返回了一个授权用的Token,这个授权token是没有次数限制的,区别于基础功能的access_token,这里把openid作为session来全局判断登录状态

access_token = get_access_token(appid, app_secret) # 单独写了一个获取基础功能access_tokende 的方法,这个是有次数限制的,每天2000次,用完就gg了,所以需要做特殊的缓存操作,下面会详细说
jsapi = get_ticket(access_token['access_token'], "jsapi") # 这个单独的方法是获取通用jssdk需要的ticket方法,ticket的使用也有次数限制,所以也需要做缓存

注意事项

  1. 设置IP白名单
  2. 设置jssdk域名白名单
  3. 签名中的本页面的url是包含http的完整信息
  4. jssdk demo页面 http://203.195.235.76/jssdk/

附件

http://ovsf3r7sm.bkt.clouddn.com/%E5%8D%A1%E5%88%B8%E7%A4%BA%E4%BE%8B.pdf