天天看點

微信小程式背景開發詳解

微信小程式背景開發 前言

微信小程式已經是家喻戶曉了,最近和同學一起剛上線了一款應用

校園懶人邦

,感興趣的朋友可以搜尋一下,一款基于校園最後一百米的概念開發的快遞&外賣配送平台,我是負責背景開發部分,這裡給朋友們介紹下相關開發經驗,開發架構和方式有很多,這裡給大家介紹一些快捷高效的方法,大家少走彎路!

開發環境

macOs 10.13.2

PyCharm 16.1

Python 2.7

vim 8.0

開發流程 項目整體結構

我們先來看下項目整體結構:

微信小程式背景開發詳解

pjt: 整個項目的代碼檔案

api: 項目的所有接口資訊

conf: 項目所有的配置資訊

dal/dao: 資料維護層,所有的操作資料庫的邏輯都要經過這裡

db: 第一版使用的自己設計的orm架構,第二版廢棄了

images: 圖檔緩存區,緩存存二維碼還有使用者頭像上傳七牛雲

impl: 接口功能實作

key: 各種加密檔案,微信支付,ssl證書等

log: 日志檔案,目前天級别生成最新的一份

model: 資料模型, 第一版已廢棄

pjt_data: 第二版使用Django QuerySet的orm架構,比自己寫的友善多了大力推薦,具體學習可以檢視這裡,内部代碼就不當做demo展示了,哈哈!

test_demo: 寫一些功能的測試類,友善線上調試

接口開發

整個接口開發使用的是Flask架構,Flask是Python編寫的輕量級Web應用架構,用過的人都知道簡單快捷, 馬上花一分鐘上手下:

安裝: sudo pip install flask

一個簡單的

demo

:

from flask import Flaskapp = Flask(__name__)@app.route("/")def hi(): return "hi!"if __name__ == "__main__": app.run()123456789

運作程式,浏覽器輸入, 可以看到頁面傳回了一個hi!,ok,這就可以了, 整個架構我們就搭建起來了,是不是很簡單,後面我們來拓展一下!

http://127.0.0.1:5000/1

詳細的flask教程可以參考這裡

項目部署 ip映射

個人選擇的伺服器是阿裡雲,性能不錯,這裡假設咱們的阿裡雲的Ip是1.2.3.4,那麼首先我們要在伺服器上建立映射關系,當我通路http://1.2.3.4:5000/時,不僅是本地能通路,任何地方都行,這個很簡單,隻要你買了伺服器,裡面有一項配置你伺服器的ip位址就行。

Nginx反向代理

現在我們可以使用http://1.2.3.4:5000/通路網站了,還缺少一些什麼?當然,微信小程式是不允許接口暴露ip位址的,你必須要用自己的域名和自定義的接口名稱,比如http://www.happypower.com/這樣才行,當我通路這個網站時候,會先到阿裡雲伺服器上做一下映射,确定我們的ip1.2.3.4,然後找到該Ip對應的伺服器,之後怎麼辦? 我需要通路的是http://1.2.3.4:5000/這個啊,Nginx幫我們做了這個事,下面簡單介紹下:

安裝:具體看這裡

安裝完了之後咱們先配置下,詳細配置可以參考這裡,這裡我簡單說下我的配置,并給出具體demo:

進入目錄/etc/nginx,每人安裝目錄可能不同,找到nginx.conf檔案,vim進行編輯,vim的操作如果不熟可以檢視這裡,不行可以先本地測好再到線上用,下面給出我配置的資訊,大家可以仿造,應該是比較精簡的:

# For more information on configuration, see:# * Official English Documentation: http://nginx.org/en/docs/# * Official Russian Documentation: http://nginx.org/ru/docs/user nginx;worker_processes auto;error_log /var/log/nginx/error.log;pid /run/nginx.pid;# Load dynamic modules. See /usr/share/nginx/README.dynamic.include /usr/share/nginx/modules/*.conf;events { worker_connections 1024;}http { log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; include /etc/nginx/mime.types; default_type application/octet-stream; # Load modular configuration files from the /etc/nginx/conf.d directory. # See http://nginx.org/en/docs/ngx_core_module.html#include # for more information. #include /etc/nginx/conf.d/*.conf; server { listen 80 default_server; listen [::]:80 default_server; server_name _; root /usr/share/nginx/html; # Load configuration files for the default server block. include /etc/nginx/default.d/*.conf; location / { return 404; } error_page 404 /404.html; location = /40x.html { } error_page 500 502 503 504 /50x.html; location = /50x.html { } }# Settings for a TLS enabled server.## server {# listen 443 ssl http2 default_server;# listen [::]:443 ssl http2 default_server;# server_name _;# root /usr/share/nginx/html;## ssl_certificate "/etc/pki/nginx/server.crt";# ssl_certificate_key "/etc/pki/nginx/private/server.key";# ssl_session_cache shared:SSL:1m;# ssl_session_timeout 10m;# ssl_ciphers HIGH:!aNULL:!MD5;# ssl_prefer_server_ciphers on;## # Load configuration files for the default server block.# include /etc/nginx/default.d/*.conf;## location / {# }## error_page 404 /404.html;# location = /40x.html {# }## error_page 500 502 503 504 /50x.html;# location = /50x.html {# }# }# 自己主要要寫的就下面這個serverserver { # 443是https監聽的端口,一般預設就好 listen 443; # 自己接口的主域名,這就是當我們通路http://www.happypower.com時就會映射到這裡 server_name www.happypower.com; ssl on; # 這是預設首頁,接口沒有首頁就注釋掉了 # root html; # index index.html index.htm; # 下面是阿裡雲下載下傳的ssl證書,後面會說到ssl的配置,看不懂沒關系 ssl_certificate ./xxx.pem; ssl_certificate_key ./xxx.key; ssl_session_timeout 5m; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; location / { # 這裡的意思是http://www.happypower.com會和http://0.0.0.0:8888做一個映射,這就解決了上面最開始說的那個問題了,這一步很關鍵! proxy_pass http://0.0.0.0:8888; } # 日志檔案路徑,有人通路你的網站時都會留下印記,有的話最好,不配置其實也行。 access_log /root/xxx/pjt/log/nginx.log;}}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114

之後咱們啟動nginx,就能通過http://www.happypower.com進行通路了,成功的話會傳回

hi! gunicorn+super多程序開啟服務+程序監控

這兩個很簡單,其實就是幾條指令的事,主要說下作用:

1 gunicorn 可以讓你的背景服務多程序方式開啟,經過測試可以提升一定的qps(每秒的請求數),簡單來說一定程度上防止你的伺服器崩掉.

2 supervisor 的作用就是對你的程序進行監控,該架構提供了一個可視化界面,可以通過這個界面去開啟,暫停和關閉你的服務程序,即使不動代碼的人也能控制背景服務。

3 具體配置沒啥好講的,學會幾個指令就行具體推薦看這裡

ssl證書

由于小程式需要的接口都是需要https的連接配接,是以咱們還需要ssl證書才行,這裡我使用的是阿裡雲伺服器,具體配置可以先參考這裡,可能有點難懂,下面簡單說下我的配置:

我選擇的是單域名免費型 DV SSL,其實一般的應用來說,單域名足夠了。

官網下載下傳ssl證書,一般是

xxx.key和xxx.pem

兩個檔案,上面Nginx反向代理配置就需要用到這個,可以傳回上面的nginx.conf檔案進行檢視!

管理證書,這個就是配置問題,官方文檔寫的很詳細

配置成功後,可以使用https://127.0.0.1:5000/ 或者https://www.happypower.com來通路你的網站,然後傳回

,當然,也有可能通路不了,一般就是配置問題,https其實也是在通路http,隻是中間多了一個驗證的過程,感興趣的可以到這裡學習下http和https的差別。

ps.

建議用什麼伺服器就用哪裡的證書,騰訊雲和阿裡雲推薦,其他的真的難配,個人遇到了很多坑!

小程式常用功能 微信支付

有些小程式涉及到微信支付的,就比如校園懶人邦,這個其實很頭疼,做過就知道,難到不是很難,過程很繁瑣,官方文檔也很多坑,下面簡單講解下:

首先一定要注冊公司,這個讓營運或者産品的同學去做會好一些

一般來說支付都是單向的,也就是使用者對公司付款,如果是公司對使用者付款則需要9個月的申請時間才行,這個比較坑,當然,退款是不需要等的.

具體細節可以先看文檔,這裡面講的還是非常清楚的,包括一些接口說明等.

這裡我使用python寫了個微信預支付的較為通用的類,大家改下參數拿去用就行,官方文檔一個個試出來的,簡直坑:

class WeiXinPay(object): """微信支付,傳回回用戶端需要參數 """ def __init__(self, uu_id, open_id, spbill_create_ip, total_fee, out_trade_no): """ :param total_fee: 訂單金額 :param spbill_create_ip: 用戶端請求IP位址 """ self.params = { 'appid': '小程式的appid', 'attach': u'你的應用名稱,我這裡是(校園懶人邦)', 'body': u'校園懶人邦-代取費', 'mch_id': '商戶id,你企業的id', 'nonce_str': '給個随機數,一般md5一下就好,時間戳啊或者别的什麼', 'notify_url': 'http://www.happypower.com/result(這個就是通知位址,會異步傳回資訊給你),你寫一個接口接收就行', 'openid': open_id, 'out_trade_no': out_trade_no, 'spbill_create_ip': spbill_create_ip, 'total_fee': str(total_fee), 'trade_type': 'JSAPI' } # 官方給的接口 self.url = 'https://api.mch.weixin.qq.com/pay/unifiedorder' self.error = None def key_value_url(self, value): """将将鍵值對轉為 key1=value1&key2=value2 """ key_az = sorted(value.keys()) pair_array = [] for k in key_az: v = value.get(k, '').strip() v = v.encode('utf8') k = k.encode('utf8') pair_array.append('%s=%s' % (k, v)) tmp = '&'.join(pair_array) return tmp def get_sign(self, params): """生成sign """ stringA = self.key_value_url(params) stringSignTemp = stringA + '&key=' + 'xxx' # APIKEY, API密鑰,需要在商戶背景設定 sign = (md5(stringSignTemp).hexdigest()).upper() params['sign'] = sign def get_req_xml(self): """拼接XML """ self.get_sign(self.params) xml = "" for k, v in self.params.items(): v = v.encode('utf8') k = k.encode('utf8') xml += '<' + k + '>' + v + '' xml += "" print xml return xml def get_prepay_id(self): """ 請求擷取prepay_id """ xml = self.get_req_xml() headers = {'Content-Type': 'application/xml'} r = requests.post(self.url, data=xml, headers=headers) re_xml = ElementTree.fromstring(r.text.encode('utf8')) xml_status = re_xml.getiterator('result_code')[0].text if xml_status != 'SUCCESS': self.error = u"連接配接微信出錯啦!" logging.error(u"連接配接微信出錯啦!") return prepay_id = re_xml.getiterator('prepay_id')[0].text self.params['package'] = 'prepay_id=%s' % prepay_id self.params['timestamp'] = str(int(time.time())) def re_finall(self): self.get_prepay_id() if self.error: return sign_again_params = { 'appId': self.params['appid'], 'timeStamp': self.params['timestamp'], 'nonceStr': self.params['nonce_str'], 'package': self.params['package'], 'signType': 'MD5' } self.get_sign(sign_again_params) sign_again_params['paySign'] = sign_again_params['sign'] sign_again_params['total_fee'] = self.params['total_fee'] sign_again_params['notify_url'] = self.params['notify_url'] sign_again_params.pop('appId') sign_again_params.pop('sign') return json.dumps(sign_again_params)1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495

5 這裡最後傳回的參數傳給前台就能支付了,前台會拿到你的prepay_id,然後就能按照指定金額支付了。

6 退款的話有些不同,首先也是具體先檢視文檔,下面我也寫了一個較為通用的類,大家覺得文檔麻煩直接用也行:

class WeiXinReturn(object): def __init__(self, out_trade_no, total_fee, refund_fee): self.params_mach = { # 申請商戶号的appid或商戶号綁定的appid 'appid': 'xxx', 'mch_id': 'xxx', 'nonce_str': '随機數', 'out_trade_no': out_trade_no, 'out_refund_no': '微信訂單号', 'total_fee': str(total_fee), 'refund_fee': str(refund_fee), 'notify_url': 'http://www.happypower.com/pay_return/result(和支付一樣的意思,就是退款的通知位址,自己開發一個接口就好)' } def pay_return(self): stringA = self.key_value_url(self.params_mach) stringSignTemp = stringA + '&key=' + 'xxx' # APIKEY, API密鑰,需要在商戶背景設定 sign = (md5(stringSignTemp).hexdigest()).upper() self.params_mach['sign'] = sign xml = "" for k, v in self.params_mach.items(): v = v.encode('utf8') k = k.encode('utf8') xml += '<' + k + '>' + v + '' xml += "" headers = {'Content-Type': 'application/xml;charset=UTF-8'} # url = 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers' url = 'https://api.mch.weixin.qq.com/secapi/pay/refund' # 請求中需要帶有支付的證書,沒有證書是無法申請的 r = requests.post(url, data=xml, headers=headers, cert=('xxx.pem', 'xxx.pem')) result = r.text re_xml = ElementTree.fromstring(r.text.encode('utf8')) xml_status = re_xml.getiterator('result_code')[0].text if xml_status == 'SUCCESS': return True return False def key_value_url(self, value): """将将鍵值對轉為 key1=value1&key2=value2 """ key_az = sorted(value.keys()) pair_array = [] for k in key_az: v = value.get(k, '').strip() v = v.encode('utf8') k = k.encode('utf8') pair_array.append('%s=%s' % (k, v)) tmp = '&'.join(pair_array) return tmp12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849

這裡需要商戶證書,具體怎麼弄看這裡

生成二維碼

這個功能比較常見了,同樣也是先看文檔,裡面有三種二維碼接口,開發階段建議使用

接口B:

https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN, 項目上線了用

接口A

會更加靈活,因為可以通過二維碼跳轉頁面且能夠攜帶參數,校園懶人邦中的分享功能就是利用接口A進行開發,主要接口A上線才好測,而接口B開發階段好測。

二維碼是圖檔,涉及到存儲問題,這裡推薦七牛雲,賬号免費有10G的使用空間,很棒的,具體思路就是先到本地做一層緩存,然後本地上傳到七牛雲,相關代碼可以參考下面的,基本已經封裝好了:

def upload_img(self, local_path, upload_name, bucket_name, ttl=7200): """ 上傳圖檔到七牛雲 :param local_path: 本地檔案路徑 :param upload_name: 上傳檔案名 :param bucket_name: 七牛申請的存儲空間名稱 :param ttl: 過期時間 :return: 傳回圖檔位址 """ from qiniu import Auth, put_file import re q = Auth(access_key=conf_test.AccessKey, secret_key=conf_test.SecretKey) token = q.upload_token(bucket_name, upload_name, ttl) ret, info = put_file(token, upload_name, local_path) pat_status = 'status_code:(.*?),' status_code = re.compile(pat_status, re.S).findall(str(info)) if len(status_code) > 0: # 成功傳回圖檔外鍊名稱,失敗傳回原因以及狀态碼 if int(status_code[0]) == 200: return {"msg":conf_test.qiniu_domain + upload_name, "status":1} return {"msg":"failed upload img failed", "status":-1}123456789101112131415161718192021

相關的sdk文檔可以參考這裡

還有一個問題就是微信小程式隻能識别https的圖檔外鍊位址,而剛開始上傳七牛雲生成的是http的連結圖檔,是以這裡需要到七牛雲上面配置下,具體的可以檢視這裡

二維碼基本流程就是這麼一套,當然緩存怎麼做方式很多,這裡僅供參考。

推送消息

給使用者發送模闆消息,提醒使用者做一些事也是經常用到的功能,具體操作還是先檢視文檔,當然,微信規定了每天使用者隻能最多收到三條消息,當然還是有别的辦法可以給使用者多發一些消息,具體往下看.

看文檔可以知道,其實隻需要有足夠的使用者form_id咱們就能對使用者無限制的發送消息,前提是使用者沒有屏蔽你的這個微信小程式,可以參考這裡,當然這裡是用java寫的,我按照這個封裝了一個python版本的,大家可以參考着用:

# 重新整理使用者form_id 存入redis def fresh_formid(self, **kwargs): """ 接收前台傳來的form_id存入redis :param kwargs: :return: """ uu_id = kwargs.get("uu_id", "") if not uu_id: return "failed: uu_id cannot be null" open_id = kwargs.get("open_id", "") if not open_id: return "failed: open_id cannot be null" form_id = kwargs.get("form_id", "") if not form_id: return "failed: form_id cannot be null" if "invalid code" in open_id: return "failed: get openid err" msg = {open_id: form_id} self.redis_cli.sadd(uu_id, json.dumps(msg)) return "success" # 發消息模闆 def send_template_msg(self, **kwargs): uu_id = kwargs.get("uu_id", "") if not uu_id: return "failed: uu_id cannot be null" data = kwargs.get("data", "") if not data: return "failed: data can not be null" # 這個token可以從文檔中去檢視怎麼得到,内部源碼不能提供 token = self.get_refresh_token() if not token: return "failed: cannot find a token" send_msg = {} # 從redis中取出使用者對應的form_id redis_msg = self.redis_cli.spop(uu_id) if not redis_msg: return "failed: cannot find a open_id in redis" msg = json.loads(redis_msg.encode("utf8")) touser = msg.keys()[0] form_id = msg[touser] template_id = kwargs.get("template_id", "") if not template_id: return "failed: template_id cannot be null" # page是使用者點開消息後跳轉到的頁面 page = kwargs.get("page", "") # 下面這些參數的含義在文檔中都能查到 emphasis_keyword = kwargs.get("emphasis_keyword", "") send_msg["touser"] = touser send_msg["template_id"] = template_id send_msg["page"] = page send_msg["form_id"] = form_id send_msg["data"] = data send_msg["emphasis_keyword"] = emphasis_keyword # 調起發送消息接口 api = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=%s" % token # post方式發起網絡請求 return self.get_html(url=api, data=send_msg)12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758

測試 黑盒測試

應用上線之前肯定要各種測試才行,功能上黑盒測試可以交給不太懂代碼的同學,也就是按照流程走通一遍,中途出現的問題記錄下來然後交給程式哥哥。

qps測試

我這裡主要介紹下qps的測試,因為我們是給使用者用的,我們必須估計一下我們的伺服器能承載多少使用者同時線上,也就是平均來說能承受多少使用者每秒的請求數量, 我們可以對最常用的幾個接口做一個極限測試,然後計算出大緻的qps, 最簡單的做法就是多線程瘋狂的call計算平均時間,這裡給出一段測試代碼:

class PressTest(object): def __init__(self): pass def get_html(self, url, headers=conf_test.HEADERS, data=None): if data: data = json.dumps(data) req = urllib2.Request(url=url, headers=headers, data=data) response = urllib2.urlopen(req) html = response.read() return html def get_test(self, api_url): result = self.get_html(url=api_url) if not result: print "url %s result is none" %api_url return return def post_test(self, api_url, **kwargs): import time start_time = time.time() result = self.get_html(url=api_url, data=kwargs) print result if not result: print "url %s result is none" % api_url end_time = time.time() all = end_time - start_time print allif __name__ == '__main__': press_test = PressTest() print "requesting........." api_url = 'https://xxx接口1' api_url2 = 'https://xxx接口2' start = time.time() times = 400 for i in range(times): t1 = threading.Thread(target=press_test.get_test, args=(api_url,)) t2 = threading.Thread(target=press_test.get_test, args=(api_url2,)) t1.start() t2.start() t1.join() t2.join() end = time.time() ave = (end - start) / 800.0 print "count:%d, start_time :%s, now_time: %s, average_qps: %s" % (800, str(start), str(end), str(ave))12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849

這裡是開了400個線程并發call這兩個常用接口,最後計算出qps(每秒的請求數),如果能達到100次/秒,基本上幾千人同時線上沒啥問題,哈哈

歡迎加入Java進階架構學習交流群:375989619 本群提供免費的學習指導 架構資料 以及免費的解答 不懂得問題都可以在本群提出來 之後還會有職業生涯規劃以及面試指導 進群修改群備注:開發年限-地區-經驗 友善架構師解答問題 免費領取架構師全套視訊!!!!!!!!

繼續閱讀