├── object ├── __init__.py ├── comment.py ├── msg.py ├── archive.py ├── blog.py └── user.py ├── static └── styles │ ├── auth │ ├── yours.css │ ├── login.css │ ├── passwd.css │ ├── register.css │ ├── role.css │ └── delete.css │ ├── oss │ └── upload.css │ ├── docx │ ├── docx.css │ └── article.css │ ├── msg │ └── msg.css │ ├── base.css │ ├── archive │ └── archive.css │ └── index │ └── hello.css ├── .gitmodules ├── templates ├── email-msg │ ├── register.txt │ └── register.html ├── about_me │ └── about_me.html ├── error.html ├── auth │ ├── passwd.html │ ├── login.html │ ├── register.html │ ├── delete.html │ ├── yours.html │ └── role.html ├── oss │ └── upload.html ├── macro.html ├── index │ ├── index.html │ └── hello.html ├── msg │ └── msg.html ├── archive │ └── archive.html ├── docx │ ├── docx.html │ └── article.html └── base.html ├── app ├── __init__.py ├── cache.py ├── http_auth.py ├── about_me.py ├── tool.py ├── index.py ├── oss.py ├── archive.py ├── msg.py ├── app.py ├── api.py ├── auth.py └── docx.py ├── Dockerfile ├── send_email └── __init__.py ├── sql ├── redis.py ├── __init__.py ├── statistics.py ├── base.py ├── cache_refresh.py ├── comment.py ├── archive.py ├── msg.py ├── mysql.py ├── user.py ├── blog.py └── cache.py ├── requirements.txt ├── main.py ├── aliyun └── __init__.py ├── gunicorn.conf.py ├── README.md ├── .dockerignore ├── .gitignore ├── configure └── __init__.py └── init.sql /object/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/styles/auth/yours.css: -------------------------------------------------------------------------------- 1 | .modal div { 2 | background-color: white; 3 | border-radius: 10px; 4 | } -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "static/editor.md"] 2 | path = static/editor.md 3 | url = https://github.com/pandao/editor.md.git 4 | -------------------------------------------------------------------------------- /templates/email-msg/register.txt: -------------------------------------------------------------------------------- 1 | 欢迎注册 {{ conf["BLOG_NAME"] }} 用户,请点击以下链接完成认证。 2 | 若您未进行过注册操作,请忽略此邮件。 3 | {{ register_url }} 4 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from .tool import role_required, form_required 2 | from .app import HBlogFlask, Hblog 3 | from .cache import cache 4 | -------------------------------------------------------------------------------- /static/styles/auth/login.css: -------------------------------------------------------------------------------- 1 | .login-form { 2 | background-color: white; 3 | border-radius: 10px; 4 | border: 2px solid #6b6882; 5 | padding: 15px; 6 | } -------------------------------------------------------------------------------- /static/styles/oss/upload.css: -------------------------------------------------------------------------------- 1 | .upload-form { 2 | background-color: white; 3 | border-radius: 10px; 4 | border: 2px solid #6b6882; 5 | padding: 15px; 6 | } -------------------------------------------------------------------------------- /static/styles/auth/passwd.css: -------------------------------------------------------------------------------- 1 | .passwd-form { 2 | background-color: white; 3 | border-radius: 10px; 4 | border: 2px solid #6b6882; 5 | padding: 15px; 6 | } -------------------------------------------------------------------------------- /static/styles/auth/register.css: -------------------------------------------------------------------------------- 1 | .register-form { 2 | background-color: white; 3 | border-radius: 10px; 4 | border: 2px solid #6b6882; 5 | padding: 15px; 6 | } -------------------------------------------------------------------------------- /static/styles/auth/role.css: -------------------------------------------------------------------------------- 1 | .role-form { 2 | background-color: white; 3 | border-radius: 10px; 4 | border: 2px solid #6b6882; 5 | padding: 15px; 6 | } 7 | -------------------------------------------------------------------------------- /static/styles/docx/docx.css: -------------------------------------------------------------------------------- 1 | .markdown { 2 | background-color: white; 3 | padding: 15px; 4 | border: 2px solid #0074D9; 5 | border-radius: 10px; 6 | } 7 | 8 | .modal div { 9 | background-color: white; 10 | border-radius: 10px; 11 | } -------------------------------------------------------------------------------- /static/styles/auth/delete.css: -------------------------------------------------------------------------------- 1 | .delete-form { 2 | background-color: white; 3 | border-radius: 10px; 4 | border: 2px solid #6b6882; 5 | padding: 15px; 6 | } 7 | 8 | .modal div { 9 | background-color: white; 10 | border-radius: 10px; 11 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-bullseye 2 | WORKDIR /app 3 | COPY . . 4 | RUN ["python", "-m", "pip", "install", "--upgrade", "pip"] 5 | RUN ["python", "-m", "pip", "install", "-r", "requirements.txt"] 6 | RUN ["python", "-m", "pip", "install", "gunicorn"] 7 | EXPOSE 80 8 | ENTRYPOINT ["python", "-m", "gunicorn", "-c", "./gunicorn.conf.py", "main:app"] -------------------------------------------------------------------------------- /static/styles/msg/msg.css: -------------------------------------------------------------------------------- 1 | .msg { 2 | margin-bottom: 20px; 3 | border-radius: 10px; 4 | border: 2px solid #FFDC00; 5 | padding: 15px; 6 | background-color: white; 7 | } 8 | 9 | .msg .msg-title { 10 | color: black; 11 | text-decoration: none; 12 | } 13 | 14 | .modal div { 15 | background-color: white; 16 | border-radius: 10px; 17 | } -------------------------------------------------------------------------------- /static/styles/base.css: -------------------------------------------------------------------------------- 1 | html { 2 | word-break: break-word; 3 | } 4 | 5 | body { 6 | overflow-y: scroll; 7 | overflow-x: hidden; 8 | } 9 | 10 | #foot { 11 | text-align: center; 12 | padding-bottom: 10px; 13 | } 14 | 15 | #ICP { 16 | background-color: rgb(255, 255, 255, 60%); 17 | } 18 | 19 | #GONG_AN { 20 | background-color: rgb(255, 255, 255, 60%); 21 | } -------------------------------------------------------------------------------- /static/styles/docx/article.css: -------------------------------------------------------------------------------- 1 | .comment { 2 | margin-bottom: 20px; 3 | border-radius: 10px; 4 | border: 2px solid #FFDC00; 5 | padding: 15px; 6 | background-color: white; 7 | } 8 | 9 | .comment .comment-title { 10 | color: black; 11 | text-decoration: none; 12 | } 13 | 14 | .modal div { 15 | background-color: white; 16 | border-radius: 10px; 17 | } -------------------------------------------------------------------------------- /app/cache.py: -------------------------------------------------------------------------------- 1 | from flask_caching import Cache 2 | from configure import conf 3 | 4 | cache = Cache(config={ 5 | 'CACHE_TYPE': 'RedisCache', 6 | 'CACHE_KEY_PREFIX': f'{conf["FLASK_CACHE_PREFIX"]}:', 7 | 'CACHE_REDIS_URL': f'redis://{conf["CACHE_REDIS_NAME"]}:{conf["CACHE_REDIS_PASSWD"]}@' 8 | f'{conf["CACHE_REDIS_HOST"]}:{conf["CACHE_REDIS_PORT"]}/{conf["CACHE_REDIS_DATABASE"]}' 9 | }) 10 | -------------------------------------------------------------------------------- /static/styles/archive/archive.css: -------------------------------------------------------------------------------- 1 | .archive { 2 | background-color: white; 3 | border-radius: 10px; 4 | border: 2px solid #39CCCC; 5 | min-width: 30%; 6 | min-height: 20%; 7 | padding: 15px; 8 | } 9 | 10 | .create { 11 | background-color: white; 12 | border-radius: 10px; 13 | border: 2px solid #1685a9; 14 | padding: 15px; 15 | } 16 | 17 | .modal div { 18 | background-color: white; 19 | border-radius: 10px; 20 | } 21 | 22 | .archive_describe { 23 | min-height: 2em; 24 | } -------------------------------------------------------------------------------- /app/http_auth.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify, g 2 | from flask_httpauth import HTTPBasicAuth 3 | from object.user import User 4 | 5 | 6 | http_auth = HTTPBasicAuth() 7 | 8 | 9 | @http_auth.verify_password 10 | def verify_passwd(email, passwd): 11 | user = User(email) 12 | g.user = user 13 | return user.check_passwd(passwd) 14 | 15 | 16 | @http_auth.error_handler 17 | def unauthorized(): 18 | rsp = jsonify({"status": 403, 'error': 'Unauthorized access'}) 19 | rsp.status_code = 403 20 | return rsp 21 | -------------------------------------------------------------------------------- /templates/about_me/about_me.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 关于我 {% endblock %} 4 | 5 | {% block style %} 6 | {{ super() }} 7 | {% endblock %} 8 | 9 | {% block content %} 10 | {% cache conf["CACHE_EXPIRE"], ":about_me" %} 11 |
12 |
13 |
14 | {{ about_me | safe }} 15 |
16 |
17 |
18 | {% endcache %} 19 | {% endblock %} -------------------------------------------------------------------------------- /app/about_me.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, current_app 2 | import app 3 | from configure import conf 4 | 5 | about_me = Blueprint("about_me", __name__) 6 | 7 | 8 | @about_me.route('/') 9 | def about_me_page(): 10 | app.HBlogFlask.print_load_page_log("about me") 11 | hblog: app.Hblog = current_app 12 | return render_template("about_me/about_me.html", about_me=hblog.about_me) 13 | 14 | 15 | @about_me.context_processor 16 | @app.cache.cached(timeout=conf["CACHE_EXPIRE"], key_prefix="inject_base:about_me") 17 | def inject_base(): 18 | """ about me 默认模板变量 """ 19 | return {"top_nav": ["", "", "", "", "active", ""]} 20 | -------------------------------------------------------------------------------- /templates/error.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 错误 {% endblock %} 4 | 5 | {% block style %} 6 | {{ super() }} 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
12 |
13 |
14 |

{{ error_info }}

15 | 回到主页 16 |
17 |
18 |
19 | {% endblock %} -------------------------------------------------------------------------------- /send_email/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, current_app 2 | from flask_mail import Mail, Message 3 | from configure import conf 4 | 5 | 6 | def send_msg(title: str, mail: Mail, to, template, **kwargs): 7 | """ 邮件发送 """ 8 | sender = conf['MAIL_SENDER'] 9 | message = Message(conf['MAIL_PREFIX'] + title, sender=sender, recipients=[to]) 10 | message.body = render_template("email-msg/" + template + ".txt", **kwargs) 11 | message.html = render_template("email-msg/" + template + ".html", **kwargs) 12 | mail.send(message) 13 | current_app.logger.info(f"Send email to {to} sender: {sender} msg: {template} kwargs: {kwargs}") 14 | -------------------------------------------------------------------------------- /sql/redis.py: -------------------------------------------------------------------------------- 1 | import redis 2 | import logging 3 | import logging.handlers 4 | from configure import conf 5 | import os 6 | 7 | 8 | class RedisDB(redis.StrictRedis): 9 | def __init__(self, host, port, username, passwd, db): 10 | super().__init__(host=host, port=port, username=username, password=passwd, db=db, decode_responses=True) 11 | 12 | # redis是线程安全的 13 | 14 | self.logger = logging.getLogger("main.database") 15 | self.logger.setLevel(conf["LOG_LEVEL"]) 16 | if len(conf["LOG_HOME"]) > 0: 17 | handle = logging.handlers.TimedRotatingFileHandler( 18 | os.path.join(conf["LOG_HOME"], f"redis-{username}@{host}.log"), backupCount=10) 19 | handle.setFormatter(logging.Formatter(conf["LOG_FORMAT"])) 20 | self.logger.addHandler(handle) 21 | -------------------------------------------------------------------------------- /templates/email-msg/register.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | {{ title }} 18 | 19 | 20 | 21 | 22 |

{{ title }}

23 |

欢迎注册 {{ conf["BLOG_NAME"] }} 用户,请点击以下链接完成认证。
若您未进行过注册操作,请忽略此邮件。

24 | 点击完成认证 25 | 26 | 27 | -------------------------------------------------------------------------------- /sql/__init__.py: -------------------------------------------------------------------------------- 1 | from sql.mysql import MysqlDB 2 | from sql.redis import RedisDB 3 | from configure import conf 4 | 5 | DB = MysqlDB 6 | db = DB(host=conf["MYSQL_URL"], 7 | name=conf["MYSQL_NAME"], 8 | passwd=conf["MYSQL_PASSWD"], 9 | port=conf["MYSQL_PORT"], 10 | database=conf["MYSQL_DATABASE"]) 11 | 12 | cache = redis.RedisDB(host=conf["CACHE_REDIS_HOST"], 13 | port=conf["CACHE_REDIS_PORT"], 14 | username=conf["CACHE_REDIS_NAME"], 15 | passwd=conf["CACHE_REDIS_PASSWD"], 16 | db=conf["CACHE_REDIS_DATABASE"]) 17 | 18 | redis = redis.RedisDB(host=conf["REDIS_HOST"], 19 | port=conf["REDIS_PORT"], 20 | username=conf["REDIS_NAME"], 21 | passwd=conf["REDIS_PASSWD"], 22 | db=conf["REDIS_DATABASE"]) 23 | -------------------------------------------------------------------------------- /sql/statistics.py: -------------------------------------------------------------------------------- 1 | from sql import redis 2 | from configure import conf 3 | 4 | 5 | PREFIX = conf["REDIS_PREFIX"] 6 | 7 | 8 | def add_hello_click(): 9 | redis.incr(f"{PREFIX}:home", amount=1) 10 | 11 | 12 | def get_hello_click(): 13 | res = redis.get(f"{PREFIX}:home") 14 | return res if res else 0 15 | 16 | 17 | def add_home_click(): 18 | redis.incr(f"{PREFIX}:index", amount=1) 19 | 20 | 21 | def get_home_click(): 22 | res = redis.get(f"{PREFIX}:index") 23 | return res if res else 0 24 | 25 | 26 | def add_blog_click(blog_id: int): 27 | redis.incr(f"{PREFIX}:blog:{blog_id}", amount=1) 28 | 29 | 30 | def get_blog_click(blog_id: int): 31 | res = redis.get(f"{PREFIX}:blog:{blog_id}") 32 | return res if res else 0 33 | 34 | 35 | def add_archive_click(archive_id: int): 36 | redis.incr(f"{PREFIX}:archive:{archive_id}", amount=1) 37 | 38 | 39 | def get_archive_click(archive_id: int): 40 | res = redis.get(f"{PREFIX}:archive:{archive_id}") 41 | return res if res else 0 42 | -------------------------------------------------------------------------------- /templates/auth/passwd.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 修改密码 {% endblock %} 4 | 5 | {% block style %} 6 | {{ super() }} 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
12 |
13 |
14 |
15 | {{ ChangePasswdForm.hidden_tag() }} 16 | {{ macro.render_field(ChangePasswdForm.old_passwd) }} 17 | {{ macro.render_field(ChangePasswdForm.passwd) }} 18 | {{ macro.render_field(ChangePasswdForm.passwd_again) }} 19 | 20 |
21 | {{ ChangePasswdForm.submit(class='btn btn-outline-danger') }} 22 |
23 |
24 |
25 |
26 |
27 | {% endblock %} -------------------------------------------------------------------------------- /templates/auth/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 登录 {% endblock %} 4 | 5 | {% block style %} 6 | {{ super() }} 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
12 |
13 |
14 | 24 |
25 |
26 |
27 | {% endblock %} -------------------------------------------------------------------------------- /templates/auth/register.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 注册 {% endblock %} 4 | 5 | {% block style %} 6 | {{ super() }} 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
12 |
13 |
14 |
15 | {{ RegisterForm.hidden_tag() }} 16 | {{ macro.render_field(RegisterForm.email) }} 17 | {{ macro.render_field(RegisterForm.passwd) }} 18 | {{ macro.render_field(RegisterForm.passwd_again) }} 19 |
20 | {{ RegisterForm.submit(class='btn btn-success me-2') }} 21 | 前往登录 22 |
23 |
24 |
25 |
26 |
27 | {% endblock %} -------------------------------------------------------------------------------- /templates/oss/upload.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 上传文件 {% endblock %} 4 | 5 | {% block style %} 6 | {{ super() }} 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
12 |
13 |
14 |
15 | {{ UploadForm.hidden_tag() }} 16 | {{ macro.render_field(UploadForm.path) }} 17 | 18 |
19 | {{ UploadForm.file(class="form-control") }} 20 | {% for error in UploadForm.file.errors %} 21 |
{{ error }}
22 | {% endfor %} 23 |
24 | 25 |
26 | {{ UploadForm.submit(class='btn btn-primary me-2') }} 27 |
28 |
29 |
30 |
31 |
32 | {% endblock %} -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aliyun-python-sdk-core==2.13.35 2 | aliyun-python-sdk-kms==2.15.0 3 | async-timeout==4.0.2 4 | beautifulsoup4==4.10.0 5 | blinker==1.4 6 | bs4==0.0.1 7 | cachelib==0.9.0 8 | certifi==2021.10.8 9 | cffi==1.15.0 10 | charset-normalizer==2.0.8 11 | click==8.0.3 12 | colorama==0.4.4 13 | crcmod==1.7 14 | cryptography==36.0.0 15 | DBUtils==3.0.2 16 | Deprecated==1.2.13 17 | dnspython==2.1.0 18 | email-validator==1.1.3 19 | Flask==2.0.2 20 | Flask-Caching==2.0.1 21 | Flask-HTTPAuth==4.7.0 22 | Flask-Login==0.5.0 23 | Flask-Mail==0.9.1 24 | Flask-Moment==1.0.5 25 | Flask-WTF==1.0.0 26 | graphviz==0.19.1 27 | greenlet==1.1.2 28 | gunicorn==20.1.0 29 | idna==3.3 30 | importlib-metadata==4.8.2 31 | itsdangerous==2.0.1 32 | Jinja2==3.0.3 33 | jmespath==0.10.0 34 | MarkupSafe==2.0.1 35 | ndg-httpsclient==0.5.1 36 | objgraph==3.5.0 37 | oss2==2.15.0 38 | packaging==21.3 39 | pyasn1==0.4.8 40 | pycparser==2.21 41 | pycryptodome==3.11.0 42 | Pympler==1.0.1 43 | PyMySQL==1.0.2 44 | pyOpenSSL==22.0.0 45 | pyparsing==3.0.6 46 | redis==4.3.4 47 | requests==2.26.0 48 | six==1.16.0 49 | soupsieve==2.3.1 50 | urllib3==1.26.7 51 | waitress==2.0.0 52 | webencodings==0.5.1 53 | Werkzeug==2.0.2 54 | wrapt==1.14.1 55 | WTForms==3.0.0 56 | zipp==3.6.0 57 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from configure import configure, conf 2 | 3 | import os 4 | import logging 5 | import threading 6 | 7 | env_dict = os.environ 8 | hblog_conf = env_dict.get("HBLOG_CONF") 9 | if hblog_conf is None: 10 | logging.info("Configure file ./etc/conf.json") 11 | configure("./etc/conf.json") 12 | else: 13 | logging.info(f"Configure file {hblog_conf}") 14 | configure(hblog_conf) 15 | 16 | from app import HBlogFlask 17 | from waitress import serve 18 | 19 | app = HBlogFlask(__name__) 20 | app.register_all_blueprint() 21 | 22 | from sql.cache import restart_clear_cache 23 | from sql.cache_refresh import refresh 24 | restart_clear_cache() # 清理缓存 25 | 26 | 27 | @app.before_first_request 28 | def before_first_requests(): 29 | class FirstRefresh(threading.Thread): 30 | def __init__(self): 31 | super(FirstRefresh, self).__init__() 32 | self.daemon = True # 设置为守护进程 33 | 34 | def run(self): 35 | refresh() 36 | 37 | class TimerRefresh(threading.Timer): 38 | def __init__(self): 39 | super(TimerRefresh, self).__init__(conf["CACHE_REFRESH_INTERVAL"], refresh) 40 | self.daemon = True # 设置为守护进程 41 | 42 | FirstRefresh().start() 43 | TimerRefresh().start() 44 | 45 | 46 | if __name__ == '__main__': 47 | logging.info("Server start on 127.0.0.1:8080") 48 | serve(app, host='0.0.0.0', port="8080") 49 | -------------------------------------------------------------------------------- /templates/macro.html: -------------------------------------------------------------------------------- 1 | {% macro get_page_list(info_lines, now_page) %} 2 | {% for line in info_lines %} 3 | {% if line %} 4 | {% if now_page == line[0] %} 5 |
  • {{ line[0] }}
  • 6 | {% else %} 7 |
  • {{ line[0] }}
  • 8 | {% endif %} 9 | {% else %} 10 |
  • ...
  • 11 | {% endif %} 12 | {% endfor %} 13 | {% endmacro %} 14 | 15 | {% macro render_field(field) %} 16 |
    17 | {% if not field.errors %} 18 | {{ field(class="form-control", placeholder=field.label.text) | safe }} 19 | {% else %} 20 | {{ field(class="form-control", placeholder=field.label.text, value="") | safe }} 21 | {% endif %} 22 | 23 | {{ field.label }} 24 | {% for error in field.errors %} 25 |
    {{ error }}
    26 | {% endfor %} 27 |
    28 | {% endmacro %} 29 | 30 | {% macro render_select_field(field) %} 31 |
    32 | {{ field(class="form-select") | safe }} 33 | {% for error in field.errors %} 34 |
    {{ error }}
    35 | {% endfor %} 36 |
    37 | {% endmacro %} -------------------------------------------------------------------------------- /app/tool.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from flask import abort, g, redirect, url_for 3 | from flask_login import current_user 4 | from flask_wtf import FlaskForm 5 | from typing import ClassVar, Optional, Callable 6 | import app 7 | 8 | 9 | def role_required(role: str, opt: str): 10 | def required(func): 11 | @wraps(func) 12 | def new_func(*args, **kwargs): 13 | if not current_user.check_role(role): # 检查相应的权限 14 | app.HBlogFlask.print_user_not_allow_opt_log(opt) 15 | return abort(403) 16 | return func(*args, **kwargs) 17 | return new_func 18 | return required 19 | 20 | 21 | 22 | def api_role_required(role: str, opt: str): 23 | def required(func): 24 | @wraps(func) 25 | def new_func(*args, **kwargs): 26 | if not g.user.check_role(role): # 检查相应的权限 27 | app.HBlogFlask.print_user_not_allow_opt_log(opt) 28 | return abort(403) 29 | return func(*args, **kwargs) 30 | return new_func 31 | return required 32 | 33 | 34 | def form_required(form: ClassVar[FlaskForm], opt: str, callback: Optional[Callable] = None, **kw): 35 | def required(func): 36 | @wraps(func) 37 | def new_func(*args, **kwargs): 38 | f = form() 39 | if not f.validate_on_submit(): 40 | app.HBlogFlask.print_form_error_log(opt) 41 | if callback is None: 42 | return abort(404) 43 | return callback(form=f, **kw, **kwargs) 44 | g.form = f 45 | return func(*args, **kwargs) 46 | return new_func 47 | return required 48 | -------------------------------------------------------------------------------- /object/comment.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from datetime import datetime 3 | 4 | from sql.comment import read_comment_list, create_comment, get_user_comment_count, delete_comment, read_comment 5 | import object.user 6 | import object.blog 7 | 8 | 9 | def load_comment_list(blog_id: int): 10 | ret = [] 11 | for i in read_comment_list(blog_id): 12 | ret.append(Comment(i)) 13 | return ret 14 | 15 | 16 | class _Comment: 17 | comment_tuple = namedtuple("Comment", "blog email content update_time") 18 | 19 | @staticmethod 20 | def get_user_comment_count(auth: "object.user"): 21 | return get_user_comment_count(auth.id) 22 | 23 | @staticmethod 24 | def create(blog: "object.blog.BlogArticle", auth: "object.user.User", content): 25 | return create_comment(blog.id, auth.id, content) 26 | 27 | 28 | class Comment(_Comment): 29 | def __init__(self, comment_id): 30 | self.id = comment_id 31 | 32 | @property 33 | def info(self): 34 | return Comment.comment_tuple(*read_comment(self.id)) 35 | 36 | @property 37 | def blog(self): 38 | return object.blog.BlogArticle(self.info.blog) 39 | 40 | @property 41 | def auth(self): 42 | return object.user.User(self.info.email) 43 | 44 | @property 45 | def content(self): 46 | return self.info.content 47 | 48 | @property 49 | def update_time(self): 50 | return datetime.utcfromtimestamp(datetime.timestamp(self.info.update_time)) 51 | 52 | def is_delete(self): 53 | return not self.auth.is_authenticated and self.blog.is_delete and len(self.content) != 0 54 | 55 | def delete(self): 56 | return delete_comment(self.id) 57 | -------------------------------------------------------------------------------- /object/msg.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from collections import namedtuple 3 | from datetime import datetime 4 | 5 | from sql.msg import read_msg_list, get_msg_count, create_msg, read_msg, get_user_msg_count, delete_msg 6 | import object.user 7 | 8 | 9 | class _Message: 10 | message_tuple = namedtuple("Message", "email content update_time secret") 11 | 12 | @staticmethod 13 | def get_message_list(limit: Optional[int] = None, offset: Optional[int] = None, show_secret: bool = False): 14 | ret = [] 15 | for i in read_msg_list(limit=limit, offset=offset, show_secret=show_secret): 16 | ret.append(Message(i)) 17 | return ret 18 | 19 | @staticmethod 20 | def get_msg_count(auth: "object.user.User" = None): 21 | if auth is None: 22 | return get_msg_count() 23 | return get_user_msg_count(auth.id) 24 | 25 | @staticmethod 26 | def create(auth: "object.user.User", content, secret: bool = False): 27 | ret = create_msg(auth.id, content, secret) 28 | if ret is not None: 29 | return Message(ret) 30 | return None 31 | 32 | 33 | class Message(_Message): 34 | def __init__(self, msg_id): 35 | self.id = msg_id 36 | 37 | @property 38 | def info(self): 39 | return Message.message_tuple(*read_msg(self.id)) 40 | 41 | @property 42 | def auth(self): 43 | return object.user.User(self.info.email) 44 | 45 | @property 46 | def content(self): 47 | return self.info.content 48 | 49 | @property 50 | def update_time(self): 51 | return datetime.utcfromtimestamp(datetime.timestamp(self.info.update_time)) 52 | 53 | @property 54 | def secret(self): 55 | return self.info.secret 56 | 57 | @property 58 | def is_delete(self): 59 | return not self.auth.is_authenticated and len(self.content) != 0 60 | 61 | def delete(self): 62 | return delete_msg(self.id) 63 | -------------------------------------------------------------------------------- /templates/auth/delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 删除用户 {% endblock %} 4 | 5 | {% block style %} 6 | {{ super() }} 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
    12 |
    13 |
    14 |
    15 | {{ DeleteUserForm.hidden_tag() }} 16 | {{ macro.render_field(DeleteUserForm.email) }} 17 | 18 | 34 | 35 |
    36 | 删除用户 37 |
    38 |
    39 |
    40 |
    41 |
    42 | {% endblock %} -------------------------------------------------------------------------------- /aliyun/__init__.py: -------------------------------------------------------------------------------- 1 | from configure import conf 2 | import oss2 3 | import logging.handlers 4 | import logging 5 | import os 6 | from urllib.parse import urljoin 7 | 8 | 9 | class Aliyun: 10 | def __init__(self, key, secret, endpoint, name, is_cname): 11 | self.key = key 12 | self.secret = secret 13 | self.auth = oss2.Auth(key, secret) 14 | self.bucket = oss2.Bucket(self.auth, endpoint, name, is_cname=is_cname) 15 | self.logger = logging.getLogger("main.aliyun") 16 | self.logger.setLevel(conf["LOG_LEVEL"]) 17 | if len(conf["LOG_HOME"]) > 0: 18 | handle = logging.handlers.TimedRotatingFileHandler( 19 | os.path.join(conf["LOG_HOME"], f"aliyun.log"), backupCount=10) 20 | handle.setFormatter(logging.Formatter(conf["LOG_FORMAT"])) 21 | self.logger.addHandler(handle) 22 | 23 | def upload_file(self, name, f): 24 | res = self.bucket.put_object(name, f) 25 | self.logger.info(f"Upload {name} " 26 | f"id: {res.request_id} status: {res.status} " 27 | f"etag: {res.etag} resp: {res.resp} " 28 | f"version id: {res.versionid} key: {self.key}") 29 | 30 | def shared_obj(self, name, time=15): 31 | if not self.bucket.object_exists(name): 32 | return None 33 | if conf["ALIYUN_BUCKET_USE_SIGN_URL"]: 34 | url = self.bucket.sign_url('GET', name, time, slash_safe=True) 35 | else: 36 | url = urljoin(conf["ALIYUN_BUCKET_ENDPOINT"], name) 37 | self.logger.debug(f"Get url {url} name: {name} time: {time}s key: {self.key}") 38 | return url 39 | 40 | 41 | if conf["USE_ALIYUN"]: 42 | aliyun = Aliyun(conf["ALIYUN_KEY"], 43 | conf["ALIYUN_SECRET"], 44 | conf["ALIYUN_BUCKET_ENDPOINT"], 45 | conf["ALIYUN_BUCKET_NAME"], 46 | conf["ALIYUN_BUCKET_IS_CNAME"]) 47 | else: 48 | aliyun = None 49 | -------------------------------------------------------------------------------- /gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | # gunicorn.conf.py 2 | import os 3 | import multiprocessing 4 | import logging.handlers 5 | import logging 6 | 7 | try: 8 | import gevent.monkey 9 | gevent.monkey.patch_all() 10 | except ImportError: 11 | pass 12 | 13 | bind = '127.0.0.1:5000' 14 | timeout = 30 # 超时 15 | 16 | worker_class = 'gevent' 17 | workers = multiprocessing.cpu_count() * 2 + 1 # 进程数 18 | threads = 2 # 指定每个进程开启的线程数 19 | 20 | hblog_path = os.path.join(os.environ['HOME'], "hblog") 21 | os.makedirs(hblog_path, exist_ok=True, mode=0o775) 22 | 23 | # 设置访问日志和错误信息日志路径 24 | log_format = ("[%(levelname)s]:%(name)s:%(asctime)s " 25 | "(%(filename)s:%(lineno)d %(funcName)s) " 26 | "%(process)d %(thread)d " 27 | "%(message)s") 28 | log_formatter = logging.Formatter(log_format) 29 | 30 | # 错误日志 31 | gunicorn_error_logger = logging.getLogger("gunicorn.error") 32 | gunicorn_error_logger.setLevel(logging.WARNING) 33 | 34 | errorlog = os.path.join(hblog_path, "gunicorn_error.log") 35 | time_handle = logging.handlers.TimedRotatingFileHandler(errorlog, when="d", backupCount=30, encoding='utf-8') 36 | gunicorn_error_logger.addHandler(time_handle) 37 | time_handle.setFormatter(log_formatter) 38 | 39 | # 一般日志 40 | gunicorn_access_logger = logging.getLogger("gunicorn.access") 41 | gunicorn_access_logger.setLevel(logging.INFO) 42 | 43 | accesslog = os.path.join(hblog_path, "gunicorn_access.log") 44 | time_handle = logging.handlers.TimedRotatingFileHandler(accesslog, when="d", backupCount=10, encoding='utf-8') 45 | gunicorn_access_logger.addHandler(time_handle) 46 | time_handle.setFormatter(log_formatter) 47 | 48 | # 输出日志 49 | gunicorn_access_logger.info("Load gunicorn conf success") 50 | gunicorn_access_logger.info(f"bind: {bind}") 51 | gunicorn_access_logger.info(f"timeout: {timeout}") 52 | gunicorn_access_logger.info(f"worker_class: {worker_class}") 53 | gunicorn_access_logger.info(f"workers: {workers}") 54 | gunicorn_access_logger.info(f"threads: {threads}") 55 | gunicorn_access_logger.info(f"hblog_path: {hblog_path}") 56 | -------------------------------------------------------------------------------- /object/archive.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | import sql.blog # 不用 from import 避免循环导入 4 | from sql.archive import (read_archive, 5 | create_archive, 6 | get_archive_list, 7 | get_blog_archive, 8 | delete_archive, 9 | add_blog_to_archive, 10 | sub_blog_from_archive) 11 | from sql.statistics import get_archive_click 12 | 13 | 14 | class _Archive: 15 | archive_tuple = namedtuple('Archive', 'name describe') 16 | 17 | @staticmethod 18 | def get_archive_list(): 19 | ret = [] 20 | for i in get_archive_list(): 21 | ret.append(Archive(i)) 22 | return ret 23 | 24 | 25 | @staticmethod 26 | def get_blog_archive(blog_id: int): 27 | archive_list = [] 28 | for i in get_blog_archive(blog_id): 29 | archive_list.append(Archive(i)) 30 | return archive_list 31 | 32 | @staticmethod 33 | def create(name, describe): 34 | ret = create_archive(name, describe) 35 | if ret is None: 36 | return None 37 | return Archive(ret) 38 | 39 | 40 | class Archive(_Archive): 41 | def __init__(self, archive_id): 42 | self.id = archive_id 43 | 44 | @property 45 | def info(self): 46 | return Archive.archive_tuple(*read_archive(self.id)) 47 | 48 | @property 49 | def name(self): 50 | return self.info.name 51 | 52 | @property 53 | def describe(self): 54 | return self.info.describe 55 | 56 | @property 57 | def count(self): 58 | return sql.blog.get_archive_blog_count(self.id) 59 | 60 | @property 61 | def clicks(self): 62 | return get_archive_click(self.id) 63 | 64 | def is_delete(self): 65 | return len(self.name) != 0 66 | 67 | def delete(self): 68 | return delete_archive(self.id) 69 | 70 | def add_blog(self, blog_id: int): 71 | add_blog_to_archive(blog_id, self.id) 72 | 73 | def sub_blog(self, blog_id: int): 74 | sub_blog_from_archive(blog_id, self.id) 75 | -------------------------------------------------------------------------------- /sql/base.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import List, Dict, Tuple, Union, Optional 3 | import logging.handlers 4 | import logging 5 | from configure import conf 6 | import os 7 | 8 | 9 | class DBException(Exception): 10 | ... 11 | 12 | 13 | class DBDoneException(DBException): 14 | ... 15 | 16 | 17 | class DBCloseException(DBException): 18 | ... 19 | 20 | 21 | class DBBit: 22 | BIT_0 = b'\x00' 23 | BIT_1 = b'\x01' 24 | 25 | 26 | class Database(metaclass=abc.ABCMeta): 27 | @abc.abstractmethod 28 | def __init__(self, host: str, name: str, passwd: str, port: str): 29 | self._host = str(host) 30 | self._name = str(name) 31 | self._passwd = str(passwd) 32 | if port is None: 33 | self._port = 3306 34 | else: 35 | self._port = int(port) 36 | self.logger = logging.getLogger("main.database") 37 | self.logger.setLevel(conf["LOG_LEVEL"]) 38 | if len(conf["LOG_HOME"]) > 0: 39 | handle = logging.handlers.TimedRotatingFileHandler( 40 | os.path.join(conf["LOG_HOME"], f"mysql-{name}@{host}.log"), backupCount=10) 41 | handle.setFormatter(logging.Formatter(conf["LOG_FORMAT"])) 42 | self.logger.addHandler(handle) 43 | 44 | @abc.abstractmethod 45 | def search(self, sql: str, *args, not_commit: bool = False): 46 | """ 47 | 执行 查询 SQL语句 48 | :parm sql: SQL语句 49 | :return: 50 | """ 51 | ... 52 | 53 | @abc.abstractmethod 54 | def insert(self, sql: str, *args, not_commit: bool = False): 55 | """ 56 | 执行 插入 SQL语句, 并提交 57 | :parm sql: SQL语句 58 | :return: 59 | """ 60 | ... 61 | 62 | @abc.abstractmethod 63 | def delete(self, sql: str, *args, not_commit: bool = False): 64 | """ 65 | 执行 删除 SQL语句, 并提交 66 | :parm sql: SQL语句 67 | :return: 68 | """ 69 | ... 70 | 71 | @abc.abstractmethod 72 | def update(self, sql: str, *args, not_commit: bool = False): 73 | """ 74 | 执行 更新 SQL语句, 并提交 75 | :parm sql: SQL语句 76 | :return: 77 | """ 78 | ... 79 | -------------------------------------------------------------------------------- /app/index.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, request 2 | from flask_login import current_user 3 | 4 | from configure import conf 5 | import app 6 | from object.blog import BlogArticle 7 | from object.msg import Message 8 | from sql.statistics import get_hello_click, add_hello_click, get_home_click, add_home_click 9 | 10 | index = Blueprint("base", __name__) 11 | 12 | 13 | @index.route('/') 14 | @app.cache.cached(timeout=conf["VIEW_CACHE_EXPIRE"]) 15 | def hello_page(): 16 | app.HBlogFlask.print_load_page_log(f"hello") 17 | add_hello_click() 18 | return render_template("index/hello.html") 19 | 20 | 21 | @index.route('/home') 22 | def index_page(): 23 | blog_list = BlogArticle.get_blog_list(limit=5, offset=0, not_top=True) 24 | msg_list = Message.get_message_list(limit=6, offset=0, show_secret=False) 25 | app.HBlogFlask.print_load_page_log(f"index") 26 | add_home_click() 27 | return render_template("index/index.html", 28 | blog_list=blog_list, 29 | msg_list=msg_list, 30 | show_email=current_user.check_role("ReadUserInfo"), 31 | hello_clicks=get_hello_click(), 32 | home_clicks=get_home_click()) 33 | 34 | 35 | @index.context_processor 36 | @app.cache.cached(timeout=conf["CACHE_EXPIRE"], key_prefix="inject_base:index") 37 | def inject_base(): 38 | """ index默认模板变量, 覆盖app变量 """ 39 | return {"top_nav": ["active", "", "", "", "", ""]} 40 | 41 | 42 | def get_icp(): 43 | for i in conf["ICP"]: 44 | if i in request.host: 45 | return conf["ICP"][i] 46 | 47 | 48 | def get_gongan(): 49 | for i in conf["GONG_AN"]: 50 | if i in request.host: 51 | return conf["GONG_AN"][i] 52 | 53 | 54 | @index.app_context_processor 55 | @app.cache.cached(timeout=conf["CACHE_EXPIRE"], key_prefix="inject_base") 56 | def inject_base(): 57 | """ app默认模板变量 """ 58 | return {"blog_name": conf['BLOG_NAME'], 59 | "top_nav": ["", "", "", "", "", ""], 60 | "blog_describe": conf['BLOG_DESCRIBE'], 61 | "conf": conf, 62 | "get_icp": get_icp, 63 | "get_gongan": get_gongan} 64 | -------------------------------------------------------------------------------- /sql/cache_refresh.py: -------------------------------------------------------------------------------- 1 | from sql import DB 2 | from configure import conf 3 | from sql.archive import read_archive, get_archive_list_iter, get_blog_archive 4 | from sql.blog import read_blog, get_blog_count, get_archive_blog_count, get_user_blog_count, get_blog_list_iter 5 | from sql.comment import read_comment, read_comment_list_iter, get_user_comment_count 6 | from sql.msg import read_msg, read_msg_list_iter, get_msg_count, get_user_msg_count 7 | from sql.user import (read_user, get_user_list_iter, get_role_list_iter, 8 | get_user_email, get_role_name, check_role, role_authority) 9 | import logging.handlers 10 | import os 11 | 12 | refresh_logger = logging.getLogger("main.refresh") 13 | refresh_logger.setLevel(conf["LOG_LEVEL"]) 14 | if len(conf["LOG_HOME"]) > 0: 15 | handle = logging.handlers.TimedRotatingFileHandler( 16 | os.path.join(conf["LOG_HOME"], f"redis-refresh.log"), backupCount=10) 17 | handle.setFormatter(logging.Formatter(conf["LOG_FORMAT"])) 18 | refresh_logger.addHandler(handle) 19 | 20 | 21 | def refresh(): 22 | refresh_logger.info("refresh redis cache started.") 23 | 24 | for i in get_archive_list_iter(): 25 | read_archive(i[0], not_cache=True) 26 | get_archive_blog_count(i[0], not_cache=True) 27 | 28 | for i in get_blog_list_iter(): 29 | read_blog(i[0], not_cache=True) 30 | get_blog_archive(i[0], not_cache=True) 31 | get_blog_count(not_cache=True) 32 | 33 | for i in read_comment_list_iter(): 34 | read_comment(i[0], not_cache=True) 35 | 36 | for i in read_msg_list_iter(): 37 | read_msg(i[0], not_cache=True) 38 | get_msg_count(not_cache=True) 39 | 40 | for i in get_user_list_iter(): 41 | email = get_user_email(i[0], not_cache=True) 42 | get_user_blog_count(i[0], not_cache=True) 43 | get_user_comment_count(i[0], not_cache=True) 44 | get_user_msg_count(i[0], not_cache=True) 45 | read_user(email, not_cache=True) 46 | 47 | for i in get_role_list_iter(): 48 | get_role_name(i[0], not_cache=True) 49 | for a in role_authority: 50 | check_role(i[0], a, not_cache=True) 51 | 52 | refresh_logger.info("refresh redis cache finished.") 53 | -------------------------------------------------------------------------------- /static/styles/index/hello.css: -------------------------------------------------------------------------------- 1 | #base { 2 | position: relative; 3 | } 4 | 5 | @keyframes shaking { 6 | from { 7 | transform: rotate(0deg); 8 | } 9 | 10 | 20% { 11 | transform: rotate(-3deg); 12 | } 13 | 14 | 40% { 15 | transform: rotate(0deg); 16 | } 17 | 18 | 60% { 19 | transform: rotate(3deg); 20 | } 21 | 22 | 80% { 23 | transform: rotate(0deg); 24 | } 25 | 26 | to { 27 | transform: rotate(0deg); 28 | } 29 | } 30 | 31 | #title-1 { 32 | text-decoration: none; 33 | border-bottom: black solid 3px; 34 | padding-bottom: 15px; 35 | 36 | width: 96%; 37 | margin: auto; 38 | 39 | font-weight: bold; 40 | text-align: center; 41 | color: black; 42 | font-size: 50px; 43 | padding-top: 5%; 44 | } 45 | 46 | #title-1:hover { 47 | animation: shaking 300ms linear 0s 1; 48 | } 49 | 50 | #btn { 51 | position: relative; 52 | display: block; 53 | text-align: center; 54 | 55 | border-radius: 10px; 56 | border-width: 3px; 57 | border-style: ridge; 58 | border-color: rgb(123, 104, 238); 59 | 60 | background-image: linear-gradient(to right, #F0FFF0 0%, #FFDC00 30%, #FFDC00 100%); 61 | 62 | font-size: 30px; 63 | height: 100px; 64 | 65 | box-shadow: 5px 5px 2px 0 black; 66 | } 67 | 68 | @media all and (max-width: 992px) { 69 | #btn { 70 | top: 120px; 71 | left: 3%; 72 | width: 94%; 73 | } 74 | } 75 | 76 | @media not all and (max-width: 992px) { 77 | #btn { 78 | top: 120px; 79 | left: 25%; 80 | width: 50%; 81 | transition: 82 | transform 1s, 83 | background-image 1s; 84 | } 85 | 86 | #btn:hover { 87 | background-image: linear-gradient(to right, #F0FFF0 0%, #F0FFF0 70%, #FFDC00 100%); 88 | transform: scale(1.5); 89 | } 90 | } 91 | 92 | #main { 93 | text-align: center; 94 | } 95 | 96 | #title-section { 97 | height: 50vh; 98 | width: 50%; 99 | margin: 10% auto; 100 | background-color: rgb(255, 255, 255, 60%); 101 | border-radius: 10px; 102 | } -------------------------------------------------------------------------------- /app/oss.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, redirect, render_template, abort, flash, url_for, request 2 | from flask_login import login_required 3 | from flask_wtf import FlaskForm 4 | from wtforms import FileField, StringField, SubmitField 5 | from wtforms.validators import DataRequired, Length 6 | from urllib.parse import urljoin 7 | import oss2 8 | 9 | from aliyun import aliyun 10 | import app 11 | 12 | oss = Blueprint("oss", __name__) 13 | 14 | 15 | class UploadForm(FlaskForm): 16 | file = FileField("选择文件", description="待上传文件", 17 | validators=[DataRequired(message="必须选择文件")]) 18 | path = StringField("存储文件夹", description="文件路径(不含文件名)", 19 | validators=[Length(-1, 30, message="文件路径长度为30个字符以内")]) 20 | submit = SubmitField("上传") 21 | 22 | def __init__(self): 23 | super(UploadForm, self).__init__() 24 | self.path.data = "hblog/" 25 | 26 | 27 | @oss.before_request 28 | def check_aliyun(): 29 | if aliyun is None: 30 | app.HBlogFlask.print_user_opt_fail_log("aliyun not used") 31 | abort(404) 32 | return 33 | 34 | 35 | @oss.route('get/') 36 | def get_page(name: str): 37 | app.HBlogFlask.print_user_opt_success_log(f"get file {name}") 38 | url = aliyun.shared_obj(name) 39 | if url is None: 40 | abort(404) 41 | return redirect(url) 42 | 43 | 44 | @oss.route('upload', methods=["GET", "POST"]) 45 | @login_required 46 | @app.role_required("ConfigureSystem", "upload file") 47 | def upload_page(): 48 | form = UploadForm() 49 | if form.validate_on_submit(): 50 | file = request.files["file"] 51 | path: str = form.path.data 52 | if len(path) > 0 and not path.endswith('/'): 53 | path += "/" 54 | if path.startswith('/'): 55 | path = path[1:] 56 | path += file.filename 57 | try: 58 | aliyun.upload_file(path, file) 59 | except oss2.exceptions.OssError: 60 | app.HBlogFlask.print_sys_opt_success_log(f"Upload file {path} fail") 61 | flash(f"文件 {file.filename} 上传失败") 62 | else: 63 | app.HBlogFlask.print_sys_opt_success_log(f"Upload file {path}") 64 | flash(f"文件 {file.filename} 已上传: {urljoin(request.host_url, url_for('oss.get_page', name=path))}") 65 | return redirect(url_for("oss.upload_page")) 66 | app.HBlogFlask.print_load_page_log(f"OSS upload") 67 | return render_template("oss/upload.html", UploadForm=form) 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HBlog 2 | ## 介绍 3 | `HBlog`是基于Python和Flask的博客系统。 4 | 5 | * 具有博文、博文评论、博文归档的功能。 6 | * 具有博客留言的功能。 7 | * 访客注册、角色管理的功能。 8 | * 文件存储的功能。 9 | * 具有管理员管理后台。 10 | 11 | ## 部署 12 | ### 下载 13 | ```shell 14 | $ useradd hblog -m 15 | $ git clone https://github.com/SuperH-0630/HBlog.git /home/hblog/.HBlog 16 | ``` 17 | 18 | ### 下载依赖 19 | ```shell 20 | $ sudo -u hblog python3.10 -m pip install -r /home/hblog/.HBlog/requirements.txt --user 21 | $ sudo -u hblog python3.10 -m pip install gunicorn gevent --user 22 | ``` 23 | 24 | ### 配置 25 | 创建配置文件`etc/hblog/conf.json`,配置文件内容如下: 26 | ```json 27 | { 28 | "DEBUG_PROFILE": false, 29 | "SECRET_KEY": "随机密钥", 30 | 31 | "BLOG_NAME": "", 32 | "BLOG_DESCRIBE": "", 33 | "INTRODUCE": { 34 | "介绍的名称": "可任意创建更多介绍" 35 | }, 36 | "INTRODUCE_LINK": { 37 | "连接名称": "可任意创建更多连接" 38 | }, 39 | "ABOUT_ME_PAGE": "about_me.html静态页面的地址", 40 | "FOOT": "页脚信息", 41 | 42 | "MYSQL_URL": "", 43 | "MYSQL_PORT": 3306, 44 | "MYSQL_NAME": "", 45 | "MYSQL_PASSWD": "", 46 | "MYSQL_DATABASE": "", 47 | 48 | "REDIS_HOST": "", 49 | "REDIS_PORT": 6379, 50 | "REDIS_NAME": "", 51 | "REDIS_PASSWD": "", 52 | "REDIS_DATABASE": 0, 53 | 54 | "CACHE_REDIS_HOST": "", 55 | "CACHE_REDIS_PORT": 6379, 56 | "CACHE_REDIS_NAME": "", 57 | "CACHE_REDIS_PASSWD": "", 58 | "CACHE_REDIS_DATABASE": 0, 59 | 60 | "MAIL_SERVER": "SMTP服务地址", 61 | "MAIL_PORT": 465, 62 | "MAIL_USE_TLS": false, 63 | "MAIL_USE_SSL": true, 64 | "MAIL_USERNAME": "", 65 | "MAIL_PASSWORD": "@0630", 66 | "MAIL_SENDER": "名字 <发件人地址>", 67 | 68 | "USE_ALIYUN": true, 69 | "ALIYUN_KEY": "阿里云OOS的账号Key", 70 | "ALIYUN_SECRET": "", 71 | "ALIYUN_BUCKET_ENDPOINT": "", 72 | "ALIYUN_BUCKET_IS_CNAME": false, 73 | "ALIYUN_BUCKET_NAME": "", 74 | "ALIYUN_BUCKET_USE_SIGN_URL": false, 75 | 76 | "LOG_LEVEL": "debug", 77 | "LOG_HOME": "log", 78 | "LOG_SENDER": true, 79 | 80 | "LOGO": "Logo的文件名,存储在static目录的相对路径", 81 | "ICP": { 82 | "备案的域名": "ICP备案" 83 | }, 84 | "GONG_AN": { 85 | "备案的域名": "公安备案" 86 | } 87 | } 88 | ``` 89 | 90 | ### 静态`AboutMe.html`页面 91 | 一个普通的HTML页面,必须包含`
    `,这部分内容会被显示在博客上。 92 | 93 | ### 创建`systemd`服务文件 94 | ```serivce 95 | [Unit] 96 | Description=HBlog server on 8080 97 | After=network.target auditd.service 98 | 99 | [Service] 100 | User=hblog 101 | Group=hblog 102 | WorkingDirectory=/home/hblog/.HBlog/ 103 | ExecStart=python3.10的路径 -m gunicorn -c /home/hblog/.HBlog/gunicorn.conf.py main:app --preload -b 0.0.0.0:8080 104 | Type=simple 105 | Environment="HBLOG_CONF=/etc/hblog/conf.json" 106 | 107 | [Install] 108 | WantedBy=multi-user.targe 109 | ``` 110 | 111 | ## 样例博客 112 | 我的博客就是用HBlog搭建的,访问:[是桓的小窝](https://www.song-zh.com)。 113 | -------------------------------------------------------------------------------- /templates/index/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 主页 {% endblock %} 4 | 5 | {% block content %} 6 |
    7 |
    8 |
    9 |
    10 |
    11 | {% for info in conf['INTRODUCE'] %} 12 |

    {{ info[0] }}

    13 | {{ info[1] | safe }} 14 | {% endfor %} 15 | 16 |
    17 | 18 |

    欢迎页点击量: {{ hello_clicks }}

    19 |

    首页点击量: {{ home_clicks }}

    20 | 21 |
    22 |
    23 | {% for link in conf['INTRODUCE_LINK'] %} 24 | {{ link }} 25 | {% endfor %} 26 |
    27 |
    28 |
    29 |
    30 |
    31 |
    32 | 33 | `
    34 | {% if current_user.check_role("ReadBlog") %} {# 检查是否具有读取权限 #} 35 | {% if current_user.check_role("ReadMsg") %} 36 |
    37 | {% for blog in blog_list %} 38 | {{ render_docx(blog, False) }} 39 | {% endfor %} 40 |
    41 | {% else %} 42 |
    43 | {% for blog in blog_list %} 44 | {{ render_docx(blog, False) }} 45 | {% endfor %} 46 |
    47 | {% endif %} 48 | {% endif %} 49 | 50 | {% if current_user.check_role("ReadMsg") %} {# 检查是否具有读取权限 #} 51 | {% if current_user.check_role("ReadBlog") %} 52 | 57 | {% else %} 58 | 63 | {% endif %} 64 | {% endif %} 65 |
    66 |
    67 | {% endblock %} -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | proarchive_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 100 | __pypackages__/ 101 | 102 | # Celery stuff 103 | celerybeat-schedule 104 | celerybeat.pid 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | 136 | # pytype static type analyzer 137 | .pytype/ 138 | 139 | # Cython debug symbols 140 | cython_debug/ 141 | 142 | conf.json -------------------------------------------------------------------------------- /templates/auth/yours.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 关于你 {% endblock %} 4 | 5 | {% block style %} 6 | {{ super() }} 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
    12 |
    13 |
    14 |
    15 |
    你的
    16 |
    17 |

    {{ current_user.email }}

    18 |

    用户组:{{ current_user.role_name }}

    19 |

    评论条数:{{ comment_count }}

    20 |

    留言条数:{{ msg_count }}

    21 |

    博客:{{ blog_count }}

    22 | 23 | 39 | 40 | 修改密码 41 | 退出登录 42 | {% if current_user.check_role('DeleteUser') %} 43 | 删除用户 44 | {% endif %} 45 | {% if current_user.check_role('ConfigureSystem') %} 46 | 角色设置 47 | 上传文件 48 | {% endif %} 49 |
    50 |
    51 |
    52 |
    53 |
    54 | {% endblock %} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | proarchive_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | # pytype static type analyzer 138 | .pytype/ 139 | 140 | # Cython debug symbols 141 | cython_debug/ 142 | 143 | etc 144 | static/logo.jpg 145 | static/wangan.png -------------------------------------------------------------------------------- /templates/index/hello.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 欢迎 {% endblock %} 4 | 5 | {% block style %} 6 | {{ super() }} 7 | 8 | 15 | {% endblock %} 16 | 17 | {% block nav %} {% endblock %} 18 | 19 | {% block content %} 20 |
    21 |

    欢迎,这里是《{{ blog_name }}》

    22 |
    23 | 24 |
    25 |
    26 | {% endblock %} 27 | 28 | {% block footer %} 29 | {% if get_icp() %} 30 | 43 | 44 | 68 | {% endif %} 69 | {% endblock %} -------------------------------------------------------------------------------- /configure/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | conf = { 5 | "DEBUG_PROFILE": False, 6 | "SECRET_KEY": "HBlog-R-Salt", 7 | "BLOG_NAME": "HBlog", 8 | "BLOG_DESCRIBE": "Huan Blog.", 9 | "FOOT": "Power by HBlog", 10 | "ABOUT_ME_PAGE": "", 11 | "INTRODUCTION": "", 12 | "INTRODUCTION_LINK": "", 13 | "MYSQL_URL": "localhost", 14 | "MYSQL_NAME": "localhost", 15 | "MYSQL_PASSWD": "123456", 16 | "MYSQL_PORT": 3306, 17 | "MYSQL_DATABASE": "HBlog", 18 | "REDIS_HOST": "localhost", 19 | "REDIS_PORT": 6379, 20 | "REDIS_NAME": "localhost", 21 | "REDIS_PASSWD": "123456", 22 | "REDIS_DATABASE": 0, 23 | "CACHE_REDIS_HOST": "localhost", 24 | "CACHE_REDIS_PORT": 6379, 25 | "CACHE_REDIS_NAME": "localhost", 26 | "CACHE_REDIS_PASSWD": "123456", 27 | "CACHE_REDIS_DATABASE": 0, 28 | "CACHE_EXPIRE": 604800, # 默认七天过期 29 | "CACHE_REFRESH_INTERVAL": 432000, # 缓存刷新时间 默认五天刷新一次 30 | "VIEW_CACHE_EXPIRE": 60, # 视图函数 31 | "LIST_CACHE_EXPIRE": 5, # 列表 排行 32 | "REDIS_PREFIX": "statistics", 33 | "CACHE_PREFIX": "hblog_cache", 34 | "FLASK_CACHE_PREFIX": "flask_cache", 35 | "MAIL_SERVER": "", 36 | "MAIL_PORT": "", 37 | "MAIL_USE_TLS": False, 38 | "MAIL_USE_SSL": False, 39 | "MAIL_PASSWORD": "", 40 | "MAIL_USERNAME": "", 41 | "MAIL_PREFIX": "", 42 | "MAIL_SENDER": "", 43 | "USE_ALIYUN": False, 44 | "ALIYUN_KEY": "", 45 | "ALIYUN_SECRET": "", 46 | "ALIYUN_BUCKET_ENDPOINT": "", 47 | "ALIYUN_BUCKET_NAME": "", 48 | "ALIYUN_BUCKET_IS_CNAME": False, 49 | "ALIYUN_BUCKET_USE_SIGN_URL": True, 50 | "LOG_HOME": "", 51 | "LOG_FORMAT": "[%(levelname)s]:%(name)s:%(asctime)s " 52 | "(%(filename)s:%(lineno)d %(funcName)s) " 53 | "%(process)d %(thread)d " 54 | "%(message)s", 55 | "LOG_LEVEL": logging.INFO, 56 | "LOG_STDERR": True, 57 | "LOGO": "logo.jpg", 58 | "ICP": {}, 59 | "GONG_AN": {}, 60 | } 61 | 62 | 63 | 64 | def configure(conf_file: str, encoding="utf-8"): 65 | """ 运行配置程序, 该函数需要在其他模块被执行前调用 """ 66 | with open(conf_file, mode="r", encoding=encoding) as f: 67 | json_str = f.read() 68 | conf.update(json.loads(json_str)) 69 | if type(conf["LOG_LEVEL"]) is str: 70 | conf["LOG_LEVEL"] = {"debug": logging.DEBUG, 71 | "info": logging.INFO, 72 | "warning": logging.WARNING, 73 | "error": logging.ERROR}.get(conf["LOG_LEVEL"]) 74 | 75 | introduce = conf["INTRODUCE"] 76 | introduce_list = [] 77 | for i in introduce: 78 | describe: str = introduce[i] 79 | describe = " ".join([f"

    {i}

    " for i in describe.split('\n')]) 80 | introduce_list.append((i, describe)) 81 | conf["INTRODUCE"] = introduce_list 82 | if len(conf["LOG_HOME"]) > 0: 83 | os.makedirs(conf["LOG_HOME"], exist_ok=True) 84 | -------------------------------------------------------------------------------- /sql/comment.py: -------------------------------------------------------------------------------- 1 | from sql import db, DB 2 | from sql.cache import (get_comment_from_cache, write_comment_to_cache, delete_comment_from_cache, 3 | get_user_comment_count_from_cache, write_user_comment_count_to_cache, 4 | delete_all_user_comment_count_from_cache, delete_user_comment_count_from_cache) 5 | 6 | 7 | def read_comment_list(blog_id: int, mysql: DB = db): 8 | """ 读取文章的 comment """ 9 | cur = mysql.search("SELECT CommentID " 10 | "FROM comment_user " 11 | "WHERE BlogID=%s " 12 | "ORDER BY UpdateTime DESC", blog_id) 13 | if cur is None or cur.rowcount == 0: 14 | return [] 15 | return [i[0] for i in cur.fetchall()] 16 | 17 | 18 | def read_comment_list_iter(mysql: DB = db): 19 | """ 读取文章的 comment """ 20 | cur = mysql.search("SELECT CommentID " 21 | "FROM comment_user " 22 | "ORDER BY UpdateTime DESC") 23 | if cur is None or cur.rowcount == 0: 24 | return [] 25 | return cur 26 | 27 | 28 | def create_comment(blog_id: int, user_id: int, content: str, mysql: DB = db): 29 | """ 新建 comment """ 30 | delete_user_comment_count_from_cache(user_id) 31 | 32 | cur = mysql.insert("INSERT INTO comment(BlogID, Auth, Content) " 33 | "VALUES (%s, %s, %s)", blog_id, user_id, content) 34 | if cur is None or cur.rowcount == 0: 35 | return False 36 | read_comment(cur.lastrowid, mysql) # 刷新缓存 37 | return True 38 | 39 | 40 | def read_comment(comment_id: int, mysql: DB = db, not_cache=False): 41 | """ 读取 comment """ 42 | if not not_cache: 43 | res = get_comment_from_cache(comment_id) 44 | if res is not None: 45 | return res 46 | 47 | cur = mysql.search("SELECT BlogID, Email, Content, UpdateTime FROM comment_user WHERE CommentID=%s", comment_id) 48 | if cur is None or cur.rowcount == 0: 49 | return [-1, "", "", 0] 50 | 51 | res = cur.fetchone() 52 | write_comment_to_cache(comment_id, *res) 53 | return res 54 | 55 | 56 | def delete_comment(comment_id: int, mysql: DB = db): 57 | """ 删除评论 """ 58 | delete_comment_from_cache(comment_id) 59 | delete_all_user_comment_count_from_cache() 60 | cur = mysql.delete("DELETE FROM comment WHERE ID=%s", comment_id) 61 | if cur is None or cur.rowcount == 0: 62 | return False 63 | return True 64 | 65 | 66 | def get_user_comment_count(user_id: int, mysql: DB = db, not_cache=False): 67 | """ 读取指定用户的 comment 个数 """ 68 | if not not_cache: 69 | res = get_user_comment_count_from_cache(user_id) 70 | if res is not None: 71 | return res 72 | 73 | cur = mysql.search("SELECT COUNT(*) FROM comment WHERE Auth=%s", user_id) 74 | if cur is None or cur.rowcount == 0: 75 | return 0 76 | 77 | res = cur.fetchone()[0] 78 | write_user_comment_count_to_cache(user_id, res) 79 | return res 80 | -------------------------------------------------------------------------------- /templates/msg/msg.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 留言 {% endblock %} 4 | 5 | {% block style %} 6 | {{ super() }} 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
    12 |
    13 |
    14 |
    15 | {{ form.hidden_tag() }} 16 |
    17 | {{ form.content(class="form-control mb-2", rows="5") }} 18 | {% for error in form.content.errors %} 19 |
    {{ error }}
    20 | {% endfor %} 21 |
    22 | 23 |
    24 | {{ form.secret(class="form-check-input") }} 25 | {{ form.secret.label(class="form-check-label") }} 26 |
    27 | 28 | 44 | 45 | 46 |
    47 |
    48 |
    49 |
    50 | {% if current_user.check_role("ReadMsg") %} {# 检查是否具有读取权限 #} 51 |
    52 |
    53 | {% for msg in msg_list %} 54 | {{ render_msg(msg, show_delete) }} 55 | {% endfor %} 56 |
    57 |
    58 | 59 | 62 | {% endif %} 63 |
    64 | {% endblock %} -------------------------------------------------------------------------------- /app/archive.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, redirect, url_for, flash, g, request 2 | from flask_login import login_required, current_user 3 | from flask_wtf import FlaskForm 4 | from wtforms import StringField, TextAreaField, SubmitField 5 | from wtforms.validators import DataRequired, Length, ValidationError 6 | 7 | import app 8 | from object.archive import Archive 9 | from configure import conf 10 | 11 | archive = Blueprint("archive", __name__) 12 | 13 | 14 | class CreateArchiveForm(FlaskForm): 15 | name = StringField("名称", description="归档名称", 16 | validators=[DataRequired(message="必须填写归档名称"), 17 | Length(1, 10, message="归档名称长度1-10个字符")]) 18 | describe = TextAreaField("描述", description="归档描述", 19 | validators=[Length(-1, 25, message="归档描述长度25个字符以内")]) 20 | submit = SubmitField("创建归档") 21 | 22 | def validate_name(self, field): 23 | name = field.data 24 | archive_list = Archive.get_archive_list() 25 | for i in archive_list: 26 | if name == i.name: 27 | raise ValidationError("归档已经存在") 28 | 29 | 30 | def __load_archive_page(form: CreateArchiveForm): 31 | archive_list = Archive.get_archive_list() 32 | app.HBlogFlask.print_load_page_log("archive list") 33 | return render_template("archive/archive.html", 34 | archive_list=archive_list, 35 | form=form, 36 | show_delete=current_user.check_role("DeleteBlog")) 37 | 38 | 39 | @archive.route('/') 40 | def archive_page(): 41 | return __load_archive_page(CreateArchiveForm()) 42 | 43 | 44 | @archive.route("/create", methods=["POST"]) 45 | @login_required 46 | @app.form_required(CreateArchiveForm, "create archive", __load_archive_page) 47 | @app.role_required("WriteBlog", "create archive") 48 | def create_archive_page(): 49 | form: CreateArchiveForm = g.form 50 | name = form.name.data 51 | describe = form.describe.data 52 | if Archive.create(name, describe): 53 | app.HBlogFlask.print_sys_opt_success_log(f"Create archive {name}") 54 | flash(f"创建归档 {name} 成功") 55 | else: 56 | app.HBlogFlask.print_sys_opt_fail_log(f"Create archive {name}") 57 | flash(f"创建归档 {name} 失败") 58 | return redirect(url_for("archive.archive_page")) 59 | 60 | 61 | @archive.route("/delete") 62 | @login_required 63 | @app.role_required("DeleteBlog", "delete archive") 64 | def delete_archive_page(): 65 | archive_id = int(request.args.get("archive", 1, type=int)) 66 | if Archive(archive_id).delete(): 67 | app.HBlogFlask.print_sys_opt_success_log(f"Delete archive {archive_id}") 68 | flash("归档删除成功") 69 | else: 70 | app.HBlogFlask.print_sys_opt_fail_log(f"Delete archive {archive_id}") 71 | flash("归档删除失败") 72 | return redirect(url_for("archive.archive_page")) 73 | 74 | 75 | @archive.context_processor 76 | @app.cache.cached(timeout=conf["CACHE_EXPIRE"], key_prefix="inject_base:archive") 77 | def inject(): 78 | """ archive 默认模板变量 """ 79 | return {"top_nav": ["", "active", "", "", "", ""]} 80 | -------------------------------------------------------------------------------- /app/msg.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, abort, redirect, url_for, flash, g, request 2 | from flask_wtf import FlaskForm 3 | from flask_login import login_required, current_user 4 | from wtforms import TextAreaField, BooleanField, SubmitField 5 | from wtforms.validators import DataRequired, Length 6 | 7 | import app 8 | from sql.base import DBBit 9 | from object.msg import Message 10 | from configure import conf 11 | 12 | msg = Blueprint("msg", __name__) 13 | 14 | 15 | class WriteForm(FlaskForm): 16 | """ 17 | 写新内容表单 18 | """ 19 | content = TextAreaField("", description="留言正文", 20 | validators=[ 21 | DataRequired("请输入留言的内容"), 22 | Length(1, 100, message="留言长度1-100个字符")]) 23 | secret = BooleanField("私密留言") 24 | submit = SubmitField("留言") 25 | 26 | 27 | def __load_msg_page(page: int, form: WriteForm): 28 | if page < 1: 29 | app.HBlogFlask.print_user_opt_fail_log(f"Load msg list with error page({page})") 30 | abort(404) 31 | return 32 | 33 | msg_list = Message.get_message_list(20, (page - 1) * 20, 34 | show_secret=current_user.check_role("ReadSecretMsg")) # 判断是否可读取私密内容 35 | max_page = app.HBlogFlask.get_max_page(Message.get_msg_count(), 20) 36 | page_list = app.HBlogFlask.get_page("msg.msg_page", page, max_page) 37 | app.HBlogFlask.print_load_page_log(f"msg (page: {page})") 38 | return render_template("msg/msg.html", 39 | msg_list=msg_list, 40 | page=page, 41 | cache_str=f":{page}", 42 | page_list=page_list, 43 | form=form, 44 | show_delete=current_user.check_role("DeleteMsg"), 45 | show_email=current_user.check_role("ReadUserInfo")) 46 | 47 | 48 | @msg.route('/') 49 | def msg_page(): 50 | page = request.args.get("page", 1, type=int) 51 | return __load_msg_page(page, WriteForm()) 52 | 53 | 54 | @msg.route('/create', methods=["POST"]) 55 | @login_required 56 | @app.form_required(WriteForm, 57 | "write msg", 58 | lambda form: __load_msg_page(request.args.get("page", 1, type=int), form)) 59 | @app.role_required("WriteMsg", "write msg") 60 | def write_msg_page(): 61 | form: WriteForm = g.form 62 | content = form.content.data 63 | secret = form.secret.data 64 | if Message.create(current_user, content, secret): 65 | app.HBlogFlask.print_user_opt_success_log("write msg") 66 | flash("留言成功") 67 | else: 68 | app.HBlogFlask.print_user_opt_fail_log("write msg") 69 | flash("留言失败") 70 | return redirect(url_for("msg.msg_page", page=1)) 71 | 72 | 73 | @msg.route('/delete') 74 | @login_required 75 | @app.role_required("DeleteMsg", "delete msg") 76 | def delete_msg_page(): 77 | msg_id = request.args.get("msg", 1, type=int) 78 | if Message(msg_id).delete(): 79 | app.HBlogFlask.print_user_opt_success_log("delete msg") 80 | flash("留言删除成功") 81 | else: 82 | app.HBlogFlask.print_user_opt_fail_log("delete msg") 83 | flash("留言删除失败") 84 | return redirect(url_for("msg.msg_page", page=1)) 85 | 86 | 87 | @msg.context_processor 88 | @app.cache.cached(timeout=conf["CACHE_EXPIRE"], key_prefix="inject_base:msg") 89 | def inject_base(): 90 | """ msg 默认模板变量 """ 91 | return {"top_nav": ["", "", "", "active", "", ""]} 92 | -------------------------------------------------------------------------------- /object/blog.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from collections import namedtuple 3 | from datetime import datetime 4 | 5 | from sql.blog import (get_blog_list, 6 | get_blog_count, 7 | get_archive_blog_list, 8 | get_archive_blog_count, 9 | get_blog_list_not_top, 10 | read_blog, 11 | update_blog, 12 | create_blog, 13 | delete_blog, 14 | set_blog_top, 15 | get_user_blog_count) 16 | from sql.statistics import get_blog_click 17 | from sql.archive import add_blog_to_archive, sub_blog_from_archive 18 | from sql.user import get_user_email 19 | from sql.base import DBBit 20 | import object.user 21 | import object.archive 22 | import object.comment 23 | 24 | 25 | class LoadBlogError(Exception): 26 | pass 27 | 28 | 29 | class _BlogArticle: 30 | article_tuple = namedtuple("Article", "auth title subtitle content update_time create_time top") 31 | 32 | @staticmethod 33 | def get_blog_list(archive_id=None, limit=None, offset=None, not_top=False): 34 | if archive_id is None: 35 | if not_top: 36 | res = get_blog_list_not_top(limit=limit, offset=offset) 37 | else: 38 | res = get_blog_list(limit=limit, offset=offset) 39 | else: 40 | res = get_archive_blog_list(archive_id, limit=limit, offset=offset) 41 | 42 | ret = [] 43 | for i in res: 44 | ret.append(BlogArticle(i)) 45 | return ret 46 | 47 | @staticmethod 48 | def get_blog_count(archive_id=None, auth=None): 49 | if archive_id is None and auth is None: 50 | return get_blog_count() 51 | if auth is None: 52 | return get_archive_blog_count(archive_id) 53 | return get_user_blog_count(auth.id) 54 | 55 | @staticmethod 56 | def create(title, subtitle, content, archive: "List[object.archive.Archive]", user: "object.user.User"): 57 | return create_blog(user.id, title, subtitle, content, archive) 58 | 59 | 60 | class BlogArticle(_BlogArticle): 61 | def __init__(self, blog_id): 62 | self.id = blog_id 63 | 64 | @property 65 | def info(self): 66 | return BlogArticle.article_tuple(*read_blog(self.id)) 67 | 68 | @property 69 | def user(self): 70 | return object.user.User(self.info.auth, is_id=True) 71 | 72 | @property 73 | def title(self): 74 | return self.info.title 75 | 76 | @property 77 | def subtitle(self): 78 | return self.info.subtitle 79 | 80 | @property 81 | def content(self): 82 | return self.info.content 83 | 84 | @property 85 | def update_time(self): 86 | return datetime.utcfromtimestamp(datetime.timestamp(self.info.update_time)) 87 | 88 | @property 89 | def create_time(self): 90 | return datetime.utcfromtimestamp(datetime.timestamp(self.info.create_time)) 91 | 92 | @property 93 | def clicks(self): 94 | return get_blog_click(self.id) 95 | 96 | @property 97 | def top(self): 98 | return self.info.top 99 | 100 | @top.setter 101 | def top(self, top: bool): 102 | set_blog_top(self.id, top) 103 | 104 | @property 105 | def comment(self): 106 | return object.comment.load_comment_list(self.id) 107 | 108 | @property 109 | def archive(self): 110 | return object.archive.Archive.get_blog_archive(self.id) 111 | 112 | @property 113 | def is_delete(self): 114 | return not self.user.is_authenticated and len(self.content) != 0 115 | 116 | def delete(self): 117 | return delete_blog(self.id) 118 | 119 | def update(self, content: str): 120 | if update_blog(self.id, content): 121 | return True 122 | return False 123 | 124 | def add_to_archive(self, archive_id: int): 125 | return add_blog_to_archive(self.id, archive_id) 126 | 127 | def sub_from_archive(self, archive_id: int): 128 | return sub_blog_from_archive(self.id, archive_id) 129 | -------------------------------------------------------------------------------- /sql/archive.py: -------------------------------------------------------------------------------- 1 | from sql import db, DB 2 | from sql.cache import (get_archive_from_cache, write_archive_to_cache, delete_archive_from_cache, 3 | get_blog_archive_from_cache, write_blog_archive_to_cache, delete_blog_archive_from_cache, 4 | delete_all_blog_archive_from_cache) 5 | from typing import Optional 6 | 7 | 8 | def create_archive(name: str, describe: str, mysql: DB = db): 9 | """ 创建新归档 """ 10 | cur = mysql.insert("INSERT INTO archive(Name, DescribeText) " 11 | "VALUES (%s, %s)", name, describe) 12 | if cur is None or cur.rowcount == 0: 13 | return None 14 | read_archive(cur.lastrowid, mysql) 15 | return cur.lastrowid 16 | 17 | 18 | def read_archive(archive_id: int, mysql: DB = db, not_cache=False): 19 | """ 获取归档 ID """ 20 | if not not_cache: 21 | res = get_archive_from_cache(archive_id) 22 | if res is not None: 23 | return res 24 | 25 | cur = mysql.search("SELECT Name, DescribeText " 26 | "FROM archive " 27 | "WHERE ID=%s", archive_id) 28 | if cur is None or cur.rowcount == 0: 29 | return ["", ""] 30 | 31 | res = cur.fetchone() 32 | write_archive_to_cache(archive_id, *res) 33 | return res 34 | 35 | 36 | def get_blog_archive(blog_id: int, mysql: DB = db, not_cache=False): 37 | """ 获取文章的归档 """ 38 | if not not_cache: 39 | res = get_blog_archive_from_cache(blog_id) 40 | if res is not None: 41 | return res 42 | 43 | cur = mysql.search("SELECT ArchiveID FROM blog_archive_with_name " 44 | "WHERE BlogID=%s " 45 | "ORDER BY ArchiveName", blog_id) 46 | if cur is None: 47 | return [] 48 | 49 | res = [i[0] for i in cur.fetchall()] 50 | write_blog_archive_to_cache(blog_id, res) 51 | return res 52 | 53 | 54 | def delete_archive(archive_id: int, mysql: DB = db): 55 | delete_archive_from_cache(archive_id) 56 | delete_all_blog_archive_from_cache() 57 | conn = mysql.get_connection() 58 | 59 | cur = mysql.delete("DELETE FROM blog_archive WHERE ArchiveID=%s", archive_id, connection=conn) 60 | if cur is None: 61 | conn.rollback() 62 | conn.close() 63 | return False 64 | 65 | cur = mysql.delete("DELETE FROM archive WHERE ID=%s", archive_id, connection=conn) 66 | if cur is None or cur.rowcount == 0: 67 | conn.rollback() 68 | conn.close() 69 | return False 70 | 71 | conn.commit() 72 | conn.close() 73 | return True 74 | 75 | 76 | def add_blog_to_archive(blog_id: int, archive_id: int, mysql: DB = db): 77 | delete_blog_archive_from_cache(blog_id) 78 | cur = mysql.search("SELECT BlogID FROM blog_archive WHERE BlogID=%s AND ArchiveID=%s", blog_id, archive_id) 79 | if cur is None: 80 | return False 81 | if cur.rowcount > 0: 82 | return True 83 | cur = mysql.insert("INSERT INTO blog_archive(BlogID, ArchiveID) VALUES (%s, %s)", blog_id, archive_id) 84 | if cur is None or cur.rowcount != 1: 85 | return False 86 | return True 87 | 88 | 89 | def sub_blog_from_archive(blog_id: int, archive_id: int, mysql: DB = db): 90 | delete_blog_archive_from_cache(blog_id) 91 | cur = mysql.delete("DELETE FROM blog_archive WHERE BlogID=%s AND ArchiveID=%s", blog_id, archive_id) 92 | if cur is None: 93 | return False 94 | return True 95 | 96 | 97 | def get_archive_list(limit: Optional[int] = None, offset: Optional[int] = None, mysql: DB = db): 98 | """ 获取归档列表 """ 99 | if limit is not None and offset is not None: 100 | cur = mysql.search("SELECT ID " 101 | "FROM archive " 102 | "ORDER BY Name " 103 | "LIMIT %s " 104 | "OFFSET %s ", limit, offset) 105 | else: 106 | cur = mysql.search("SELECT ID " 107 | "FROM archive " 108 | "ORDER BY Name") 109 | 110 | if cur is None or cur.rowcount == 0: 111 | return [] 112 | return [i[0] for i in cur.fetchall()] 113 | 114 | 115 | 116 | def get_archive_list_iter(mysql: DB = db): 117 | """ 获取归档列表 """ 118 | cur = mysql.search("SELECT ID " 119 | "FROM archive " 120 | "ORDER BY Name") 121 | 122 | if cur is None or cur.rowcount == 0: 123 | return [] 124 | return cur 125 | -------------------------------------------------------------------------------- /templates/archive/archive.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 归档 {% endblock %} 4 | 5 | {% block style %} 6 | {{ super() }} 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 | {% if form and current_user.check_role("WriteBlog") %} 12 |
    13 |
    14 |
    15 |
    16 |
    17 | {{ form.hidden_tag() }} 18 |
    19 | {{ macro.render_field(form.name) }} 20 | {{ macro.render_field(form.describe) }} 21 |
    22 | 23 | 39 | 40 | 42 |
    43 |
    44 |
    45 |
    46 |
    47 |
    48 | {% endif %} 49 | 50 | {% if current_user.check_role("ReadBlog") %} 51 |
    52 | {% for archive in archive_list %} 53 |
    54 |

    {{ archive.name }}

    55 |
    56 |

    {{ archive.describe }}

    57 |

    篇数: {{ archive.count }}

    58 |

    点击量: {{ archive.clicks }}

    59 | 进入 60 | {% if show_delete %} 61 | 78 | 79 | 81 | {% endif %} 82 |
    83 | {% endfor %} 84 |
    85 | {% endif %} 86 | {% endblock %} -------------------------------------------------------------------------------- /sql/msg.py: -------------------------------------------------------------------------------- 1 | from sql import db, DB 2 | from sql.base import DBBit 3 | from sql.cache import (get_msg_from_cache, write_msg_to_cache, delete_msg_from_cache, 4 | get_msg_cout_from_cache, write_msg_count_to_cache, delete_msg_count_from_cache, 5 | get_user_msg_count_from_cache, write_user_msg_count_to_cache, 6 | delete_all_user_msg_count_from_cache, delete_user_msg_count_from_cache) 7 | 8 | from typing import Optional 9 | 10 | 11 | def read_msg_list(limit: Optional[int] = None, 12 | offset: Optional[int] = None, 13 | show_secret: bool = False, 14 | mysql: DB = db): 15 | if show_secret: 16 | if limit is not None and offset is not None: 17 | cur = mysql.search("SELECT MsgID " 18 | "FROM message_user " 19 | "ORDER BY UpdateTime DESC " 20 | "LIMIT %s " 21 | "OFFSET %s", limit, offset) 22 | else: 23 | cur = mysql.search("SELECT MsgID " 24 | "FROM message_user " 25 | "ORDER BY UpdateTime DESC") 26 | else: 27 | if limit is not None and offset is not None: 28 | cur = mysql.search("SELECT MsgID " 29 | "FROM message_user " 30 | "WHERE Secret=0 " 31 | "ORDER BY UpdateTime DESC " 32 | "LIMIT %s " 33 | "OFFSET %s", limit, offset) 34 | else: 35 | cur = mysql.search("SELECT MsgID " 36 | "FROM message_user " 37 | "WHERE Secret=0 " 38 | "ORDER BY UpdateTime DESC") 39 | if cur is None or cur.rowcount == 0: 40 | return [] 41 | return [i[0] for i in cur.fetchall()] 42 | 43 | 44 | def read_msg_list_iter(mysql: DB = db): 45 | cur = mysql.search("SELECT MsgID " 46 | "FROM message_user " 47 | "ORDER BY UpdateTime DESC") 48 | if cur is None or cur.rowcount == 0: 49 | return [] 50 | return cur 51 | 52 | 53 | def create_msg(auth: int, content: str, secret: bool = False, mysql: DB = db): 54 | delete_msg_count_from_cache() 55 | delete_user_msg_count_from_cache(auth) 56 | 57 | cur = mysql.insert("INSERT INTO message(Auth, Content, Secret) " 58 | "VALUES (%s, %s, %s)", auth, content, 1 if secret else 0) 59 | if cur is None or cur.rowcount != 1: 60 | return None 61 | read_msg(cur.lastrowid, mysql) # 刷新缓存 62 | return cur.lastrowid 63 | 64 | 65 | def read_msg(msg_id: int, mysql: DB = db, not_cache=False): 66 | if not not_cache: 67 | res = get_msg_from_cache(msg_id) 68 | if res is not None: 69 | return res 70 | 71 | cur = mysql.search("SELECT Email, Content, UpdateTime, Secret " 72 | "FROM message_user " 73 | "WHERE MsgID=%s", msg_id) 74 | if cur is None or cur.rowcount == 0: 75 | return ["", "", "0", False] 76 | 77 | res = cur.fetchone() 78 | write_msg_to_cache(msg_id, *res, is_db_bit=True) 79 | return [*res[:3], res[-1] == DBBit.BIT_1] 80 | 81 | 82 | def delete_msg(msg_id: int, mysql: DB = db): 83 | delete_msg_from_cache(msg_id) 84 | delete_msg_count_from_cache() 85 | delete_all_user_msg_count_from_cache() 86 | cur = mysql.delete("DELETE FROM message WHERE ID=%s", msg_id) 87 | if cur is None or cur.rowcount == 0: 88 | return False 89 | return True 90 | 91 | 92 | def get_msg_count(mysql: DB = db, not_cache=False): 93 | if not not_cache: 94 | res = get_msg_cout_from_cache() 95 | if res is not None: 96 | return res 97 | 98 | cur = mysql.search("SELECT COUNT(*) FROM message") 99 | if cur is None or cur.rowcount == 0: 100 | return 0 101 | res = cur.fetchone()[0] 102 | write_msg_count_to_cache(res) 103 | return res 104 | 105 | 106 | def get_user_msg_count(user_id: int, mysql: DB = db, not_cache=False): 107 | if not not_cache: 108 | res = get_user_msg_count_from_cache(user_id) 109 | if res is not None: 110 | return res 111 | 112 | cur = mysql.search("SELECT COUNT(*) FROM message WHERE Auth=%s", user_id) 113 | if cur is None or cur.rowcount == 0: 114 | return 0 115 | res = cur.fetchone()[0] 116 | write_user_msg_count_to_cache(user_id, res) 117 | return res 118 | -------------------------------------------------------------------------------- /sql/mysql.py: -------------------------------------------------------------------------------- 1 | import pymysql 2 | from dbutils.pooled_db import PooledDB 3 | from dbutils.steady_db import SteadyDBCursor 4 | from sql.base import Database, DBException, DBCloseException 5 | from typing import Optional, Union 6 | import inspect 7 | 8 | 9 | class MysqlConnectException(DBCloseException): 10 | """Mysql Connect error""" 11 | 12 | 13 | class MysqlDB(Database): 14 | class Result: 15 | def __init__(self, cur: SteadyDBCursor): 16 | self.res: list = cur.fetchall() 17 | self.lastrowid: int = cur.lastrowid 18 | self.rowcount: int = cur.rowcount 19 | 20 | def fetchall(self): 21 | return self.res 22 | 23 | def fetchone(self): 24 | return self.res[0] 25 | 26 | def __iter__(self): 27 | return self.res.__iter__() 28 | 29 | class Connection: 30 | def __init__(self, conn): 31 | self.conn = conn 32 | self.cur = conn.cursor() 33 | 34 | def get_cursor(self): 35 | return self.cur 36 | 37 | def commit(self): 38 | self.conn.commit() 39 | 40 | def rollback(self): 41 | self.conn.rollback() 42 | 43 | def close(self): 44 | self.cur.close() 45 | self.conn.close() 46 | 47 | 48 | def __init__(self, 49 | host: Optional[str], 50 | name: Optional[str], 51 | passwd: Optional[str], 52 | port: Optional[str], 53 | database: str = "HBlog"): 54 | if host is None or name is None: 55 | raise DBException 56 | 57 | super(MysqlDB, self).__init__(host=host, name=name, passwd=passwd, port=port) 58 | self.database = database 59 | 60 | self.pool = PooledDB(pymysql, 61 | mincached=1, 62 | maxcached=4, 63 | maxconnections=16, 64 | blocking=True, 65 | host=self._host, 66 | port=self._port, 67 | user=self._name, 68 | passwd=self._passwd, 69 | db=self.database) 70 | 71 | self.logger.info(f"MySQL({self._name}@{self._host}) connect") 72 | 73 | def get_connection(self): 74 | return MysqlDB.Connection(self.pool.connection()) 75 | 76 | def search(self, sql: str, *args) -> Union[None, Result]: 77 | return self.__search(sql, args) 78 | 79 | def insert(self, sql: str, *args, connection: Connection = None) -> Union[None, Result]: 80 | return self.__done(sql, args, connection) 81 | 82 | def delete(self, sql: str, *args, connection: Connection = None) -> Union[None, Result]: 83 | return self.__done(sql, args, connection) 84 | 85 | def update(self, sql: str, *args, connection: Connection = None) -> Union[None, Result]: 86 | return self.__done(sql, args, connection) 87 | 88 | def __search(self, sql, args) -> Union[None, Result]: 89 | conn = self.pool.connection() 90 | cur = conn.cursor() 91 | 92 | try: 93 | cur.execute(query=sql, args=args) 94 | except pymysql.MySQLError: 95 | self.logger.error(f"MySQL({self._name}@{self._host}) SQL {sql} with {args} error {inspect.stack()[2][2]} " 96 | f"{inspect.stack()[2][1]} {inspect.stack()[2][3]}", exc_info=True, stack_info=True) 97 | return None 98 | else: 99 | return MysqlDB.Result(cur) 100 | finally: 101 | cur.close() 102 | conn.close() 103 | 104 | def __done(self, sql, args, connection: Connection = None) -> Union[None, Result]: 105 | if connection: 106 | cur = connection.get_cursor() 107 | conn = None 108 | else: 109 | conn = self.pool.connection() 110 | cur = conn.cursor() 111 | 112 | try: 113 | cur.execute(query=sql, args=args) 114 | if conn: 115 | conn.commit() 116 | except pymysql.MySQLError: 117 | if conn: 118 | conn.rollback() 119 | self.logger.error(f"MySQL({self._name}@{self._host}) SQL {sql} error {inspect.stack()[2][2]} " 120 | f"{inspect.stack()[2][1]} {inspect.stack()[2][3]}", exc_info=True, stack_info=True) 121 | return None 122 | else: 123 | return MysqlDB.Result(cur) 124 | finally: 125 | if not connection: 126 | cur.close() 127 | conn.close() 128 | -------------------------------------------------------------------------------- /object/user.py: -------------------------------------------------------------------------------- 1 | from flask_login import UserMixin, AnonymousUserMixin 2 | from werkzeug.security import generate_password_hash, check_password_hash 3 | from itsdangerous import URLSafeTimedSerializer as Serializer 4 | from itsdangerous.exc import BadData 5 | from collections import namedtuple 6 | 7 | from configure import conf 8 | from sql.user import (read_user, 9 | check_role, 10 | create_user, 11 | get_role_name, 12 | delete_user, 13 | change_passwd_hash, 14 | create_role, 15 | delete_role, 16 | set_user_role, 17 | get_role_list, 18 | role_authority, 19 | get_user_email) 20 | import object.blog 21 | import object.comment 22 | import object.msg 23 | 24 | 25 | class AnonymousUser(AnonymousUserMixin): 26 | def __init__(self): 27 | super(AnonymousUser, self).__init__() 28 | self.role = 4 # 默认角色 29 | self.email = "" # 无邮箱 30 | self.passwd_hash = "" # 无密码 31 | 32 | def check_role(self, operate: str): 33 | return check_role(self.role, operate) 34 | 35 | @property 36 | def id(self): 37 | return 0 38 | 39 | 40 | class _User(UserMixin): 41 | user_tuple = namedtuple("User", "passwd role id") 42 | 43 | @staticmethod 44 | def create(email, passwd_hash): 45 | if create_user(email, passwd_hash) is not None: 46 | return User(email) 47 | return None 48 | 49 | @staticmethod 50 | def creat_token(email: str, passwd_hash: str): 51 | s = Serializer(conf["SECRET_KEY"]) 52 | return s.dumps({"email": email, "passwd_hash": passwd_hash}) 53 | 54 | @staticmethod 55 | def load_token(token: str): 56 | s = Serializer(conf["SECRET_KEY"]) 57 | try: 58 | token = s.loads(token, max_age=3600) 59 | return token['email'], token['passwd_hash'] 60 | except BadData: 61 | return None 62 | 63 | @staticmethod 64 | def get_passwd_hash(passwd: str): 65 | return generate_password_hash(passwd) 66 | 67 | @staticmethod 68 | def create_role(name: str, authority): 69 | return create_role(name, authority) 70 | 71 | @staticmethod 72 | def delete_role(role_id: int): 73 | return delete_role(role_id) 74 | 75 | @staticmethod 76 | def get_role_list(): 77 | return get_role_list() 78 | 79 | 80 | class User(_User): 81 | RoleAuthorize = role_authority 82 | 83 | def __init__(self, email, is_id=False): 84 | if is_id: 85 | self.email = get_user_email(email) 86 | else: 87 | self.email = email 88 | 89 | def get_id(self): 90 | """Flask要求的方法""" 91 | return self.email 92 | 93 | @property 94 | def is_active(self): 95 | """Flask要求的属性, 表示用户是否激活(可登录), HGSSystem没有封禁用户系统, 所有用户都是被激活的""" 96 | return self.id != -1 97 | 98 | @property 99 | def is_authenticated(self): 100 | """Flask要求的属性, 表示登录的凭据是否正确, 这里检查是否能 load_user_by_id""" 101 | return self.is_active 102 | 103 | @property 104 | def star_email(self): 105 | if len(self.email) <= 4: 106 | return f"{self.email[0]}****" 107 | else: 108 | email = f"{self.email[0]}****{self.email[5:]}" 109 | return email 110 | 111 | @property 112 | def info(self): 113 | return User.user_tuple(*read_user(self.email)) 114 | 115 | @property 116 | def passwd_hash(self): 117 | return self.info.passwd 118 | 119 | @property 120 | def role(self): 121 | return self.info.role 122 | 123 | @property 124 | def role_name(self): 125 | return get_role_name(self.info.role) 126 | 127 | @property 128 | def id(self): 129 | return self.info.id 130 | 131 | @property 132 | def count(self): 133 | msg = object.msg.Message.get_msg_count(self) 134 | comment = object.comment.Comment.get_user_comment_count(self) 135 | blog = object.blog.BlogArticle.get_blog_count(None, self) 136 | return msg, comment, blog 137 | 138 | def check_passwd(self, passwd: str): 139 | return check_password_hash(self.passwd_hash, passwd) 140 | 141 | def check_role(self, operate: str): 142 | return check_role(self.role, operate) 143 | 144 | def delete(self): 145 | return delete_user(self.id) 146 | 147 | def change_passwd(self, passwd): 148 | return change_passwd_hash(self.email, self.get_passwd_hash(passwd)) 149 | 150 | def set_user_role(self, role_id: int): 151 | return set_user_role(role_id, self.id) 152 | -------------------------------------------------------------------------------- /templates/docx/docx.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 博客 {% endblock %} 4 | 5 | {% block style %} 6 | {{ super() }} 7 | 8 | 9 | 10 | {% endblock %} 11 | 12 | {% block content %} 13 |
    14 | {% if form and current_user.check_role("WriteBlog") %} 15 | {# 判断是否有权限写博客 #} 16 |
    17 |
    18 |
    19 |
    20 | {{ form.hidden_tag() }} 21 | {{ macro.render_field(form.title) }} 22 | {{ macro.render_field(form.subtitle) }} 23 | {{ macro.render_select_field(form.archive) }} 24 |
    25 | {{ form.content(class="form-control mb-2", style="display:none;") }} 26 |
    27 | {% for error in form.content.errors %} 28 |
    {{ error }}
    29 | {% endfor %} 30 | 31 | 47 | 48 |
    49 | 50 |
    51 |
    52 |
    53 |
    54 |
    55 |
    56 | {% endif %} 57 | 58 | {% if current_user.check_role("ReadBlog") %} 59 | {# 检查是否具有读取权限 #} 60 | {% cache conf["LIST_CACHE_EXPIRE"], ":blog", ":page", cache_str %} 61 |
    62 |
    63 | {% for blog in blog_list %} 64 | {% if blog.top %} 65 | {{ render_docx_top(blog, show_delete) }} 66 | {% else %} 67 | {{ render_docx(blog, show_delete) }} 68 | {% endif %} 69 | {% endfor %} 70 |
    71 |
    72 | 73 | 74 | 77 | 78 | {% endcache %} 79 | 80 | {% endif %} 81 |
    82 | {% endblock %} 83 | 84 | {% block javascript %} 85 | {{ super() }} 86 | 87 | {% if form and current_user.check_role("WriteBlog") %} 88 | 89 | 90 | 109 | {% endif %} 110 | {% endblock %} -------------------------------------------------------------------------------- /init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS role -- 角色表 2 | ( 3 | RoleID INT PRIMARY KEY AUTO_INCREMENT, 4 | RoleName char(20) NOT NULL UNIQUE, 5 | WriteBlog bit DEFAULT 0, -- 写博客 6 | WriteComment bit DEFAULT 1, -- 写评论 7 | WriteMsg bit DEFAULT 1, -- 写留言 8 | CreateUser bit DEFAULT 0, -- 创建新用户 9 | 10 | ReadBlog bit DEFAULT 1, -- 读博客 11 | ReadComment bit DEFAULT 1, -- 读评论 12 | ReadMsg bit DEFAULT 1, -- 读留言 13 | ReadSecretMsg bit DEFAULT 0, -- 读私密留言 14 | ReadUserInfo bit DEFAULT 0, -- 读取用户信息 15 | 16 | DeleteBlog bit DEFAULT 0, -- 删除博客 17 | DeleteComment bit DEFAULT 0, -- 删除评论 18 | DeleteMsg bit DEFAULT 0, -- 删除留言 19 | DeleteUser bit DEFAULT 0, -- 删除用户 20 | 21 | ConfigureSystem bit DEFAULT 0, -- 配置系统 22 | ReadSystem bit DEFAULT 0 -- 读系统信息 23 | ) CHARACTER SET utf8 COLLATE utf8_unicode_ci; 24 | 25 | CREATE TABLE IF NOT EXISTS user -- 创建用户表 26 | ( 27 | ID INT PRIMARY KEY AUTO_INCREMENT, 28 | Email char(32) NOT NULL UNIQUE, 29 | PasswdHash char(128) NOT NULL, 30 | Role INT NOT NULL DEFAULT 3, 31 | FOREIGN KEY (Role) REFERENCES role (RoleID) 32 | ) CHARACTER SET utf8 COLLATE utf8_unicode_ci; 33 | 34 | INSERT INTO role (RoleID, 35 | RoleName, 36 | WriteBlog, 37 | WriteComment, 38 | WriteMsg, 39 | CreateUser, 40 | ReadBlog, 41 | ReadComment, 42 | ReadMsg, 43 | ReadSecretMsg, 44 | ReadUserInfo, 45 | DeleteBlog, 46 | DeleteComment, 47 | DeleteMsg, 48 | DeleteUser, 49 | ConfigureSystem, 50 | ReadSystem) 51 | VALUES (1, 'Admin', 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1), -- 管理员用户 52 | (2, 'Coordinator', 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1); -- 协管员用户 53 | 54 | INSERT INTO role (RoleID, RoleName) 55 | VALUES (3, 'Default'); -- 默认用户 56 | 57 | INSERT INTO role (RoleID, RoleName, WriteComment, WriteMsg) 58 | VALUES (4, 'Anonymous', 0, 0); -- 默认用户 59 | 60 | CREATE TABLE IF NOT EXISTS blog -- 创建博客表 61 | ( 62 | ID INT PRIMARY KEY AUTO_INCREMENT, -- 文章 ID 63 | Auth INT NOT NULL, -- 作者 64 | Title char(20) NOT NULL, -- 标题 65 | SubTitle char(20) NOT NULL, -- 副标题 66 | Content TEXT NOT NULL, -- 内容 67 | CreateTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建的时间 68 | UpdateTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建的时间 69 | Top BIT NOT NULL DEFAULT 0, -- 置顶 70 | FOREIGN KEY (Auth) REFERENCES user (ID) 71 | ) CHARACTER SET utf8 COLLATE utf8_unicode_ci; 72 | 73 | CREATE TABLE IF NOT EXISTS archive -- 归档表 74 | ( 75 | ID INT PRIMARY KEY AUTO_INCREMENT, -- 归档 ID 76 | Name CHAR(30) NOT NULL UNIQUE, -- 归档名称 77 | DescribeText char(100) NOT NULL -- 描述 78 | ) CHARACTER SET utf8 COLLATE utf8_unicode_ci; 79 | 80 | CREATE TABLE IF NOT EXISTS blog_archive -- 归档表 81 | ( 82 | BlogID INT, -- 文章ID 83 | ArchiveID INT, -- 归档ID 84 | FOREIGN KEY (BlogID) REFERENCES blog (ID), 85 | FOREIGN KEY (ArchiveID) REFERENCES archive (ID) 86 | ) CHARACTER SET utf8 COLLATE utf8_unicode_ci; 87 | 88 | CREATE VIEW blog_archive_with_name AS 89 | SELECT BlogID, ArchiveID, archive.Name As ArchiveName, archive.DescribeText AS DescribeText 90 | FROM blog_archive 91 | LEFT JOIN archive on blog_archive.ArchiveID = archive.ID; 92 | 93 | CREATE VIEW blog_with_archive AS 94 | SELECT blog.ID AS BlogID, 95 | blog_archive.ArchiveID AS ArchiveID, 96 | Auth, 97 | Title, 98 | SubTitle, 99 | Content, 100 | CreateTime, 101 | UpdateTime, 102 | Top 103 | FROM blog 104 | RIGHT JOIN blog_archive ON blog.ID = blog_archive.BlogID; 105 | 106 | CREATE TABLE IF NOT EXISTS comment -- 评论表 107 | ( 108 | ID INT PRIMARY KEY AUTO_INCREMENT, -- 评论 ID 109 | BlogID INT NOT NULL, -- 博客 ID 110 | Auth INT NOT NULL, -- 作者 111 | Content TEXT NOT NULL, -- 内容 112 | CreateTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建的时间 113 | UpdateTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建的时间 114 | FOREIGN KEY (BlogID) REFERENCES blog (ID), 115 | FOREIGN KEY (Auth) REFERENCES user (ID) 116 | ) CHARACTER SET utf8 COLLATE utf8_unicode_ci; 117 | 118 | CREATE VIEW comment_user AS 119 | SELECT comment.ID as CommentID, BlogID, Auth, user.Email as Email, Content, CreateTime, UpdateTime 120 | FROM comment 121 | LEFT JOIN user on user.ID = comment.Auth 122 | ORDER BY UpdateTime DESC; 123 | 124 | CREATE TABLE IF NOT EXISTS message -- 留言表 125 | ( 126 | ID INT PRIMARY KEY AUTO_INCREMENT, -- 留言 ID 127 | Auth INT NOT NULL, -- 作者 128 | Content TEXT NOT NULL, -- 内容 129 | Secret BIT NOT NULL DEFAULT 0, -- 私密内容 130 | CreateTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建的时间 131 | UpdateTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建的时间 132 | FOREIGN KEY (Auth) REFERENCES user (ID) 133 | ) CHARACTER SET utf8 COLLATE utf8_unicode_ci; 134 | 135 | 136 | CREATE VIEW message_user AS 137 | SELECT message.ID as MsgID, Auth, user.Email as Email, Content, CreateTime, UpdateTime, Secret 138 | FROM message 139 | LEFT JOIN user on user.ID = message.Auth 140 | ORDER BY UpdateTime DESC; 141 | -------------------------------------------------------------------------------- /templates/auth/role.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 角色管理 {% endblock %} 4 | 5 | {% block style %} 6 | {{ super() }} 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
    12 |
    13 |
    14 | 25 | 26 |
    27 |
    28 |
    29 |
    30 | {{ CreateRoleForm.hidden_tag() }} 31 | {{ macro.render_field(CreateRoleForm.name) }} 32 | {{ macro.render_select_field(CreateRoleForm.authority) }} 33 | 34 | 52 | 53 |
    54 | 创建角色 55 |
    56 |
    57 |
    58 | 59 |
    60 |
    61 | {{ DeleteRoleForm.hidden_tag() }} 62 | {{ macro.render_field(DeleteRoleForm.name) }} 63 | 64 | 82 | 83 |
    84 | 删除角色 85 |
    86 |
    87 |
    88 | 89 |
    90 |
    91 | {{ SetRoleForm.hidden_tag() }} 92 | {{ macro.render_field(SetRoleForm.email) }} 93 | {{ macro.render_field(SetRoleForm.name) }} 94 | 95 | 113 | 114 |
    115 | 设置角色 116 |
    117 |
    118 |
    119 |
    120 |
    121 |
    122 |
    123 |
    124 | {% endblock %} 125 | -------------------------------------------------------------------------------- /sql/user.py: -------------------------------------------------------------------------------- 1 | from sql import db, DB 2 | from sql.base import DBBit 3 | from sql.cache import (get_user_from_cache, write_user_to_cache, delete_user_from_cache, 4 | get_user_email_from_cache, write_user_email_to_cache, delete_user_email_from_cache, 5 | get_role_name_from_cache, write_role_name_to_cache, delete_role_name_from_cache, 6 | get_role_operate_from_cache, write_role_operate_to_cache, delete_role_operate_from_cache) 7 | import object.user 8 | 9 | from typing import List 10 | 11 | role_authority = ["WriteBlog", "WriteComment", "WriteMsg", "CreateUser", 12 | "ReadBlog", "ReadComment", "ReadMsg", "ReadSecretMsg", "ReadUserInfo", 13 | "DeleteBlog", "DeleteComment", "DeleteMsg", "DeleteUser", 14 | "ConfigureSystem", "ReadSystem"] 15 | 16 | 17 | def read_user(email: str, mysql: DB = db, not_cache=False): 18 | """ 读取用户 """ 19 | if not not_cache: 20 | res = get_user_from_cache(email) 21 | if res is not None: 22 | return res 23 | 24 | cur = mysql.search("SELECT PasswdHash, Role, ID FROM user WHERE Email=%s", email) 25 | if cur is None or cur.rowcount != 1: 26 | return ["", -1, -1] 27 | 28 | res = cur.fetchone() 29 | write_user_to_cache(email, *res) 30 | return res 31 | 32 | 33 | def create_user(email: str, passwd: str, mysql: DB = db): 34 | """ 创建用户 """ 35 | if len(email) == 0: 36 | return None 37 | 38 | cur = mysql.search("SELECT COUNT(*) FROM user") 39 | passwd = object.user.User.get_passwd_hash(passwd) 40 | if cur is None or cur.rowcount == 0 or cur.fetchone()[0] == 0: 41 | # 创建为管理员用户 42 | cur = mysql.insert("INSERT INTO user(Email, PasswdHash, Role) " 43 | "VALUES (%s, %s, %s)", email, passwd, 1) 44 | else: 45 | cur = mysql.insert("INSERT INTO user(Email, PasswdHash) " 46 | "VALUES (%s, %s)", email, passwd) 47 | if cur is None or cur.rowcount != 1: 48 | return None 49 | read_user(email, mysql) # 刷新缓存 50 | return cur.lastrowid 51 | 52 | 53 | def delete_user(user_id: int, mysql: DB = db): 54 | """ 删除用户 """ 55 | delete_user_from_cache(get_user_email(user_id)) 56 | delete_user_email_from_cache(user_id) 57 | 58 | conn = mysql.get_connection() 59 | cur = mysql.delete("DELETE FROM message WHERE Auth=%s", user_id, connection=conn) 60 | if cur is None: 61 | conn.rollback() 62 | conn.close() 63 | return False 64 | 65 | cur = mysql.delete("DELETE FROM comment WHERE Auth=%s", user_id, connection=conn) 66 | if cur is None: 67 | conn.rollback() 68 | conn.close() 69 | return False 70 | 71 | cur = mysql.delete("DELETE FROM blog WHERE Auth=%s", user_id, connection=conn) 72 | if cur is None: 73 | conn.rollback() 74 | conn.close() 75 | return False 76 | 77 | cur = mysql.delete("DELETE FROM user WHERE ID=%s", user_id, connection=conn) 78 | if cur is None or cur.rowcount == 0: 79 | conn.rollback() 80 | conn.close() 81 | return False 82 | 83 | conn.commit() 84 | conn.close() 85 | return True 86 | 87 | 88 | def change_passwd_hash(user_email: str, passwd_hash: str, mysql: DB = db): 89 | delete_user_from_cache(user_email) 90 | cur = mysql.update("UPDATE user " 91 | "SET PasswdHash=%s " 92 | "WHERE Email=%s", passwd_hash, user_email) 93 | read_user(user_email, mysql) # 刷新缓存 94 | if cur is None or cur.rowcount == 0: 95 | return False 96 | return True 97 | 98 | 99 | def get_user_email(user_id, mysql: DB = db, not_cache=False): 100 | """ 获取用户邮箱 """ 101 | if not not_cache: 102 | res = get_user_email_from_cache(user_id) 103 | if res is not None: 104 | return res 105 | 106 | cur = mysql.search("SELECT Email FROM user WHERE ID=%s", user_id) 107 | if cur is None or cur.rowcount == 0: 108 | return None 109 | 110 | res = cur.fetchone()[0] 111 | write_user_email_to_cache(user_id, res) 112 | return res 113 | 114 | 115 | def __authority_to_sql(authority): 116 | """ authority 转换为 Update语句, 不检查合法性 """ 117 | sql = [] 118 | args = [] 119 | for i in authority: 120 | sql.append(f"{i}=%s") 121 | args.append(authority[i]) 122 | return ",".join(sql), args 123 | 124 | 125 | def create_role(name: str, authority: List[str], mysql: DB = db): 126 | conn = mysql.get_connection() 127 | cur = mysql.insert("INSERT INTO role(RoleName) VALUES (%s)", name, connection=conn) 128 | if cur is None or cur.rowcount == 0: 129 | conn.rollback() 130 | conn.close() 131 | return False 132 | 133 | sql, args = __authority_to_sql({i: (1 if i in authority else 0) for i in role_authority}) 134 | cur = mysql.update(f"UPDATE role " 135 | f"SET {sql} " 136 | f"WHERE RoleName=%s", *args, name, connection=conn) 137 | if cur is None or cur.rowcount == 0: 138 | conn.rollback() 139 | conn.close() 140 | return False 141 | 142 | conn.commit() 143 | conn.close() 144 | return True 145 | 146 | 147 | def delete_role(role_id: int, mysql: DB = db): 148 | delete_role_name_from_cache(role_id) 149 | delete_role_operate_from_cache(role_id) 150 | 151 | cur = mysql.delete("DELETE FROM role WHERE RoleID=%s", role_id) 152 | if cur is None or cur.rowcount == 0: 153 | return False 154 | return True 155 | 156 | 157 | def set_user_role(role_id: int, user_id: str, mysql: DB = db): 158 | cur = mysql.update("UPDATE user " 159 | "SET Role=%s " 160 | "WHERE ID=%s", role_id, user_id) 161 | if cur is None or cur.rowcount == 0: 162 | return False 163 | return True 164 | 165 | 166 | def get_role_name(role: int, mysql: DB = db, not_cache=False): 167 | """ 获取用户角色名称 """ 168 | if not not_cache: 169 | res = get_role_name_from_cache(role) 170 | if res is not None: 171 | return res 172 | 173 | cur = mysql.search("SELECT RoleName FROM role WHERE RoleID=%s", role) 174 | if cur is None or cur.rowcount == 0: 175 | return None 176 | 177 | res = cur.fetchone()[0] 178 | write_role_name_to_cache(role, res) 179 | return res 180 | 181 | 182 | def __check_operate(operate): 183 | return operate in role_authority 184 | 185 | 186 | def check_role(role: int, operate: str, mysql: DB = db, not_cache=False): 187 | """ 检查角色权限(通过角色ID) """ 188 | if not __check_operate(operate): # 检查, 防止SQL注入 189 | return False 190 | 191 | if not not_cache: 192 | res = get_role_operate_from_cache(role, operate) 193 | if res is not None: 194 | return res 195 | 196 | cur = mysql.search(f"SELECT {operate} FROM role WHERE RoleID=%s", role) 197 | if cur is None or cur.rowcount == 0: 198 | return False 199 | 200 | res = cur.fetchone()[0] == DBBit.BIT_1 201 | write_role_operate_to_cache(role, operate, res) 202 | return res 203 | 204 | 205 | def get_role_list(mysql: DB = db): 206 | """ 获取归档列表 """ 207 | cur = mysql.search("SELECT RoleID, RoleName FROM role") 208 | if cur is None or cur.rowcount == 0: 209 | return [] 210 | return cur.fetchall() 211 | 212 | 213 | def get_role_list_iter(mysql: DB = db): 214 | """ 获取归档列表 """ 215 | cur = mysql.search("SELECT RoleID, RoleName FROM role") 216 | if cur is None or cur.rowcount == 0: 217 | return [] 218 | return cur 219 | 220 | 221 | def get_user_list_iter(mysql: DB = db): 222 | """ 获取归档列表 """ 223 | cur = mysql.search("SELECT ID FROM user") 224 | if cur is None or cur.rowcount == 0: 225 | return [] 226 | return cur 227 | -------------------------------------------------------------------------------- /sql/blog.py: -------------------------------------------------------------------------------- 1 | from sql import db, DB 2 | from sql.base import DBBit 3 | from sql.archive import add_blog_to_archive 4 | from sql.cache import (write_blog_to_cache, get_blog_from_cache, delete_blog_from_cache, 5 | write_blog_count_to_cache, get_blog_count_from_cache, delete_blog_count_from_cache, 6 | write_archive_blog_count_to_cache, get_archive_blog_count_from_cache, 7 | delete_all_archive_blog_count_from_cache, delete_archive_blog_count_from_cache, 8 | write_user_blog_count_to_cache, get_user_blog_count_from_cache, 9 | delete_all_user_blog_count_from_cache, delete_user_blog_count_from_cache, 10 | delete_blog_archive_from_cache) 11 | import object.archive 12 | 13 | from typing import Optional, List 14 | 15 | 16 | def create_blog(auth_id: int, title: str, subtitle: str, content: str, 17 | archive_list: List[object.archive.Archive], mysql: DB = db) -> bool: 18 | """ 写入新的blog """ 19 | delete_blog_count_from_cache() 20 | delete_user_blog_count_from_cache(auth_id) 21 | # archive cache 在下面循环删除 22 | 23 | cur = mysql.insert("INSERT INTO blog(Auth, Title, SubTitle, Content) " 24 | "VALUES (%s, %s, %s, %s)", auth_id, title, subtitle, content) 25 | if cur is None or cur.rowcount == 0: 26 | return False 27 | 28 | blog_id = cur.lastrowid 29 | for archive in archive_list: 30 | if not add_blog_to_archive(blog_id, archive.id): 31 | return False 32 | delete_archive_blog_count_from_cache(archive.id) 33 | read_blog(blog_id, mysql) # 刷新缓存 34 | return True 35 | 36 | 37 | def update_blog(blog_id: int, content: str, mysql: DB = db) -> bool: 38 | """ 更新博客文章 """ 39 | delete_blog_from_cache(blog_id) 40 | 41 | cur = mysql.update("Update blog " 42 | "SET UpdateTime=CURRENT_TIMESTAMP(), Content=%s " 43 | "WHERE ID=%s", content, blog_id) 44 | if cur is None or cur.rowcount != 1: 45 | return False 46 | read_blog(blog_id, mysql) # 刷新缓存 47 | return True 48 | 49 | 50 | def read_blog(blog_id: int, mysql: DB = db, not_cache=False) -> list: 51 | """ 读取blog内容 """ 52 | if not not_cache: 53 | res = get_blog_from_cache(blog_id) 54 | if res is not None: 55 | return res 56 | 57 | cur = mysql.search("SELECT Auth, Title, SubTitle, Content, UpdateTime, CreateTime, Top " 58 | "FROM blog " 59 | "WHERE ID=%s", blog_id) 60 | if cur is None or cur.rowcount == 0: 61 | return [-1, "", "", "", 0, -1, False] 62 | res = cur.fetchone() 63 | write_blog_to_cache(blog_id, *res, is_db_bit=True) 64 | return [*res[:6], res[-1] == DBBit.BIT_1] 65 | 66 | 67 | def delete_blog(blog_id: int, mysql: DB = db): 68 | delete_blog_count_from_cache() 69 | delete_all_archive_blog_count_from_cache() 70 | delete_all_user_blog_count_from_cache() 71 | delete_blog_from_cache(blog_id) 72 | delete_blog_archive_from_cache(blog_id) 73 | 74 | conn = mysql.get_connection() 75 | cur = mysql.delete("DELETE FROM blog_archive WHERE BlogID=%s", blog_id, connection=conn) 76 | if cur is None: 77 | conn.rollback() 78 | conn.close() 79 | return False 80 | 81 | cur = mysql.delete("DELETE FROM comment WHERE BlogID=%s", blog_id, connection=conn) 82 | if cur is None: 83 | conn.rollback() 84 | conn.close() 85 | return False 86 | 87 | cur = mysql.delete("DELETE FROM blog WHERE ID=%s", blog_id, connection=conn) 88 | if cur is None or cur.rowcount == 0: 89 | conn.rollback() 90 | conn.close() 91 | return False 92 | 93 | conn.commit() 94 | conn.close() 95 | return True 96 | 97 | 98 | def set_blog_top(blog_id: int, top: bool = True, mysql: DB = db): 99 | delete_blog_from_cache(blog_id) 100 | cur = mysql.update("UPDATE blog " 101 | "SET Top=%s " 102 | "WHERE ID=%s", 1 if top else 0, blog_id) 103 | if cur is None or cur.rowcount != 1: 104 | return False 105 | read_blog(blog_id, mysql) # 刷新缓存 106 | return True 107 | 108 | 109 | def get_blog_list(limit: Optional[int] = None, offset: Optional[int] = None, mysql: DB = db) -> list: 110 | """ 获得 blog 列表 """ 111 | if limit is not None and offset is not None: 112 | cur = mysql.search("SELECT ID " 113 | "FROM blog " 114 | "ORDER BY Top DESC, CreateTime DESC, Title, SubTitle " 115 | "LIMIT %s OFFSET %s", limit, offset) 116 | else: 117 | cur = mysql.search("SELECT ID " 118 | "FROM blog " 119 | "ORDER BY Top DESC, CreateTime DESC, Title, SubTitle") 120 | if cur is None or cur.rowcount == 0: 121 | return [] 122 | return [i[0] for i in cur.fetchall()] 123 | 124 | 125 | def get_blog_list_iter(mysql: DB = db): 126 | """ 获得 blog 列表 """ 127 | cur = mysql.search("SELECT ID " 128 | "FROM blog " 129 | "ORDER BY Top DESC, CreateTime DESC, Title, SubTitle") 130 | if cur is None or cur.rowcount == 0: 131 | return [] 132 | return cur 133 | 134 | 135 | def get_blog_list_not_top(limit: Optional[int] = None, offset: Optional[int] = None, mysql: DB = db) -> list: 136 | """ 获得blog列表 忽略置顶 """ 137 | if limit is not None and offset is not None: 138 | cur = mysql.search("SELECT ID " 139 | "FROM blog " 140 | "ORDER BY CreateTime DESC, Title, SubTitle " 141 | "LIMIT %s OFFSET %s", limit, offset) 142 | else: 143 | cur = mysql.search("SELECT ID " 144 | "FROM blog " 145 | "ORDER BY CreateTime DESC, Title, SubTitle") 146 | if cur is None or cur.rowcount == 0: 147 | return [] 148 | return [i[0] for i in cur.fetchall()] 149 | 150 | 151 | def get_archive_blog_list(archive_id, limit: Optional[int] = None, 152 | offset: Optional[int] = None, 153 | mysql: DB = db) -> list: 154 | """ 获得指定归档的 blog 列表 """ 155 | if limit is not None and offset is not None: 156 | cur = mysql.search("SELECT BlogID " 157 | "FROM blog_with_archive " 158 | "WHERE ArchiveID=%s " 159 | "ORDER BY Top DESC, CreateTime DESC, Title, SubTitle " 160 | "LIMIT %s OFFSET %s", archive_id, limit, offset) 161 | else: 162 | cur = mysql.search("SELECT BlogID " 163 | "FROM blog_with_archive " 164 | "WHERE ArchiveID=%s " 165 | "ORDER BY Top DESC, CreateTime DESC, Title, SubTitle") 166 | if cur is None or cur.rowcount == 0: 167 | return [] 168 | return [i[0] for i in cur.fetchall()] 169 | 170 | 171 | def get_blog_count(mysql: DB = db, not_cache=False) -> int: 172 | """ 统计 blog 个数 """ 173 | if not not_cache: 174 | res = get_blog_count_from_cache() 175 | if res is not None: 176 | return res 177 | 178 | cur = mysql.search("SELECT COUNT(*) FROM blog") 179 | if cur is None or cur.rowcount == 0: 180 | return 0 181 | 182 | res = cur.fetchone()[0] 183 | write_blog_count_to_cache(res) 184 | return res 185 | 186 | 187 | def get_archive_blog_count(archive_id, mysql: DB = db, not_cache=False) -> int: 188 | """ 统计指定归档的 blog 个数 """ 189 | if not not_cache: 190 | res = get_archive_blog_count_from_cache(archive_id) 191 | if res is not None: 192 | return res 193 | 194 | cur = mysql.search("SELECT COUNT(*) FROM blog_with_archive WHERE ArchiveID=%s", archive_id) 195 | if cur is None or cur.rowcount == 0: 196 | return 0 197 | 198 | res = cur.fetchone()[0] 199 | write_archive_blog_count_to_cache(archive_id, res) 200 | return res 201 | 202 | 203 | def get_user_blog_count(user_id: int, mysql: DB = db, not_cache=False) -> int: 204 | """ 获得指定用户的 blog 个数 """ 205 | if not not_cache: 206 | res = get_user_blog_count_from_cache(user_id) 207 | if res is not None: 208 | return res 209 | 210 | cur = mysql.search("SELECT COUNT(*) FROM blog WHERE Auth=%s", user_id) 211 | if cur is None or cur.rowcount == 0: 212 | return 0 213 | 214 | res = cur.fetchone()[0] 215 | write_user_blog_count_to_cache(user_id, res) 216 | return res 217 | -------------------------------------------------------------------------------- /app/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from flask import Flask, url_for, request, current_app, render_template, Response, jsonify 5 | from flask_mail import Mail 6 | from flask_login import LoginManager, current_user 7 | from flask_moment import Moment 8 | from flask.logging import default_handler 9 | from typing import Optional, Union 10 | 11 | import logging.handlers 12 | import logging 13 | from bs4 import BeautifulSoup 14 | 15 | from configure import conf 16 | from object.user import AnonymousUser, User 17 | from app.cache import cache 18 | from app.http_auth import http_auth 19 | 20 | if conf["DEBUG_PROFILE"]: 21 | from werkzeug.middleware.profiler import ProfilerMiddleware 22 | 23 | 24 | class HBlogFlask(Flask): 25 | def __init__(self, import_name: str, *args, **kwargs): 26 | super(HBlogFlask, self).__init__(import_name, *args, **kwargs) 27 | self.about_me = "" 28 | self.update_configure() 29 | 30 | if conf["DEBUG_PROFILE"]: 31 | self.wsgi_app = ProfilerMiddleware(self.wsgi_app, sort_by=("cumtime",)) 32 | 33 | self.login_manager = LoginManager() 34 | self.login_manager.init_app(self) 35 | self.login_manager.anonymous_user = AnonymousUser # 设置未登录的匿名对象 36 | self.login_manager.login_view = "auth.login_page" 37 | 38 | self.mail = Mail(self) 39 | self.moment = Moment(self) 40 | self.cache = cache 41 | self.cache.init_app(self) 42 | self.http_auth = http_auth 43 | 44 | self.logger.removeHandler(default_handler) 45 | self.logger.setLevel(conf["LOG_LEVEL"]) 46 | self.logger.propagate = False 47 | if len(conf["LOG_HOME"]) > 0: 48 | handle = logging.handlers.TimedRotatingFileHandler( 49 | os.path.join(conf["LOG_HOME"], f"flask.log"), backupCount=10) 50 | handle.setFormatter(logging.Formatter(conf["LOG_FORMAT"])) 51 | self.logger.addHandler(handle) 52 | if conf["LOG_STDERR"]: 53 | handle = logging.StreamHandler(sys.stderr) 54 | handle.setFormatter(logging.Formatter(conf["LOG_FORMAT"])) 55 | self.logger.addHandler(handle) 56 | 57 | @self.login_manager.user_loader 58 | def user_loader(email: str): 59 | user = User(email) 60 | if user.info.id == -1: 61 | return None 62 | return user 63 | 64 | for i in [400, 401, 403, 404, 405, 408, 410, 413, 414, 423, 500, 501, 502]: 65 | def create_error_handle(status): 66 | def error_handle(e): 67 | self.print_load_page_log(status) 68 | if "/api" in request.base_url: 69 | rsp = jsonify({"status": status, "error": str(e)}) 70 | rsp.status_code = status 71 | return rsp 72 | data = render_template('error.html', error_code=status, error_info=e) 73 | return Response(response=data, status=status) 74 | return error_handle 75 | 76 | self.errorhandler(i)(create_error_handle(i)) 77 | 78 | def register_all_blueprint(self): 79 | import app.index as index 80 | import app.archive as archive 81 | import app.docx as docx 82 | import app.msg as msg 83 | import app.oss as oss 84 | import app.auth as auth 85 | import app.about_me as about_me 86 | import app.api as api 87 | 88 | self.register_blueprint(index.index, url_prefix="/") 89 | self.register_blueprint(archive.archive, url_prefix="/archive") 90 | self.register_blueprint(docx.docx, url_prefix="/docx") 91 | self.register_blueprint(msg.msg, url_prefix="/msg") 92 | self.register_blueprint(auth.auth, url_prefix="/auth") 93 | self.register_blueprint(about_me.about_me, url_prefix="/about") 94 | self.register_blueprint(oss.oss, url_prefix="/oss") 95 | self.register_blueprint(api.api, url_prefix="/api") 96 | 97 | def update_configure(self): 98 | """ 更新配置 """ 99 | self.config.update(conf) 100 | about_me_page = conf["ABOUT_ME_PAGE"] 101 | if len(about_me_page) > 0 and os.path.exists(about_me_page): 102 | with open(about_me_page, "r", encoding='utf-8') as f: 103 | bs = BeautifulSoup(f.read(), "html.parser") 104 | self.about_me = str(bs.find("body").find("div", class_="about-me")) # 提取about-me部分的内容 105 | 106 | @staticmethod 107 | def get_max_page(count: int, count_page: int): 108 | """ 计算页码数 (共计count个元素, 每页count_page个元素) """ 109 | return (count // count_page) + (0 if count % count_page == 0 else 1) 110 | 111 | @staticmethod 112 | def get_page(url, page: int, count: int): 113 | """ 计算页码的按钮 """ 114 | if count <= 9: 115 | page_list = [[i + 1, url_for(url, page=i + 1)] for i in range(count)] 116 | elif page <= 5: 117 | """ 118 | [1][2][3][4][5][6][...][count - 1][count] 119 | """ 120 | page_list = [[i + 1, url_for(url, page=i + 1)] for i in range(6)] 121 | page_list += [None, [count - 1, url_for(url, page=count - 1)], [count, url_for(url, page=count)]] 122 | elif page >= count - 5: 123 | """ 124 | [1][2][...][count - 5][count - 4][count - 3][count - 2][count - 1][count] 125 | """ 126 | page_list: Optional[list] = [[1, url_for(url, page=1)], [2, url_for(url, page=2)], None] 127 | page_list += [[count - 5 + i, url_for(url, page=count - 5 + i)] for i in range(6)] 128 | else: 129 | """ 130 | [1][2][...][page - 2][page - 1][page][page + 1][page + 2][...][count - 1][count] 131 | """ 132 | page_list: Optional[list] = [[1, url_for(url, page=1)], [2, url_for(url, page=2)], None] 133 | page_list += [[page - 2 + i, url_for(url, page=page - 2 + i)] for i in range(5)] 134 | page_list += [None, [count - 1, url_for(url, page=count - 1)], [count, url_for(url, page=count)]] 135 | return page_list 136 | 137 | @staticmethod 138 | def __get_log_request_info(): 139 | return (f"user: '{current_user.email}' " 140 | f"url: '{request.url}' blueprint: '{request.blueprint}' " 141 | f"args: {request.args} form: {request.form} " 142 | f"accept_encodings: '{request.accept_encodings}' " 143 | f"accept_charsets: '{request.accept_charsets}' " 144 | f"accept_mimetypes: '{request.accept_mimetypes}' " 145 | f"accept_languages: '{request.accept_languages}'") 146 | 147 | @staticmethod 148 | def print_load_page_log(page: str): 149 | current_app.logger.debug( 150 | f"[{request.method}] Load - '{page}' " + HBlogFlask.__get_log_request_info()) 151 | 152 | @staticmethod 153 | def print_form_error_log(opt: str): 154 | current_app.logger.warning( 155 | f"[{request.method}] '{opt}' - Bad form " + HBlogFlask.__get_log_request_info()) 156 | 157 | @staticmethod 158 | def print_sys_opt_fail_log(opt: str): 159 | current_app.logger.error( 160 | f"[{request.method}] System {opt} - fail " + HBlogFlask.__get_log_request_info()) 161 | 162 | @staticmethod 163 | def print_sys_opt_success_log(opt: str): 164 | current_app.logger.warning( 165 | f"[{request.method}] System {opt} - success " + HBlogFlask.__get_log_request_info()) 166 | 167 | @staticmethod 168 | def print_user_opt_fail_log(opt: str): 169 | current_app.logger.debug( 170 | f"[{request.method}] User {opt} - fail " + HBlogFlask.__get_log_request_info()) 171 | 172 | @staticmethod 173 | def print_user_opt_success_log(opt: str): 174 | current_app.logger.debug( 175 | f"[{request.method}] User {opt} - success " + HBlogFlask.__get_log_request_info()) 176 | 177 | @staticmethod 178 | def print_user_opt_error_log(opt: str): 179 | current_app.logger.warning( 180 | f"[{request.method}] User {opt} - system fail " + HBlogFlask.__get_log_request_info()) 181 | 182 | @staticmethod 183 | def print_import_user_opt_success_log(opt: str): 184 | current_app.logger.info( 185 | f"[{request.method}] User {opt} - success " + HBlogFlask.__get_log_request_info()) 186 | 187 | @staticmethod 188 | def print_user_not_allow_opt_log(opt: str): 189 | current_app.logger.info( 190 | f"[{request.method}] User '{opt}' - reject " + HBlogFlask.__get_log_request_info()) 191 | 192 | 193 | Hblog = Union[HBlogFlask, Flask] 194 | -------------------------------------------------------------------------------- /app/api.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request, abort, g 2 | from configure import conf 3 | from json import loads 4 | from datetime import datetime 5 | 6 | from object.archive import Archive 7 | from object.blog import BlogArticle 8 | from object.msg import Message 9 | from object.comment import Comment 10 | from object.user import User 11 | 12 | from app.http_auth import http_auth 13 | from app.tool import api_role_required 14 | 15 | 16 | api = Blueprint("api", __name__) 17 | 18 | 19 | @api.route("/", methods=["GET", "POST"]) 20 | def api_say_hello(): 21 | json = loads(request.get_json()) 22 | name = "unknown" 23 | if json: 24 | name = json.get("name", "unknown") 25 | res = {"status": 200, name: "Hello!"} 26 | return res 27 | 28 | 29 | @api.route("/get_introduce") 30 | @http_auth.login_required 31 | def api_get_introduce(): 32 | title = request.args.get("title", "", type=str).lower() 33 | 34 | res = {"status": 200, "introduce": {}} 35 | have_found = False 36 | for info in conf['INTRODUCE']: 37 | if title is None or title == info[0].lower(): 38 | res["introduce"][info[0]] = info[1] 39 | have_found = True 40 | 41 | if not have_found: 42 | abort(404) 43 | return jsonify(res) 44 | 45 | 46 | @api.route("/find_me") 47 | @http_auth.login_required 48 | def api_get_find_me(): 49 | where = request.args.get("where", None, type=str) 50 | if where: 51 | where = where.lower() 52 | 53 | res = {"status": 200, "content": {}} 54 | have_found = False 55 | for i in conf['INTRODUCE_LINK']: 56 | if where is None or where == i.lower(): 57 | res["content"][i] = conf['INTRODUCE_LINK'][i] 58 | have_found = True 59 | 60 | if not have_found: 61 | abort(404) 62 | return jsonify(res) 63 | 64 | 65 | @api.route("/archive_list") 66 | @http_auth.login_required 67 | @api_role_required("ReadBlog", "api get archive list") 68 | def api_get_archive_list(): 69 | archive_list = Archive.get_archive_list() 70 | res = {"status": 200} 71 | res_list = [] 72 | for i in archive_list: 73 | res_list.append({ 74 | "name": i.name, 75 | "describe": i.describe, 76 | "count": i.count, 77 | "id": i.id, 78 | }) 79 | 80 | res["archive"] = res_list 81 | return jsonify(res) 82 | 83 | 84 | @api.route("/archive/") 85 | @http_auth.login_required 86 | @api_role_required("ReadBlog", "api get archive") 87 | def get_get_archive(archive_id): 88 | archive = Archive(archive_id) 89 | if len(archive.name) == 0: 90 | abort(404) 91 | return { 92 | "status": 200, 93 | "archive": { 94 | "name": archive.name, 95 | "describe": archive.describe, 96 | "count": archive.count, 97 | "id": archive.id, 98 | } 99 | } 100 | 101 | 102 | @api.route("/archive_blog_list//") 103 | @http_auth.login_required 104 | @api_role_required("ReadBlog", "api get archive blog list") 105 | def api_get_archive_blog_list(archive_id: int, page: int): 106 | blog_list = BlogArticle.get_blog_list(archive_id=archive_id, limit=20, offset=(page - 1) * 20) 107 | res = {"status": 200} 108 | res_list = [] 109 | for i in blog_list: 110 | res_list.append({ 111 | "auth": i.user.id, 112 | "title": i.title, 113 | "subtitle": i.subtitle, 114 | "update_time": datetime.timestamp(i.update_time), 115 | "create_time": datetime.timestamp(i.create_time), 116 | "top": i.top, 117 | "id": i.id, 118 | }) 119 | 120 | res["blog"] = res_list 121 | return jsonify(res) 122 | 123 | 124 | @api.route("/blog_list/") 125 | @http_auth.login_required 126 | @api_role_required("ReadBlog", "api get blog list") 127 | def api_get_blog_list(page: int): 128 | blog_list = BlogArticle.get_blog_list(limit=20, offset=(page - 1) * 20) 129 | res = {"status": 200} 130 | res_list = [] 131 | for i in blog_list: 132 | res_list.append({ 133 | "auth": i.user.id, 134 | "title": i.title, 135 | "subtitle": i.subtitle, 136 | "update_time": datetime.timestamp(i.update_time), 137 | "create_time": datetime.timestamp(i.create_time), 138 | "top": i.top, 139 | "id": i.id, 140 | }) 141 | 142 | res["blog"] = res_list 143 | return jsonify(res) 144 | 145 | 146 | 147 | @api.route("/blog/") 148 | @http_auth.login_required 149 | @api_role_required("ReadBlog", "api get blog") 150 | def api_get_blog(blog_id: int): 151 | blog = BlogArticle(blog_id) 152 | return { 153 | "status": 200, 154 | "blog": { 155 | "auth": blog.user.id, 156 | "title": blog.title, 157 | "subtitle": blog.subtitle, 158 | "update_time": datetime.timestamp(blog.update_time), 159 | "create_time": datetime.timestamp(blog.create_time), 160 | "top": blog.top, 161 | "content": blog.content, 162 | "id": blog.id, 163 | } 164 | } 165 | 166 | 167 | @api.route("/get_blog_comment/") 168 | @http_auth.login_required 169 | @api_role_required("ReadComment", "api get blog comment") 170 | def api_get_blog_comment(blog_id: int): 171 | blog = BlogArticle(blog_id) 172 | res = {"status": 200} 173 | res_list = [] 174 | for i in blog.comment: 175 | res_list.append({ 176 | "auth": i.auth.id, 177 | "update_time": datetime.timestamp(i.update_time), 178 | "id": i.id, 179 | }) 180 | 181 | res["comment"] = res_list 182 | return jsonify(res) 183 | 184 | 185 | @api.route("/comment/") 186 | @http_auth.login_required 187 | @api_role_required("ReadComment", "api get comment") 188 | def api_get_comment(comment_id: int): 189 | comment = Comment(comment_id) 190 | return { 191 | "status": 200, 192 | "blog": { 193 | "auth": comment.auth.id, 194 | "update_time": datetime.timestamp(comment.update_time), 195 | "content": comment.content, 196 | "id": comment.id, 197 | } 198 | } 199 | 200 | 201 | @api.route("/msg_list/") 202 | @http_auth.login_required 203 | @api_role_required("ReadMsg", "api get msg list") 204 | def api_get_not_secret_msg_list(page: int): 205 | msg_list = Message.get_message_list(20, (page - 1) * 20, False) 206 | res = {"status": 200} 207 | res_list = [] 208 | for i in msg_list: 209 | res_list.append({ 210 | "secret": i.secret, 211 | "auth": i.auth.id, 212 | "update_time": datetime.timestamp(i.update_time), 213 | "id": i.id, 214 | }) 215 | 216 | res["blog"] = res_list 217 | return jsonify(res) 218 | 219 | 220 | @api.route("/s_msg_list/") 221 | @http_auth.login_required 222 | @api_role_required("ReadMsg", "api get all msg secret list") 223 | @api_role_required("ReadSecretMsg", "api get all secret list") 224 | def api_get_secret_msg_list(page: int): 225 | msg_list = Message.get_message_list(20, (page - 1) * 20, request.args.get("secret", 1, type=int) != 0) 226 | res = {"status": 200} 227 | res_list = [] 228 | for i in msg_list: 229 | res_list.append({ 230 | "secret": i.secret, 231 | "auth": i.auth.id, 232 | "update_time": datetime.timestamp(i.update_time), 233 | "id": i.id, 234 | }) 235 | 236 | res["blog"] = res_list 237 | return jsonify(res) 238 | 239 | 240 | @api.route("/msg/") 241 | @http_auth.login_required 242 | @api_role_required("ReadMsg", "api get msg") 243 | def api_get_msg(msg_id: int): 244 | msg = Message(msg_id) 245 | if msg.secret: 246 | abort(404) 247 | return { 248 | "status": 200, 249 | "blog": { 250 | "auth": msg.auth.id, 251 | "update_time": datetime.timestamp(msg.update_time), 252 | "content": msg.content, 253 | "id": msg.id, 254 | } 255 | } 256 | 257 | 258 | @api.route("/s_msg/") 259 | @http_auth.login_required 260 | @api_role_required("ReadMsg", "api get secret msg") 261 | @api_role_required("ReadSecretMsg", "api get secret msg") 262 | def api_get_secret_msg(msg_id: int): 263 | msg = Message(msg_id) 264 | return { 265 | "status": 200, 266 | "blog": { 267 | "auth": msg.auth.id, 268 | "update_time": datetime.timestamp(msg.update_time), 269 | "content": msg.content, 270 | "id": msg.id, 271 | } 272 | } 273 | 274 | 275 | @api.route("/user/") 276 | @http_auth.login_required 277 | @api_role_required("ReadUserInfo", "api get user info") 278 | def api_get_user(user_id: int): 279 | user = User(user_id, is_id=True) 280 | return { 281 | "status": 200, 282 | "blog": { 283 | "role": user.role, 284 | "email": user.email, 285 | "id": user.id, 286 | } 287 | } 288 | 289 | -------------------------------------------------------------------------------- /templates/docx/article.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 文档 {% endblock %} 4 | 5 | {% block style %} 6 | {{ super() }} 7 | 8 | 9 | 10 | {% endblock %} 11 | 12 | {% block content %} 13 |
    14 | {% if current_user.check_role("ReadBlog") %} 15 | {# 检查是否具有读取权限 #} 16 |
    17 |
    18 |

    {{ article.title }} {{ article.subtitle }} {{ moment(article.update_time).format('YYYY-MM-DD HH:mm:ss') }} / {{ moment(article.create_time).format('YYYY-MM-DD HH:mm:ss') }}

    19 | 点击量: {{ article.clicks }} 20 | 21 | {% for archive in article.archive %} 22 | {{ archive.name }} 23 | {% endfor %} 24 | 下载 25 |
    26 | 27 |
    28 | {% if can_update %} 29 | {{ view.hidden_tag() }} 30 | {{ view.blog_id() }} 31 | {% endif %} 32 |
    33 | {{ view.content(class="form-control mb-2", style="display:none;") }} 34 |
    35 | {% for error in view.content.errors %} 36 |
    {{ error }}
    37 | {% endfor %} 38 | 39 | {% if can_update %} 40 | 56 | {% endif %} 57 |
    58 | {% if can_update %} 59 | 80 | 81 |
    82 |
    83 | 更新博文 84 | {% if article.top %} 85 | 取消置顶 86 | {% else %} 87 | 置顶文章 88 | {% endif %} 89 | 更新归档 90 |
    91 |
    92 | {% endif %} 93 |
    94 |
    95 | {% endif %} 96 | 97 |
    98 |
    99 | {% if current_user.check_role("WriteComment") %} 100 |
    101 |
    102 | {{ form.hidden_tag() }} 103 |
    104 | {{ form.content(class="form-control mb-2", rows="3") }} 105 | {% for error in form.content.errors %} 106 |
    {{ error }}
    107 | {% endfor %} 108 |
    109 | 110 | 126 | 127 | 128 |
    129 |
    130 |
    131 | {% endif %} 132 | 133 | {% if current_user.check_role("ReadComment") %} 134 | {% for comment in article.comment %} 135 | {{ render_comment(comment, show_delete) }} 136 | {% endfor %} 137 | {% endif %} 138 |
    139 |
    140 |
    141 | {% endblock %} 142 | 143 | {% block javascript %} 144 | {{ super() }} 145 | 146 | 147 | 148 | 149 | 178 | {% endblock %} -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% import "macro.html" as macro %} 2 | 3 | {% macro render_docx_color(blog, color, show_delete) %} 4 | {# 使用到moment, 不能放进macro #} 5 | {% if show_delete %} 6 | 23 | {% endif %} 24 | 25 |
    26 |
    27 | {{ blog.title }} 28 | {% if blog.subtitle %} 29 |
    30 | {{ blog.subtitle }} 31 | {% endif %} 32 |
    33 |
    34 | Update Date: {{ moment(blog.update_time).format('YYYY-MM-DD HH:mm:ss') }} 35 |
    36 | Create Date: {{ moment(blog.create_time).format('YYYY-MM-DD HH:mm:ss') }} 37 | 38 |
    39 | {% if show_delete %} 40 | 删除 42 | {% endif %} 43 | 44 | 前往 45 |
    46 |
    47 |
    48 | {% endmacro %} 49 | 50 | {% macro render_docx(blog, show_delete) %} 51 | {# 使用到moment, 不能放进macro #} 52 | {{ render_docx_color(blog, "bg-primary", show_delete) }} 53 | {% endmacro %} 54 | 55 | {% macro render_docx_top(blog, show_delete) %} 56 | {# 使用到moment, 不能放进macro #} 57 | {{ render_docx_color(blog, "bg-dark", show_delete) }} 58 | {% endmacro %} 59 | 60 | {% macro render_msg(msg, shod_delete)%} 61 | {# 使用到moment, 不能放进macro #} 62 | {% if show_delete %} 63 | 80 | {% endif %} 81 | 82 |
    83 |
    84 | {% if show_email %} {# 判断是否可读取用户信息 #} 85 | {{ msg.auth.email }} 86 | {% else %} 87 | {{ msg.auth.star_email }} 88 | {% endif %} 89 |
    90 |
    91 |

    {{ msg.content.replace('\n', '
    ') | safe }}

    92 |
    93 | {% if show_delete %} 94 | 删除 96 | {% endif %} 97 | 98 | {% if msg.secret %} 99 | [私] 100 | {% endif %} 101 | 102 | {{ moment(msg.update_time).fromNow(refresh=True) }} 103 |
    104 |
    105 |
    106 | {% endmacro %} 107 | 108 | {% macro render_comment(comment, shod_delete)%} 109 | {# 使用到moment, 不能放进macro #} 110 | {% if show_delete %} 111 | 128 | {% endif %} 129 | 130 |
    {# 此处使用上下间距 #} 131 |
    132 | {% if show_email %} {# 判断是否可读取用户信息 #} 133 | {{ comment.auth.email }} 134 | {% else %} 135 | {{ comment.auth.star_email }} 136 | {% endif %} 137 |
    138 |
    139 |

    {{ comment.content.replace('\n', '
    ') | safe }}

    140 |
    141 | {% if show_delete %} 142 | 删除 144 | {% endif %} 145 | {{ moment(comment.update_time).fromNow(refresh=True) }} 146 |
    147 |
    148 |
    149 | {% endmacro %} 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | {% block icon %} 158 | 159 | {% endblock %} 160 | 161 | {% block font %} 162 | 163 | 164 | 165 | {% endblock %} 166 | 167 | {% block style %} 168 | 169 | 170 | 178 | {% endblock %} 179 | 180 | {% block javascript %} 181 | 182 | {{ moment.include_moment() }} 183 | {{ moment.lang("zh-CN") }} 184 | {% endblock %} 185 | 186 | {% block title %} {% endblock %} - {{ blog_name }} 187 | 188 | 189 | {% block nav %} 190 |

    《{{ blog_name }}》—— {{ blog_describe }}

    191 | 192 |
    193 | 240 | 241 |
    242 | {% for message in get_flashed_messages() %} 243 |
    244 | 245 | {{ message }} 246 |
    247 | {% endfor %} 248 |
    249 |
    250 | {% endblock %} 251 | 252 |
    253 | {% block content %} {% endblock %} 254 |
    255 | 256 | {% block footer %} 257 | {# footer 最后加载 #} 258 | 275 | 299 | {% endblock %} 300 | 301 | {% block javascript_foot %} {% endblock %} 302 | 303 | 304 | -------------------------------------------------------------------------------- /app/auth.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, redirect, flash, url_for, request, abort, current_app, g 2 | from flask_login import login_required, login_user, current_user, logout_user 3 | from flask_wtf import FlaskForm 4 | from wtforms import (EmailField, 5 | StringField, 6 | PasswordField, 7 | BooleanField, 8 | SelectMultipleField, 9 | SelectField, 10 | SubmitField, 11 | ValidationError) 12 | from wtforms.validators import DataRequired, Length, Regexp, EqualTo 13 | from urllib.parse import urljoin 14 | 15 | import app 16 | from object.user import User 17 | from send_email import send_msg 18 | from configure import conf 19 | 20 | auth = Blueprint("auth", __name__) 21 | 22 | 23 | class AuthField(FlaskForm): 24 | @staticmethod 25 | def email_field(name: str, description: str): 26 | """ 提前定义 email 字段的生成函数,供下文调用 """ 27 | return EmailField(name, description=description, 28 | validators=[ 29 | DataRequired(f"必须填写{name}"), 30 | Length(1, 32, message=f"{name}长度1-32个字符"), 31 | Regexp(r"^[a-zA-Z0-9_\.\-]+@[a-zA-Z0-9_\-]+(\.[a-zA-Z0-9_\.]+)+$", 32 | message=f"{name}不满足正则表达式")]) 33 | 34 | @staticmethod 35 | def passwd_field(name: str, description: str): 36 | """ 提前定义 passwd 字段的生成函数,供下文调用 """ 37 | return PasswordField(name, description=description, 38 | validators=[ 39 | DataRequired(f"必须填写{name}"), 40 | Length(8, 32, message=f"{name}长度为8-32位")]) 41 | 42 | @staticmethod 43 | def passwd_again_field(name: str, description: str, passwd: str = "passwd"): 44 | """ 提前定义 passwd again 字段的生成函数,供下文调用 """ 45 | return PasswordField(f"重复{name}", description=description, 46 | validators=[ 47 | DataRequired(message=f"必须再次填写{name}"), 48 | EqualTo(passwd, message=f"两次输入的{name}不相同")]) 49 | 50 | 51 | class EmailPasswd(AuthField): 52 | email = AuthField.email_field("邮箱", "用户邮箱") 53 | passwd = AuthField.passwd_field("密码", "用户密码") 54 | 55 | 56 | class LoginForm(EmailPasswd): 57 | remember = BooleanField("记住我") 58 | submit = SubmitField("登录") 59 | 60 | 61 | class RegisterForm(EmailPasswd): 62 | passwd_again = AuthField.passwd_again_field("密码", "用户密码") 63 | submit = SubmitField("注册") 64 | 65 | def validate_email(self, field): 66 | """ 检验email是否合法 """ 67 | if User(field.data).info[2] != -1: 68 | raise ValidationError("邮箱已被注册") 69 | 70 | 71 | class ChangePasswdForm(AuthField): 72 | old_passwd = AuthField.passwd_field("旧密码", "用户原密码") 73 | passwd = AuthField.passwd_field("新密码", "用户新密码") 74 | passwd_again = AuthField.passwd_again_field("新密码", "用户新密码") 75 | submit = SubmitField("修改密码") 76 | 77 | def validate_passwd(self, field): 78 | """ 检验新旧密码是否相同 """ 79 | if field.data == self.old_passwd.data: 80 | raise ValidationError("新旧密码不能相同") 81 | 82 | 83 | class DeleteUserForm(AuthField): 84 | email = AuthField.email_field("邮箱", "用户邮箱") 85 | submit = SubmitField("删除用户") 86 | 87 | def __init__(self): 88 | super(DeleteUserForm, self).__init__() 89 | self.email_user = None 90 | 91 | def validate_email(self, field): 92 | """ 检验用户是否存在 """ 93 | if User(field.data).info[2] == -1: 94 | raise ValidationError("邮箱用户不存在") 95 | 96 | 97 | class CreateRoleForm(AuthField): 98 | name = StringField("角色名称", validators=[DataRequired()]) 99 | authority = SelectMultipleField("权限", coerce=str, choices=User.RoleAuthorize) 100 | submit = SubmitField("创建角色") 101 | 102 | 103 | class RoleForm(AuthField): 104 | name = SelectField("角色名称", validators=[DataRequired()], coerce=int) 105 | 106 | def __init__(self): 107 | super(RoleForm, self).__init__() 108 | self.name_res = [] 109 | self.name_choices = [] 110 | for i in User.get_role_list(): 111 | self.name_res.append(i[0]) 112 | self.name_choices.append((i[0], i[1])) 113 | self.name.choices = self.name_choices 114 | 115 | def validate_name(self, field): 116 | """ 检验角色是否存在 """ 117 | if field.data not in self.name_res: 118 | raise ValidationError("角色不存在") 119 | 120 | 121 | class DeleteRoleForm(RoleForm): 122 | submit = SubmitField("删除角色") 123 | 124 | 125 | class SetRoleForm(RoleForm): 126 | email = AuthField.email_field("邮箱", "用户邮箱") 127 | submit = SubmitField("设置角色") 128 | 129 | def __init__(self): 130 | super(SetRoleForm, self).__init__() 131 | self.email_user = None 132 | 133 | def validate_email(self, field): 134 | if User(field.data).info[2] == -1: 135 | raise ValidationError("邮箱用户不存在") 136 | 137 | 138 | @auth.route('/user/yours') 139 | @login_required 140 | def yours_page(): 141 | msg_count, comment_count, blog_count = current_user.count 142 | app.HBlogFlask.print_load_page_log("user info") 143 | return render_template("auth/yours.html", msg_count=msg_count, comment_count=comment_count, blog_count=blog_count) 144 | 145 | 146 | @auth.route('/user/login', methods=["GET", "POST"]) 147 | def login_page(): 148 | if current_user.is_authenticated: # 用户已经成功登陆 149 | app.HBlogFlask.print_user_not_allow_opt_log("login") 150 | return redirect(url_for("auth.yours_page")) 151 | 152 | form = LoginForm() 153 | if form.validate_on_submit(): 154 | user = User(form.email.data) 155 | if user.info[2] != -1 and user.check_passwd(form.passwd.data): 156 | login_user(user, form.remember.data) 157 | next_page = request.args.get("next", None, type=str) 158 | if next_page is None or not next_page.startswith('/'): 159 | next_page = url_for('base.index_page') 160 | flash("登陆成功") 161 | app.HBlogFlask.print_user_opt_success_log(f"login {form.email.data}") 162 | return redirect(next_page) 163 | flash("账号或密码错误") 164 | app.HBlogFlask.print_user_opt_fail_log(f"login {form.email.data}") 165 | return redirect(url_for("auth.login_page")) 166 | app.HBlogFlask.print_load_page_log("user login") 167 | return render_template("auth/login.html", form=form) 168 | 169 | 170 | @auth.route('/user/register', methods=["GET", "POST"]) 171 | def register_page(): 172 | if current_user.is_authenticated: 173 | app.HBlogFlask.print_user_not_allow_opt_log("register") 174 | return redirect(url_for("auth.yours_page")) 175 | 176 | form = RegisterForm() 177 | if form.validate_on_submit(): 178 | token = User.creat_token(form.email.data, form.passwd.data) 179 | register_url = urljoin(request.host_url, url_for("auth.confirm_page", token=token)) 180 | hblog: app.Hblog = current_app 181 | send_msg("注册确认", hblog.mail, form.email.data, "register", register_url=register_url) 182 | flash("注册提交成功, 请进入邮箱点击确认注册链接") 183 | app.HBlogFlask.print_import_user_opt_success_log(f"register {form.email.data}") 184 | return redirect(url_for("base.index_page")) 185 | app.HBlogFlask.print_load_page_log("user register") 186 | return render_template("auth/register.html", RegisterForm=form) 187 | 188 | 189 | @auth.route('/user/confirm') 190 | def confirm_page(): 191 | token = request.args.get("token", None, type=str) 192 | if token is None: 193 | app.HBlogFlask.print_user_opt_fail_log(f"Confirm (bad token)") 194 | abort(404) 195 | return 196 | 197 | token = User.load_token(token) 198 | if token is None: 199 | app.HBlogFlask.print_user_opt_fail_log(f"Confirm (bad token)") 200 | abort(404) 201 | return 202 | 203 | if User(token[0]).info[2] != -1: 204 | app.HBlogFlask.print_user_opt_fail_log(f"Confirm (bad token)") 205 | abort(404) 206 | return 207 | 208 | User.create(token[0], token[1]) 209 | current_app.logger.info(f"{token[0]} confirm success") 210 | app.HBlogFlask.print_import_user_opt_success_log(f"confirm {token[0]}") 211 | flash(f"用户{token[0]}认证完成") 212 | return redirect(url_for("base.index_page")) 213 | 214 | 215 | @auth.route('/user/logout') 216 | @login_required 217 | def logout_page(): 218 | app.HBlogFlask.print_import_user_opt_success_log(f"logout") 219 | logout_user() 220 | flash("退出登录成功") 221 | return redirect(url_for("base.index_page")) 222 | 223 | 224 | @auth.route('/user/set/passwd', methods=['GET', 'POST']) 225 | @login_required 226 | def change_passwd_page(): 227 | form = ChangePasswdForm() 228 | if form.validate_on_submit(): 229 | if not current_user.check_passwd(form.old_passwd.data): 230 | app.HBlogFlask.print_user_opt_error_log(f"change passwd") 231 | flash("旧密码错误") 232 | elif current_user.change_passwd(form.passwd.data): 233 | app.HBlogFlask.print_user_opt_success_log(f"change passwd") 234 | flash("密码修改成功") 235 | logout_user() 236 | return redirect(url_for("auth.login_page")) 237 | else: 238 | app.HBlogFlask.print_user_opt_error_log(f"change passwd") 239 | flash("密码修改失败") 240 | return redirect(url_for("auth.change_passwd_page")) 241 | app.HBlogFlask.print_load_page_log("user change passwd") 242 | return render_template("auth/passwd.html", ChangePasswdForm=form) 243 | 244 | 245 | @auth.route('/user/delete', methods=['GET', 'POST']) 246 | @login_required 247 | @app.role_required("DeleteUser", "delete user") 248 | def delete_user_page(): 249 | form = DeleteUserForm() 250 | if form.validate_on_submit(): 251 | user = form.email_user 252 | if user.delete(): 253 | app.HBlogFlask.print_sys_opt_success_log(f"{current_user.email} delete user {form.email.data} success") 254 | flash("用户删除成功") 255 | else: 256 | app.HBlogFlask.print_sys_opt_fail_log(f"{current_user.email} delete user {form.email.data} fail") 257 | flash("用户删除失败") 258 | return redirect(url_for("auth.delete_user_page")) 259 | app.HBlogFlask.print_load_page_log("delete user") 260 | return render_template("auth/delete.html", DeleteUserForm=form) 261 | 262 | 263 | @auth.route('/role', methods=['GET']) 264 | @login_required 265 | @app.role_required("ConfigureSystem", "load role setting") 266 | def role_page(): 267 | app.HBlogFlask.print_load_page_log("role setting") 268 | return render_template("auth/role.html", 269 | CreateRoleForm=CreateRoleForm(), 270 | DeleteRoleForm=DeleteRoleForm(), 271 | SetRoleForm=SetRoleForm()) 272 | 273 | 274 | @auth.route('/role/create', methods=['POST']) 275 | @login_required 276 | @app.form_required(CreateRoleForm, "create role") 277 | @app.role_required("ConfigureSystem", "create role") 278 | def role_create_page(): 279 | form: CreateRoleForm = g.form 280 | name = form.name.data 281 | if User.create_role(name, form.authority.data): 282 | app.HBlogFlask.print_sys_opt_success_log(f"Create role success: {name}") 283 | flash("角色创建成功") 284 | else: 285 | app.HBlogFlask.print_sys_opt_success_log(f"Create role fail: {name}") 286 | flash("角色创建失败") 287 | return redirect(url_for("auth.role_page")) 288 | 289 | 290 | @auth.route('/role/delete', methods=['POST']) 291 | @login_required 292 | @app.form_required(DeleteRoleForm, "delete role") 293 | @app.role_required("ConfigureSystem", "delete role") 294 | def role_delete_page(): 295 | form: DeleteRoleForm = g.form 296 | if User.delete_role(form.name.data): 297 | app.HBlogFlask.print_sys_opt_success_log(f"Delete role success: {form.name.data}") 298 | flash("角色删除成功") 299 | else: 300 | app.HBlogFlask.print_sys_opt_fail_log(f"Delete role fail: {form.name.data}") 301 | flash("角色删除失败") 302 | return redirect(url_for("auth.role_page")) 303 | 304 | 305 | @auth.route('/role/set', methods=['POST']) 306 | @login_required 307 | @app.form_required(SetRoleForm, "assign user a role") 308 | @app.role_required("ConfigureSystem", "assign user a role") 309 | def role_set_page(): 310 | form: SetRoleForm = g.form 311 | user = form.email_user 312 | if user.set_user_role(form.name.data): 313 | app.HBlogFlask.print_sys_opt_success_log(f"Role assign {form.email.data} -> {form.name.data}") 314 | flash("角色设置成功") 315 | else: 316 | app.HBlogFlask.print_sys_opt_fail_log(f"Role assign {form.email.data} -> {form.name.data}") 317 | flash("角色设置失败") 318 | return redirect(url_for("auth.role_page")) 319 | 320 | 321 | @auth.context_processor 322 | @app.cache.cached(timeout=conf["CACHE_EXPIRE"], key_prefix="inject_base:auth") 323 | def inject_base(): 324 | """ auth 默认模板变量 """ 325 | return {"top_nav": ["", "", "", "", "", "active"]} 326 | -------------------------------------------------------------------------------- /sql/cache.py: -------------------------------------------------------------------------------- 1 | from sql import cache, DB 2 | from sql.base import DBBit 3 | from configure import conf 4 | 5 | from redis import RedisError 6 | from functools import wraps 7 | from datetime import datetime 8 | 9 | CACHE_TIME = int(conf["CACHE_EXPIRE"]) 10 | CACHE_PREFIX = conf["CACHE_PREFIX"] 11 | 12 | 13 | def __try_redis(ret=None): 14 | def try_redis(func): 15 | @wraps(func) 16 | def try_func(*args, **kwargs): 17 | try: 18 | res = func(*args, **kwargs) 19 | except RedisError: 20 | cache.logger.error(f"Redis error with {args} {kwargs}", exc_info=True, stack_info=True) 21 | return ret 22 | return res 23 | 24 | return try_func 25 | 26 | return try_redis 27 | 28 | 29 | @__try_redis(None) 30 | def get_msg_from_cache(msg_id: int): 31 | msg = cache.hgetall(f"{CACHE_PREFIX}:msg:{msg_id}") 32 | if len(msg) != 4: 33 | return None 34 | return [msg.get("Email", ""), 35 | msg.get("Content"), 36 | datetime.fromtimestamp(float(msg.get("UpdateTime", 0.0))), 37 | msg.get("Secret", "False") == "True"] 38 | 39 | 40 | @__try_redis(None) 41 | def write_msg_to_cache(msg_id: int, email: str, content: str, update_time: str | datetime, secret: bool, 42 | is_db_bit=False): 43 | cache_name = f"{CACHE_PREFIX}:msg:{msg_id}" 44 | cache.delete(cache_name) 45 | cache.hset(cache_name, mapping={ 46 | "Email": email, 47 | "Content": content, 48 | "UpdateTime": datetime.timestamp(update_time), 49 | "Secret": str(secret == DBBit.BIT_1 if is_db_bit else secret) 50 | }) 51 | cache.expire(cache_name, CACHE_TIME) 52 | 53 | 54 | @__try_redis(None) 55 | def delete_msg_from_cache(msg_id: int): 56 | cache.delete(f"{CACHE_PREFIX}:msg:{msg_id}") 57 | 58 | 59 | @__try_redis(None) 60 | def get_msg_cout_from_cache(): 61 | count = cache.get(f"{CACHE_PREFIX}:msg_count") 62 | if count is not None: 63 | return int(count) 64 | 65 | 66 | @__try_redis(None) 67 | def write_msg_count_to_cache(count): 68 | count = cache.set(f"{CACHE_PREFIX}:msg_count", str(count)) 69 | cache.expire(f"{CACHE_PREFIX}:msg_count", CACHE_TIME) 70 | return count 71 | 72 | 73 | @__try_redis(None) 74 | def delete_msg_count_from_cache(): 75 | cache.delete(f"{CACHE_PREFIX}:msg_count") 76 | 77 | 78 | @__try_redis(None) 79 | def get_user_msg_count_from_cache(user_id: int): 80 | count = cache.get(f"{CACHE_PREFIX}:msg_count:{user_id}") 81 | if count is not None: 82 | return int(count) 83 | 84 | 85 | @__try_redis(None) 86 | def write_user_msg_count_to_cache(user_id, count): 87 | cache_name = f"{CACHE_PREFIX}:msg_count:{user_id}" 88 | count = cache.set(cache_name, str(count)) 89 | cache.expire(cache_name, CACHE_TIME) 90 | return count 91 | 92 | 93 | @__try_redis(None) 94 | def delete_user_msg_count_from_cache(user_id): 95 | cache.delete(f"{CACHE_PREFIX}:msg_count:{user_id}") 96 | 97 | 98 | @__try_redis(None) 99 | def delete_all_user_msg_count_from_cache(): 100 | for i in cache.keys(f"{CACHE_PREFIX}:msg_count:*"): 101 | cache.delete(i) 102 | 103 | 104 | @__try_redis(None) 105 | def get_blog_from_cache(blog_id: int): 106 | blog = cache.hgetall(f"{CACHE_PREFIX}:blog:{blog_id}") 107 | if len(blog) != 7: 108 | return None 109 | return [int(blog.get("Auth", -1)), 110 | blog.get("Title"), 111 | blog.get("SubTitle"), 112 | blog.get("Content"), 113 | datetime.fromtimestamp(float(blog.get("UpdateTime", 0.0))), 114 | datetime.fromtimestamp(float(blog.get("CreateTime", 0.0))), 115 | blog.get("Top", "False") == "True"] 116 | 117 | 118 | @__try_redis(None) 119 | def write_blog_to_cache(blog_id: int, auth_id: str, title: str, subtitle: str, content: str, 120 | update_time: str | datetime, create_time: str | datetime, top: bool, is_db_bit=False): 121 | cache_name = f"{CACHE_PREFIX}:blog:{blog_id}" 122 | cache.delete(cache_name) 123 | cache.hset(cache_name, mapping={ 124 | "Auth": auth_id, 125 | "Title": title, 126 | "SubTitle": subtitle, 127 | "Content": content, 128 | "UpdateTime": datetime.timestamp(update_time), 129 | "CreateTime": datetime.timestamp(create_time), 130 | "Top": str(top == DBBit.BIT_1 if is_db_bit else top) 131 | }) 132 | cache.expire(cache_name, CACHE_TIME) 133 | 134 | 135 | @__try_redis(None) 136 | def delete_blog_from_cache(blog_id: int): 137 | cache.delete(f"{CACHE_PREFIX}:blog:{blog_id}") 138 | 139 | 140 | @__try_redis(None) 141 | def get_blog_count_from_cache(): 142 | count = cache.get(f"{CACHE_PREFIX}:blog_count") 143 | if count is not None: 144 | return int(count) 145 | 146 | 147 | @__try_redis(None) 148 | def write_blog_count_to_cache(count): 149 | count = cache.set(f"{CACHE_PREFIX}:blog_count", str(count)) 150 | cache.expire(f"{CACHE_PREFIX}:blog_count", CACHE_TIME) 151 | return count 152 | 153 | 154 | @__try_redis(None) 155 | def delete_blog_count_from_cache(): 156 | cache.delete(f"{CACHE_PREFIX}:blog_count") 157 | 158 | 159 | @__try_redis(None) 160 | def get_archive_blog_count_from_cache(archive_id: int): 161 | count = cache.get(f"{CACHE_PREFIX}:blog_count:archive:{archive_id}") 162 | if count is not None: 163 | return int(count) 164 | 165 | 166 | @__try_redis(None) 167 | def write_archive_blog_count_to_cache(archive_id, count): 168 | cache_name = f"{CACHE_PREFIX}:blog_count:archive:{archive_id}" 169 | count = cache.set(cache_name, str(count)) 170 | cache.expire(cache_name, CACHE_TIME) 171 | return count 172 | 173 | 174 | @__try_redis(None) 175 | def delete_all_archive_blog_count_from_cache(): 176 | for i in cache.keys(f"{CACHE_PREFIX}:blog_count:archive:*"): 177 | cache.delete(i) 178 | 179 | 180 | @__try_redis(None) 181 | def delete_archive_blog_count_from_cache(archive_id: int): 182 | cache.delete(f"{CACHE_PREFIX}:blog_count:archive:{archive_id}") 183 | 184 | 185 | @__try_redis(None) 186 | def get_user_blog_count_from_cache(user_id: int): 187 | count = cache.get(f"{CACHE_PREFIX}:blog_count:user:{user_id}") 188 | if count is not None: 189 | return int(count) 190 | 191 | 192 | @__try_redis(None) 193 | def write_user_blog_count_to_cache(user_id, count): 194 | cache_name = f"{CACHE_PREFIX}:blog_count:user:{user_id}" 195 | count = cache.set(cache_name, str(count)) 196 | cache.expire(cache_name, CACHE_TIME) 197 | return count 198 | 199 | 200 | @__try_redis(None) 201 | def delete_all_user_blog_count_from_cache(): 202 | for i in cache.keys(f"{CACHE_PREFIX}:blog_count:user:*"): 203 | cache.delete(i) 204 | 205 | 206 | @__try_redis(None) 207 | def delete_user_blog_count_from_cache(user_id: int): 208 | cache.delete(f"{CACHE_PREFIX}:blog_count:user:{user_id}") 209 | 210 | 211 | @__try_redis(None) 212 | def get_archive_from_cache(archive_id: int): 213 | archive = cache.hgetall(f"{CACHE_PREFIX}:archive:{archive_id}") 214 | if len(archive) != 2: 215 | return None 216 | return [archive.get("Name", ""), archive.get("DescribeText")] 217 | 218 | 219 | @__try_redis(None) 220 | def write_archive_to_cache(archive_id: int, name: str, describe: str): 221 | cache_name = f"{CACHE_PREFIX}:archive:{archive_id}" 222 | cache.delete(cache_name) 223 | cache.hset(cache_name, mapping={ 224 | "Name": name, 225 | "DescribeText": describe, 226 | }) 227 | cache.expire(cache_name, CACHE_TIME) 228 | 229 | 230 | @__try_redis(None) 231 | def delete_archive_from_cache(archive_id: int): 232 | cache.delete(f"{CACHE_PREFIX}:archive:{archive_id}") 233 | 234 | 235 | @__try_redis(None) 236 | def get_blog_archive_from_cache(blog_id: int): 237 | blog_archive = cache.lrange(f"{CACHE_PREFIX}:blog_archive:{blog_id}", 0, -1) 238 | if len(blog_archive) == 0: 239 | return None 240 | elif blog_archive[0] == '-1': 241 | return [] 242 | return blog_archive 243 | 244 | 245 | @__try_redis(None) 246 | def write_blog_archive_to_cache(blog_id: int, archive): 247 | cache_name = f"{CACHE_PREFIX}:blog_archive:{blog_id}" 248 | cache.delete(cache_name) 249 | if len(archive) == 0: 250 | cache.rpush(cache_name, -1) 251 | else: 252 | cache.rpush(cache_name, *archive) 253 | cache.expire(cache_name, CACHE_TIME) 254 | 255 | 256 | @__try_redis(None) 257 | def delete_blog_archive_from_cache(blog_id: int): 258 | cache.delete(f"{CACHE_PREFIX}:blog_archive:{blog_id}") 259 | 260 | 261 | @__try_redis(None) 262 | def delete_all_blog_archive_from_cache(): 263 | for i in cache.keys(f"{CACHE_PREFIX}:blog_archive:*"): 264 | cache.delete(i) 265 | 266 | 267 | @__try_redis(None) 268 | def get_comment_from_cache(comment_id: int): 269 | comment = cache.hgetall(f"{CACHE_PREFIX}:comment:{comment_id}") 270 | if len(comment) != 4: 271 | return None 272 | return [comment.get("BlogID", ""), 273 | comment.get("Email", ""), 274 | comment.get("Content", ""), 275 | datetime.fromtimestamp(float(comment.get("UpdateTime", 0.0)))] 276 | 277 | 278 | @__try_redis(None) 279 | def write_comment_to_cache(comment_id: int, blog_id: str, email: str, content: str, update_time: str | datetime): 280 | cache_name = f"{CACHE_PREFIX}:comment:{comment_id}" 281 | cache.delete(cache_name) 282 | cache.hset(cache_name, mapping={ 283 | "BlogID": blog_id, 284 | "Email": email, 285 | "Content": content, 286 | "UpdateTime": datetime.timestamp(update_time) 287 | }) 288 | cache.expire(cache_name, CACHE_TIME) 289 | 290 | 291 | @__try_redis(None) 292 | def delete_comment_from_cache(comment_id: int): 293 | cache.delete(f"{CACHE_PREFIX}:comment:{comment_id}") 294 | 295 | 296 | @__try_redis(None) 297 | def get_user_comment_count_from_cache(user_id: int): 298 | count = cache.get(f"{CACHE_PREFIX}:comment_count:{user_id}") 299 | if count is not None: 300 | return int(count) 301 | 302 | 303 | @__try_redis(None) 304 | def write_user_comment_count_to_cache(user_id, count): 305 | cache_name = f"{CACHE_PREFIX}:comment_count:{user_id}" 306 | count = cache.set(cache_name, str(count)) 307 | cache.expire(cache_name, CACHE_TIME) 308 | return count 309 | 310 | 311 | @__try_redis(None) 312 | def delete_user_comment_count_from_cache(user_id: int): 313 | cache.delete(f"{CACHE_PREFIX}:comment_count:{user_id}") 314 | 315 | 316 | @__try_redis(None) 317 | def delete_all_user_comment_count_from_cache(): 318 | for i in cache.keys(f"{CACHE_PREFIX}:comment_count:*"): 319 | cache.delete(i) 320 | 321 | 322 | @__try_redis(None) 323 | def get_user_from_cache(email: str): 324 | user = cache.hgetall(f"{CACHE_PREFIX}:user:{email}") 325 | if len(user) != 3: 326 | return None 327 | return [user.get("PasswdHash", ""), 328 | int(user.get("Role", "")), 329 | int(user.get("ID", ""))] 330 | 331 | 332 | @__try_redis(None) 333 | def write_user_to_cache(email: str, passwd_hash: str, role: int, user_id: int): 334 | cache_name = f"{CACHE_PREFIX}:user:{email}" 335 | cache.delete(cache_name) 336 | cache.hset(cache_name, mapping={ 337 | "PasswdHash": passwd_hash, 338 | "Role": role, 339 | "ID": user_id, 340 | }) 341 | cache.expire(cache_name, CACHE_TIME) 342 | 343 | 344 | @__try_redis(None) 345 | def delete_user_from_cache(email: str): 346 | cache.delete(f"{CACHE_PREFIX}:user:{email}") 347 | 348 | 349 | @__try_redis(None) 350 | def get_user_email_from_cache(user_id: int): 351 | email = cache.get(f"{CACHE_PREFIX}:user_email:{user_id}") 352 | if email is None or len(email) == 0: 353 | return None 354 | return email 355 | 356 | 357 | @__try_redis(None) 358 | def write_user_email_to_cache(user_id: int, email: str): 359 | cache_name = f"{CACHE_PREFIX}:user_email:{user_id}" 360 | cache.set(cache_name, email) 361 | cache.expire(cache_name, CACHE_TIME) 362 | 363 | 364 | @__try_redis(None) 365 | def delete_user_email_from_cache(user_id: int): 366 | cache.delete(f"{CACHE_PREFIX}:user_email:{user_id}") 367 | 368 | 369 | @__try_redis(None) 370 | def get_role_name_from_cache(role_id: int): 371 | role_name = cache.get(f"{CACHE_PREFIX}:role_name:{role_id}") 372 | if role_name is None or len(role_name) == 0: 373 | return None 374 | return role_name 375 | 376 | 377 | @__try_redis(None) 378 | def write_role_name_to_cache(role_id: int, name: str): 379 | cache_name = f"{CACHE_PREFIX}:role_name:{role_id}" 380 | cache.set(cache_name, name) 381 | cache.expire(cache_name, CACHE_TIME) 382 | 383 | 384 | @__try_redis(None) 385 | def delete_role_name_from_cache(role_id: int): 386 | cache.delete(f"{CACHE_PREFIX}:role_name:{role_id}") 387 | 388 | 389 | def get_role_operate_from_cache(role_id: int, operate: str): 390 | res = cache.get(f"{CACHE_PREFIX}:operate:{role_id}:{operate}") 391 | if res is None or len(res) == 0: 392 | return None 393 | return res == "True" 394 | 395 | 396 | @__try_redis(None) 397 | def write_role_operate_to_cache(role_id: int, operate: str, res: bool): 398 | cache_name = f"{CACHE_PREFIX}:operate:{role_id}:{operate}" 399 | cache.set(cache_name, str(res)) 400 | cache.expire(cache_name, CACHE_TIME) 401 | 402 | 403 | @__try_redis(None) 404 | def delete_role_operate_from_cache(role_id: int): 405 | for i in cache.keys(f"{CACHE_PREFIX}:operate:{role_id}:*"): 406 | cache.delete(i) 407 | 408 | 409 | @__try_redis(None) 410 | def restart_clear_cache(): 411 | """ 412 | 重启服务时必须要清理的缓存 413 | 包括Hblog-Cache和Flask-Cache 414 | """ 415 | 416 | # 删除全部Flask缓存 417 | for i in cache.keys("flask_cache:*"): 418 | cache.delete(i) 419 | -------------------------------------------------------------------------------- /app/docx.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, abort, redirect, url_for, flash, make_response, g, request 2 | from flask_wtf import FlaskForm 3 | from flask_login import login_required, current_user 4 | from wtforms import HiddenField, TextAreaField, StringField, SelectMultipleField, SubmitField, ValidationError 5 | from wtforms.validators import DataRequired, Length 6 | from typing import Optional 7 | 8 | import app 9 | from sql.base import DBBit 10 | from sql.statistics import add_blog_click, add_archive_click 11 | from object.blog import BlogArticle 12 | from object.comment import Comment 13 | from object.archive import Archive 14 | from configure import conf 15 | 16 | docx = Blueprint("docx", __name__) 17 | 18 | 19 | class EditorMD(FlaskForm): 20 | content = TextAreaField("博客内容", validators=[DataRequired(message="必须输入博客文章")]) 21 | 22 | 23 | class WriteBlogForm(EditorMD): 24 | title = StringField("标题", description="博文主标题", 25 | validators=[ 26 | DataRequired(message="必须填写标题"), 27 | Length(1, 20, message="标题长度1-20个字符")]) 28 | subtitle = StringField("副标题", description="博文副标题", 29 | validators=[Length(-1, 20, message="副标题长度20个字符以内")]) 30 | archive = SelectMultipleField("归档", coerce=int) 31 | submit = SubmitField("提交博客") 32 | 33 | def __init__(self, default: bool = False, **kwargs): 34 | super().__init__(**kwargs) 35 | if default: 36 | self.content.data = "# Blog Title\n## Blog subtitle\nHello, World" 37 | archive = Archive.get_archive_list() 38 | self.archive_res = [] 39 | self.archive_choices = [(-1, "None")] 40 | for i in archive: 41 | self.archive_res.append(i.id) 42 | self.archive_choices.append((i.id, f"{i.name} ({i.count})")) 43 | self.archive.choices = self.archive_choices 44 | 45 | def validate_archive(self, field): 46 | if -1 in field.data: 47 | if len(field.data) != 1: 48 | raise ValidationError("归档指定错误(none归档不能和其他归档同时被指定)") 49 | else: 50 | for i in field.data: 51 | if i not in self.archive_res: 52 | raise ValidationError("错误的归档被指定") 53 | 54 | 55 | class UpdateBlogForm(EditorMD): 56 | blog_id = HiddenField("ID", validators=[DataRequired()]) 57 | submit = SubmitField("更新博客") 58 | 59 | def __init__(self, blog: Optional[BlogArticle] = None, **kwargs): 60 | super().__init__(**kwargs) 61 | if blog is not None: 62 | self.blog_id.data = blog.id 63 | self.content.data = blog.content 64 | 65 | 66 | class UpdateBlogArchiveForm(FlaskForm): 67 | blog_id = HiddenField("ID", validators=[DataRequired()]) 68 | archive = SelectMultipleField("归档", coerce=int) 69 | add = SubmitField("加入归档") 70 | sub = SubmitField("去除归档") 71 | 72 | def __init__(self, blog: Optional[BlogArticle] = None, **kwargs): 73 | super().__init__(**kwargs) 74 | archive = Archive.get_archive_list() 75 | self.archive_res = [] 76 | self.archive_choices = [] 77 | for i in archive: 78 | archive_id = i.id 79 | self.archive_res.append(archive_id) 80 | self.archive_choices.append((archive_id, f"{i.name} ({i.count})")) 81 | self.archive.choices = self.archive_choices 82 | if blog is not None: 83 | self.archive_data = [] 84 | self.archive.data = self.archive_data 85 | for a in blog.archive: 86 | a: Archive 87 | self.archive_data.append(a) 88 | self.blog_id.data = blog.id 89 | 90 | def validate_archive(self, field): 91 | for i in field.data: 92 | if i not in self.archive_res: 93 | raise ValidationError("错误的归档被指定") 94 | 95 | 96 | class WriteCommentForm(FlaskForm): 97 | content = TextAreaField("", description="评论正文", 98 | validators=[DataRequired(message="请输入评论的内容"), 99 | Length(1, 100, message="请输入1-100个字的评论")]) 100 | submit = SubmitField("评论") 101 | 102 | 103 | def __load_docx_page(page: int, form: WriteBlogForm): 104 | if page < 1: 105 | app.HBlogFlask.print_user_opt_fail_log(f"Load docx list with error page({page})") 106 | abort(404) 107 | return 108 | 109 | blog_list = BlogArticle.get_blog_list(limit=20, offset=(page - 1) * 20) 110 | max_page = app.HBlogFlask.get_max_page(BlogArticle.get_blog_count(), 20) 111 | page_list = app.HBlogFlask.get_page("docx.docx_page", page, max_page) 112 | app.HBlogFlask.print_load_page_log(f"docx list (page: {page})") 113 | return render_template("docx/docx.html", 114 | page=page, 115 | cache_str=f":{page}", 116 | blog_list=blog_list, 117 | page_list=page_list, 118 | form=form, 119 | show_delete=current_user.check_role("DeleteBlog")) 120 | 121 | 122 | @docx.route('/') 123 | def docx_page(): 124 | page = request.args.get("page", 1, type=int) 125 | return __load_docx_page(page, WriteBlogForm(True)) 126 | 127 | 128 | @docx.route('/archive') 129 | def archive_page(): 130 | page = request.args.get("page", 1, type=int) 131 | archive = request.args.get("archive", 1, type=int) 132 | if page < 1: 133 | app.HBlogFlask.print_user_opt_fail_log(f"Load archive-docx list with error page({page}) archive: {archive}") 134 | abort(404) 135 | return 136 | 137 | blog_list = BlogArticle.get_blog_list(archive_id=archive, limit=20, offset=(page - 1) * 20) 138 | max_page = app.HBlogFlask.get_max_page(BlogArticle.get_blog_count(archive_id=archive), 20) 139 | page_list = app.HBlogFlask.get_page("docx.archive_page", page, max_page) 140 | add_archive_click(archive) 141 | app.HBlogFlask.print_load_page_log(f"archive-docx list (archive-id: {archive} page: {page})") 142 | return render_template("docx/docx.html", 143 | page=page, 144 | cache_str=f":{page}", 145 | blog_list=blog_list, 146 | page_list=page_list, 147 | form=None) 148 | 149 | 150 | def __load_article_page(blog_id: int, form: WriteCommentForm, 151 | view: Optional[UpdateBlogForm] = None, 152 | archive: Optional[UpdateBlogArchiveForm] = None): 153 | article = BlogArticle(blog_id) 154 | if article is None: 155 | app.HBlogFlask.print_user_opt_fail_log(f"Load article with error id({blog_id})") 156 | abort(404) 157 | return 158 | app.HBlogFlask.print_load_page_log(f"article (id: {blog_id})") 159 | if view is None: 160 | view = UpdateBlogForm(article) 161 | if archive is None: 162 | archive = UpdateBlogArchiveForm(article) 163 | add_blog_click(article.id) 164 | return render_template("docx/article.html", 165 | article=article, 166 | cache_str=f":{article.id}", 167 | archive_list=article.archive, 168 | form=form, 169 | view=view, 170 | archive=archive, 171 | can_update=current_user.check_role("WriteBlog"), 172 | show_delete=current_user.check_role("DeleteComment"), 173 | show_email=current_user.check_role("ReadUserInfo")) 174 | 175 | 176 | @docx.route('/article') 177 | def article_page(): 178 | blog_id = request.args.get("blog", 1, type=int) 179 | return __load_article_page(blog_id, WriteCommentForm()) 180 | 181 | 182 | @docx.route('/article/download') 183 | def article_down_page(): 184 | blog_id = request.args.get("blog", 1, type=int) 185 | article = BlogArticle(blog_id) 186 | if article is None: 187 | app.HBlogFlask.print_user_opt_fail_log(f"Download article with error id({blog_id})") 188 | abort(404) 189 | return 190 | 191 | response = make_response(article.content) 192 | response.headers["Content-Disposition"] = f"attachment;filename={article.title.encode().decode('latin-1')}.md" 193 | app.HBlogFlask.print_load_page_log(f"download article (id: {blog_id})") 194 | return response 195 | 196 | 197 | @docx.route('/article/create', methods=["POST"]) 198 | @login_required 199 | @app.form_required(WriteBlogForm, 200 | "write blog", 201 | lambda form: __load_docx_page(request.args.get("page", 1, type=int), form)) 202 | @app.role_required("WriteBlog", "write blog") 203 | def create_docx_page(): 204 | form: WriteBlogForm = g.form 205 | title = form.title.data 206 | subtitle = form.subtitle.data 207 | archive = [] 208 | if -1 not in form.archive.data: 209 | for i in form.archive.data: 210 | i = Archive(i) 211 | if i is not None: 212 | archive.append(i) 213 | 214 | if BlogArticle.create(title, subtitle, form.content.data, archive, current_user): 215 | app.HBlogFlask.print_sys_opt_success_log("write blog") 216 | flash(f"博客 {title} 发表成功") 217 | else: 218 | app.HBlogFlask.print_sys_opt_fail_log("write blog") 219 | flash(f"博客 {title} 发表失败") 220 | return redirect(url_for("docx.docx_page", page=1)) 221 | 222 | 223 | @docx.route('/article/update', methods=["POST"]) 224 | @login_required 225 | @app.form_required(UpdateBlogForm, "update blog", 226 | lambda form: __load_article_page(form.id.data, WriteCommentForm(), form)) 227 | @app.role_required("WriteBlog", "write blog") 228 | def update_docx_page(): 229 | form: UpdateBlogForm = g.form 230 | if BlogArticle(form.blog_id.data).update(form.content.data): 231 | app.HBlogFlask.print_sys_opt_success_log("update blog") 232 | flash("博文更新成功") 233 | else: 234 | app.HBlogFlask.print_sys_opt_fail_log("update blog") 235 | flash("博文更新失败") 236 | return redirect(url_for("docx.article_page", blog=form.blog_id.data)) 237 | 238 | 239 | @docx.route("/article/delete") 240 | @login_required 241 | @app.role_required("DeleteBlog", "delete blog") 242 | def delete_blog_page(): 243 | blog_id = request.args.get("blog", None, type=int) 244 | if not blog_id: 245 | return abort(400) 246 | if BlogArticle(blog_id).delete(): 247 | app.HBlogFlask.print_sys_opt_success_log("delete blog") 248 | flash("博文删除成功") 249 | else: 250 | app.HBlogFlask.print_sys_opt_fail_log("delete blog") 251 | flash("博文删除失败") 252 | return redirect(url_for("docx.docx_page", page=1)) 253 | 254 | 255 | @docx.route("/article/set/top") 256 | @login_required 257 | @app.role_required("WriteBlog", "set blog top") 258 | def set_blog_top_page(): 259 | blog_id = request.args.get("blog", None, type=int) 260 | top = request.args.get("top", 0, type=int) != 0 261 | if not blog_id: 262 | return abort(400) 263 | blog = BlogArticle(blog_id) 264 | blog.top = top 265 | if top == blog.top: 266 | app.HBlogFlask.print_sys_opt_success_log(f"set blog top ({top})") 267 | flash(f"博文{'取消' if not top else ''}置顶成功") 268 | else: 269 | app.HBlogFlask.print_sys_opt_fail_log(f"set blog top ({top})") 270 | flash(f"博文{'取消' if not top else ''}置顶失败") 271 | return redirect(url_for("docx.article_page", blog=blog_id)) 272 | 273 | 274 | @docx.route("/article/set/archive", methods=["POST"]) 275 | @login_required 276 | @app.form_required(UpdateBlogArchiveForm, "update archive", 277 | lambda form: __load_article_page(form.id.data, WriteCommentForm(), UpdateBlogForm(), form)) 278 | @app.role_required("WriteBlog", "update archive") 279 | def update_archive_page(): 280 | form: UpdateBlogArchiveForm = g.form 281 | article = BlogArticle(form.blog_id.data) 282 | add = request.args.get("add", 0, type=int) != 0 283 | for i in form.archive.data: 284 | if add: 285 | article.add_to_archive(i) 286 | else: 287 | article.sub_from_archive(i) 288 | flash("归档设定完成") 289 | return redirect(url_for("docx.article_page", blog=form.blog_id.data)) 290 | 291 | 292 | @docx.route('/comment/create', methods=["POST"]) 293 | @login_required 294 | @app.form_required(WriteCommentForm, "write comment", 295 | lambda form: __load_article_page(request.args.get("blog", 1, type=int), form)) 296 | @app.role_required("WriteComment", "write comment") 297 | def comment_page(): 298 | blog_id = request.args.get("blog", 1, type=int) 299 | form: WriteCommentForm = g.form 300 | content = form.content.data 301 | if Comment.create(BlogArticle(blog_id), current_user, content): 302 | app.HBlogFlask.print_user_opt_success_log("comment") 303 | flash("评论成功") 304 | else: 305 | app.HBlogFlask.print_user_opt_error_log("comment") 306 | flash("评论失败") 307 | return redirect(url_for("docx.article_page", blog=blog_id)) 308 | 309 | 310 | @docx.route("/comment/delete") 311 | @login_required 312 | @app.role_required("DeleteComment", "delete comment") 313 | def delete_comment_page(): 314 | comment_id = request.args.get("comment", 1, type=int) 315 | comment = Comment(comment_id) 316 | blog_id = comment.blog.id 317 | if blog_id == -1: 318 | abort(404) 319 | 320 | if comment.delete(): 321 | app.HBlogFlask.print_sys_opt_success_log("delete comment") 322 | flash("博文评论删除成功") 323 | else: 324 | app.HBlogFlask.print_sys_opt_fail_log("delete comment") 325 | flash("博文评论删除失败") 326 | return redirect(url_for("docx.article_page", blog=blog_id)) 327 | 328 | 329 | @docx.context_processor 330 | @app.cache.cached(timeout=conf["CACHE_EXPIRE"], key_prefix="inject_base:docx") 331 | def inject_base(): 332 | """ docx 默认模板变量 """ 333 | return {"top_nav": ["", "", "active", "", "", ""]} 334 | --------------------------------------------------------------------------------