import datetime import json import os from abc import ABC from typing import Optional, Callable, Awaitable from paste.rbac.rbac_user import RbacUser from paste.util.encoder import JsonDumpsEncoder from paste.web.handler import RequestHandler from paste.web.param_aware_loader import ParamAwareLoader class AppHandler(RequestHandler, ABC): """ 控制器基类。 """ commands: dict[str, Callable] = {} """ API 接口命令字典,其结构为命令名称指向对应的方法。 其结构如下:: { command_name: method } """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.user: Optional[RbacUser] = None """ 当前登录用户对象。 """ self.start_at = datetime.datetime.now() """ 实例初始化时间。 """ self.command = "" """ 命令。 """ self.request_params = {} """ 命令参数。 """ async def after_auth_token(self, token_payload: dict): """ 初始化登录用户信息。 """ from paste.security import token if token_payload != token.PRIVATE_ISS: from jwt import InvalidTokenError raise InvalidTokenError() async def run_command(self): """ 根据请求参数运行命令方法,返回命令执行结果。 """ self.get_request_params() assert self.command in self.commands, '请提供正确的命令参数.' # 读取命令方法对象,并执行 _cmd_func = self.commands[self.command] _result = _cmd_func(self, **self.request_params) # 处理异步方法执行 if isinstance(_result, Awaitable): _result = await _result return _result async def gen_html(self, template_file: str, **kwargs): """ 生成 HTML 内容。 :param template_file: 模板文件 :param kwargs: 参数数据字典 :return: 返回生成的 HTML 文件内容 """ # 将参数字典转换为 namedtuple,名称固定 template_data_obj = self.dict_to_namedtuple('TemplateData', {**kwargs}) # 手动构建完整命名空间,加入自定义参数,传给生成器 namespace = self.get_template_namespace() namespace.update({'td': template_data_obj}) # 获取模板文件 template_file = f"{self.application.settings.get('template_path')}/{template_file}" # 用参数感知模板加载器,加载模板文件,并传入 namespace 以便在加载完成后,执行数据准备 loader = ParamAwareLoader(os.path.dirname(template_file), namespace=namespace) # 从文件中加载模板,同步完成数据准备 template = await loader.load_with_prepare(os.path.basename(template_file)) # 渲染模板,传入需要的数据 output = template.generate(**namespace) return output def response_ok(self, **kwargs): self.log_request_end() super().response_ok(**kwargs) def response_error(self, e: Exception, status_code: int = 200, api_status_code: int = None, **kwargs): self.log_request_end() if api_status_code is None: api_status_code = status_code self.set_status(status_code=status_code) chunk = {'code': api_status_code, 'status': 'error'} chunk.update(kwargs) if len(e.args) > 0 and isinstance(e.args[0], str): chunk['msg'] = e.args[0] if len(e.args) > 1: if isinstance(e.args[1], dict): chunk.update(e.args[1]) elif isinstance(e.args[1], list): chunk['errors'] = e.args[1] self.write(json.dumps(chunk, cls=JsonDumpsEncoder, ensure_ascii=False)) self.set_header('Content-Type', 'application/json') def get_request_params(self): """ 读取命令名称及请求参数。注意,参数命名应当避开 cmd 和 params。 该方法自动合并参数,并输出请求开始日志。 支持通过 Form 或 Json 两种方式提交请求并读取相应参数。 如使用 Form 方式,则应当在 form-data 中包含名为 cmd 的输入项,其值为对应的命令,其他输入项为命令参数。 注意:不会自动读取上传的文件数据,可通过:: self.request.files 方法读取上传的文件。 如使用 Json 方式,则应当遵循以下结构:: { cmd: command_name, params: { key: value } } :return: 命令,命令对应的参数 """ _arguments = self.request_arguments() _cmd = _arguments.get('cmd', None) _params = _arguments.get('params', None) if _params is None: _arguments.pop('cmd', None) _params = _arguments self.command = _cmd self.request_params = _params # 合成规则参数到参数字典,规则参数可在相应规则中修改 self.request_params.update(self.rule_kwargs) # 取得命令和参数之后,记录请求开始日志 self.log_request_start() return self.command, self.request_params def log_request_end(self): end_at = datetime.datetime.now() total_delta = (end_at - self.start_at).total_seconds() _spend = f"耗时:{total_delta:f} 秒." _user_name = self.user.username if self.user else 'Unknown' _log = f"O 用户:{_user_name} 完成 {self.request.uri}" _log = f"{_log} 接口命令 {self.command},{_spend}" if self.command else f"{_log} 请求,{_spend}" self.log(_log) def log_request_start(self): """ 收到请求时记录的日志 """ _user_name = self.user.username if self.user else 'Unknown' _log = f"I 用户:{_user_name} 请求 {self.request.uri}" _log = f"{_log} 接口命令 {self.command}." if self.command else f"{_log}." self.log(_log)