本文共 11217 字,大约阅读时间需要 37 分钟。
废话不多说,直接上干货
源码下载:
链接:https://pan.baidu.com/s/1OFMCtNglHHQnBlfedpdPmw
提取码:w465
文章大纲:
1. 需求背景
2. 技术选型
3. 代码开发
4. 部署项目到阿里云
大致想实现一个 端口-手机号 配置的功能,并且可以接收到指定手机发来的短信,将消息过滤后转发至钉钉群。
ps:该项目仅是服务端,另一端是Android,Android配置硬件实现端口切换【一个端口对应一张手机卡,共16个端口】,使手机使用某张卡,随后将收到的短信发送至服务端,服务端过滤消息后,将消息转发到钉钉群。这样就可以方便整个公司的员工接收短信验证码,提高效率。
原型大致如下 :
(1)端口配置:
此处包含 增删改查 方法,相关数据存在redis,后文会介绍为什么选用redis。
(2)号码切换
切换到指定手机号,就可以收到该手机号发来的短信。
(1)Python 3.7,想挑战一下自己,并且可以学习一门新的语言,所以选择了Python
(2)WSGI,没有选用基于WSGI的框架(flask或者django),因为时间有限
(3)Redis,没有选用数据库,是因为该项目与业务关系不大,只是为了方便接收验证码
(4)阿里云函数计算 HTTP触发器
将项目部署到阿里云之前,需要让项目跑起来,并单测完成。
本地选用的是wsgiref.simple_server,来模拟一个服务。
实现simple_server有三种方式(一个函数,一个类,或者一个重载了__call__的类的实例),我选用的是最后一种方式。
ps:由于没有使用web框架,整个项目实现思想有点类似于Java Web
项目结构:
下面直接贴代码
(1)fcIndex.py
from sms_log import SMSLogfrom urllib.parse import unquotefrom redis_util import RedisUtilfrom port_phone_conf import PortPhoneConffrom 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 jsonimport timefrom redis_util import RedisUtilclass 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 jsonimport redisclass 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 jsonimport requestsclass 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__ 来测试功能了!
本地单测完成,就可以部署到阿里云了,不要沾沾自喜,后面还有好多坑等着呢。
(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 SMSLogfrom urllib.parse import unquotefrom redis_util import RedisUtilfrom port_phone_conf import PortPhoneConffrom wsgiref.simple_server import make_serverdef 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 itemsdef 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
转载地址:http://riqen.baihongyu.com/