diff --git a/applications/common/admin_log.py b/applications/common/admin_log.py index a75080d..ea1298f 100644 --- a/applications/common/admin_log.py +++ b/applications/common/admin_log.py @@ -5,16 +5,15 @@ from applications.extensions import db from applications.models import AdminLog -def login_log(request, uid, is_access): +def normal_log(method, url, ip, user_agent, desc, uid, is_access): info = { - 'method': request.method, - 'url': request.path, - 'ip': request.remote_addr, - 'user_agent': str_escape(request.headers.get('User-Agent')), - 'desc': str_escape(request.form.get('username')), + 'method': method, + 'url': url, + 'ip': ip, + 'user_agent': user_agent, + 'desc': desc, 'uid': uid, 'success': int(is_access) - } log = AdminLog( url=info.get('url'), @@ -26,33 +25,25 @@ def login_log(request, uid, is_access): success=info.get('success') ) db.session.add(log) - db.session.flush() db.session.commit() return log.id -def admin_log(request, is_access): +def login_log(request, uid, is_access): + method = request.method + url = request.path + ip = request.remote_addr + user_agent = str_escape(request.headers.get('User-Agent')) + desc = str_escape(request.form.get('username')) + return normal_log(method, url, ip, user_agent, desc, uid, is_access) + + +def admin_log(request, is_access, desc=None): + method = request.method + url = request.path + ip = request.remote_addr + user_agent = str_escape(request.headers.get('User-Agent')) request_data = request.json if request.headers.get('Content-Type') == 'application/json' else request.values - info = { - 'method': request.method, - 'url': request.path, - 'ip': request.remote_addr, - 'user_agent': str_escape(request.headers.get('User-Agent')), - 'desc': str_escape(str(dict(request_data))), - 'uid': current_user.id, - 'success': int(is_access) - - } - log = AdminLog( - url=info.get('url'), - ip=info.get('ip'), - user_agent=info.get('user_agent'), - desc=info.get('desc'), - uid=info.get('uid'), - method=info.get('method'), - success=info.get('success') - ) - db.session.add(log) - db.session.commit() - - return log.id + if desc is None: + desc = str_escape(str(dict(request_data))) + return normal_log(method, url, ip, user_agent, desc, current_user.id, is_access) diff --git a/applications/common/utils/cache.py b/applications/common/utils/cache.py index cc130f8..9faf638 100644 --- a/applications/common/utils/cache.py +++ b/applications/common/utils/cache.py @@ -53,4 +53,3 @@ def cache_auto_internal(key, call, expired=5): cache_set_internal(key, data, expired) return data - diff --git a/applications/common/utils/captcha.py b/applications/common/utils/captcha.py index 83145b5..06917b3 100644 --- a/applications/common/utils/captcha.py +++ b/applications/common/utils/captcha.py @@ -7,20 +7,23 @@ class vieCode: __width = 120 # 画布宽度 __heigth = 45 # 画布高度 __length = 4 # 验证码长度 - __draw = None # 画布 - __img = None # 图片资源 + __draw = None # 画布对象 + __img = None # 图片对象 __code = None # 验证码字符 __str = None # 自定义验证码字符集 - __inCurve = True # 是否画干扰线 - __inNoise = True # 是否画干扰点 - __type = 2 # 验证码类型 1、纯字母 2、数字字母混合 - __fontPatn = 'applications/common/utils/fonts/captcha.ttf' # 字体 + __inCurve = True # 是否绘制干扰曲线 + __inNoise = True # 是否绘制干扰点 + __type = 2 # 验证码类型:1-纯字母,2-数字字母混合 + __fontPatn = 'applications/common/utils/fonts/captcha.ttf' # 字体路径 def GetCodeImage(self, size=80, length=4): - '''获取验证码图片 - @param int size 验证码大小 - @param int length 验证码长度 - ''' + """ + 生成验证码图片及其对应的验证码字符。 + + :param size: 验证码字体大小,默认为 80。 + :param length: 验证码字符长度,默认为 4。 + :return: 返回验证码图片对象和验证码字符。 + """ # 准备基础数据 self.__length = length self.__fontSize = size @@ -37,14 +40,18 @@ class vieCode: return self.__img, self.__code def __cerateFilter(self): - '''模糊处理''' + """ + 对验证码图片进行模糊处理,增加识别难度。 + """ self.__img = self.__img.filter(ImageFilter.BLUR) filter = ImageFilter.ModeFilter(8) self.__img = self.__img.filter(filter) def __createCode(self): - '''创建验证码字符''' - # 是否自定义字符集合 + """ + 生成验证码字符。 + """ + # 是否使用自定义字符集 if not self.__str: # 源文本 number = "3456789" @@ -55,32 +62,37 @@ class vieCode: else: self.__str = srcLetter + srcUpper + number - # 构造验证码 + # 随机生成验证码字符 self.__code = random.sample(self.__str, self.__length) def __createImage(self): - '''创建画布''' + """ + 创建画布并设置背景颜色。 + """ bgColor = (random.randint(200, 255), random.randint(200, 255), random.randint(200, 255)) self.__img = Image.new('RGB', (self.__width, self.__heigth), bgColor) self.__draw = ImageDraw.Draw(self.__img) def __createNoise(self): - '''画干扰点''' + """ + 在验证码图片上绘制干扰点。 + """ if not self.__inNoise: return font = ImageFont.truetype(self.__fontPatn, int(self.__fontSize / 1.5)) for i in range(5): - # 杂点颜色 + # 干扰点颜色 noiseColor = (random.randint(150, 200), random.randint(150, 200), random.randint(150, 200)) putStr = random.sample(self.__str, 2) for j in range(2): - # 绘杂点 + # 绘制干扰点 size = (random.randint(-10, self.__width), random.randint(-10, self.__heigth)) self.__draw.text(size, putStr[j], font=font, fill=noiseColor) - pass def __createCurve(self): - '''画干扰线''' + """ + 在验证码图片上绘制干扰曲线。 + """ if not self.__inCurve: return x = y = 0 @@ -93,7 +105,7 @@ class vieCode: xend = random.randint(self.__width / 2, self.__width * 2) w = (2 * math.pi) / t - # 画曲线 + # 绘制曲线 color = (random.randint(30, 150), random.randint(30, 150), random.randint(30, 150)) for x in range(xend): if w != 0: @@ -107,15 +119,17 @@ class vieCode: i -= i def __printString(self): - '''打印验证码字符串''' + """ + 在画布上打印验证码字符。 + """ font = ImageFont.truetype(self.__fontPatn, self.__fontSize) x = 0 - # 打印字符到画板 + # 打印字符到画布 for i in range(self.__length): # 设置字体随机颜色 color = (random.randint(30, 150), random.randint(30, 150), random.randint(30, 150)) - # 计算座标 + # 计算坐标 x = random.uniform(self.__fontSize * i * 0.95, self.__fontSize * i * 1.1) y = self.__fontSize * random.uniform(0.3, 0.5) # 打印字符 - self.__draw.text((x, y), self.__code[i], font=font, fill=color) + self.__draw.text((x, y), self.__code[i], font=font, fill=color) \ No newline at end of file diff --git a/applications/common/utils/http.py b/applications/common/utils/http.py index 680df5f..91a8051 100644 --- a/applications/common/utils/http.py +++ b/applications/common/utils/http.py @@ -2,23 +2,40 @@ from flask import jsonify def success_api(msg: str = "成功"): - """ 成功响应 默认值“成功” """ + """ + 返回成功的 API 响应。 + + :param msg: 成功消息内容,默认为 "成功"。 + :return: 返回 JSON 格式的响应,包含 `success` 和 `msg` 字段。 + """ return jsonify(success=True, msg=msg) def fail_api(msg: str = "失败"): - """ 失败响应 默认值“失败” """ + """ + 返回失败的 API 响应。 + + :param msg: 失败消息内容,默认为 "失败"。 + :return: 返回 JSON 格式的响应,包含 `success` 和 `msg` 字段。 + """ return jsonify(success=False, msg=msg) def table_api(msg: str = "", count=0, data=None, limit=10): - """ 动态表格渲染响应 """ + """ + 返回动态表格渲染所需的 API 响应。 + + :param msg: 响应消息内容,默认为空字符串。 + :param count: 数据总数,默认为 0。 + :param data: 表格数据,默认为 None。 + :param limit: 每页数据条数,默认为 10。 + :return: 返回 JSON 格式的响应,包含 `msg`、`code`、`data`、`count` 和 `limit` 字段。 + """ res = { 'msg': msg, 'code': 0, 'data': data, 'count': count, 'limit': limit - } - return jsonify(res) + return jsonify(res) \ No newline at end of file diff --git a/applications/common/utils/mail.py b/applications/common/utils/mail.py index ecb29bb..b03158e 100644 --- a/applications/common/utils/mail.py +++ b/applications/common/utils/mail.py @@ -1,5 +1,5 @@ """ -集成了对 Pear Admin Flask 二次开发的的邮件操作,并给了相对应的示例。 +集成了对 Pear Admin Flask 二次开发的邮件操作模块,并提供了相应的示例。 """ from flask import current_app from flask_mail import Message @@ -7,32 +7,31 @@ from flask_mail import Message from applications.common.curd import model_to_dicts from applications.common.helper import ModelFilter from applications.extensions import db, flask_mail -from applications.extensions.init_mail import mail from applications.models import Mail from applications.schemas import MailOutSchema def get_all(receiver=None, subject=None, content=None): """ - 获取邮件 + 获取邮件列表,支持根据接收者、主题和内容进行筛选。 - 返回的列表中的字典构造如下:: + 返回的列表中的字典结构如下:: { - "content": "", # html内容 - "create_at": "2022-12-25T10:51:17", # 时间 + "content": "", # HTML 内容 + "create_at": "2022-12-25T10:51:17", # 创建时间 "id": 17, # 邮件ID - "realname": "超级管理", # 创建者 + "realname": "超级管理", # 创建者姓名 "receiver": "", # 接收者 - "subject": "" # 主题 + "subject": "" # 邮件主题 } - :param receiver: 发送者 - :param subject: 邮件标题 - :param content: 邮件内容 - :return: 列表 + :param receiver: 接收者邮箱地址,支持模糊查询。 + :param subject: 邮件主题,支持模糊查询。 + :param content: 邮件内容,支持模糊查询。 + :return: 返回符合条件的邮件列表。 """ - # 查询参数构造 + # 构造查询条件 mf = ModelFilter() if receiver: mf.contains(field_name="receiver", value=receiver) @@ -40,31 +39,27 @@ def get_all(receiver=None, subject=None, content=None): mf.contains(field_name="subject", value=subject) if content: mf.exact(field_name="content", value=content) - # orm查询 - # 使用分页获取data需要.items + + # 查询邮件数据并分页 mail = Mail.query.filter(mf.get_filter(Mail)).layui_paginate() return model_to_dicts(schema=MailOutSchema, data=mail.items) def add(receiver, subject, content, user_id): """ - 发送一封邮件,若发送成功立刻提交数据库。 + 发送一封邮件,并将发送记录保存到数据库。 **该方法被邮件发送的视图函数调用。** - :param receiver: 接收者 多个用英文分号隔开 - :param subject: 邮件主题 - :param content: 邮件 html - :param user_id: 发送用户ID(谁发送的?) 可以用 from flask_login import current_user ; current_user.id 来表示当前登录用户 - :return: 成功与否 + :param receiver: 接收者邮箱地址,多个邮箱用英文分号隔开。 + :param subject: 邮件主题。 + :param content: 邮件内容(HTML 格式)。 + :param user_id: 发送者用户ID,表示谁发送了这封邮件。 + 可以使用 `from flask_login import current_user; current_user.id` 获取当前登录用户的ID。 + :return: 发送成功返回 True,失败报错。 """ - try: - msg = Message(subject=subject, recipients=receiver.split(";"), html=content) - flask_mail.send(msg) - except BaseException as e: - current_app.log_exception(e) - return False + send_mail(subject=subject, recipients=receiver.split(";"), content=content) + # 保存邮件记录到数据库 mail = Mail(receiver=receiver, subject=subject, content=content, user_id=user_id) - db.session.add(mail) db.session.commit() return True @@ -72,10 +67,10 @@ def add(receiver, subject, content, user_id): def delete(id): """ - 删除邮件记录,立刻写入数据库。 + 删除指定的邮件记录。 - :param id: 邮件ID - :return: 成功与否 + :param id: 邮件ID。 + :return: 删除成功返回 True,失败返回 False。 """ res = Mail.query.filter_by(id=id).delete() if not res: @@ -83,14 +78,16 @@ def delete(id): db.session.commit() return True + def send_mail(subject, recipients, content): - """原发送邮件函数,不会记录邮件发送记录 + """ + 发送邮件(不记录发送日志)。 - 失败报错,请注意使用 try 拦截。 + 注意:如果发送失败会抛出异常,请使用 try-except 进行捕获。 - :param subject: 主题 - :param recipients: 接收者 多个用英文分号隔开 - :param content: 邮件 html + :param subject: 邮件主题。 + :param recipients: 接收者邮箱地址,多个邮箱用英文分号隔开。 + :param content: 邮件内容(HTML 格式)。 """ message = Message(subject=subject, recipients=recipients, html=content) - mail.send(message) + flask_mail.send(message) diff --git a/applications/common/utils/rights.py b/applications/common/utils/rights.py index 640a129..c498c2f 100644 --- a/applications/common/utils/rights.py +++ b/applications/common/utils/rights.py @@ -5,7 +5,9 @@ from applications.common.admin_log import admin_log def authorize(power: str, log: bool = False): - """用户权限判断,用于判断目前会话用户是否拥有访问权限 + """ + 用户权限判断,用于判断目前会话用户是否拥有访问权限。 + 在模板中有与之对应的全局非修饰函数 authorize ,此函数定义位于 `applications/extensions/init_template_directives.py` 。 :param power: 权限标识 :type power: str diff --git a/applications/common/utils/validate.py b/applications/common/utils/validate.py index 8d90948..086cbd0 100644 --- a/applications/common/utils/validate.py +++ b/applications/common/utils/validate.py @@ -1,187 +1,240 @@ -# xss过滤 +# XSS 过滤 import validators from markupsafe import escape from validators import validator def str_escape(s): + """ + 对字符串进行 XSS 过滤,返回转义后的安全字符串。 + + :param s: 需要转义的字符串。 + :return: 返回转义后的字符串,如果输入为空则返回 None。 + """ if not s: return None return str(escape(s)) -between = validators.between -""" -验证数字是否介于最小值和/或最大值之间。 -这将适用于任何类似的类型,如浮点数、小数和日期,而不仅仅是整数。 -between(value, min=None, max=None) +def between(*args, **kwargs): + """ + 验证数字是否介于最小值和最大值之间。 + 适用于整数、浮点数、小数和日期等类型。 - min-数字的最小必需值。如果未提供,则不会检查最小值。 - max-数字的最大值。如果未提供,将不检查最大值。 + :param value: 需要验证的数字。 + :param min: 数字的最小值(可选)。 + :param max: 数字的最大值(可选)。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 - >>> from datetime import datetime - + 示例: >>> between(5, min=2) True - + >>> between(13.2, min=13, max=14) True - + >>> between(500, max=400) ValidationFailure(func=between, args=...) - - >>> between( - ... datetime(2000, 11, 11), - ... min=datetime(1999, 11, 11) - ... ) - True -""" + """ + return validators.between(*args, **kwargs) -domain = validators.domain -""" -返回给定值是否为有效域 -如果值是有效域名,则此函数返回 True ,否则返回 ValidationFailure -domain(value) - value-要验证的属性域字符串 - + +def domain(*args, **kwargs): + """ + 验证给定值是否为有效的域名。 + + :param value: 需要验证的域名字符串。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 + + 示例: >>> domain('example.com') True - + >>> domain('example.com/') ValidationFailure(func=domain, ...) -""" + """ + return validators.domain(*args, **kwargs) -email = validators.email -""" - 验证电子邮件地址。验证成功时返回 True ,验证失败时返回 - - >>> email('someone@example.com') - True - - >>> email('bogus@@') - ValidationFailure(func=email, ...) -""" -iban = validators.iban -""" -返回给定值是否为有效的IBAN代码。 -如果值是有效的IBAN,则此函数返回 True ,否则返回 ValidationFailure 。 +def email(*args, **kwargs): + """ + 验证给定值是否为有效的电子邮件地址。 - >>> iban('DE29100500001061045672') - True - - >>> iban('123456') - ValidationFailure(func=iban, ...) -""" + :param value: 需要验证的电子邮件地址。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 -ipv4 = validators.ipv4 -""" -返回给定值是否为有效的IPv4地址。 + 示例: + >>> email('someone@example.com') + True - >>> ipv4('123.0.0.7') - True - - >>> ipv4('900.80.70.11') - ValidationFailure(func=ipv4, args={'value': '900.80.70.11'}) -""" + >>> email('bogus@@') + ValidationFailure(func=email, ...) + """ + return validators.email(*args, **kwargs) -ipv6 = validators.ipv6 -""" -返回给定值是否为有效的IP版本6地址。 - >>> ipv6('abcd:ef::42:1') - True - - >>> ipv6('abc.0.0.1') - ValidationFailure(func=ipv6, args={'value': 'abc.0.0.1'}) -""" -length = validators.length -""" -返回给定字符串的长度是否在指定范围内。 - >>> length('something', min=2) - True - - >>> length('something', min=9, max=9) - True - - >>> length('something', max=5) - ValidationFailure(func=length, ...) -""" +def iban(*args, **kwargs): + """ + 验证给定值是否为有效的 IBAN 代码。 -mac_address = validators.mac_address -""" -返回给定值是否为有效MAC地址。 -如果该值是有效的MAC地址,则此函数返回 True ,否则返回 ValidationFailure 。 + :param value: 需要验证的 IBAN 代码。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 - >>> mac_address('01:23:45:67:ab:CD') - True - - >>> mac_address('00:00:00:00:00') - ValidationFailure(func=mac_address, args={'value': '00:00:00:00:00'}) -""" + 示例: + >>> iban('DE29100500001061045672') + True -slug = validators.slug -""" -验证给定值是否为有效的块。 -有效的短信息只能包含字母数字字符、连字符和下划线。 - >>> slug('my.slug') - ValidationFailure(func=slug, args={'value': 'my.slug'}) - - >>> slug('my-slug-2134') - True -""" + >>> iban('123456') + ValidationFailure(func=iban, ...) + """ + return validators.iban(*args, **kwargs) -#truthy = validators.truthy -""" -验证给定值不是错误值。 -""" -url = validators.url -""" -返回给定值是否为有效URL。 -如果值是有效URL,则此函数返回 True ,否则返回 ValidationFailure 。 +def ipv4(*args, **kwargs): + """ + 验证给定值是否为有效的 IPv4 地址。 - >>> url('http://foobar.dk') - True - - >>> url('http://10.0.0.1') - True - - >>> url('http://foobar.d') - ValidationFailure(func=url, ...) - - >>> url('http://10.0.0.1', public=True) - ValidationFailure(func=url, ...) -""" + :param value: 需要验证的 IPv4 地址。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 -uuid = validators.uuid -""" -返回给定值是否为有效UUID。 -如果值是有效的UUID,则此函数返回 True ,否则返回 ValidationFailure 。 + 示例: + >>> ipv4('123.0.0.7') + True - >>> uuid('2bc1c94f-0deb-43e9-92a1-4775189ec9f8') - True - - >>> uuid('2bc1c94f 0deb-43e9-92a1-4775189ec9f8') - ValidationFailure(func=uuid, ...) -""" + >>> ipv4('900.80.70.11') + ValidationFailure(func=ipv4, args={'value': '900.80.70.11'}) + """ + return validators.ipv4(*args, **kwargs) + + +def ipv6(*args, **kwargs): + """ + 验证给定值是否为有效的 IPv6 地址。 + + :param value: 需要验证的 IPv6 地址。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 + + 示例: + >>> ipv6('abcd:ef::42:1') + True + + >>> ipv6('abc.0.0.1') + ValidationFailure(func=ipv6, args={'value': 'abc.0.0.1'}) + """ + return validators.ipv6(*args, **kwargs) + + +def length(*args, **kwargs): + """ + 验证给定字符串的长度是否在指定范围内。 + + :param value: 需要验证的字符串。 + :param min: 字符串的最小长度(可选)。 + :param max: 字符串的最大长度(可选)。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 + + 示例: + >>> length('something', min=2) + True + + >>> length('something', min=9, max=9) + True + + >>> length('something', max=5) + ValidationFailure(func=length, ...) + """ + return validators.length(*args, **kwargs) + + +def mac_address(*args, **kwargs): + """ + 验证给定值是否为有效的 MAC 地址。 + + :param value: 需要验证的 MAC 地址。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 + + 示例: + >>> mac_address('01:23:45:67:ab:CD') + True + + >>> mac_address('00:00:00:00:00') + ValidationFailure(func=mac_address, args={'value': '00:00:00:00:00'}) + """ + return validators.mac_address(*args, **kwargs) + + +def slug(*args, **kwargs): + """ + 验证给定值是否为有效的 Slug 格式。 + 有效的 Slug 只能包含字母数字字符、连字符和下划线。 + + :param value: 需要验证的字符串。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 + + 示例: + >>> slug('my.slug') + ValidationFailure(func=slug, args={'value': 'my.slug'}) + + >>> slug('my-slug-2134') + True + """ + return validators.slug(*args, **kwargs) + + +def url(*args, **kwargs): + """ + 验证给定值是否为有效的 URL。 + + :param value: 需要验证的 URL。 + :param public: 是否仅允许公共 URL(可选)。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 + + 示例: + >>> url('http://foobar.dk') + True + + >>> url('http://10.0.0.1') + True + + >>> url('http://foobar.d') + ValidationFailure(func=url, ...) + + >>> url('http://10.0.0.1', public=True) + ValidationFailure(func=url, ...) + """ + return validators.url(*args, **kwargs) + + +def uuid(*args, **kwargs): + """ + 验证给定值是否为有效的 UUID。 + + :param value: 需要验证的 UUID。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 + + 示例: + >>> uuid('2bc1c94f-0deb-43e9-92a1-4775189ec9f8') + True + + >>> uuid('2bc1c94f 0deb-43e9-92a1-4775189ec9f8') + ValidationFailure(func=uuid, ...) + """ + return validators.uuid(*args, **kwargs) @validator def even(value): - return not (value % 2) + """ + 验证给定值是否为偶数。 + :param value: 需要验证的数字。 + :return: 如果是偶数返回 True,否则返回 ValidationFailure。 -""" -一个装饰器,它使给定的函数验证器 -每当给定函数被调用并返回 False 值时,这个装饰器返回 ValidationFailure 对象。 ->>> @validator -... def even(value): -... return not (value % 2) + 示例: + >>> even(4) + True ->>> even(4) -True - ->>> even(5) -ValidationFailure(func=even, args={'value': 5}) -""" + >>> even(5) + ValidationFailure(func=even, args={'value': 5}) + """ + return not (value % 2) \ No newline at end of file diff --git a/applications/view/system/mail.py b/applications/view/system/mail.py index 01ec31a..51f3f16 100644 --- a/applications/view/system/mail.py +++ b/applications/view/system/mail.py @@ -1,14 +1,16 @@ from flask import Blueprint, render_template, request, current_app from flask_login import current_user -from flask_mail import Message + from applications.common.curd import model_to_dicts from applications.common.helper import ModelFilter from applications.common.utils.http import table_api, fail_api, success_api from applications.common.utils.rights import authorize from applications.common.utils.validate import str_escape -from applications.extensions import db, flask_mail +from applications.extensions import db from applications.models import Mail from applications.schemas import MailOutSchema +from applications.common.utils import mail +from applications.common.admin_log import admin_log bp = Blueprint('adminMail', __name__, url_prefix='/mail') @@ -61,17 +63,13 @@ def save(): user_id = current_user.id try: - msg = Message(subject=subject, recipients=receiver.split(";"), body=content) - flask_mail.send(msg) + if mail.add(receiver=receiver, subject=subject, content=content, user_id=user_id): + return success_api(msg="增加成功") except Exception as e: current_app.log_exception(e) - return fail_api(msg="发送失败,请检查邮件配置或发送人邮箱是否写错") + admin_log(request, False, desc="发送日志失败:" + str(e)) - mail = Mail(receiver=receiver, subject=subject, content=content, user_id=user_id) - - db.session.add(mail) - db.session.commit() - return success_api(msg="增加成功") + return success_api(msg="发送失败,请检查日志。") # 删除用户 diff --git a/applications/view/system/passport.py b/applications/view/system/passport.py index acb21bd..6004981 100644 --- a/applications/view/system/passport.py +++ b/applications/view/system/passport.py @@ -75,6 +75,7 @@ def login_post(): # session['role'] = [roles] return success_api(msg="登录成功") + login_log(request, uid=user.id, is_access=False) return fail_api(msg="用户名或密码错误") diff --git a/docs/source/function/utils/cache.rst b/docs/source/function/utils/cache.rst index 839f6bf..5d6d087 100644 --- a/docs/source/function/utils/cache.rst +++ b/docs/source/function/utils/cache.rst @@ -1,2 +1,61 @@ -程序缓存模块 -===================== \ No newline at end of file +:mod:`cache` -- 应用缓存模块 +================================ + +:mod:`cache` 模块源代码在文件夹 `applications/common/utils/cache.py` 下,主要用于简单的程序数据缓存。 + +目前此模块仅启用应用程序缓存,暂时没有联动 Redis 等数据库缓存的功能,后续有意向添加。您可以在自己的项目中添加相关的函数,当然也非常欢迎提交 PR ,一起完善项目。 + +.. warning:: + + 此模块目前仅用于简单的缓存记录,不能记录大量的、持久的缓存。缓存内容存在内存中,在程序结束后清空!具体的例子是 `系统监控` 页面,用于缓存 5 秒内的 CPU 与内存 + 的监控数据。 + +.. module:: cache + +变量 +----------- + +.. py:data:: cache_dict + + 一个字典,用于存放缓存数据与缓存的过期时间戳。 + +函数 +----------- + +.. function:: cache_set_internal(key, value, expired=5) + + 程序内部实现的记录缓存,用于简单、体量不大的缓存记录,在程序结束后销毁。对于高速、体量大的环境请配置 Redis 等服务自行记录。 + 记录缓存,存储键值对,并记录当前时间作为缓存的时间戳。 + + :param key: 键 + :param value: 值 + :param expired: 过期时间(秒),默认5秒 + +.. function:: cache_get_internal(key) + + 获取缓存,根据键从缓存中获取值,并检查是否过期。 + + :param key: 键 + :return: 如果缓存存在且未过期,返回缓存的值;否则返回 None + +.. function:: cache_auto_internal(key, call, expired=5) + + 如果缓存存在直接返回缓存内容,缓存不存在或者过期执行 call 函数,并取得返回值记录并返回。 + + :param key: 键 + :param call: 获取新值的地方 + :param expired: 过期时间(秒),默认5秒 + + **示例:** + + .. code-block:: python + + from application.common.utils.cache import cache_auto_internal + + def fetch_data(): + # 模拟从数据库或接口获取数据 + return "new_data" + + # 使用 cache_auto_internal 获取缓存或调用 fetch_data 获取新值 + result = cache_auto_internal("my_key", fetch_data, expired=10) + print(result) # 输出: "new_data"(如果缓存不存在或已过期) \ No newline at end of file diff --git a/docs/source/function/utils/captcha.rst b/docs/source/function/utils/captcha.rst new file mode 100644 index 0000000..0838a6d --- /dev/null +++ b/docs/source/function/utils/captcha.rst @@ -0,0 +1,114 @@ +:mod:`captcha` -- 验证码生成模块 +================================== + +:mod:`captcha` 模块源代码在文件夹 `applications/common/utils/captcha.py` 下,主要用于生成验证码图片。 + +.. module:: captcha + +类 +------ + +.. class:: vieCode + + 生成验证码图片。 + + .. py:attribute:: __fontSize + :type: int + + 字体大小,默认为 20。 + + .. py:attribute:: __width + :type: int + + 画布宽度,默认为 120。 + + .. py:attribute:: __heigth + :type: int + + 画布高度,默认为 45。 + + .. py:attribute:: __length + :type: int + + 验证码长度,默认为 4。 + + .. py:attribute:: __draw + :type: ImageDraw.Draw + + 画布对象。 + + .. py:attribute:: __img + :type: Image.Image + + 图片对象。 + + .. py:attribute:: __code + :type: list + + 验证码字符。 + + .. py:attribute:: __str + :type: str + + 自定义验证码字符集。 + + .. py:attribute:: __inCurve + :type: bool + + 是否绘制干扰曲线,默认为 True。 + + .. py:attribute:: __inNoise + :type: bool + + 是否绘制干扰点,默认为 True。 + + .. py:attribute:: __type + :type: int + + 验证码类型:1-纯字母,2-数字字母混合,默认为 2。 + + .. py:attribute:: __fontPatn + :type: str + + 字体路径,默认为 ``applications/common/utils/fonts/captcha.ttf``。 + + .. method:: GetCodeImage(size=80, length=4) + + 生成验证码图片及其对应的验证码字符。 + + :param size: 验证码字体大小,默认为 80。 + :param length: 验证码字符长度,默认为 4。 + :return: 返回验证码图片对象和验证码字符。 + + **示例:** + + .. code-block:: python + + vc = vieCode() + img, code = vc.GetCodeImage(size=60, length=6) + img.show() # 显示验证码图片 + print("验证码:", code) # 输出验证码字符 + + .. method:: __cerateFilter() + + 对验证码图片进行模糊处理,增加识别难度。 + + .. method:: __createCode() + + 生成验证码字符。 + + .. method:: __createImage() + + 创建画布并设置背景颜色。 + + .. method:: __createNoise() + + 在验证码图片上绘制干扰点。 + + .. method:: __createCurve() + + 在验证码图片上绘制干扰曲线。 + + .. method:: __printString() + + 在画布上打印验证码字符。 \ No newline at end of file diff --git a/docs/source/function/utils/http.rst b/docs/source/function/utils/http.rst new file mode 100644 index 0000000..7d89bfc --- /dev/null +++ b/docs/source/function/utils/http.rst @@ -0,0 +1,39 @@ +:mod:`http` -- JSON 响应正文生成模块 +======================================= + +:mod:`http` 模块源代码在文件夹 `applications/common/utils/http.py` 下,主要用于生成 JSON 格式的响应正文。 + +对于大部分 JSON 格式响应的数据,请尽量遵循响应格式规范。如此方便后续前后端的分离和项目的构建。 + +.. module:: http + +函数 +------------ + +.. function:: success_api(msg: str = "成功") + + 返回成功的 API 响应。 + + :param msg: 成功消息内容,默认为 "成功"。 + :return: 返回 JSON 格式的响应,包含 `success` 和 `msg` 字段。 + + +.. function:: fail_api(msg: str = "失败") + + 返回失败的 API 响应。 + + :param msg: 失败消息内容,默认为 "失败"。 + :return: 返回 JSON 格式的响应,包含 `success` 和 `msg` 字段。 + + +.. function:: table_api(msg: str = "", count=0, data=None, limit=10) + + 返回动态表格渲染所需的 API 响应。 + + :param msg: 响应消息内容,默认为空字符串。 + :param count: 数据总数,默认为 0。 + :param data: 表格数据,默认为 None。 + :param limit: 每页数据条数,默认为 10。 + :return: 返回 JSON 格式的响应,包含 `msg`、`code`、`data`、`count` 和 `limit` 字段。 + + diff --git a/docs/source/function/utils/index.rst b/docs/source/function/utils/index.rst index 50c0807..b85744f 100644 --- a/docs/source/function/utils/index.rst +++ b/docs/source/function/utils/index.rst @@ -1,3 +1,5 @@ +.. module:: applications.common.utils + .. title:: 辅助函数 目录索引 @@ -6,3 +8,9 @@ :maxdepth: 1 cache + captcha + http + mail + rights + upload + validate \ No newline at end of file diff --git a/docs/source/function/utils/mail.rst b/docs/source/function/utils/mail.rst new file mode 100644 index 0000000..78a09bd --- /dev/null +++ b/docs/source/function/utils/mail.rst @@ -0,0 +1,60 @@ +:mod:`mail` -- 邮件模块 +================================== + +:mod:`mail` 模块源代码在文件夹 `applications/common/utils/mail.py` 下,主要用于邮件的发送。 + +.. module:: mail + +函数 +--------------- + +.. function:: get_all(receiver=None, subject=None, content=None) + + 获取邮件列表,支持根据接收者、主题和内容进行筛选。 + + 返回的列表中的字典结构如下:: + + { + "content": "", # HTML 内容 + "create_at": "2022-12-25T10:51:17", # 创建时间 + "id": 17, # 邮件ID + "realname": "超级管理", # 创建者姓名 + "receiver": "", # 接收者 + "subject": "" # 邮件主题 + } + + :param receiver: 接收者邮箱地址,支持模糊查询。 + :param subject: 邮件主题,支持模糊查询。 + :param content: 邮件内容,支持模糊查询。 + :return: 返回符合条件的邮件列表。 + + +.. function:: add(receiver, subject, content, user_id) + + 发送一封邮件,并将发送记录保存到数据库。 **该方法被邮件发送的视图函数调用。** + + :param receiver: 接收者邮箱地址,多个邮箱用英文分号隔开。 + :param subject: 邮件主题。 + :param content: 邮件内容(HTML 格式)。 + :param user_id: 发送者用户ID,表示谁发送了这封邮件。 + 可以使用 `from flask_login import current_user; current_user.id` 获取当前登录用户的ID。 + :return: 发送成功返回 True,失败报错。 + + +.. function:: delete(id) + + 删除指定的邮件记录。 + + :param id: 邮件ID。 + :return: 删除成功返回 True,失败返回 False。 + + +.. function:: send_mail(subject, recipients, content) + + 发送邮件(不记录发送日志)。 + + 注意:如果发送失败会抛出异常,请使用 try-except 进行捕获。 + + :param subject: 邮件主题。 + :param recipients: 接收者邮箱地址,多个邮箱用英文分号隔开。 + :param content: 邮件内容(HTML 格式)。 \ No newline at end of file diff --git a/docs/source/function/utils/rights.rst b/docs/source/function/utils/rights.rst new file mode 100644 index 0000000..7dd2019 --- /dev/null +++ b/docs/source/function/utils/rights.rst @@ -0,0 +1,61 @@ +:mod:`rights` -- 权限验证模块 +================================== + +:mod:`rights` 模块源代码在文件夹 `applications/common/utils/rights.py` 下,主要用于权限验证。 + +.. module:: rights + +函数 +--------------- + +.. function:: authorize(power: str, log: bool = False) + + 用户权限判断,用于判断目前会话用户是否拥有访问权限。此函数是一个修饰器,可用于修饰视图函数。 + 在模板中有与之对应的全局非修饰函数 authorize ,此函数定义位于 `applications/extensions/init_template_directives.py` 。 + 示例中将会展示两种方式的用法。 + + :param power: 权限标识 + :type power: str + :param log: 是否记录日志,默认为 False + :type log: bool, optional + + **修饰函数示例:** + + .. code-block:: python + + @app.route("/test") + @authorize("system:power:remove", log=True) + def test_index(): + return 'You are allowed.' + + + **在前端模板中:** + + .. code-block:: html + + {% if authorize("system:user:edit") %} + + {% endif %} + + + .. important:: + + `if authorize("system:user:edit")` 的方式仅适用于 **前端模板渲染** ,不得用于后端代码判断。 + 如果后端想要使用,请使用 **power in session.get('permissions')** 来判断, `power` 是 `权限标识` 。 + + .. code-block:: python + + from flask import session + + ... + + @bp.get('/test') + def test(): + if 'system:user:edit' in session.get('permissions'): + ... + + ... + + diff --git a/docs/source/function/utils/upload.rst b/docs/source/function/utils/upload.rst new file mode 100644 index 0000000..4f3b989 --- /dev/null +++ b/docs/source/function/utils/upload.rst @@ -0,0 +1,34 @@ +:mod:`upload` -- 文件上传模块 +================================== + +:mod:`upload` 模块源代码在文件夹 `applications/common/utils/upload.py` 下,主要用于文件上传,目前主要用于图片上传。 + +.. module:: upload + +函数 +--------------- + +.. function:: get_photo(page, limit) + + 分页获取图片列表,并按创建时间降序排列。 + + :param page: 当前页码。 + :param limit: 每页显示的图片数量。 + :return: 返回图片列表和图片总数。 + + +.. function:: upload_one(photo, mime) + + 上传一张图片,并保存图片信息到数据库。 + + :param photo: 图片文件对象。 + :param mime: 图片的 MIME 类型。 + :return: 返回图片的访问 URL。 + + +.. function:: delete_photo_by_id(_id) + + 根据图片 ID 删除图片及其记录。 + + :param _id: 图片 ID。 + :return: 返回删除操作的结果(数据库中删除的个数,成功非0)。 \ No newline at end of file diff --git a/docs/source/function/utils/validate.rst b/docs/source/function/utils/validate.rst new file mode 100644 index 0000000..6bb62cd --- /dev/null +++ b/docs/source/function/utils/validate.rst @@ -0,0 +1,231 @@ +:mod:`validate` -- 效验模块 +================================== + +:mod:`validate` 模块源代码在文件夹 `applications/common/utils/rights.py` 下,主要用于数据效验与过滤。 + +.. module:: validate + +函数 +------------- + +.. function:: str_escape(s) + + 对字符串进行 XSS 过滤,返回转义后的安全字符串。 + + :param s: 需要转义的字符串。 + :return: 返回转义后的字符串,如果输入为空则返回 None。 + + +.. function:: between(*args, **kwargs) + + 验证数字是否介于最小值和最大值之间。 + 适用于整数、浮点数、小数和日期等类型。 + + :param value: 需要验证的数字。 + :param min: 数字的最小值(可选)。 + :param max: 数字的最大值(可选)。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 + + **示例:** + + .. code-block:: python + + from applications.common.utils.validate import between + + between(5, min=2) # True + between(13.2, min=13, max=14) # True + between(500, max=400) # ValidationFailure(func=between, args=...) + + +.. function:: domain(*args, **kwargs) + + 验证给定值是否为有效的域名。 + + :param value: 需要验证的域名字符串。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 + + **示例:** + + .. code-block:: python + + from applications.common.utils.validate import domain + + domain('example.com') # True + domain('example.com/') # ValidationFailure(func=domain, ...) + + +.. function:: email(*args, **kwargs) + + 验证给定值是否为有效的电子邮件地址。 + + :param value: 需要验证的电子邮件地址。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 + + **示例:** + + .. code-block:: python + + from applications.common.utils.validate import email + + email('someone@example.com') # True + email('bogus@@') # ValidationFailure(func=email, ...) + + +.. function:: iban(*args, **kwargs) + + 验证给定值是否为有效的 IBAN 代码。 + + :param value: 需要验证的 IBAN 代码。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 + + **示例:** + + .. code-block:: python + + from applications.common.utils.validate import iban + + iban('DE29100500001061045672') # True + iban('123456') # ValidationFailure(func=iban, ...) + + +.. function:: ipv4(*args, **kwargs) + + 验证给定值是否为有效的 IPv4 地址。 + + :param value: 需要验证的 IPv4 地址。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 + + **示例:** + + .. code-block:: python + + from applications.common.utils.validate import ipv4 + + ipv4('123.0.0.7') # True + ipv4('900.80.70.11') # ValidationFailure(func=ipv4, args={'value': '900.80.70.11'}) + + +.. function:: ipv6(*args, **kwargs) + + 验证给定值是否为有效的 IPv6 地址。 + + :param value: 需要验证的 IPv6 地址。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 + + **示例:** + + .. code-block:: python + + from applications.common.utils.validate import ipv6 + + ipv6('abcd:ef::42:1') # True + ipv6('abc.0.0.1') # ValidationFailure(func=ipv6, args={'value': 'abc.0.0.1'}) + + +.. function:: length(*args, **kwargs) + + 验证给定字符串的长度是否在指定范围内。 + + :param value: 需要验证的字符串。 + :param min: 字符串的最小长度(可选)。 + :param max: 字符串的最大长度(可选)。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 + + **示例:** + + .. code-block:: python + + from applications.common.utils.validate import length + + length('something', min=2) # True + length('something', min=9, max=9) # True + length('something', max=5) # ValidationFailure(func=length, ...) + + +.. function:: mac_address(*args, **kwargs) + + 验证给定值是否为有效的 MAC 地址。 + + :param value: 需要验证的 MAC 地址。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 + + **示例:** + + .. code-block:: python + + from applications.common.utils.validate import mac_address + + mac_address('01:23:45:67:ab:CD') # True + mac_address('00:00:00:00:00') # ValidationFailure(func=mac_address, args={'value': '00:00:00:00:00'}) + + +.. function:: slug(*args, **kwargs) + + 验证给定值是否为有效的 Slug 格式。 + 有效的 Slug 只能包含字母数字字符、连字符和下划线。 + + :param value: 需要验证的字符串。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 + + **示例:** + + .. code-block:: python + + from applications.common.utils.validate import slug + + slug('my.slug') # ValidationFailure(func=slug, args={'value': 'my.slug'}) + slug('my-slug-2134') # True + + +.. function:: url(*args, **kwargs) + + 验证给定值是否为有效的 URL。 + + :param value: 需要验证的 URL。 + :param public: 是否仅允许公共 URL(可选)。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 + + **示例:** + + .. code-block:: python + + from applications.common.utils.validate import url + + url('http://foobar.dk') # True + url('http://10.0.0.1') # True + url('http://foobar.d') # ValidationFailure(func=url, ...) + url('http://10.0.0.1', public=True) # ValidationFailure(func=url, ...) + + +.. function:: uuid(*args, **kwargs) + + 验证给定值是否为有效的 UUID。 + + :param value: 需要验证的 UUID。 + :return: 如果验证成功返回 True,否则返回 ValidationFailure。 + + **示例:** + + .. code-block:: python + + from applications.common.utils.validate import uuid + + uuid('2bc1c94f-0deb-43e9-92a1-4775189ec9f8') # True + uuid('2bc1c94f 0deb-43e9-92a1-4775189ec9f8') # ValidationFailure(func=uuid, ...) + + +.. function:: even(value) + + 验证给定值是否为偶数。 + + :param value: 需要验证的数字。 + :return: 如果是偶数返回 True,否则返回 ValidationFailure。 + + **示例:** + + .. code-block:: python + + from applications.common.utils.validate import even + + even(4) # True + even(5) # ValidationFailure(func=even, args={'value': 5}) \ No newline at end of file