廢話不多說,直接上乾貨
源碼下載:
鏈接:https://pan.baidu.com/s/1OFMCtNglHHQnBlfedpdPmw
提取碼:w465
文章大綱:
1. 需求背景
2. 技術選型
3. 代碼開發
4. 部署項目到阿里雲
1. 需求背景
大致想實現一個 端口-手機號 配置的功能,並且可以接收到指定手機發來的短信,將消息過濾後轉發至釘釘羣。
ps:該項目僅是服務端,另一端是Android,Android配置硬件實現端口切換【一個端口對應一張手機卡,共16個端口】,使手機使用某張卡,隨後將收到的短信發送至服務端,服務端過濾消息後,將消息轉發到釘釘羣。這樣就可以方便整個公司的員工接收短信驗證碼,提高效率。
原型大致如下 :
(1)端口配置:
此處包含 增刪改查 方法,相關數據存在redis,後文會介紹爲什麼選用redis。
(2)號碼切換
切換到指定手機號,就可以收到該手機號發來的短信。
2. 技術選型
(1)Python 3.7,想挑戰一下自己,並且可以學習一門新的語言,所以選擇了Python
(2)WSGI,沒有選用基於WSGI的框架(flask或者django),因爲時間有限
(3)Redis,沒有選用數據庫,是因爲該項目與業務關係不大,只是爲了方便接收驗證碼
(4)阿里雲函數計算 HTTP觸發器
3. 代碼開發
將項目部署到阿里雲之前,需要讓項目跑起來,並單測完成。
本地選用的是wsgiref.simple_server,來模擬一個服務。
實現simple_server有三種方式(一個函數,一個類,或者一個重載了__call__的類的實例),我選用的是最後一種方式。
ps:由於沒有使用web框架,整個項目實現思想有點類似於Java Web
項目結構:
下面直接貼代碼
(1)fcIndex.py
from sms_log import SMSLog
from urllib.parse import unquote
from redis_util import RedisUtil
from port_phone_conf import PortPhoneConf
from wsgiref.simple_server import make_server
"""路由配置"""
URL_PATTERNS = (
('/', 'list'),
('/list', 'list'),
('/insert', 'insert'),
('/update', 'update'),
('/delete', 'delete'),
('/sms', 'sms'),
('/set_current_conf', 'set_current_conf')
)
class AppClass:
@staticmethod
def _match(path):
"""解析應該調用哪個app"""
path = path.split('/')[1]
# path = path[path.rfind('/'):]
for url, app in URL_PATTERNS:
if path in url:
return app
@staticmethod
def parse_params(params):
"""解析請求參數"""
if not params:
return None
result = {}
items = params.split('&')
for item in items:
result[item.split("=")[0]] = item.split('=')[1]
return result
def __call__(self, environ, start_response):
"""入口"""
path = environ.get('PATH_INFO', '/')
app = self._match(path)
if app:
if app in ['list']:
"""get"""
params = self.parse_params(environ['QUERY_STRING'])
else:
"""post"""
request_body_size = int(environ.get("CONTENT_LENGTH", 0))
request_body_str = unquote(environ["wsgi.input"].read(request_body_size).decode('utf-8'))
params = self.parse_params(request_body_str)
app = globals()[app]
result = app(params)
start_response("200 OK", [('Content-type', 'text/plain; charset=utf-8')])
return result
else:
start_response("404 NOT FOUND", [('Content-type', 'text/plain')])
return [b"Page dose not exists!"]
def list(params):
"""端口配置列表展示 """
items = PortPhoneConf().list().encode()
if not items:
items = []
return [items, ]
def insert(params):
"""新增配置"""
try:
port_no = params['port_no']
phone_no = params['phone_no']
key_words = params['key_words']
except Exception:
raise Exception('參數錯誤,請刷新頁面後重試')
PortPhoneConf().insert(port_no, phone_no, key_words)
return [b"success"]
def update(params):
"""修改配置"""
try:
port_no = params['port_no']
phone_no = params['phone_no']
key_words = params['key_words']
except Exception:
raise Exception('參數錯誤,請刷新頁面後重試')
PortPhoneConf().update(port_no, phone_no, key_words)
return [b"success"]
def delete(params):
"""刪除配置"""
try:
port_no = params['port_no']
except Exception:
raise Exception('參數錯誤,請刷新頁面後重試')
PortPhoneConf().delete(port_no)
return [b"success"]
def sms(params):
"""接收短信,並轉發到釘釘羣"""
try:
phone_no = params['phone_no']
msg = params['msg']
except Exception:
raise Exception('參數錯誤,請刷新頁面後重試')
current_phone_no = RedisUtil.comm_get(RedisUtil.CURRENT_PORT_KEY)
if phone_no == current_phone_no:
raise Exception(phone_no + "對應端口未開啓")
SMSLog().redirect_to_ding_talk(msg)
return [b"success"]
def set_current_conf(params):
"""設置當前開啓的端口"""
try:
phone_no = params['phone_no']
except Exception:
raise Exception('參數錯誤,請刷新頁面後重試')
RedisUtil.comm_set(RedisUtil.CURRENT_PORT_KEY, phone_no)
return [b"success"]
if __name__ == "__main__":
handler = AppClass()
httpd = make_server('', 8080, handler)
print("Serving on port 8080...")
httpd.serve_forever()
(2)port_phone_conf.py
import json
import time
from redis_util import RedisUtil
class PortPhoneConf:
"""端口-手機號操作類"""
def insert(self, port_no, phone_no, key_words):
"""新增端口配置"""
conf = {'port_no': port_no,
'phone_no': phone_no,
'key_words': key_words,
'gmt_create': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
'gmt_modified': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())}
items = self.list()
if not items:
items = []
else:
items = json.loads(items)
items.append(conf)
RedisUtil.comm_set(RedisUtil.CONF_KEY, items)
def update(self, port_no, phone_no, key_words):
"""修改端口配置"""
items = json.loads(self.list())
for item in items:
if item["port_no"] == port_no:
item["phone_no"] = phone_no
item["key_words"] = key_words
item["gmt_modified"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
RedisUtil.comm_set(RedisUtil.CONF_KEY, items)
def delete(self, port_no):
"""刪除端口配置"""
global items
if port_no:
items = json.loads(self.list())
for item in items:
if item["port_no"] == port_no:
items.remove(item)
break
RedisUtil.comm_set(RedisUtil.CONF_KEY, items)
def list(self):
"""獲取端口配置列表"""
return RedisUtil.comm_get(RedisUtil.CONF_KEY)
(3)redis_util.py
import json
import redis
class RedisUtil:
"""獲取redis連接,並且操作redis"""
CONF_KEY = "select::port_phone_conf"
CURRENT_PORT_KEY = "select::port_phone_conf_current"
@staticmethod
def get_conn():
"""返回連接"""
conn = redis.Redis(host="106.15.176.xxx", port=6379)
return conn
@staticmethod
def comm_set(key, values):
"""通用set方法"""
global conn
try:
conn = RedisUtil.get_conn()
conn.set(key, json.dumps(values, ensure_ascii=False).encode("utf-8"))
finally:
conn.close()
@staticmethod
def comm_get(key):
"""通用get方法"""
global conn
try:
conn = RedisUtil.get_conn()
items = conn.get(key)
finally:
conn.close()
if items:
items = items.decode("utf-8")
return items
(4)sms_log.py
import json
import requests
class SMSLog:
@staticmethod
def redirect_to_ding_talk(msg):
"""過濾消息並轉發到釘釘羣"""
if ("阿里雲" in msg) or ("釘釘" in msg):
url = 'https://oapi.dingtalk.com/robot/send?access_token=xxx'
data = {"msgtype": "text", "text": {"content": msg}}
headers = {'Content-Type': 'application/json'}
requests.request("post", url, json=data, headers=headers)
(5)調試
接下來,就可以運行 fcIndex.py#__main__ 來測試功能了!
4. 部署項目到阿里雲
本地單測完成,就可以部署到阿里雲了,不要沾沾自喜,後面還有好多坑等着呢。
(1)創建函數和服務
進入阿里雲函數計算控制檯#函數_服務 #新建服務
新增函數
特別注意:函數入口一點要寫對,不然會找不到入口(重要)
(2)下載自定義模塊代碼包
查看函數計算的文檔,我們會發現,Python只提供了標準模塊和一些常用模塊。如果需要使用自定義的模塊,則需要將它們與代碼一起打包,在這個項目中,自定義模塊是requests和redis。官方推薦使用Fun,個人覺得代價有點高,放棄。文檔中有這樣一段話:如果沒有 docker 環境,且不涉及動態鏈接庫(.so)、編譯二進制程序等,只是安裝語言依賴,那麼可以直接使用 pip install -t . PyMySQL
的方式進行安裝。這種方式不管函數計算的執行環境中是否安裝了這些 python 庫,都會下載下來,會增加代碼包的大小。
進入項目目錄 ,執行以下命令,下載代碼包。
pip install -t . redis;
pip install -t . requests;
再來看看我們的項目目錄:
(3)修改 fcIndex.py 文件
查看文檔發現,不支持最後一種方式【ps:最後一種方式只是爲了單測方便】,所以我們的代碼需要變動一下,改變後的代碼如下:
from sms_log import SMSLog
from urllib.parse import unquote
from redis_util import RedisUtil
from port_phone_conf import PortPhoneConf
from wsgiref.simple_server import make_server
def handler(environ, start_response):
"""函數計算調用入口"""
return AppClass(environ, start_response)
"""路由配置"""
URL_PATTERNS = (
('/', 'list'),
('/list', 'list'),
('/insert', 'insert'),
('/update', 'update'),
('/delete', 'delete'),
('/sms', 'sms'),
('/set_current_conf', 'set_current_conf')
)
class AppClass:
@staticmethod
def _match(path):
"""解析應該調用哪個app"""
path = path.split('/')[1]
# path = path[path.rfind('/'):]
for url, app in URL_PATTERNS:
if path in url:
return app
@staticmethod
def parse_params(params):
"""解析請求參數"""
if not params:
return None
result = {}
items = params.split('&')
for item in items:
result[item.split("=")[0]] = item.split('=')[1]
return result
def __init__(self, environ, start_response):
self.environ = environ
self.start_response = start_response
def __iter__(self):
path = self.environ.get('PATH_INFO', '/')
app = self._match(path)
if app:
if app in ['list']:
"""get"""
params = {}
# params = self.parse_params(self.environ['QUERY_STRING'])
else:
"""post"""
request_body_size = int(self.environ.get("CONTENT_LENGTH", 0))
request_body_str = unquote(self.environ["wsgi.input"].read(request_body_size).decode('utf-8'))
params = self.parse_params(request_body_str)
app = globals()[app]
result = app(params)
self.start_response("200 OK", [('Content-type', 'text/plain; charset=utf-8')])
yield result
else:
self.start_response("404 NOT FOUND", [('Content-type', 'text/plain')])
yield b"Page dose not exists!"
def list(params):
"""端口配置列表展示 """
items = PortPhoneConf().list().encode()
if not items:
items = []
return items
def insert(params):
"""新增配置"""
try:
port_no = params['port_no']
phone_no = params['phone_no']
key_words = params['key_words']
except Exception:
raise Exception('參數錯誤,請刷新頁面後重試')
PortPhoneConf().insert(port_no, phone_no, key_words)
return b"success"
def update(params):
"""修改配置"""
try:
port_no = params['port_no']
phone_no = params['phone_no']
key_words = params['key_words']
except Exception:
raise Exception('參數錯誤,請刷新頁面後重試')
PortPhoneConf().update(port_no, phone_no, key_words)
return b"success"
def delete(params):
"""刪除配置"""
try:
port_no = params['port_no']
except Exception:
raise Exception('參數錯誤,請刷新頁面後重試')
PortPhoneConf().delete(port_no)
return b"success"
def sms(params):
"""接收短信,並轉發到釘釘羣"""
try:
phone_no = params['phone_no']
msg = params['msg']
except Exception:
raise Exception('參數錯誤,請刷新頁面後重試')
current_phone_no = RedisUtil.comm_get(RedisUtil.CURRENT_PORT_KEY)
if phone_no == current_phone_no:
raise Exception(phone_no + "對應端口未開啓")
SMSLog().redirect_to_ding_talk(msg)
return b"success"
def set_current_conf(params):
"""設置當前開啓的端口"""
try:
phone_no = params['phone_no']
except Exception:
raise Exception('參數錯誤,請刷新頁面後重試')
RedisUtil.comm_set(RedisUtil.CURRENT_PORT_KEY, phone_no)
return b"success"
(4)上傳代碼
快成功了。。。
(5)測試
保存之後,選擇在線編輯,進行測試,nice