From f4e7e1b3d2069ffe0dc38dfc4ea7c45c0cb30c68 Mon Sep 17 00:00:00 2001 From: zwf <2466627138@qq.com> Date: Tue, 2 Jun 2026 16:30:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=8C=83=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/01_hello_world/config.json | 36 + examples/01_hello_world/handler.py | 16 + examples/01_hello_world/main.py | 20 + examples/02_background_task/config.json | 36 + examples/02_background_task/handler.py | 34 + examples/02_background_task/main.py | 20 + examples/03_redis_stream/config.json | 52 + examples/03_redis_stream/handler.py | 69 + examples/03_redis_stream/main.py | 20 + examples/03_redis_stream/stream_service.py | 152 ++ examples/04_tasks_service/config.json | 21 + examples/04_tasks_service/task_service.py | 96 + examples/05_gen_models/config.json | 45 + examples/05_gen_models/main.py | 11 + examples/05_gen_models/models/db_models.py | 576 +++++ .../06_chart_example/chart_bar_example.py | 153 ++ .../06_chart_example/chart_pie_example.py | 110 + examples/06_chart_example/charts/charts.html | 16 + .../charts/horizontal_stacked_bars.svg | 1905 +++++++++++++++++ examples/06_chart_example/charts/lines.svg | 1006 +++++++++ .../charts/percent_stacked_bars.svg | 1210 +++++++++++ .../06_chart_example/charts/pie_chart.svg | 1373 ++++++++++++ examples/06_chart_example/charts/splines.svg | 1213 +++++++++++ .../06_chart_example/charts/vertical_bars.svg | 1217 +++++++++++ .../06_chart_example/line_chart_example.py | 124 ++ examples/12_batch_api_calls/README.md | 47 + 26 files changed, 9578 insertions(+) create mode 100644 examples/01_hello_world/config.json create mode 100644 examples/01_hello_world/handler.py create mode 100644 examples/01_hello_world/main.py create mode 100644 examples/02_background_task/config.json create mode 100644 examples/02_background_task/handler.py create mode 100644 examples/02_background_task/main.py create mode 100644 examples/03_redis_stream/config.json create mode 100644 examples/03_redis_stream/handler.py create mode 100644 examples/03_redis_stream/main.py create mode 100644 examples/03_redis_stream/stream_service.py create mode 100644 examples/04_tasks_service/config.json create mode 100644 examples/04_tasks_service/task_service.py create mode 100644 examples/05_gen_models/config.json create mode 100644 examples/05_gen_models/main.py create mode 100644 examples/05_gen_models/models/db_models.py create mode 100644 examples/06_chart_example/chart_bar_example.py create mode 100644 examples/06_chart_example/chart_pie_example.py create mode 100644 examples/06_chart_example/charts/charts.html create mode 100644 examples/06_chart_example/charts/horizontal_stacked_bars.svg create mode 100644 examples/06_chart_example/charts/lines.svg create mode 100644 examples/06_chart_example/charts/percent_stacked_bars.svg create mode 100644 examples/06_chart_example/charts/pie_chart.svg create mode 100644 examples/06_chart_example/charts/splines.svg create mode 100644 examples/06_chart_example/charts/vertical_bars.svg create mode 100644 examples/06_chart_example/line_chart_example.py create mode 100644 examples/12_batch_api_calls/README.md diff --git a/examples/01_hello_world/config.json b/examples/01_hello_world/config.json new file mode 100644 index 0000000..be87dc7 --- /dev/null +++ b/examples/01_hello_world/config.json @@ -0,0 +1,36 @@ +{ + "app_name": "Hello Paste", + + "logger_desc": "用于日志输出的配置,各服务可以有自己的配置,但要使用独立配置时,必须编写额外代码", + "logger": { + "default": { + "desc": "默认日志配置,该配置小节的名称已经配置在 PASTE 框架中", + "basic": { + "filename": "logs/root.log", + "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + "level": 20 + }, + "filename": "logs/default.log", + "name": "Demo", + "max_bytes": 20971520, + "backup_count": 40 + } + }, + + "tornado_desc": "用于 Tornado 服务的配置,每一项后面允许设置多个服务", + "tornado": { + "demo": { + "autoreload": false, + "handlers_pkg": "examples.01_hello_world", + "port": 9000, + "static_path": "static", + "template_path": "templates", + "swagger_title": "DemoAPI", + "swagger_description": "Demo API", + "swagger_api_version": "1.0.1", + "swagger_contact": "email@qq.com" + } + }, + + "version": "1.0.1" +} diff --git a/examples/01_hello_world/handler.py b/examples/01_hello_world/handler.py new file mode 100644 index 0000000..6c5a403 --- /dev/null +++ b/examples/01_hello_world/handler.py @@ -0,0 +1,16 @@ +from paste.core.logging import echo_log +from paste.web.decorators import route +from paste.web.handler import RequestHandler + + +@route("/hello") +class HelloHandler(RequestHandler): + """ + 演示一个请求。 + """ + async def get(self): + """ + 常规请求。 + """ + echo_log(f"Received request!") + self.response_ok(message="Hello from paste!") \ No newline at end of file diff --git a/examples/01_hello_world/main.py b/examples/01_hello_world/main.py new file mode 100644 index 0000000..aea774f --- /dev/null +++ b/examples/01_hello_world/main.py @@ -0,0 +1,20 @@ +from tornado.ioloop import IOLoop + +from paste.core import config +from paste.core.logging import set_logger_config +from paste.web.application import Application + +if __name__ == "__main__": + # 日志配置 + logger_config_name = 'logger.default' + set_logger_config(logger_config_name) + # 应用配置 + demo_config: dict = config.get_config('tornado.demo', {}) + port = config.get_config('tornado.demo.port', 9000) + # 创建应用 + app = Application(**demo_config) + app.listen(port) + handlers_pkg = config.get_config('tornado.demo.handlers_pkg') + print(f"App {handlers_pkg} is running at http://localhost:{port}") + # 启动监听 + IOLoop.current().start() \ No newline at end of file diff --git a/examples/02_background_task/config.json b/examples/02_background_task/config.json new file mode 100644 index 0000000..4089a4f --- /dev/null +++ b/examples/02_background_task/config.json @@ -0,0 +1,36 @@ +{ + "app_name": "Background Task Demo", + + "logger_desc": "用于日志输出的配置,各服务可以有自己的配置,但要使用独立配置时,必须编写额外代码", + "logger": { + "default": { + "desc": "默认日志配置,该配置小节的名称已经配置在 PASTE 框架中", + "basic": { + "filename": "logs/root.log", + "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + "level": 20 + }, + "filename": "logs/default.log", + "name": "Demo", + "max_bytes": 20971520, + "backup_count": 40 + } + }, + + "tornado_desc": "用于 Tornado 服务的配置,每一项后面允许设置多个服务", + "tornado": { + "demo": { + "autoreload": false, + "handlers_pkg": "examples.02_background_task", + "port": 9000, + "static_path": "static", + "template_path": "templates", + "swagger_title": "DemoAPI", + "swagger_description": "Demo API", + "swagger_api_version": "1.0.1", + "swagger_contact": "email@qq.com" + } + }, + + "version": "1.0.1" +} diff --git a/examples/02_background_task/handler.py b/examples/02_background_task/handler.py new file mode 100644 index 0000000..737e048 --- /dev/null +++ b/examples/02_background_task/handler.py @@ -0,0 +1,34 @@ +import asyncio +import logging + +from paste.core import aio_pool +from paste.core.logging import echo_log +from paste.web.decorators import route +from paste.web.handler import RequestHandler + + +@route("/background") +class HelloHandler(RequestHandler): + """ + 演示一个请求,其中包含异步后台任务。 + """ + + async def background_task(self): + """ + 模拟后台异步处理任务:仅做延时,代表执行数据库写入、消息推送、文件处理等。 + """ + try: + for i in range(10): + echo_log(f"后台任务开始执行-{i}...") + await asyncio.sleep(0.8) # 模拟耗时操作 + echo_log("后台任务完成:模拟处理完毕。") + except Exception as e: + echo_log(f"后台任务异常: {e}", level=logging.ERROR) + + async def get(self): + """ + 常规请求,先执行后台任务,再响应前端,但是不等待任务完成。 + """ + echo_log(f"Received request!") + await aio_pool.run_background_task(self.background_task()) + self.response_ok(message="Response from paste!") \ No newline at end of file diff --git a/examples/02_background_task/main.py b/examples/02_background_task/main.py new file mode 100644 index 0000000..aea774f --- /dev/null +++ b/examples/02_background_task/main.py @@ -0,0 +1,20 @@ +from tornado.ioloop import IOLoop + +from paste.core import config +from paste.core.logging import set_logger_config +from paste.web.application import Application + +if __name__ == "__main__": + # 日志配置 + logger_config_name = 'logger.default' + set_logger_config(logger_config_name) + # 应用配置 + demo_config: dict = config.get_config('tornado.demo', {}) + port = config.get_config('tornado.demo.port', 9000) + # 创建应用 + app = Application(**demo_config) + app.listen(port) + handlers_pkg = config.get_config('tornado.demo.handlers_pkg') + print(f"App {handlers_pkg} is running at http://localhost:{port}") + # 启动监听 + IOLoop.current().start() \ No newline at end of file diff --git a/examples/03_redis_stream/config.json b/examples/03_redis_stream/config.json new file mode 100644 index 0000000..b959ff1 --- /dev/null +++ b/examples/03_redis_stream/config.json @@ -0,0 +1,52 @@ +{ + "app_name": "Redis Stream Demo", + + "redis_desc": "Redis 数据库连接配置及相关描述", + "redis": { + "connection": { + "url": "redis://:HaitenRedis@20250703@100.64.0.1:3379/2", + "max_connections": 1000, + "encoding": "utf-8", + "decode_responses": true + }, + "streams": { + "demo": { + "group": "DEMO_PROCESSORS", + "consumer": "demo_worker" + } + } + }, + + "logger_desc": "用于日志输出的配置,各服务可以有自己的配置,但要使用独立配置时,必须编写额外代码", + "logger": { + "default": { + "desc": "默认日志配置,该配置小节的名称已经配置在 PASTE 框架中", + "basic": { + "filename": "logs/root.log", + "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + "level": 20 + }, + "filename": "logs/default.log", + "name": "Demo", + "max_bytes": 20971520, + "backup_count": 40 + } + }, + + "tornado_desc": "用于 Tornado 服务的配置,每一项后面允许设置多个服务", + "tornado": { + "demo": { + "autoreload": false, + "handlers_pkg": "examples.03_redis_stream", + "port": 9000, + "static_path": "static", + "template_path": "templates", + "swagger_title": "DemoAPI", + "swagger_description": "Demo API", + "swagger_api_version": "1.0.1", + "swagger_contact": "email@qq.com" + } + }, + + "version": "1.0.1" +} diff --git a/examples/03_redis_stream/handler.py b/examples/03_redis_stream/handler.py new file mode 100644 index 0000000..f8232c6 --- /dev/null +++ b/examples/03_redis_stream/handler.py @@ -0,0 +1,69 @@ +import datetime +import json +import logging + +from paste.core.logging import echo_log +from paste.db.redis import StreamActor +from paste.web.decorators import route +from paste.web.handler import RequestHandler + +@route("/stream") +class MessageHandler(RequestHandler): + """ + 演示请求发布 Redis Stream 消息。 + """ + + # 从配置中加载 Stream 配置路径 + stream_config_path = "redis.streams.demo" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # 初始化 StreamActor 实例(按配置创建) + self.actor = StreamActor.new_actor(self.stream_config_path) + + async def post(self): + """ + 接收前端 POST 请求,发布消息到 Redis Stream,立即响应。 + 请求体格式: + { + "user_id": "123", + "event": "login", + "data": {"ip": "192.168.1.1"} + } + """ + try: + # 1. 获取请求参数 + body = self.request_arguments() + user_id = body.get("user_id") + event = body.get("event") + data = body.get("data", {}) + + if not user_id or not event: + self.response_error( + Exception("参数缺失:必须提供 user_id 和 event"), + status_code=400, + api_status_code=400 + ) + return + + # 2. 构造消息数据 + message_data = { + "user_id": user_id, + "event": event, + "timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat() + 'Z', + "data": json.dumps(data) + } + + # 3. 异步发布消息(立即返回,不等待消费) + msg_id = await self.actor.publish(message_data) + + # 4. 响应成功 + self.response_ok( + message="消息已成功发布", + message_id=msg_id, + stream=self.stream_config_path + ) + + except Exception as e: + echo_log('异常', logging.ERROR, True) + self.response_error(e, status_code=500, api_status_code=500) \ No newline at end of file diff --git a/examples/03_redis_stream/main.py b/examples/03_redis_stream/main.py new file mode 100644 index 0000000..aea774f --- /dev/null +++ b/examples/03_redis_stream/main.py @@ -0,0 +1,20 @@ +from tornado.ioloop import IOLoop + +from paste.core import config +from paste.core.logging import set_logger_config +from paste.web.application import Application + +if __name__ == "__main__": + # 日志配置 + logger_config_name = 'logger.default' + set_logger_config(logger_config_name) + # 应用配置 + demo_config: dict = config.get_config('tornado.demo', {}) + port = config.get_config('tornado.demo.port', 9000) + # 创建应用 + app = Application(**demo_config) + app.listen(port) + handlers_pkg = config.get_config('tornado.demo.handlers_pkg') + print(f"App {handlers_pkg} is running at http://localhost:{port}") + # 启动监听 + IOLoop.current().start() \ No newline at end of file diff --git a/examples/03_redis_stream/stream_service.py b/examples/03_redis_stream/stream_service.py new file mode 100644 index 0000000..c30b3e3 --- /dev/null +++ b/examples/03_redis_stream/stream_service.py @@ -0,0 +1,152 @@ +""" +演示 Redis Stream 消息队列服务。 +""" +import asyncio +import logging +import os +import socket +import sys +from typing import Optional + +import redis + +from paste.core import aio_pool +from paste.core.logging import set_logger_config, echo_log, get_logger +from paste.db.redis import StreamActor +from paste.service.daemonize import DaemonizeService + +logger_config_name = 'logger.default' +""" +配置文件中日志配置字段名称。 +""" + +current_event_loop = None +""" +事件循环对象。 +""" + +pid_file = os.path.join(os.path.curdir, 'stream_service.pid') +""" +PID 文件路径。 +""" + +service_name = 'Redis Stream 消息队列服务' +""" +服务名称。 +""" + +# 配置路径:从 config.json 中读取 +stream_config_path = "redis.streams.demo" + +# 创建 StreamActor 实例 +stream_actor: Optional[StreamActor] = None + + +async def process_message(data: dict): + """ + 业务回调:处理每条消息 + """ + user_id = data.get("user_id", "unknown") + event = data.get("event", "") + stream_data = data.get("data", "") + timestamp = data.get("timestamp", "") + + echo_log(f"消费消息: user_id={user_id}, event='{event}', stream_data='{stream_data}', time={timestamp}") + + # 模拟处理:写入数据库、发送邮件、更新缓存... + # 示例:记录日志 + 模拟耗时 + for i in range(10): + echo_log(f"后台任务开始执行-{i}...") + await asyncio.sleep(0.8) + + echo_log(f"消息处理完成: {user_id}") + return True + + +def current_loop() -> asyncio.AbstractEventLoop: + """ + 这里必须采用方法,在适当的时间点创建事件循环对象,否则会导致服务无法启动。 + :return: 事件循环对象 + """ + global current_event_loop + if current_event_loop is None: + current_event_loop = asyncio.new_event_loop() + return current_event_loop + + +def start_service(): + """ + 控制台服务方式启动。 + """ + set_logger_config(logger_config_name) + echo_log(f"正在启动{service_name}...") + + try: + # 检测 Redis 连接 + echo_log('检测 Redis 服务...') + _runner = aio_pool.get_aio_runner() + _runner(StreamActor.ping()) + echo_log('Redis 服务正常.') + + # 创建 StreamActor 监听服务 + global stream_actor + stream_actor = StreamActor.new_actor(stream_config_path) + echo_log(f"{service_name}已启动,正在监听{service_name}...") + _runner(stream_actor.run_forever(process_message, is_delete=True)) + except (redis.exceptions.TimeoutError, socket.timeout): + echo_log('Redis 服务异常.', level=logging.ERROR, is_log_exc=True) + echo_log(f"{service_name}启动失败.") + except KeyboardInterrupt: + echo_log(msg='KeyboardInterrupt') + stop_service() + except Exception as e: + echo_log(msg=e, level=logging.ERROR, is_log_exc=True) + echo_log(f"{service_name}因未知异常启动失败.") + + +def stop_service(): + """ + 停止服务。 + """ + echo_log(f"正在停止{service_name}...") + # 停止监听 + stream_actor.subscribe_stop() + # 停止事件循环 + current_loop().stop() + echo_log(f"{service_name}已停止.") + + +def start(): + """ + 驻内存服务方式启动。 + """ + set_logger_config(logger_config_name) + get_logger() + ds = DaemonizeService(pid_file=pid_file, name=service_name) + ds.set_start_callback(start_service) + ds.set_term_callback(stop_service) + ds.start() + + +def stop(): + """ + 驻内存服务方式停止。 + """ + set_logger_config(logger_config_name) + get_logger() + ds = DaemonizeService(pid_file=pid_file, name=service_name) + ds.set_start_callback(start_service) + ds.set_term_callback(stop_service) + ds.stop() + + +if __name__ == "__main__": + if len(sys.argv) > 1: + if sys.argv[1] == "start": + start_service() + elif sys.argv[1] == "stop": + stop_service() + else: + print("用法: python service/stream_service.py start") + else: + start_service() \ No newline at end of file diff --git a/examples/04_tasks_service/config.json b/examples/04_tasks_service/config.json new file mode 100644 index 0000000..a9aab5f --- /dev/null +++ b/examples/04_tasks_service/config.json @@ -0,0 +1,21 @@ +{ + "app_name": "Paste 测试", + + "logger_desc": "用于日志输出的配置,各服务可以有自己的配置,但要使用独立配置时,必须编写额外代码", + "logger": { + "default": { + "desc": "默认日志配置,该配置小节的名称已经配置在 PASTE 框架中", + "basic": { + "filename": "logs/root.log", + "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + "level": 20 + }, + "filename": "logs/default.log", + "name": "Demo", + "max_bytes": 20971520, + "backup_count": 40 + } + }, + + "version": "1.0.1" +} diff --git a/examples/04_tasks_service/task_service.py b/examples/04_tasks_service/task_service.py new file mode 100644 index 0000000..18cfa7e --- /dev/null +++ b/examples/04_tasks_service/task_service.py @@ -0,0 +1,96 @@ +""" +系统服务,用于读取服务配置文件,启动或停止相关的服务。 +""" +import asyncio +import os +import sys +from typing import Optional + +from paste.core.logging import echo_log, set_logger_config +from paste.service.task_service import TaskService + +logger_config_name = 'logger.default' +""" +配置文件中日志配置字段名称。 +""" + +task_serv: Optional[TaskService] = None +""" +任务服务对象。 +""" + +pid_file = os.path.join(os.path.curdir, 'task_service.pid') +""" +PID 文件路径。 +""" + +service_name = '计划任务服务' +""" +服务名称。 +""" + + +def init_task_service(): + """ + 初始化服务对象并安装具体任务。 + """ + global task_serv + task_serv = TaskService(service_name=service_name, pid_file=pid_file) + + # 每隔 2 秒钟执行 + task_serv.add_task(creator=task_serv.create_delay_task, fn=renew_token, delay=2) + + return task_serv + + +async def renew_token(): + """ + 演示更新 Token + """ + echo_log(f"执行:更新 Token.") + + _renewed = False + for i in range(2): + echo_log(f"后台任务开始执行-{i}...") + await asyncio.sleep(0.5) # 模拟耗时操作 + _renewed = True + echo_log(f"更新处理完成,{'已' if _renewed else '未'}更新.") + + +def start_service(): + """ + 控制台服务方式启动。 + """ + set_logger_config(logger_config_name) + _ts = init_task_service() + _ts.start_service(env_check=False) + + +def start(): + """ + 驻内存服务方式启动。 + """ + set_logger_config(logger_config_name) + _ts = init_task_service() + _ts.start() + + +def stop(): + """ + 驻内存服务方式停止。 + """ + set_logger_config(logger_config_name) + _ts = init_task_service() + _ts.stop() + + +if __name__ == "__main__": + if len(sys.argv) > 1: + if sys.argv[1] == "start": + start_service() + elif sys.argv[1] == "stop": + stop() + else: + print("用法: python service/tsk_service.py start") + else: + start_service() diff --git a/examples/05_gen_models/config.json b/examples/05_gen_models/config.json new file mode 100644 index 0000000..b2bb0f4 --- /dev/null +++ b/examples/05_gen_models/config.json @@ -0,0 +1,45 @@ +{ + "app_name": "Paste 测试", + + "db_engine_desc": "数据库连接信息,包含普通连接、异步连接以及连接选项,其中连接选项的配置必须对应 create_engine 或 create_async_engine 方法参数,后面加 _xx 后缀的,仅用于保存信息", + "db_engine": { + "engine": "mysql+pymysql://haiten:HaitenDB%4020250702@100.64.0.1:3360/haiten", + "async_engine": "mysql+aiomysql://haiten:HaitenDB%4020250702@100.64.0.1:3360/haiten", + "engine_option": { + "echo": false, + "pool_pre_ping": true, + "pool_size": 20, + "max_overflow": 200 + } + }, + + "logger_desc": "用于日志输出的配置,各服务可以有自己的配置,但要使用独立配置时,必须编写额外代码", + "logger": { + "default": { + "desc": "默认日志配置,该配置小节的名称已经配置在 PASTE 框架中", + "basic": { + "filename": "logs/root.log", + "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + "level": 20 + }, + "filename": "logs/default.log", + "name": "Demo", + "max_bytes": 20971520, + "backup_count": 40 + } + }, + + "rbac_desc": "RBAC 基础信息配置", + "rbac": { + "table": { + "assignment": "hat_auth_assignment", + "item": "hat_auth_item", + "item_child": "hat_auth_item_child", + "rule": "hat_auth_rule", + "user": "hat_user" + }, + "user_class": "paste.rbac.rbac_user.RbacUser" + }, + + "version": "1.0.1" +} diff --git a/examples/05_gen_models/main.py b/examples/05_gen_models/main.py new file mode 100644 index 0000000..9fe01ea --- /dev/null +++ b/examples/05_gen_models/main.py @@ -0,0 +1,11 @@ +from paste.core import aio_pool +from paste.core.logging import set_logger_config +from paste.db import gen_models + +if __name__ == "__main__": + # 日志配置 + logger_config_name = 'logger.default' + set_logger_config(logger_config_name) + # 生成模型代码 + _runner = aio_pool.get_aio_runner() + _runner(gen_models.sqlacodegen()) \ No newline at end of file diff --git a/examples/05_gen_models/models/db_models.py b/examples/05_gen_models/models/db_models.py new file mode 100644 index 0000000..9776c9f --- /dev/null +++ b/examples/05_gen_models/models/db_models.py @@ -0,0 +1,576 @@ +# coding: utf-8 +from sqlalchemy import CheckConstraint, Column, Date, DateTime, Float, ForeignKey, Index, String, TIMESTAMP, Text, text +from sqlalchemy.dialects.mysql import BIGINT, INTEGER, MEDIUMTEXT +from sqlalchemy.orm import relationship +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() +metadata = Base.metadata + + +class HatArticle(Base): + __tablename__ = 'hat_article' + __table_args__ = {'comment': '文章'} + + id = Column(BIGINT(20), primary_key=True, comment='ID') + title = Column(String(300), comment='标题') + content = Column(MEDIUMTEXT, comment='内容') + cover_image = Column(String(300), nullable=False, server_default=text("''"), comment='封面图片路径') + overview = Column(String(300), nullable=False, server_default=text("''"), comment='概述') + type = Column(String(50), nullable=False, server_default=text("'采编'"), comment='类型:原创、转载、首发、采编') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='修改者') + + +class HatArticleCategory(Base): + __tablename__ = 'hat_article_category' + __table_args__ = {'comment': '文章类别表'} + + id = Column(BIGINT(20), primary_key=True, comment='ID') + category_name = Column(String(64), nullable=False, unique=True, server_default=text("''"), comment='类别名称') + parent_id = Column(BIGINT(20), nullable=False, server_default=text("0"), comment='父类别ID(默认为0)') + description = Column(String(500), nullable=False, server_default=text("''"), comment='类别描述') + sort_order = Column(INTEGER(11), nullable=False, server_default=text("1"), comment='排序值') + status = Column(INTEGER(11), nullable=False, index=True, server_default=text("0"), comment='状态(默认0,锁定1)') + created_at = Column(TIMESTAMP, nullable=False, server_default=text("current_timestamp()")) + created_by = Column(String(64), nullable=False, server_default=text("'API'")) + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()")) + updated_by = Column(String(64), nullable=False, server_default=text("'API'")) + + +class HatClass(Base): + __tablename__ = 'hat_classes' + __table_args__ = {'comment': '班级表'} + + id = Column(BIGINT(20), primary_key=True, comment='系统编号') + name = Column(String(100), nullable=False, comment='班级名称') + year = Column(String(100), nullable=False, comment='班级年份') + adviser = Column(String(255), nullable=False, comment='辅导员') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='修改者') + + +class HatClassroom(Base): + __tablename__ = 'hat_classroom' + __table_args__ = {'comment': '教室'} + + id = Column(BIGINT(20), primary_key=True, comment='系统编号') + name = Column(String(50), nullable=False, comment='教室名称') + total = Column(INTEGER(10), nullable=False, server_default=text("60"), comment='容纳人数') + description = Column(String(500), comment='描述') + + +class HatCourse(Base): + __tablename__ = 'hat_course' + __table_args__ = {'comment': '课程表,在专业教学计划表,学分对接表中关联'} + + id = Column(BIGINT(20), primary_key=True, comment='系统编号') + name = Column(String(255), nullable=False, unique=True, comment='课程名称') + name_en = Column(String(255), comment='英文名称') + code = Column(String(50), nullable=False, comment='课程代码') + material = Column(String(1000), comment='所选教材') + description = Column(Text, comment='课程描述') + category = Column(String(50), nullable=False, comment='授课方(中方课程或外方课程)') + status = Column(INTEGER(20), nullable=False, server_default=text("10"), comment='当前状态') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='修改者') + + +class HatCourseSchedule(Base): + __tablename__ = 'hat_course_schedule' + __table_args__ = {'comment': '课表'} + + id = Column(BIGINT(20), primary_key=True, comment='系统编号') + academic_year = Column(String(32), nullable=False, comment='学年') + semester = Column(String(64), nullable=False, comment='上课学期') + status = Column(INTEGER(20), nullable=False, server_default=text("10"), comment='状态') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='修改者') + + +class HatEnrollStudent(Base): + __tablename__ = 'hat_enroll_student' + __table_args__ = ( + Index('hat_enroll_student_id_card_number_phone_un', 'id_card_number', 'phone', unique=True), + {'comment': '学生报名信息表'} + ) + + id = Column(BIGINT(20), primary_key=True, comment='ID') + student_number = Column(String(50), nullable=False, index=True, comment='学号') + name = Column(String(128), nullable=False, index=True, comment='姓名') + gender = Column(String(10), nullable=False, comment='性别') + native_place = Column(String(50), comment='籍贯') + id_card_number = Column(String(50), nullable=False, comment='身份证') + province = Column(String(128), nullable=False, index=True, comment='省') + city = Column(String(128), comment='市') + date_of_birth = Column(Date, comment='出生年月') + politics_status = Column(String(50), comment='政治面貌') + nation = Column(String(128), comment='民族') + house_address = Column(String(255), nullable=False, comment='家庭地址') + post_code = Column(String(10), nullable=False, comment='邮政编码') + phone = Column(String(50), nullable=False, comment='学生手机') + educational_level = Column(String(128), comment='文化程度') + school_of_graduation = Column(String(128), index=True, comment='毕业学校') + graduate_date = Column(Date, comment='毕业日期') + awards = Column(String(128), comment='奖励') + hobby = Column(String(128), comment='爱好特长') + cee_id = Column(String(50), comment='准考证号') + cee_scores = Column(String(50), comment='高考总分') + cee_english = Column(String(50), comment='英语成绩') + cee_chinese = Column(String(50), comment='语文成绩') + cee_math = Column(String(50), comment='数学成绩') + cee_type = Column(String(50), comment='高考科类') + ielts = Column(String(50), comment='雅思成绩') + no_cee_reasons = Column(String(128), comment='不参加高考原因') + major = Column(String(128), comment='首选专业') + major2 = Column(String(128), comment='次选专业') + major3 = Column(String(128), comment='再选专业') + accommodation = Column(String(10), comment='是否住宿') + allocate = Column(String(50), comment='服从调配') + abroad = Column(String(50), comment='出国留学') + parent_name1 = Column(String(128), nullable=False, comment='家长姓名') + parent_phone1 = Column(String(50), nullable=False, comment='电话') + relation1 = Column(String(10), comment='关系') + parent_name2 = Column(String(128), comment='家长姓名') + parent_phone2 = Column(String(50), comment='电话') + relation2 = Column(String(10), comment='关系') + information_source = Column(String(128), comment='信息来源') + referrer = Column(String(128), comment='推荐人') + admission_major = Column(String(128), comment='录取专业') + admission_at = Column(DateTime, comment='录取时间') + intensive_training = Column(String(50), comment='强化训练') + status = Column(INTEGER(20), nullable=False, server_default=text("10"), comment='当前状态') + remark = Column(String(255), comment='备注') + created_at = Column(DateTime, nullable=False, comment='创建时间') + + +class HatMajor(Base): + __tablename__ = 'hat_major' + __table_args__ = {'comment': '专业'} + + id = Column(BIGINT(20), primary_key=True, comment='系统编号') + name = Column(String(255), nullable=False, unique=True, comment='专业名称') + name_en = Column(String(255), comment='英文名称') + code = Column(String(255), nullable=False, comment='专业代码') + description = Column(Text, comment='介绍') + discipline = Column(String(255), nullable=False, comment='学科门类') + status = Column(INTEGER(20), nullable=False, server_default=text("10"), comment='当前状态') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='修改者') + + +class HatPerson(Base): + __tablename__ = 'hat_person' + __table_args__ = ( + Index('hat_person_name_cer_idx', 'name', 'cer_no', 'cer_type_name'), + {'comment': '企业人员'} + ) + + id = Column(BIGINT(20), primary_key=True, comment='ID') + name = Column(String(100), nullable=False, server_default=text("''"), comment='姓名') + sex = Column(String(1), nullable=False, server_default=text("''"), comment='性别') + cer_type_name = Column(String(100), nullable=False, server_default=text("''"), comment='身份证件类型') + cer_no = Column(String(40), nullable=False, server_default=text("''"), comment='证件号') + tel = Column(String(110), nullable=False, server_default=text("''"), comment='联系电话') + school = Column(String(200), nullable=False, server_default=text("''"), comment='毕业院校') + edu_bac = Column(String(20), nullable=False, server_default=text("''"), comment='文化程度') + major = Column(String(30), nullable=False, server_default=text("''"), comment='所学专业') + lite_deg = Column(String(2), nullable=False, server_default=text("''"), comment='文化程度') + edu_bac_code = Column(String(30), nullable=False, server_default=text("''"), comment='学历') + title = Column(String(40), nullable=False, server_default=text("''"), comment='职称') + com_addr = Column(String(512), nullable=False, server_default=text("''"), comment='通信地址') + postal_code = Column(String(6), nullable=False, server_default=text("''"), comment='邮编编码') + email = Column(String(100), nullable=False, server_default=text("''"), comment='电子邮箱') + status = Column(String(64), nullable=False, server_default=text("''"), comment='状态') + avatar = Column(String(300), server_default=text("''"), comment='头像') + created_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='修改者') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + + +class HatStudent(Base): + __tablename__ = 'hat_student' + __table_args__ = {'comment': '学生信息表'} + + id = Column(BIGINT(20), primary_key=True, comment='ID') + student_number = Column(String(50), nullable=False, comment='学号') + name = Column(String(128), nullable=False, comment='姓名') + gender = Column(String(10), nullable=False, comment='性别') + native_place = Column(String(50), comment='籍贯') + id_card_number = Column(String(50), nullable=False, comment='身份证') + province = Column(String(128), nullable=False, comment='省') + city = Column(String(128), comment='市') + date_of_birth = Column(Date, comment='出生年月') + politics_status = Column(String(50), comment='政治面貌') + nation = Column(String(128), comment='民族') + house_address = Column(String(255), nullable=False, comment='家庭地址') + post_code = Column(String(10), nullable=False, comment='邮政编码') + phone = Column(String(50), nullable=False, comment='学生手机') + educational_level = Column(String(128), comment='文化程度') + school_of_graduation = Column(String(128), comment='毕业学校') + graduate_time = Column(Date, comment='毕业时间') + awards = Column(String(128), comment='奖励') + hobby = Column(String(128), comment='爱好特长') + cee_id = Column(String(50), comment='准考证号') + cee_scores = Column(String(50), comment='高考总分') + cee_english = Column(String(50), comment='英语成绩') + cee_chinese = Column(String(50), comment='语文成绩') + cee_math = Column(String(50), comment='数学成绩') + cee_type = Column(String(50), comment='高考科类') + ielts = Column(String(50), comment='雅思成绩') + major = Column(String(128), comment='专业') + allocate = Column(String(50), comment='服从调配') + abroad = Column(String(50), comment='出国留学') + parent_name1 = Column(String(128), nullable=False, comment='家长姓名') + parent_phone1 = Column(String(50), nullable=False, comment='电话') + relation1 = Column(String(10), comment='关系') + parent_name2 = Column(String(128), comment='家长姓名') + parent_phone2 = Column(String(50), comment='电话') + relation2 = Column(String(10), comment='关系') + intensive_training = Column(String(50), comment='强化训练') + status = Column(INTEGER(20), nullable=False, server_default=text("10"), comment='当前状态') + created_at = Column(DateTime, nullable=False, comment='创建时间') + + +class HatUser(Base): + __tablename__ = 'hat_user' + __table_args__ = {'comment': '用户'} + + id = Column(BIGINT(20), primary_key=True, comment='系统编号') + username = Column(String(255), nullable=False, unique=True, comment='用户名') + password_hash = Column(String(255), nullable=False, comment='密码') + password_reset_token = Column(String(255), comment='重置标记') + auth_key = Column(String(255), comment='授权码') + status = Column(INTEGER(11), nullable=False, server_default=text("0"), comment='用户状态') + type = Column(String(64), nullable=False, server_default=text("'user'"), comment='用户类型') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='更新时间') + + +class HatArticlePublish(Base): + __tablename__ = 'hat_article_publish' + __table_args__ = ( + Index('hat_article_publish_un', 'article_id', 'category_id', unique=True), + {'comment': '文章发布表'} + ) + + id = Column(BIGINT(20), primary_key=True, comment='ID') + article_id = Column(ForeignKey('hat_article.id', ondelete='CASCADE'), nullable=False, comment='文章ID') + category_id = Column(ForeignKey('hat_article_category.id', ondelete='CASCADE'), nullable=False, index=True, comment='文章类别ID') + sort_order = Column(INTEGER(11), nullable=False, index=True, server_default=text("0"), comment='排序') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='修改者') + + article = relationship('HatArticle') + category = relationship('HatArticleCategory') + + +class HatClassesStudent(Base): + __tablename__ = 'hat_classes_student' + __table_args__ = ( + Index('classes_id_student_id_un', 'class_id', 'student_id', unique=True), + {'comment': '班级学生关系表'} + ) + + id = Column(BIGINT(20), primary_key=True, comment='系统编号') + class_id = Column(ForeignKey('hat_classes.id', ondelete='CASCADE'), nullable=False, comment='班级编号') + student_id = Column(ForeignKey('hat_student.id', ondelete='CASCADE'), nullable=False, index=True, comment='学生编号') + created_at = Column(DateTime, nullable=False, comment='创建时间') + updated_at = Column(DateTime, nullable=False, comment='更新时间') + + _class = relationship('HatClass') + student = relationship('HatStudent') + + +class HatCourseScheduleDetail(Base): + __tablename__ = 'hat_course_schedule_detail' + __table_args__ = {'comment': '课表明细'} + + id = Column(BIGINT(20), primary_key=True, comment='系统编号') + schedule_id = Column(ForeignKey('hat_course_schedule.id', ondelete='CASCADE'), nullable=False, index=True, comment='课表') + course_id = Column(ForeignKey('hat_course.id'), nullable=False, index=True, comment='课程') + classroom_id = Column(ForeignKey('hat_classroom.id'), nullable=False, index=True, comment='教室') + teacher = Column(String(255), nullable=False, comment='任课老师') + week_day = Column(INTEGER(20), nullable=False, comment='上课日期,0~1为周日~周六') + sequence = Column(INTEGER(20), nullable=False, comment='序号,从1开始,代表是一天中的第几节课') + + classroom = relationship('HatClassroom') + course = relationship('HatCourse') + schedule = relationship('HatCourseSchedule') + + +class HatExamPaper(Base): + __tablename__ = 'hat_exam_paper' + __table_args__ = {'comment': '考卷'} + + id = Column(BIGINT(20), primary_key=True, comment='系统编号') + name = Column(String(255), nullable=False, comment='考卷名称') + course_id = Column(ForeignKey('hat_course.id'), nullable=False, index=True, comment='考试课程') + score = Column(Float(asdecimal=True), server_default=text("0"), comment='分值') + status = Column(INTEGER(20), nullable=False, server_default=text("10"), comment='当前状态') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='修改者') + + course = relationship('HatCourse') + + +class HatExamination(Base): + __tablename__ = 'hat_examination' + __table_args__ = {'comment': '考务表'} + + id = Column(BIGINT(20), primary_key=True, comment='系统编号') + course_id = Column(ForeignKey('hat_course.id'), nullable=False, index=True, comment='考试课程') + academic_year = Column(String(200), nullable=False, comment='学年') + semester = Column(String(200), nullable=False, comment='学期') + exam_time = Column(DateTime, nullable=False, comment='考试时间') + classroom_id = Column(ForeignKey('hat_classroom.id'), index=True, comment='考场教室') + exam_paper_id = Column(BIGINT(20), comment='考卷') + exam_format = Column(String(50), nullable=False, comment='考试形式,线下、线上') + exam_method = Column(String(50), nullable=False, comment='考试方式,开卷、闭卷') + exam_type = Column(String(50), nullable=False, comment='考试性质,入学、期中、期末、补考、重修') + time_length = Column(INTEGER(20), server_default=text("60"), comment='考试时长') + chief_examiner = Column(String(50), nullable=False, comment='主考老师') + invigilator = Column(String(50), comment='监考老师') + status = Column(INTEGER(20), nullable=False, server_default=text("10"), comment='当前状态') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='修改者') + + classroom = relationship('HatClassroom') + course = relationship('HatCourse') + + +class HatQuestionMaterial(Base): + __tablename__ = 'hat_question_material' + __table_args__ = {'comment': '考题素材(考题用到的素材,目前仅支持文字素材,可增加附件用于增加其他素材,如图片、声音等)'} + + id = Column(BIGINT(20), primary_key=True, comment='系统编号') + title = Column(String(255), nullable=False, comment='标题') + content = Column(Text, nullable=False, comment='文章内容') + course_id = Column(ForeignKey('hat_course.id'), nullable=False, index=True, comment='课程') + status = Column(INTEGER(20), nullable=False, server_default=text("10"), comment='当前状态') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='修改者') + + course = relationship('HatCourse') + + +class HatStudentPortrait(Base): + __tablename__ = 'hat_student_portrait' + __table_args__ = {'comment': '学生头像'} + + id = Column(BIGINT(20), primary_key=True, comment='系统编号') + student_id = Column(ForeignKey('hat_student.id', ondelete='CASCADE'), nullable=False, unique=True, comment='考生') + portrait = Column(String(500), nullable=False, comment='头像') + created_at = Column(DateTime, nullable=False, comment='创建时间') + updated_at = Column(DateTime, nullable=False, comment='更新时间') + + student = relationship('HatStudent') + + +class HatStudentUnusual(Base): + __tablename__ = 'hat_student_unusual' + __table_args__ = {'comment': '学生异动'} + + id = Column(BIGINT(20), primary_key=True, comment='系统编号') + student_id = Column(ForeignKey('hat_student.id'), nullable=False, index=True, comment='考生') + type = Column(String(100), nullable=False, comment='异动类型') + memo = Column(String(1024), comment='备注') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='修改者') + + student = relationship('HatStudent') + + +class HatUserPerson(Base): + __tablename__ = 'hat_user_person' + __table_args__ = {'comment': '用户人员'} + + id = Column(BIGINT(20), primary_key=True, comment='ID') + user_id = Column(ForeignKey('hat_user.id'), nullable=False, unique=True, comment='用户ID') + person_id = Column(ForeignKey('hat_person.id', ondelete='CASCADE', onupdate='CASCADE'), nullable=False, index=True, comment='人员ID') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='更新时间') + + person = relationship('HatPerson') + user = relationship('HatUser') + + +class HatEnrollStudentExam(Base): + __tablename__ = 'hat_enroll_student_exam' + __table_args__ = ( + Index('hat_enroll_student_exam_un', 'examination_id', 'student_id', unique=True), + {'comment': '参加入学考试的学生'} + ) + + id = Column(BIGINT(20), primary_key=True, comment='系统编号') + examination_id = Column(ForeignKey('hat_examination.id'), nullable=False, comment='考务编号') + student_id = Column(ForeignKey('hat_enroll_student.id'), nullable=False, index=True, comment='考生') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='修改者') + + examination = relationship('HatExamination') + student = relationship('HatEnrollStudent') + + +class HatEnrollStudentScore(Base): + __tablename__ = 'hat_enroll_student_score' + __table_args__ = ( + CheckConstraint('json_valid(`answer`)'), + CheckConstraint('json_valid(`question_score`)'), + Index('hat_enroll_student_score_un', 'examination_id', 'student_id', unique=True), + {'comment': '入学考试成绩'} + ) + + id = Column(BIGINT(20), primary_key=True, comment='系统编号') + examination_id = Column(ForeignKey('hat_examination.id'), nullable=False, comment='考务安排') + student_id = Column(ForeignKey('hat_enroll_student.id'), nullable=False, index=True, comment='考生') + started_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='考试开始时间') + submit_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='交卷时间') + submit_method = Column(String(16), nullable=False, server_default=text("'N'"), comment='交卷方式') + answer = Column(Text, nullable=False, comment='答案(JSON数据)') + question_score = Column(Text, comment='各题得分') + test_score = Column(String(100), nullable=False, comment='考试成绩') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='更新时间') + + examination = relationship('HatExamination') + student = relationship('HatEnrollStudent') + + +class HatQuestion(Base): + __tablename__ = 'hat_question' + __table_args__ = {'comment': '考题(可关联到素材表,对关联到同一素材的所有考题,按照时间先后排序)'} + + id = Column(BIGINT(20), primary_key=True, comment='系统编号') + question = Column(String(1800), nullable=False, comment='提问') + options = Column(String(1800), comment='选项(json)') + answer = Column(String(1800), nullable=False, comment='答案(json)') + category = Column(String(50), nullable=False, comment='题型(判断题、单项选择题、多项选择题、不定项选择题、填空题、完形填空、阅读理解、简答题、论述题、作文)') + course_id = Column(ForeignKey('hat_course.id'), nullable=False, index=True, comment='课程') + material_id = Column(ForeignKey('hat_question_material.id'), index=True, comment='素材') + score = Column(Float(asdecimal=True), server_default=text("0"), comment='分值') + difficulty = Column(Float(asdecimal=True), server_default=text("1"), comment='难度系数') + status = Column(INTEGER(20), nullable=False, server_default=text("10"), comment='当前状态') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='修改者') + + course = relationship('HatCourse') + material = relationship('HatQuestionMaterial') + + +class HatStudentAttendance(Base): + __tablename__ = 'hat_student_attendance' + __table_args__ = {'comment': '学生考勤'} + + id = Column(BIGINT(20), primary_key=True, comment='系统编号') + student_id = Column(ForeignKey('hat_student.id'), nullable=False, index=True, comment='考生') + schedule_detail_id = Column(ForeignKey('hat_course_schedule_detail.id'), nullable=False, index=True, comment='课表明细编号') + sequence = Column(INTEGER(20), nullable=False, comment='序号') + time_at = Column(DateTime, nullable=False, comment='考勤时间') + type = Column(String(100), nullable=False, comment='考勤类型') + memo = Column(String(1024), comment='备注') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='修改者') + + schedule_detail = relationship('HatCourseScheduleDetail') + student = relationship('HatStudent') + + +class HatStudentExam(Base): + __tablename__ = 'hat_student_exam' + __table_args__ = ( + Index('hat_student_exam_un', 'examination_id', 'student_id', unique=True), + {'comment': '参加考试的学生'} + ) + + id = Column(BIGINT(20), primary_key=True, comment='系统编号') + examination_id = Column(ForeignKey('hat_examination.id', ondelete='CASCADE'), nullable=False, comment='考务编号') + student_id = Column(ForeignKey('hat_student.id', ondelete='CASCADE'), nullable=False, index=True, comment='考生') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'API'"), comment='修改者') + + examination = relationship('HatExamination') + student = relationship('HatStudent') + + +class HatStudentScore(Base): + __tablename__ = 'hat_student_score' + __table_args__ = ( + CheckConstraint('json_valid(`answer`)'), + CheckConstraint('json_valid(`question_score`)'), + Index('hat_student_score_un', 'examination_id', 'student_id', unique=True), + {'comment': '考试成绩'} + ) + + id = Column(BIGINT(20), primary_key=True, comment='系统编号') + examination_id = Column(ForeignKey('hat_examination.id'), nullable=False, comment='考务安排') + student_id = Column(ForeignKey('hat_student.id'), nullable=False, index=True, comment='考生') + started_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='考试开始时间') + submit_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='交卷时间') + submit_method = Column(String(16), nullable=False, server_default=text("'N'"), comment='交卷方式') + answer = Column(Text, nullable=False, comment='答案(JSON数据)') + question_score = Column(Text, comment='各题得分') + test_score = Column(String(100), nullable=False, comment='考试成绩') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='更新时间') + + examination = relationship('HatExamination') + student = relationship('HatStudent') + + +class HatStudentUnusualAttachment(Base): + __tablename__ = 'hat_student_unusual_attachment' + __table_args__ = {'comment': '学生异动附件'} + + id = Column(BIGINT(20), primary_key=True, comment='系统编号') + unusual_id = Column(ForeignKey('hat_student_unusual.id', ondelete='CASCADE', onupdate='CASCADE'), nullable=False, index=True, comment='异动编号') + name = Column(String(255), nullable=False, comment='附件名称') + created_at = Column(DateTime, nullable=False, comment='创建时间') + updated_at = Column(DateTime, nullable=False, comment='更新时间') + + unusual = relationship('HatStudentUnusual') + + +class HatExamPaperQuestion(Base): + __tablename__ = 'hat_exam_paper_question' + __table_args__ = ( + Index('hat_exam_paper_question_un', 'exam_paper_id', 'question_id', unique=True), + {'comment': '考卷考题'} + ) + + id = Column(BIGINT(20), primary_key=True, comment='系统编号') + exam_paper_id = Column(ForeignKey('hat_exam_paper.id', ondelete='CASCADE', onupdate='CASCADE'), nullable=False, comment='考卷') + question_id = Column(ForeignKey('hat_question.id'), nullable=False, index=True, comment='考题') + sort = Column(INTEGER(20), nullable=False, server_default=text("0"), comment='排序') + + exam_paper = relationship('HatExamPaper') + question = relationship('HatQuestion') diff --git a/examples/06_chart_example/chart_bar_example.py b/examples/06_chart_example/chart_bar_example.py new file mode 100644 index 0000000..05d628d --- /dev/null +++ b/examples/06_chart_example/chart_bar_example.py @@ -0,0 +1,153 @@ +import numpy as np +import os +import traceback +from paste.chart.bar import ( + gen_vertical_bars, + gen_horizontal_stacked_bars, + gen_percent_stacked_bars +) + + +class ChartBarExample: + """ + 图表测试管理器:封装对 paste.chart.bar 中三个函数的调用。 + 不修改任何参数结构,仅提供清晰的调用封装与输出管理。 + """ + + def __init__(self, output_directory="./charts"): + """ + 初始化测试器,定义所有测试数据。 + 数据结构完全匹配原始函数调用方式。 + """ + self.output_directory = output_directory + os.makedirs(self.output_directory, exist_ok=True) + + # 纵向堆叠柱形图数据(直接对应 gen_vertical_bars 参数) + self.primary_vals = [10, 20, 15, 25, 18] + self.nested_vals = [5, 8, 3, 10, 6] + self.x_labels_vert = ['产品1', '产品2', '产品3', '产品4', '产品5'] + self.group_labels_vert = ['销售量', '退货量'] + + # 横向堆叠柱形图数据(直接对应 gen_horizontal_stacked_bars 参数) + self.data_matrix = np.array([ + [10, 20, 15], + [15, 12, 18], + [8, 16, 10], + [12, 14, 13] + ]) + self.x_labels_hori = ['线上销售', '门店销售', '批发销售'] + self.y_labels_hori = ['北京', '上海', '广州', '深圳'] + self.y_data_unit_hori = '万元' + self.title_hori = '销售构成' + + # 百分比堆叠柱形图数据(直接对应 gen_percent_stacked_bars 参数) + self.data_percent = { + 'A组': [10, 20, 15, 18], + 'B组': [5, 10, 5, 8], + 'C组': [3, 7, 10, 4] + } + self.x_labels_percent = ['Q1', 'Q2', 'Q3', 'Q4'] + self.title_percent = '季度占比' + + def generate_vertical_bars(self) -> str: + """调用 gen_vertical_bars,参数完全一致""" + try: + return gen_vertical_bars( + self.primary_vals, + self.nested_vals, + self.x_labels_vert, + self.group_labels_vert + ) + except Exception as e: + print(f"纵向堆叠柱形图生成失败: {e}") + traceback.print_exc() + raise + + def generate_horizontal_stacked_bars(self) -> str: + """调用 gen_horizontal_stacked_bars,参数完全一致""" + try: + return gen_horizontal_stacked_bars( + self.data_matrix, + self.x_labels_hori, + self.y_labels_hori, + self.y_data_unit_hori, + self.title_hori + ) + except Exception as e: + print(f"横向堆叠柱形图生成失败: {e}") + traceback.print_exc() + raise + + def generate_percent_stacked_bars(self) -> str: + """调用 gen_percent_stacked_bars,参数完全一致""" + try: + return gen_percent_stacked_bars( + self.data_percent, + self.x_labels_percent, + self.title_percent + ) + except Exception as e: + print(f"百分比堆叠柱形图生成失败: {e}") + traceback.print_exc() + raise + + def save_svg(self, svg_data: str, filename: str) -> None: + """ + 将 SVG 的 base64 Data URL 写入文件(保留原始 SVG 格式)。 + 注意:svg_data 是 "data:image/svg+xml;base64,...",需提取真实 SVG 内容。 + """ + if not svg_data or not isinstance(svg_data, str): + print(f"生成的 SVG 数据无效(为空或非字符串): {filename}") + return + + # 提取 base64 编码部分(去除 data URL 前缀) + if svg_data.startswith("data:image/svg+xml;base64,"): + base64_content = svg_data[len("data:image/svg+xml;base64,"):] + try: + # 解码 base64 得到原始 SVG 字符串 + import base64 + svg_content = base64.b64decode(base64_content).decode('utf-8') + except Exception as e: + print(f"解码 base64 失败: {e}") + svg_content = svg_data # 退化为直接写入 + else: + # 如果不是标准格式,直接写入(兼容调试) + svg_content = svg_data + + filepath = os.path.join(self.output_directory, filename) + with open(filepath, 'w', encoding='utf-8') as f: + f.write(svg_content) + print(f"已保存: {filepath}") + + def run(self) -> None: + """按顺序执行所有图表生成与保存""" + print("开始生成图表...") + try: + print("生成纵向堆叠柱形图...") + svg1 = self.generate_vertical_bars() + self.save_svg(svg1, "vertical_bars.svg") + + print("生成横向堆叠柱形图...") + svg2 = self.generate_horizontal_stacked_bars() + self.save_svg(svg2, "horizontal_stacked_bars.svg") + + print("生成百分比堆叠柱形图...") + svg3 = self.generate_percent_stacked_bars() + self.save_svg(svg3, "percent_stacked_bars.svg") + + print("\n所有图表已成功生成。") + print(f"输出目录: {self.output_directory}") + print("文件列表:") + print(" - vertical_bars.svg") + print(" - horizontal_stacked_bars.svg") + print(" - percent_stacked_bars.svg") + + except Exception as e: + print(f"\n测试失败: {e}") + traceback.print_exc() + + +# 程序入口 +if __name__ == "__main__": + tester = ChartBarExample() + tester.run() \ No newline at end of file diff --git a/examples/06_chart_example/chart_pie_example.py b/examples/06_chart_example/chart_pie_example.py new file mode 100644 index 0000000..d5a61f2 --- /dev/null +++ b/examples/06_chart_example/chart_pie_example.py @@ -0,0 +1,110 @@ +import os +import traceback +from paste.chart.pie import gen_pie + + +class ChartPieExample: + """ + 环形图测试管理器:封装对 paste.chart.pie.gen_pie 的调用。 + 数据结构严格匹配函数参数要求,支持扩展更多测试用例。 + """ + + def __init__(self, output_directory="./charts"): + """ + 初始化测试器,定义所有测试数据。 + 数据结构完全匹配 gen_pie 函数的参数要求。 + """ + self.output_directory = output_directory + os.makedirs(self.output_directory, exist_ok=True) + + # 构造符合 gen_pie 要求的 DataFrame 数据(模拟真实业务场景) + # 假设业务场景:网络设备统计(服务器、交换机、路由器等) + import pandas as pd + self.data_df = pd.DataFrame({ + 'device_count': [35, 28, 22, 15, 10], # value_column + 'percentage': ['35.2%', '28.1%', '22.0%', '15.0%', '9.7%'], # percentage_column + 'device_type': ['服务器', '交换机', '路由器', '防火墙', 'AP'] # legend_labels + }) + + # 测试参数 + self.value_column = 'device_count' + self.percentage_column = 'percentage' + self.legend_labels = 'device_type' + self.color_palette = 'BuPu' # 可尝试 'viridis', 'Set3', 'plasma' + self.dpi = 128 + + # 输出文件名 + self.filename = "pie_chart.svg" + + def generate_pie_chart(self) -> str: + """ + 调用 gen_pie 函数,参数完全一致。 + 注意:gen_pie 接收的是 pandas.DataFrame,不是列表。 + """ + try: + svg_data = gen_pie( + data_df=self.data_df, + value_column=self.value_column, + percentage_column=self.percentage_column, + legend_labels=self.legend_labels, + color_palette=self.color_palette, + dpi=self.dpi + ) + if not svg_data or not isinstance(svg_data, str): + raise ValueError("gen_pie 返回的 SVG 数据为空或类型错误") + return svg_data + except Exception as e: + print(f"环形图生成失败: {e}") + traceback.print_exc() + raise + + def save_svg(self, svg_data: str, filename: str) -> None: + """ + 将 SVG 的 base64 Data URL 写入文件(保留原始 SVG 格式)。 + 注意:svg_data 是 "data:image/svg+xml;base64,...",需提取真实 SVG 内容。 + """ + if not svg_data or not isinstance(svg_data, str): + print(f"生成的 SVG 数据无效(为空或非字符串): {filename}") + return + + # 提取 base64 编码部分(去除 data URL 前缀) + if svg_data.startswith("data:image/svg+xml;base64,"): + base64_content = svg_data[len("data:image/svg+xml;base64,"):] + try: + # 解码 base64 得到原始 SVG 字符串 + import base64 + svg_content = base64.b64decode(base64_content).decode('utf-8') + except Exception as e: + print(f"解码 base64 失败: {e}") + svg_content = svg_data # 退化为直接写入 + else: + # 如果不是标准格式,直接写入(兼容调试) + svg_content = svg_data + + filepath = os.path.join(self.output_directory, filename) + with open(filepath, 'w', encoding='utf-8') as f: + f.write(svg_content) + print(f"已保存: {filepath}") + + def run(self) -> None: + """按顺序执行图表生成与保存""" + print("开始生成环形图...") + try: + print("正在生成环形图...") + svg_data = self.generate_pie_chart() + self.save_svg(svg_data, self.filename) + + print(f"\n环形图已成功生成。") + print(f"输出目录: {self.output_directory}") + print(f"文件列表:") + print(f" - {self.filename}") + + except Exception as e: + print(f"\n测试失败: {e}") + traceback.print_exc() + + +# 程序入口 +if __name__ == "__main__": + tester = ChartPieExample() + tester.run() \ No newline at end of file diff --git a/examples/06_chart_example/charts/charts.html b/examples/06_chart_example/charts/charts.html new file mode 100644 index 0000000..2d09caf --- /dev/null +++ b/examples/06_chart_example/charts/charts.html @@ -0,0 +1,16 @@ + + +
+ + +