From 9f639cfbe8d78a5d5518c3e7c10e3860fbdeb50d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=90=B4=E4=BF=8A=E9=BE=99?= <2831713936@qq.com>
Date: Wed, 15 Oct 2025 23:17:22 +0800
Subject: [PATCH] =?UTF-8?q?severmanager=E4=BB=A3=E7=A0=81=E6=A0=87?=
=?UTF-8?q?=E6=B3=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.idea/djangoBlogStudy.iml | 24 +-
.idea/modules.xml | 4 -
src/servermanager/MemcacheStorage.py | 96 +++++++-
src/servermanager/admin.py | 33 ++-
src/servermanager/api/blogapi.py | 73 +++++-
src/servermanager/api/commonapi.py | 97 ++++++--
src/servermanager/apps.py | 18 ++
src/servermanager/migrations/0001_initial.py | 51 ++--
...002_alter_emailsendlog_options_and_more.py | 49 ++--
src/servermanager/models.py | 67 ++++-
src/servermanager/robot.py | 233 +++++++++++++++---
src/servermanager/tests.py | 127 +++++++---
src/servermanager/urls.py | 32 +++
src/servermanager/views.py | 3 +
14 files changed, 762 insertions(+), 145 deletions(-)
diff --git a/.idea/djangoBlogStudy.iml b/.idea/djangoBlogStudy.iml
index 8b8c395..8b5895a 100644
--- a/.idea/djangoBlogStudy.iml
+++ b/.idea/djangoBlogStudy.iml
@@ -1,7 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
@@ -9,4 +23,12 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
index dc7543e..eb9a5a1 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -2,11 +2,7 @@
-<<<<<<< HEAD
-=======
-
->>>>>>> master
\ No newline at end of file
diff --git a/src/servermanager/MemcacheStorage.py b/src/servermanager/MemcacheStorage.py
index 38a7990..0ad8021 100644
--- a/src/servermanager/MemcacheStorage.py
+++ b/src/servermanager/MemcacheStorage.py
@@ -1,32 +1,108 @@
-from werobot.session import SessionStorage
-from werobot.utils import json_loads, json_dumps
+from werobot.session import SessionStorage # WeRoBot 框架的会话存储基类
+from werobot.utils import json_loads, json_dumps # WeRoBot 提供的 JSON 序列化/反序列化工具
-from djangoblog.utils import cache
+from djangoblog.utils import cache # 项目中封装的缓存对象(如 Redis 或 Memcached)
class MemcacheStorage(SessionStorage):
+ """
+ 自定义的会话存储类,基于缓存系统(如 Redis/Memcached)实现 WeRoBot 微信机器人的会话管理。
+ 用于在微信消息处理过程中持久化用户会话数据(如上下文、状态等)。
+
+ 通过将用户 ID 作为键,会话数据作为值,存储在缓存中,实现跨请求的会话跟踪。
+ """
+
def __init__(self, prefix='ws_'):
- self.prefix = prefix
- self.cache = cache
+ """
+ 初始化 MemcacheStorage 实例。
+
+ 参数:
+ prefix (str): 缓存键的前缀,默认为 'ws_'(weobot session 的缩写),用于避免键名冲突。
+
+ 功能:
+ - 设置键名前缀
+ - 绑定项目全局的缓存实例(cache)
+ """
+ self.prefix = prefix # 设置键名前缀
+ self.cache = cache # 使用项目中已配置的缓存后端(如 Redis)
@property
def is_available(self):
+ """
+ 检查当前会话存储是否可用(即缓存系统是否正常工作)。
+
+ 实现方式:
+ 1. 尝试写入一个测试键值对('checkavaliable': '1')
+ 2. 立即读取该键的值
+ 3. 如果读取的值与写入的值一致,则认为存储可用
+
+ 返回:
+ bool: True 表示缓存可用;False 表示不可用(如缓存服务宕机)
+ """
value = "1"
- self.set('checkavaliable', value=value)
- return value == self.get('checkavaliable')
+ self.set('checkavaliable', value=value) # 写入测试数据
+ return value == self.get('checkavaliable') # 验证能否正确读取
def key_name(self, s):
+ """
+ 为给定的会话 ID 生成带前缀的完整缓存键名。
+
+ 参数:
+ s (str): 原始会话 ID(通常是微信用户的 OpenID)
+
+ 返回:
+ str: 格式为 '{prefix}{s}' 的完整键名,用于避免命名冲突。
+
+ 示例:
+ key_name('abc123') -> 'ws_abc123'
+ """
return '{prefix}{s}'.format(prefix=self.prefix, s=s)
def get(self, id):
+ """
+ 根据会话 ID 从缓存中获取会话数据。
+
+ 参数:
+ id (str): 会话 ID(如用户 OpenID)
+
+ 返回:
+ dict: 会话数据的字典;如果缓存中不存在,则返回空字典 {}
+
+ 流程:
+ 1. 使用 key_name() 生成带前缀的键
+ 2. 从缓存中获取 JSON 字符串
+ 3. 使用 json_loads 将 JSON 字符串反序列化为 Python 字典
+ """
id = self.key_name(id)
- session_json = self.cache.get(id) or '{}'
+ session_json = self.cache.get(id) or '{}' # 如果未找到,返回空 JSON 字符串
return json_loads(session_json)
def set(self, id, value):
+ """
+ 将会话数据存储到缓存中。
+
+ 参数:
+ id (str): 会话 ID
+ value (dict): 要存储的会话数据(Python 字典)
+
+ 流程:
+ 1. 使用 key_name() 生成带前缀的键
+ 2. 将字典序列化为 JSON 字符串
+ 3. 存入缓存(使用默认过期时间)
+ """
id = self.key_name(id)
- self.cache.set(id, json_dumps(value))
+ self.cache.set(id, json_dumps(value)) # 序列化并存储
def delete(self, id):
+ """
+ 根据会话 ID 从缓存中删除会话数据。
+
+ 参数:
+ id (str): 会话 ID
+
+ 流程:
+ 1. 使用 key_name() 生成带前缀的键
+ 2. 调用缓存的 delete 方法删除该键
+ """
id = self.key_name(id)
- self.cache.delete(id)
+ self.cache.delete(id)
\ No newline at end of file
diff --git a/src/servermanager/admin.py b/src/servermanager/admin.py
index f26f4f6..83bc509 100644
--- a/src/servermanager/admin.py
+++ b/src/servermanager/admin.py
@@ -1,19 +1,48 @@
from django.contrib import admin
+
# Register your models here.
+# 本文件用于将数据模型注册到 Django 管理后台(Admin),并自定义其展示和操作方式。
+# 当前未立即注册模型,需在下方通过 admin.site.register() 显式注册。
class CommandsAdmin(admin.ModelAdmin):
+ """
+ Django Admin 配置类,用于自定义 'commands' 模型在管理后台中的列表展示。
+ 控制在模型列表页面显示哪些字段。
+ """
+
+ # 定义在管理后台的模型列表页面中要显示的字段列
+ # 用户将看到三列:'title'(命令标题)、'command'(命令内容)、'describe'(描述)
list_display = ('title', 'command', 'describe')
class EmailSendLogAdmin(admin.ModelAdmin):
+ """
+ Django Admin 配置类,用于自定义 'EmailSendLog' 模型在管理后台中的行为。
+ 该日志模型主要用于查看,禁止添加新记录,并将所有字段设为只读。
+ """
+
+ # 定义在管理后台的模型列表页面中要显示的字段列
+ # 显示:邮件标题、收件人、发送结果、创建时间
list_display = ('title', 'emailto', 'send_result', 'creation_time')
+
+ # 定义在编辑或查看单条记录时,哪些字段为只读(不可编辑)
+ # 所有字段均设为只读,防止日志被意外修改
readonly_fields = (
'title',
'emailto',
'send_result',
'creation_time',
- 'content')
+ 'content'
+ )
+ # 重写权限方法,禁止用户在管理后台添加新的 EmailSendLog 记录
+ # 因为邮件日志应由程序自动创建,不允许手动添加
+ #
+ # 参数:
+ # request: 当前的 HTTP 请求对象
+ #
+ # 返回:
+ # bool: 始终返回 False,表示禁止添加操作
def has_add_permission(self, request):
- return False
+ return False
\ No newline at end of file
diff --git a/src/servermanager/api/blogapi.py b/src/servermanager/api/blogapi.py
index 8a4d6ac..3eda38c 100644
--- a/src/servermanager/api/blogapi.py
+++ b/src/servermanager/api/blogapi.py
@@ -4,24 +4,77 @@ from blog.models import Article, Category
class BlogApi:
+ """
+ 博客 API 类,封装了与博客相关的数据查询功能,包括全文搜索、分类获取、分类文章获取和最新文章获取。
+ """
+
def __init__(self):
- self.searchqueryset = SearchQuerySet()
- self.searchqueryset.auto_query('')
- self.__max_takecount__ = 8
+ """
+ 初始化 BlogApi 实例。
+ 创建一个 SearchQuerySet 对象用于全文搜索,并执行一个空查询以准备搜索环境。
+ 同时设置默认的最大返回记录数。
+ """
+ self.searchqueryset = SearchQuerySet() # Haystack 的搜索查询集对象
+ self.searchqueryset.auto_query('') # 执行空查询,初始化搜索状态
+ self.__max_takecount__ = 8 # 设置最大返回文章数量为 8 篇
def search_articles(self, query):
- sqs = self.searchqueryset.auto_query(query)
- sqs = sqs.load_all()
- return sqs[:self.__max_takecount__]
+ """
+ 根据用户输入的关键词查询文章。
+
+ 参数:
+ query (str): 用户输入的搜索关键词。
+
+ 返回:
+ QuerySet: 包含匹配文章的查询集(最多返回 __max_takecount__ 篇文章),
+ 并已加载文章的完整模型实例(通过 load_all)。
+
+ 说明:
+ 使用 Haystack 的 auto_query 进行全文搜索,并加载所有关联的文章对象。
+ """
+ sqs = self.searchqueryset.auto_query(query) # 根据关键词执行全文搜索
+ sqs = sqs.load_all() # 加载搜索结果对应的真实模型实例(Article 对象)
+ return sqs[:self.__max_takecount__] # 返回最多 __max_takecount__ 条搜索结果
def get_category_lists(self):
+ """
+ 获取所有文章分类的列表。
+
+ 返回:
+ QuerySet: 包含数据库中所有 Category 对象的查询集。
+
+ 说明:
+ 此方法返回所有已创建的分类,可用于展示分类导航等。
+ """
return Category.objects.all()
def get_category_articles(self, categoryname):
- articles = Article.objects.filter(category__name=categoryname)
+ """
+ 根据分类名称获取该分类下的文章列表。
+
+ 参数:
+ categoryname (str): 分类的名称。
+
+ 返回:
+ QuerySet 或 None: 如果存在该分类的文章,则返回最多 __max_takecount__ 篇文章的查询集;
+ 如果该分类下没有文章,则返回 None。
+
+ 说明:
+ 通过文章模型的外键关系(category__name)进行过滤,获取指定分类的文章。
+ """
+ articles = Article.objects.filter(category__name=categoryname) # 查询指定分类的文章
if articles:
- return articles[:self.__max_takecount__]
- return None
+ return articles[:self.__max_takecount__] # 存在文章则返回最多 __max_takecount__ 篇
+ return None # 无文章则返回 None
def get_recent_articles(self):
- return Article.objects.all()[:self.__max_takecount__]
+ """
+ 获取最新的文章列表(按模型默认排序,通常是按创建时间倒序)。
+
+ 返回:
+ QuerySet: 包含最新文章的查询集(最多返回 __max_takecount__ 篇文章)。
+
+ 说明:
+ 此方法返回数据库中最新的若干篇文章,可用于展示“最新文章”或“热门文章”。
+ """
+ return Article.objects.all()[:self.__max_takecount__]
\ No newline at end of file
diff --git a/src/servermanager/api/commonapi.py b/src/servermanager/api/commonapi.py
index 83ad9ff..47be2b0 100644
--- a/src/servermanager/api/commonapi.py
+++ b/src/servermanager/api/commonapi.py
@@ -5,60 +5,129 @@ import openai
from servermanager.models import commands
+# 获取当前模块的 logger,用于记录日志
logger = logging.getLogger(__name__)
+# 从环境变量中读取 OpenAI API 密钥
openai.api_key = os.environ.get('OPENAI_API_KEY')
+
+# 如果设置了 HTTP 代理,则配置 openai 库使用该代理
if os.environ.get('HTTP_PROXY'):
openai.proxy = os.environ.get('HTTP_PROXY')
class ChatGPT:
+ """
+ 封装与 OpenAI 的 ChatGPT 模型进行交互的功能。
+ 提供静态方法 `chat` 用于发送用户提示并获取模型回复。
+ """
@staticmethod
def chat(prompt):
+ """
+ 调用 OpenAI 的 GPT-3.5 Turbo 模型生成回复。
+
+ 参数:
+ prompt (str): 用户输入的提示文本(问题或请求)。
+
+ 返回:
+ str: 模型生成的回复内容;如果调用失败,则返回错误提示。
+
+ 流程:
+ 1. 使用 openai.ChatCompletion.create 发起请求,指定模型为 "gpt-3.5-turbo"。
+ 2. 将用户提示作为 role="user" 的消息发送。
+ 3. 提取并返回模型返回的第一条消息内容。
+ 4. 如果发生异常(如网络错误、认证失败等),记录错误日志并返回友好提示。
+ """
try:
- completion = openai.ChatCompletion.create(model="gpt-3.5-turbo",
- messages=[{"role": "user", "content": prompt}])
- return completion.choices[0].message.content
+ completion = openai.ChatCompletion.create(
+ model="gpt-3.5-turbo", # 使用 GPT-3.5 Turbo 模型
+ messages=[{"role": "user", "content": prompt}] # 构造对话消息
+ )
+ return completion.choices[0].message.content # 返回模型生成的文本
except Exception as e:
- logger.error(e)
- return "服务器出错了"
+ logger.error(e) # 记录异常信息到日志
+ return "服务器出错了" # 返回用户友好的错误提示
class CommandHandler:
+ """
+ 命令处理器类,用于管理、查找和执行预定义的系统命令。
+ 从数据库加载命令列表,支持通过名称查找并执行命令,以及获取帮助信息。
+ """
+
def __init__(self):
- self.commands = commands.objects.all()
+ """
+ 初始化 CommandHandler 实例。
+ 从数据库中加载所有预定义的命令对象,存储在实例变量 self.commands 中。
+ """
+ self.commands = commands.objects.all() # 查询数据库中所有命令记录
def run(self, title):
"""
- 运行命令
- :param title: 命令
- :return: 返回命令执行结果
+ 根据命令标题查找并执行对应的系统命令。
+
+ 参数:
+ title (str): 用户输入的命令标题。
+
+ 返回:
+ str: 命令执行后的输出结果;如果未找到命令,则返回帮助提示。
+
+ 流程:
+ 1. 在 self.commands 列表中查找 title(忽略大小写)匹配的命令。
+ 2. 如果找到匹配的命令,调用私有方法 __run_command__ 执行其对应的操作。
+ 3. 如果未找到,则返回提示信息,引导用户输入 'hepme' 获取帮助。
"""
cmd = list(
filter(
- lambda x: x.title.upper() == title.upper(),
+ lambda x: x.title.upper() == title.upper(), # 忽略大小写比较
self.commands))
if cmd:
- return self.__run_command__(cmd[0].command)
+ return self.__run_command__(cmd[0].command) # 执行找到的命令
else:
return "未找到相关命令,请输入hepme获得帮助。"
def __run_command__(self, cmd):
+ """
+ 执行给定的系统命令(shell 命令)。
+
+ 参数:
+ cmd (str): 要执行的系统命令字符串。
+
+ 返回:
+ str: 命令执行的标准输出;如果执行出错,则返回错误提示。
+
+ 说明:
+ 使用 os.popen 打开一个管道来执行命令,并读取其输出。
+ 捕获所有异常(包括执行失败、权限问题等),防止程序崩溃。
+ """
try:
- res = os.popen(cmd).read()
+ res = os.popen(cmd).read() # 执行命令并读取输出
return res
except BaseException:
return '命令执行出错!'
def get_help(self):
+ """
+ 生成帮助信息,列出所有可用的命令及其描述。
+
+ 返回:
+ str: 格式化的帮助文本,每行包含一个命令的标题和描述,用冒号分隔。
+
+ 说明:
+ 遍历 self.commands 中的所有命令,拼接成一个字符串,用于向用户展示可用命令。
+ """
rsp = ''
for cmd in self.commands:
- rsp += '{c}:{d}\n'.format(c=cmd.title, d=cmd.describe)
+ rsp += '{c}:{d}\n'.format(c=cmd.title, d=cmd.describe) # 拼接命令帮助信息
return rsp
if __name__ == '__main__':
+ """
+ 主程序入口,用于测试 ChatGPT 类的功能。
+ 创建 ChatGPT 实例,发送一个测试提示,并打印模型回复。
+ """
chatbot = ChatGPT()
prompt = "写一篇1000字关于AI的论文"
- print(chatbot.chat(prompt))
+ print(chatbot.chat(prompt))
\ No newline at end of file
diff --git a/src/servermanager/apps.py b/src/servermanager/apps.py
index 03cc38d..154f901 100644
--- a/src/servermanager/apps.py
+++ b/src/servermanager/apps.py
@@ -1,5 +1,23 @@
+# 导入 AppConfig,它是 Django 中用于配置应用程序的基类
from django.apps import AppConfig
class ServermanagerConfig(AppConfig):
+ """
+ 这是 'servermanager' 应用的配置类。
+ 它继承自 Django 的 AppConfig,用于定义该应用在 Django 项目中的元数据和初始化行为。
+
+ 当 Django 启动时,会扫描 INSTALLED_APPS 中的应用,并加载其对应的 AppConfig。
+ 此类的作用是告诉 Django 这个应用的基本信息,例如它的 Python 路径(name)。
+ """
+
+ # 指定该应用的完整 Python 导入路径
+ # Django 使用此属性来唯一标识和导入 'servermanager' 应用
+ # 通常与应用所在文件夹的名称一致
name = 'servermanager'
+
+ # 注意:
+ # - 该类目前只设置了最基本的 name 属性。
+ # - 如果需要在应用启动时执行某些初始化代码,可以重写 ready() 方法。
+ # 例如:注册信号处理器、启动后台任务等。
+ # - 由于当前没有复杂的初始化需求,因此未定义其他方法或属性。
diff --git a/src/servermanager/migrations/0001_initial.py b/src/servermanager/migrations/0001_initial.py
index bbdbf77..799c1fc 100644
--- a/src/servermanager/migrations/0001_initial.py
+++ b/src/servermanager/migrations/0001_initial.py
@@ -1,45 +1,68 @@
-# Generated by Django 4.1.7 on 2023-03-02 07:14
-
from django.db import migrations, models
class Migration(migrations.Migration):
+ """
+ Django 迁移类,定义了数据库模式的变更。
+ 此迁移是应用的初始迁移(initial=True),用于创建两个新的数据模型表:
+ - commands:存储可执行命令的信息
+ - EmailSendLog:记录邮件发送的日志信息
+ """
+ # 标记此迁移为“初始迁移”,表示这是应用的第一个迁移文件
initial = True
+ # 定义此迁移所依赖的其他迁移
+ # 当前为空列表,表示该迁移不依赖于任何其他迁移(即它是最初的)
dependencies = [
]
+ # 定义此迁移要执行的数据库操作列表
operations = [
+ # 第一个操作:创建名为 'commands' 的数据模型表
migrations.CreateModel(
- name='commands',
- fields=[
+ name='commands', # 数据库表对应的模型名称
+ fields=[ # 定义该模型包含的字段(即数据库表的列)
+ # 主键 ID 字段:自增的 BigAutoField
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ # 命令标题字段:最大长度 300 的字符串
('title', models.CharField(max_length=300, verbose_name='命令标题')),
+ # 实际执行的命令字段:最大长度 2000 的字符串
('command', models.CharField(max_length=2000, verbose_name='命令')),
+ # 命令描述字段:最大长度 300 的字符串
('describe', models.CharField(max_length=300, verbose_name='命令描述')),
+ # 创建时间字段:自动在对象创建时设置当前时间
('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+ # 最后修改时间字段:每次对象保存时自动更新为当前时间
('last_mod_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
],
- options={
- 'verbose_name': '命令',
- 'verbose_name_plural': '命令',
+ options={ # 模型的元选项配置
+ 'verbose_name': '命令', # 单数形式的可读名称
+ 'verbose_name_plural': '命令', # 复数形式的可读名称(中文通常与单数相同)
},
),
+
+ # 第二个操作:创建名为 'EmailSendLog' 的数据模型表
migrations.CreateModel(
- name='EmailSendLog',
- fields=[
+ name='EmailSendLog', # 数据库表对应的模型名称
+ fields=[ # 定义该模型包含的字段(即数据库表的列)
+ # 主键 ID 字段:自增的 BigAutoField
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ # 收件人邮箱字段:最大长度 300 的字符串
('emailto', models.CharField(max_length=300, verbose_name='收件人')),
+ # 邮件标题字段:最大长度 2000 的字符串
('title', models.CharField(max_length=2000, verbose_name='邮件标题')),
+ # 邮件内容字段:长文本字段
('content', models.TextField(verbose_name='邮件内容')),
+ # 发送结果字段:布尔值,默认为 False(表示失败)
('send_result', models.BooleanField(default=False, verbose_name='结果')),
+ # 创建时间字段:自动在记录创建时设置当前时间
('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
],
- options={
- 'verbose_name': '邮件发送log',
- 'verbose_name_plural': '邮件发送log',
- 'ordering': ['-created_time'],
+ options={ # 模型的元选项配置
+ 'verbose_name': '邮件发送log', # 单数形式的可读名称
+ 'verbose_name_plural': '邮件发送log', # 复数形式的可读名称
+ 'ordering': ['-created_time'], # 默认排序规则:按创建时间倒序排列(最新的在前)
},
),
- ]
+ ]
\ No newline at end of file
diff --git a/src/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py b/src/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py
index 4858857..83d86d5 100644
--- a/src/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py
+++ b/src/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py
@@ -1,32 +1,51 @@
-# Generated by Django 4.2.5 on 2023-09-06 13:19
-
from django.db import migrations
class Migration(migrations.Migration):
+ """
+ Django 迁移类,定义了对数据库模式的变更。
+ 此迁移文件基于 'servermanager' 应用的初始迁移(0001_initial)进行修改,
+ 主要目的是统一字段命名规范(如将 created_time 改为 creation_time)并更新模型排序规则。
+ """
+ # 定义此迁移所依赖的其他迁移
+ # 依赖于 servermanager 应用的 0001_initial 迁移,确保基础表已存在
dependencies = [
- ('servermanager', '0001_initial'),
+ ('servermanager', '0001_initial'), # 格式:(应用名, 迁移文件名)
]
+ # 定义此迁移要执行的数据库操作列表
operations = [
+ # 操作 1:修改模型 EmailSendLog 的元选项(Meta options)
migrations.AlterModelOptions(
- name='emailsendlog',
- options={'ordering': ['-creation_time'], 'verbose_name': '邮件发送log', 'verbose_name_plural': '邮件发送log'},
+ name='emailsendlog', # 要修改的模型名称
+ options={
+ # 更新排序字段为 'creation_time'(倒序),确保最新记录排在前面
+ 'ordering': ['-creation_time'],
+ # 保持模型的可读名称不变
+ 'verbose_name': '邮件发送log',
+ 'verbose_name_plural': '邮件发送log'
+ },
),
+
+ # 操作 2:将 commands 模型中的字段 created_time 重命名为 creation_time
migrations.RenameField(
- model_name='commands',
- old_name='created_time',
- new_name='creation_time',
+ model_name='commands', # 要操作的模型名称
+ old_name='created_time', # 原字段名
+ new_name='creation_time', # 新字段名
),
+
+ # 操作 3:将 commands 模型中的字段 last_mod_time 重命名为 last_modify_time
migrations.RenameField(
- model_name='commands',
- old_name='last_mod_time',
- new_name='last_modify_time',
+ model_name='commands', # 要操作的模型名称
+ old_name='last_mod_time', # 原字段名
+ new_name='last_modify_time', # 新字段名
),
+
+ # 操作 4:将 emailsendlog 模型中的字段 created_time 重命名为 creation_time
migrations.RenameField(
- model_name='emailsendlog',
- old_name='created_time',
- new_name='creation_time',
+ model_name='emailsendlog', # 要操作的模型名称
+ old_name='created_time', # 原字段名
+ new_name='creation_time', # 新字段名
),
- ]
+ ]
\ No newline at end of file
diff --git a/src/servermanager/models.py b/src/servermanager/models.py
index 4326c65..3fb10a6 100644
--- a/src/servermanager/models.py
+++ b/src/servermanager/models.py
@@ -2,32 +2,89 @@ from django.db import models
# Create your models here.
+# 定义了两个数据模型:
+# 1. commands:用于存储可执行的系统命令
+# 2. EmailSendLog:用于记录邮件发送的历史日志
+
+
class commands(models.Model):
+ """
+ 数据模型:commands
+ 用于存储系统中预定义的可执行命令,例如服务器管理命令、脚本路径等。
+ 可通过 Django Admin 管理后台进行增删改查。
+ """
+
+ # 命令的标题/名称,用于标识该命令(如 "重启服务")
title = models.CharField('命令标题', max_length=300)
+
+ # 实际要执行的命令字符串(如 "systemctl restart nginx")
command = models.CharField('命令', max_length=2000)
+
+ # 对该命令的简要描述,说明其用途
describe = models.CharField('命令描述', max_length=300)
+
+ # 记录该命令首次创建的时间,自动在创建时设置为当前时间
creation_time = models.DateTimeField('创建时间', auto_now_add=True)
+
+ # 记录该命令最后一次修改的时间,每次保存时自动更新为当前时间
last_modify_time = models.DateTimeField('修改时间', auto_now=True)
def __str__(self):
+ """
+ 返回该模型实例的字符串表示。
+ 在 Django Admin 或外键显示时,将显示命令的标题。
+
+ 返回:
+ str: 命令的标题(title 字段)
+ """
return self.title
class Meta:
- verbose_name = '命令'
- verbose_name_plural = verbose_name
+ """
+ 模型元数据配置。
+ 定义模型的可读名称,用于在 Django Admin 后台显示。
+ """
+ verbose_name = '命令' # 单数形式的名称
+ verbose_name_plural = verbose_name # 复数形式的名称(中文通常与单数相同)
class EmailSendLog(models.Model):
+ """
+ 数据模型:EmailSendLog
+ 用于记录系统发送邮件的日志信息,包括收件人、内容、结果和时间。
+ 所有字段均为只读,仅用于查看历史记录,不可手动修改。
+ """
+
+ # 邮件收件人地址(支持多个,以逗号分隔)
emailto = models.CharField('收件人', max_length=300)
+
+ # 邮件标题
title = models.CharField('邮件标题', max_length=2000)
+
+ # 邮件正文内容,支持长文本
content = models.TextField('邮件内容')
+
+ # 邮件是否发送成功,True 表示成功,False 表示失败
send_result = models.BooleanField('结果', default=False)
+
+ # 邮件记录创建时间,自动在插入时设置为当前时间
creation_time = models.DateTimeField('创建时间', auto_now_add=True)
def __str__(self):
+ """
+ 返回该模型实例的字符串表示。
+ 在 Django Admin 或外键显示时,将显示邮件的标题。
+
+ 返回:
+ str: 邮件的标题(title 字段)
+ """
return self.title
class Meta:
- verbose_name = '邮件发送log'
- verbose_name_plural = verbose_name
- ordering = ['-creation_time']
+ """
+ 模型元数据配置。
+ 定义模型的可读名称和默认排序规则。
+ """
+ verbose_name = '邮件发送log' # 单数形式的名称
+ verbose_name_plural = verbose_name # 复数形式的名称
+ ordering = ['-creation_time'] # 默认排序:按创建时间倒序排列(最新的在前)
\ No newline at end of file
diff --git a/src/servermanager/robot.py b/src/servermanager/robot.py
index 7b45736..f9729f9 100644
--- a/src/servermanager/robot.py
+++ b/src/servermanager/robot.py
@@ -1,46 +1,77 @@
-import logging
-import os
-import re
-
-import jsonpickle
-from django.conf import settings
-from werobot import WeRoBot
-from werobot.replies import ArticlesReply, Article
-from werobot.session.filestorage import FileStorage
-
-from djangoblog.utils import get_sha256
-from servermanager.api.blogapi import BlogApi
-from servermanager.api.commonapi import ChatGPT, CommandHandler
-from .MemcacheStorage import MemcacheStorage
-
-robot = WeRoBot(token=os.environ.get('DJANGO_WEROBOT_TOKEN')
- or 'lylinux', enable_session=True)
+import logging # 用于记录日志信息
+import os # 用于操作系统相关功能(如环境变量、文件路径)
+import re # 正则表达式,用于文本匹配和提取
+
+import jsonpickle # 将 Python 对象序列化为 JSON 字符串(支持复杂对象)
+from django.conf import settings # Django 配置设置,如 BASE_DIR
+from werobot import WeRoBot # WeRoBot 微信机器人框架核心类
+from werobot.replies import ArticlesReply, Article # 构造图文回复消息
+from werobot.session.filestorage import FileStorage # 文件存储会话后端(备用)
+
+# 项目内部工具与 API
+from djangoblog.utils import get_sha256 # SHA256 加密函数,用于密码安全
+from servermanager.api.blogapi import BlogApi # 博客文章搜索/获取接口
+from servermanager.api.commonapi import ChatGPT, CommandHandler # 聊天与命令处理
+from .MemcacheStorage import MemcacheStorage # 自定义基于缓存的会话存储
+
+
+# 初始化微信机器人实例
+robot = WeRoBot(
+ token=os.environ.get('DJANGO_WEROBOT_TOKEN') or 'lylinux', # 微信公众号 Token
+ enable_session=True # 启用会话功能,用于维护用户状态
+)
+
+# 创建自定义的缓存会话存储实例(基于 Redis/Memcached)
memstorage = MemcacheStorage()
+
+# 检查缓存存储是否可用
if memstorage.is_available:
+ # 如果可用,使用 MemcacheStorage 作为会话后端
robot.config['SESSION_STORAGE'] = memstorage
else:
- if os.path.exists(os.path.join(settings.BASE_DIR, 'werobot_session')):
- os.remove(os.path.join(settings.BASE_DIR, 'werobot_session'))
+ # 如果不可用,回退到文件存储
+ # 删除旧的会话文件(避免冲突或损坏)
+ session_file = os.path.join(settings.BASE_DIR, 'werobot_session')
+ if os.path.exists(session_file):
+ os.remove(session_file)
+ # 使用本地文件存储会话数据
robot.config['SESSION_STORAGE'] = FileStorage(filename='werobot_session')
-blogapi = BlogApi()
-cmd_handler = CommandHandler()
-logger = logging.getLogger(__name__)
+# 实例化博客 API 和命令处理器
+blogapi = BlogApi() # 提供博客文章查询服务
+cmd_handler = CommandHandler() # 提供服务器命令执行功能
+logger = logging.getLogger(__name__) # 获取当前模块的日志记录器
def convert_to_article_reply(articles, message):
+ """
+ 将博客文章列表转换为微信图文消息回复格式。
+
+ 参数:
+ articles (list): 包含文章对象的列表(通常来自 blogapi 查询结果)
+ message: 当前微信消息对象,用于构造回复
+
+ 返回:
+ ArticlesReply: 可直接返回给用户的图文消息回复对象
+
+ 流程:
+ 1. 创建一个图文回复对象
+ 2. 遍历每篇文章,提取标题、摘要、首张图片、链接
+ 3. 构造 Article 对象并添加到回复中
+ """
reply = ArticlesReply(message=message)
- from blog.templatetags.blog_tags import truncatechars_content
+ from blog.templatetags.blog_tags import truncatechars_content # 导入截断内容的模板标签
+
for post in articles:
+ # 使用正则从文章内容中提取第一张图片 URL(png/jpg)
imgs = re.findall(r'(?:http\:|https\:)?\/\/.*\.(?:png|jpg)', post.body)
- imgurl = ''
- if imgs:
- imgurl = imgs[0]
+ imgurl = imgs[0] if imgs else '' # 如果有图取第一张,否则为空
+
article = Article(
title=post.title,
- description=truncatechars_content(post.body),
+ description=truncatechars_content(post.body), # 截断内容作为描述
img=imgurl,
- url=post.get_full_url()
+ url=post.get_full_url() # 文章完整 URL
)
reply.add_article(article)
return reply
@@ -48,11 +79,23 @@ def convert_to_article_reply(articles, message):
@robot.filter(re.compile(r"^\?.*"))
def search(message, session):
+ """
+ 处理以 '?' 开头的消息,用于搜索博客文章。
+
+ 示例: "?python" 搜索包含 python 的文章
+
+ 参数:
+ message: 微信消息对象
+ session: 当前用户会话对象
+
+ 返回:
+ 图文消息 或 文本提示
+ """
s = message.content
- searchstr = str(s).replace('?', '')
- result = blogapi.search_articles(searchstr)
+ searchstr = str(s).replace('?', '') # 去掉问号
+ result = blogapi.search_articles(searchstr) # 调用博客 API 搜索
if result:
- articles = list(map(lambda x: x.object, result))
+ articles = list(map(lambda x: x.object, result)) # 提取文章对象
reply = convert_to_article_reply(articles, message)
return reply
else:
@@ -61,13 +104,33 @@ def search(message, session):
@robot.filter(re.compile(r'^category\s*$', re.I))
def category(message, session):
+ """
+ 处理 "category" 消息,返回所有文章分类目录。
+
+ 参数:
+ message: 微信消息对象
+ session: 当前用户会话对象
+
+ 返回:
+ 文本消息:列出所有分类名称
+ """
categorys = blogapi.get_category_lists()
- content = ','.join(map(lambda x: x.name, categorys))
+ content = ','.join(map(lambda x: x.name, categorys)) # 拼接分类名
return '所有文章分类目录:' + content
@robot.filter(re.compile(r'^recent\s*$', re.I))
def recents(message, session):
+ """
+ 处理 "recent" 消息,返回最新发布的文章。
+
+ 参数:
+ message: 微信消息对象
+ session: 当前用户会话对象
+
+ 返回:
+ 图文消息(最新文章) 或 文本提示
+ """
articles = blogapi.get_recent_articles()
if articles:
reply = convert_to_article_reply(articles, message)
@@ -78,6 +141,16 @@ def recents(message, session):
@robot.filter(re.compile('^help$', re.I))
def help(message, session):
+ """
+ 处理 "help" 消息,返回帮助文档。
+
+ 参数:
+ message: 微信消息对象
+ session: 当前用户会话对象
+
+ 返回:
+ 文本消息:详细的使用说明和命令列表
+ """
return '''欢迎关注!
默认会与图灵机器人聊天~~
你可以通过下面这些命令来获得信息
@@ -100,65 +173,132 @@ def help(message, session):
@robot.filter(re.compile(r'^weather\:.*$', re.I))
def weather(message, session):
+ """
+ 处理 "weather:" 开头的消息(天气查询功能)。
+ 当前为占位符,功能正在建设中。
+
+ 参数:
+ message: 微信消息对象
+ session: 当前用户会话对象
+
+ 返回:
+ 文本消息:提示功能建设中
+ """
return "建设中..."
@robot.filter(re.compile(r'^idcard\:.*$', re.I))
def idcard(message, session):
+ """
+ 处理 "idcard:" 开头的消息(身份证信息查询功能)。
+ 当前为占位符,功能正在建设中。
+
+ 参数:
+ message: 微信消息对象
+ session: 当前用户会话对象
+
+ 返回:
+ 文本消息:提示功能建设中
+ """
return "建设中..."
@robot.handler
def echo(message, session):
+ """
+ 默认消息处理器,当没有其他 filter 匹配时调用。
+ 创建 MessageHandler 实例处理消息。
+
+ 参数:
+ message: 微信消息对象
+ session: 当前用户会话对象
+
+ 返回:
+ 处理结果(文本或图文消息)
+ """
handler = MessageHandler(message, session)
return handler.handler()
class MessageHandler:
+ """
+ 消息处理器类,负责处理用户消息,尤其是管理员命令和认证流程。
+ 使用会话(session)维护用户状态(是否管理员、是否已认证等)。
+ """
+
def __init__(self, message, session):
- userid = message.source
self.message = message
self.session = session
- self.userid = userid
+ self.userid = message.source # 用户唯一标识(OpenID)
+
+ # 尝试从会话中加载用户信息
try:
- info = session[userid]
- self.userinfo = jsonpickle.decode(info)
+ info = session[self.userid]
+ self.userinfo = jsonpickle.decode(info) # 反序列化为 WxUserInfo 对象
except Exception as e:
+ # 如果出错(首次访问或会话丢失),创建默认用户信息
userinfo = WxUserInfo()
self.userinfo = userinfo
@property
def is_admin(self):
+ """判断当前用户是否为管理员"""
return self.userinfo.isAdmin
@property
def is_password_set(self):
+ """判断当前管理员是否已完成密码验证"""
return self.userinfo.isPasswordSet
def save_session(self):
+ """
+ 将当前用户信息保存回会话。
+ 使用 jsonpickle 序列化对象,并存入 session。
+ """
info = jsonpickle.encode(self.userinfo)
self.session[self.userid] = info
def handler(self):
+ """
+ 核心消息处理逻辑,根据用户状态和输入内容返回相应响应。
+
+ 处理流程:
+ 1. 管理员退出登录
+ 2. 管理员登录请求
+ 3. 管理员密码验证
+ 4. 执行管理员命令
+ 5. 默认:调用 ChatGPT 进行聊天
+
+ 返回:
+ str: 要回复给用户的消息内容
+ """
info = self.message.content
+ # 退出管理员模式
if self.userinfo.isAdmin and info.upper() == 'EXIT':
self.userinfo = WxUserInfo()
self.save_session()
return "退出成功"
+
+ # 请求进入管理员模式
if info.upper() == 'ADMIN':
self.userinfo.isAdmin = True
self.save_session()
return "输入管理员密码"
+
+ # 管理员密码验证阶段
if self.userinfo.isAdmin and not self.userinfo.isPasswordSet:
+ # 获取配置中的管理员密码(SHA256 加密后)
passwd = settings.WXADMIN
if settings.TESTING:
- passwd = '123'
+ passwd = '123' # 测试环境下使用简单密码
+ # 验证用户输入的密码(双重 SHA256)
if passwd.upper() == get_sha256(get_sha256(info)).upper():
self.userinfo.isPasswordSet = True
self.save_session()
return "验证通过,请输入命令或者要执行的命令代码:输入helpme获得帮助"
else:
+ # 密码错误次数限制
if self.userinfo.Count >= 3:
self.userinfo = WxUserInfo()
self.save_session()
@@ -166,22 +306,33 @@ class MessageHandler:
self.userinfo.Count += 1
self.save_session()
return "验证失败,请重新输入管理员密码:"
+
+ # 管理员已认证,可执行命令
if self.userinfo.isAdmin and self.userinfo.isPasswordSet:
if self.userinfo.Command != '' and info.upper() == 'Y':
+ # 用户确认执行命令
return cmd_handler.run(self.userinfo.Command)
else:
if info.upper() == 'HELPME':
+ # 显示命令帮助
return cmd_handler.get_help()
+ # 记录待执行的命令,等待用户确认
self.userinfo.Command = info
self.save_session()
return "确认执行: " + info + " 命令?"
+ # 默认行为:调用 ChatGPT 进行普通聊天
return ChatGPT.chat(info)
class WxUserInfo():
+ """
+ 微信用户信息类,用于在会话中存储用户状态。
+ 包括是否为管理员、是否已通过密码验证、尝试次数、待执行命令等。
+ """
+
def __init__(self):
- self.isAdmin = False
- self.isPasswordSet = False
- self.Count = 0
- self.Command = ''
+ self.isAdmin = False # 是否请求成为管理员
+ self.isPasswordSet = False # 是否已通过密码验证
+ self.Count = 0 # 密码尝试次数
+ self.Command = '' # 待执行的命令
\ No newline at end of file
diff --git a/src/servermanager/tests.py b/src/servermanager/tests.py
index 22a6689..c97e75e 100644
--- a/src/servermanager/tests.py
+++ b/src/servermanager/tests.py
@@ -1,79 +1,148 @@
from django.test import Client, RequestFactory, TestCase
-from django.utils import timezone
+from django.utils import timezone # 用于处理时间相关的测试
+
+# 导入 WeRoBot 消息类,用于模拟微信消息
from werobot.messages.messages import TextMessage
-from accounts.models import BlogUser
-from blog.models import Category, Article
-from servermanager.api.commonapi import ChatGPT
-from .models import commands
-from .robot import MessageHandler, CommandHandler
-from .robot import search, category, recents
+# 导入项目中的模型和 API
+from accounts.models import BlogUser # 用户模型(用于创建管理员)
+from blog.models import Category, Article # 博客分类和文章模型
+from servermanager.api.commonapi import ChatGPT # 聊天功能接口
+from .models import commands # 命令模型
+from .robot import MessageHandler, CommandHandler # 核心机器人处理器
+from .robot import search, category, recents # 微信机器人命令函数
-# Create your tests here.
class ServerManagerTest(TestCase):
+ """
+ Django 测试用例类,用于对 servermanager 应用的核心功能进行单元测试。
+ 测试内容包括:
+ - ChatGPT 聊天功能
+ - 微信机器人命令响应(搜索、分类、最新文章)
+ - 服务器命令执行
+ - 管理员会话流程
+ """
+
def setUp(self):
- self.client = Client()
- self.factory = RequestFactory()
+ """
+ 在每个测试方法执行前自动运行的初始化方法。
+ 设置测试所需的公共环境:
+ - 创建测试客户端(用于模拟 HTTP 请求)
+ - 创建请求工厂(用于构造请求对象)
+
+ 注意:虽然此处创建了 factory,但在当前测试中并未实际使用。
+ """
+ self.client = Client() # Django 测试客户端
+ self.factory = RequestFactory() # 请求工厂,用于创建模拟请求
def test_chat_gpt(self):
+ """
+ 测试 ChatGPT 聊天功能是否正常工作。
+
+ 步骤:
+ 1. 调用 ChatGPT.chat 方法发送问候语 "你好"
+ 2. 断言返回内容不为 None,表示接口有响应
+
+ 目的:验证聊天接口的基本可用性。
+ """
content = ChatGPT.chat("你好")
self.assertIsNotNone(content)
def test_validate_comment(self):
+ """
+ 综合测试方法,覆盖多个功能点。
+ 名称 'validate_comment' 不准确,实际测试的是 servermanager 的核心功能。
+
+ 测试流程:
+ 1. 创建管理员用户并登录
+ 2. 创建分类和文章用于测试搜索功能
+ 3. 测试微信机器人的 search、category、recents 命令
+ 4. 测试命令执行(CommandHandler)
+ 5. 模拟完整管理员会话流程(登录、执行命令、退出等)
+ """
+
+ # 1. 创建超级用户
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1")
+ # 使用测试客户端登录该用户
self.client.login(username='liangliangyy1', password='liangliangyy1')
+ # 2. 创建博客分类
c = Category()
c.name = "categoryccc"
c.save()
+ # 3. 创建一篇已发布的文章
article = Article()
article.title = "nicetitleccc"
article.body = "nicecontentccc"
article.author = user
article.category = c
- article.type = 'a'
- article.status = 'p'
+ article.type = 'a' # 文章类型
+ article.status = 'p' # 发布状态
article.save()
+
+ # 4. 模拟微信文本消息
s = TextMessage([])
- s.content = "nice"
- rsp = search(s, None)
+ s.content = "nice" # 搜索关键词
+
+ # 5. 测试 search 命令
+ rsp = search(s, None) # 调用 search 过滤器
+ # 断言有响应(即使未找到文章也应返回提示)
+
+ # 6. 测试 category 命令
rsp = category(None, None)
- self.assertIsNotNone(rsp)
+ self.assertIsNotNone(rsp) # 确保返回了分类列表
+
+ # 7. 测试 recents 命令
rsp = recents(None, None)
+ # 断言返回结果不是“暂时还没有文章”,说明能获取到文章
self.assertTrue(rsp != '暂时还没有文章')
+ # 8. 测试命令执行功能
cmd = commands()
cmd.title = "test"
- cmd.command = "ls"
+ cmd.command = "ls" # 测试命令(列出目录)
cmd.describe = "test"
- cmd.save()
+ cmd.save() # 保存到数据库
+ # 实例化命令处理器
cmdhandler = CommandHandler()
+ # 执行名为 'test' 的命令
rsp = cmdhandler.run('test')
+ # 断言命令执行有返回结果
self.assertIsNotNone(rsp)
- s.source = 'u'
- s.content = 'test'
+
+ # 9. 模拟管理员会话流程
+ s.source = 'u' # 设置用户标识(OpenID)
+ s.content = 'test' # 用户输入内容
+
+ # 创建消息处理器实例,传入模拟消息和空会话
msghandler = MessageHandler(s, {})
- # msghandler.userinfo.isPasswordSet = True
- # msghandler.userinfo.isAdmin = True
- msghandler.handler()
- s.content = 'y'
- msghandler.handler()
- s.content = 'idcard:12321233'
+ # 模拟用户行为序列:
+ msghandler.handler() # 处理 "test" 消息(应提示确认)
+
+ s.content = 'y' # 用户确认执行
+ msghandler.handler() # 执行命令
+
+ s.content = 'idcard:12321233' # 尝试身份证查询(占位功能)
msghandler.handler()
- s.content = 'weather:上海'
+
+ s.content = 'weather:上海' # 尝试天气查询(占位功能)
msghandler.handler()
- s.content = 'admin'
+
+ s.content = 'admin' # 请求进入管理员模式
msghandler.handler()
- s.content = '123'
+
+ s.content = '123' # 输入密码(测试环境下有效)
msghandler.handler()
- s.content = 'exit'
+ s.content = 'exit' # 退出管理员模式
msghandler.handler()
+
+ # 注意:此测试未包含 assert 断言来验证每个步骤的结果,
+ # 主要是通过调用来检查是否抛出异常,确保代码路径可执行。
\ No newline at end of file
diff --git a/src/servermanager/urls.py b/src/servermanager/urls.py
index 8d134d2..94e3826 100644
--- a/src/servermanager/urls.py
+++ b/src/servermanager/urls.py
@@ -1,10 +1,42 @@
+# 导入 Django URL 路由模块
from django.urls import path
+# 导入 WeRoBot 与 Django 集成的工具函数
from werobot.contrib.django import make_view
+# 从当前应用的 robot 模块导入已配置的 WeRoBot 实例
from .robot import robot
+# 定义应用命名空间
app_name = "servermanager"
+"""
+应用命名空间,用于在 Django 项目中唯一标识此应用的 URL。
+在模板或 reverse() 函数中可通过 'servermanager:xxx' 引用此应用的 URL。
+"""
+
+# 定义 URL 路由列表
urlpatterns = [
+ # 将微信机器人接入点绑定到特定 URL 路径
path(r'robot', make_view(robot)),
+ """
+ URL 路由配置:
+
+ path(r'robot', make_view(robot))
+
+ 功能说明:
+ - 将路径 '/robot' 映射到 WeRoBot 的请求处理视图。
+ - 当微信服务器向该路径发送 GET(验证)或 POST(消息)请求时,
+ Django 会将其转发给 WeRoBot 框架处理。
+ - make_view(robot) 是 werobot 提供的适配器函数,它将 WeRoBot 实例
+ 转换为一个兼容 Django 的视图函数(View)。
+
+ 参数说明:
+ - r'robot': URL 路径,用户访问 /robot 时触发。
+ 注意:此处使用了原始字符串(r''),但无特殊转义需求,可简写为 'robot'。
+ - make_view(robot): 将 robot 实例封装为 Django 视图。
+ 典型使用场景:
+ 在微信公众平台开发者配置中,将服务器地址(URL)设置为:
+ https://yourdomain.com/servermanager/robot/
+ 并配合 Token 验证机器人身份。
+ """
]
diff --git a/src/servermanager/views.py b/src/servermanager/views.py
index 60f00ef..2f76781 100644
--- a/src/servermanager/views.py
+++ b/src/servermanager/views.py
@@ -1 +1,4 @@
# Create your views here.
+"""
+视图模块(views.py)
+"""
\ No newline at end of file