diff --git a/ruoyi-fastapi-backend/assets/font/Arial.ttf b/ruoyi-fastapi-backend/assets/font/Arial.ttf new file mode 100644 index 0000000..9512aea Binary files /dev/null and b/ruoyi-fastapi-backend/assets/font/Arial.ttf differ diff --git a/ruoyi-fastapi-backend/config/__init__.py b/ruoyi-fastapi-backend/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ruoyi-fastapi-backend/config/constant.py b/ruoyi-fastapi-backend/config/constant.py new file mode 100644 index 0000000..eb77464 --- /dev/null +++ b/ruoyi-fastapi-backend/config/constant.py @@ -0,0 +1,483 @@ +from config.env import DataBaseConfig + + +class CommonConstant: + """ + 常用常量 + + WWW: www主域 + HTTP: http请求 + HTTPS: https请求 + LOOKUP_RMI: RMI远程方法调用 + LOOKUP_LDAP: LDAP远程方法调用 + LOOKUP_LDAPS: LDAPS远程方法调用 + YES: 是否为系统默认(是) + NO: 是否为系统默认(否) + DEPT_NORMAL: 部门正常状态 + DEPT_DISABLE: 部门停用状态 + UNIQUE: 校验是否唯一的返回标识(是) + NOT_UNIQUE: 校验是否唯一的返回标识(否) + """ + + WWW = 'www.' + HTTP = 'http://' + HTTPS = 'https://' + LOOKUP_RMI = 'rmi:' + LOOKUP_LDAP = 'ldap:' + LOOKUP_LDAPS = 'ldaps:' + YES = 'Y' + NO = 'N' + DEPT_NORMAL = '0' + DEPT_DISABLE = '1' + UNIQUE = True + NOT_UNIQUE = False + + +class HttpStatusConstant: + """ + 返回状态码 + + SUCCESS: 操作成功 + CREATED: 对象创建成功 + ACCEPTED: 请求已经被接受 + NO_CONTENT: 操作已经执行成功,但是没有返回数据 + MOVED_PERM: 资源已被移除 + SEE_OTHER: 重定向 + NOT_MODIFIED: 资源没有被修改 + BAD_REQUEST: 参数列表错误(缺少,格式不匹配) + UNAUTHORIZED: 未授权 + FORBIDDEN: 访问受限,授权过期 + NOT_FOUND: 资源,服务未找到 + BAD_METHOD: 不允许的http方法 + CONFLICT: 资源冲突,或者资源被锁 + UNSUPPORTED_TYPE: 不支持的数据,媒体类型 + ERROR: 系统内部错误 + NOT_IMPLEMENTED: 接口未实现 + WARN: 系统警告消息 + """ + + SUCCESS = 200 + CREATED = 201 + ACCEPTED = 202 + NO_CONTENT = 204 + MOVED_PERM = 301 + SEE_OTHER = 303 + NOT_MODIFIED = 304 + BAD_REQUEST = 400 + UNAUTHORIZED = 401 + FORBIDDEN = 403 + NOT_FOUND = 404 + BAD_METHOD = 405 + CONFLICT = 409 + UNSUPPORTED_TYPE = 415 + ERROR = 500 + NOT_IMPLEMENTED = 501 + WARN = 601 + + +class JobConstant: + """ + 定时任务常量 + + JOB_ERROR_LIST: 定时任务禁止调用模块及违规字符串列表 + JOB_WHITE_LIST: 定时任务允许调用模块列表 + """ + + JOB_ERROR_LIST = [ + 'app', + 'config', + 'exceptions', + 'import ', + 'middlewares', + 'module_admin', + 'open(', + 'os.', + 'server', + 'sub_applications', + 'subprocess.', + 'sys.', + 'utils', + 'while ', + '__import__', + '"', + "'", + ',', + '?', + ':', + ';', + '/', + '|', + '+', + '-', + '=', + '~', + '!', + '#', + '$', + '%', + '^', + '&', + '*', + '<', + '>', + '(', + ')', + '[', + ']', + '{', + '}', + ' ', + ] + JOB_WHITE_LIST = ['module_task'] + + +class MenuConstant: + """ + 菜单常量 + + TYPE_DIR: 菜单类型(目录) + TYPE_MENU: 菜单类型(菜单) + TYPE_BUTTON: 菜单类型(按钮) + YES_FRAME: 是否菜单外链(是) + NO_FRAME: 是否菜单外链(否) + LAYOUT: Layout组件标识 + PARENT_VIEW: ParentView组件标识 + INNER_LINK: InnerLink组件标识 + """ + + TYPE_DIR = 'M' + TYPE_MENU = 'C' + TYPE_BUTTON = 'F' + YES_FRAME = 0 + NO_FRAME = 1 + LAYOUT = 'Layout' + PARENT_VIEW = 'ParentView' + INNER_LINK = 'InnerLink' + + +class GenConstant: + """ + 代码生成常量 + + TPL_CRUD: 单表(增删改查 + TPL_TREE: 树表(增删改查) + TPL_SUB: 主子表(增删改查) + TREE_CODE: 树编码字段 + TREE_PARENT_CODE: 树父编码字段 + TREE_NAME: 树名称字段 + PARENT_MENU_ID: 上级菜单ID字段 + PARENT_MENU_NAME: 上级菜单名称字段 + COLUMNTYPE_STR: 数据库字符串类型 + COLUMNTYPE_TEXT: 数据库文本类型 + COLUMNTYPE_TIME: 数据库时间类型 + COLUMNTYPE_GEOMETRY: 数据库字空间类型 + COLUMNTYPE_NUMBER: 数据库数字类型 + COLUMNNAME_NOT_EDIT: 页面不需要编辑字段 + COLUMNNAME_NOT_LIST: 页面不需要显示的列表字段 + COLUMNNAME_NOT_QUERY: 页面不需要查询字段 + BASE_ENTITY: Entity基类字段 + TREE_ENTITY: Tree基类字段 + HTML_INPUT: 文本框 + HTML_TEXTAREA: 文本域 + HTML_SELECT: 下拉框 + HTML_RADIO: 单选框 + HTML_CHECKBOX: 复选框 + HTML_DATETIME: 日期控件 + HTML_IMAGE_UPLOAD: 图片上传控件 + HTML_FILE_UPLOAD: 文件上传控件 + HTML_EDITOR: 富文本控件 + TYPE_DECIMAL: 高精度计算类型 + TYPE_DATE: 时间类型 + QUERY_LIKE: 模糊查询 + QUERY_EQ: 相等查询 + REQUIRE: 需要 + DB_TO_SQLALCHEMY_TYPE_MAPPING: 数据库类型与sqlalchemy类型映射 + DB_TO_PYTHON_TYPE_MAPPING: 数据库类型与python类型映射 + """ + + TPL_CRUD = 'crud' + TPL_TREE = 'tree' + TPL_SUB = 'sub' + TREE_CODE = 'treeCode' + TREE_PARENT_CODE = 'treeParentCode' + TREE_NAME = 'treeName' + PARENT_MENU_ID = 'parentMenuId' + PARENT_MENU_NAME = 'parentMenuName' + COLUMNTYPE_STR = ( + ['character varying', 'varchar', 'character', 'char'] + if DataBaseConfig.db_type == 'postgresql' + else ['char', 'varchar', 'nvarchar', 'varchar2'] + ) + COLUMNTYPE_TEXT = ( + ['text', 'citext'] if DataBaseConfig.db_type == 'postgresql' else ['tinytext', 'text', 'mediumtext', 'longtext'] + ) + COLUMNTYPE_TIME = ( + [ + 'date', + 'time', + 'time with time zone', + 'time without time zone', + 'timestamp', + 'timestamp with time zone', + 'timestamp without time zone', + 'interval', + ] + if DataBaseConfig.db_type == 'postgresql' + else ['datetime', 'time', 'date', 'timestamp'] + ) + COLUMNTYPE_GEOMETRY = ( + ['point', 'line', 'lseg', 'box', 'path', 'polygon', 'circle'] + if DataBaseConfig.db_type == 'postgresql' + else [ + 'geometry', + 'point', + 'linestring', + 'polygon', + 'multipoint', + 'multilinestring', + 'multipolygon', + 'geometrycollection', + ] + ) + COLUMNTYPE_NUMBER = [ + 'tinyint', + 'smallint', + 'mediumint', + 'int', + 'number', + 'integer', + 'bit', + 'bigint', + 'float', + 'double', + 'decimal', + ] + COLUMNNAME_NOT_ADD_SHOW = ['create_by', 'create_time'] + COLUMNNAME_NOT_EDIT_SHOW = ['update_by', 'update_time'] + COLUMNNAME_NOT_EDIT = ['id', 'create_by', 'create_time', 'del_flag'] + COLUMNNAME_NOT_LIST = ['id', 'create_by', 'create_time', 'del_flag', 'update_by', 'update_time'] + COLUMNNAME_NOT_QUERY = ['id', 'create_by', 'create_time', 'del_flag', 'update_by', 'update_time', 'remark'] + BASE_ENTITY = ['createBy', 'createTime', 'updateBy', 'updateTime', 'remark'] + TREE_ENTITY = ['parentName', 'parentId', 'orderNum', 'ancestors', 'children'] + HTML_INPUT = 'input' + HTML_TEXTAREA = 'textarea' + HTML_SELECT = 'select' + HTML_RADIO = 'radio' + HTML_CHECKBOX = 'checkbox' + HTML_DATETIME = 'datetime' + HTML_IMAGE_UPLOAD = 'imageUpload' + HTML_FILE_UPLOAD = 'fileUpload' + HTML_EDITOR = 'editor' + TYPE_DECIMAL = 'Decimal' + TYPE_DATE = ['date', 'time', 'datetime'] + QUERY_LIKE = 'LIKE' + QUERY_EQ = 'EQ' + REQUIRE = '1' + DB_TO_SQLALCHEMY_TYPE_MAPPING = ( + { + 'boolean': 'Boolean', + 'smallint': 'SmallInteger', + 'integer': 'Integer', + 'bigint': 'BigInteger', + 'real': 'Float', + 'double precision': 'Float', + 'numeric': 'Numeric', + 'character varying': 'String', + 'character': 'String', + 'text': 'Text', + 'bytea': 'LargeBinary', + 'date': 'Date', + 'time': 'Time', + 'time with time zone': 'Time', + 'time without time zone': 'Time', + 'timestamp': 'DateTime', + 'timestamp with time zone': 'DateTime', + 'timestamp without time zone': 'DateTime', + 'interval': 'Interval', + 'json': 'JSON', + 'jsonb': 'JSONB', + 'uuid': 'Uuid', + 'inet': 'INET', + 'cidr': 'CIDR', + 'macaddr': 'MACADDR', + 'point': 'Geometry', + 'line': 'Geometry', + 'lseg': 'Geometry', + 'box': 'Geometry', + 'path': 'Geometry', + 'polygon': 'Geometry', + 'circle': 'Geometry', + 'bit': 'Bit', + 'bit varying': 'Bit', + 'tsvector': 'TSVECTOR', + 'tsquery': 'TSQUERY', + 'xml': 'String', + 'array': 'ARRAY', + 'composite': 'JSON', + 'enum': 'Enum', + 'range': 'Range', + 'money': 'Numeric', + 'pg_lsn': 'BigInteger', + 'txid_snapshot': 'String', + 'oid': 'BigInteger', + 'regproc': 'String', + 'regclass': 'String', + 'regtype': 'String', + 'regrole': 'String', + 'regnamespace': 'String', + 'int2vector': 'ARRAY', + 'oidvector': 'ARRAY', + 'pg_node_tree': 'Text', + } + if DataBaseConfig.db_type == 'postgresql' + else { + # 数值类型 + 'TINYINT': 'SmallInteger', + 'SMALLINT': 'SmallInteger', + 'MEDIUMINT': 'Integer', + 'INT': 'Integer', + 'INTEGER': 'Integer', + 'BIGINT': 'BigInteger', + 'FLOAT': 'Float', + 'DOUBLE': 'Float', + 'DECIMAL': 'DECIMAL', + 'BIT': 'Integer', + # 日期和时间类型 + 'DATE': 'Date', + 'TIME': 'Time', + 'DATETIME': 'DateTime', + 'TIMESTAMP': 'TIMESTAMP', + 'YEAR': 'Integer', + # 字符串类型 + 'CHAR': 'CHAR', + 'VARCHAR': 'String', + 'TINYTEXT': 'Text', + 'TEXT': 'Text', + 'MEDIUMTEXT': 'Text', + 'LONGTEXT': 'Text', + 'BINARY': 'BINARY', + 'VARBINARY': 'VARBINARY', + 'TINYBLOB': 'LargeBinary', + 'BLOB': 'LargeBinary', + 'MEDIUMBLOB': 'LargeBinary', + 'LONGBLOB': 'LargeBinary', + # 枚举和集合类型 + 'ENUM': 'Enum', + 'SET': 'String', + # JSON 类型 + 'JSON': 'JSON', + # 空间数据类型(需要扩展支持,如 GeoAlchemy2) + 'GEOMETRY': 'Geometry', # 需要安装 geoalchemy2 + 'POINT': 'Geometry', + 'LINESTRING': 'Geometry', + 'POLYGON': 'Geometry', + 'MULTIPOINT': 'Geometry', + 'MULTILINESTRING': 'Geometry', + 'MULTIPOLYGON': 'Geometry', + 'GEOMETRYCOLLECTION': 'Geometry', + } + ) + DB_TO_PYTHON_TYPE_MAPPING = ( + { + 'boolean': 'bool', + 'smallint': 'int', + 'integer': 'int', + 'bigint': 'int', + 'real': 'float', + 'double precision': 'float', + 'numeric': 'Decimal', + 'character varying': 'str', + 'character': 'str', + 'text': 'str', + 'bytea': 'bytes', + 'date': 'date', + 'time': 'time', + 'time with time zone': 'time', + 'time without time zone': 'time', + 'timestamp': 'datetime', + 'timestamp with time zone': 'datetime', + 'timestamp without time zone': 'datetime', + 'interval': 'timedelta', + 'json': 'dict', + 'jsonb': 'dict', + 'uuid': 'str', + 'inet': 'str', + 'cidr': 'str', + 'macaddr': 'str', + 'point': 'list', + 'line': 'list', + 'lseg': 'list', + 'box': 'list', + 'path': 'list', + 'polygon': 'list', + 'circle': 'list', + 'bit': 'int', + 'bit varying': 'int', + 'tsvector': 'str', + 'tsquery': 'str', + 'xml': 'str', + 'array': 'list', + 'composite': 'dict', + 'enum': 'str', + 'range': 'list', + 'money': 'Decimal', + 'pg_lsn': 'int', + 'txid_snapshot': 'str', + 'oid': 'int', + 'regproc': 'str', + 'regclass': 'str', + 'regtype': 'str', + 'regrole': 'str', + 'regnamespace': 'str', + 'int2vector': 'list', + 'oidvector': 'list', + 'pg_node_tree': 'str', + } + if DataBaseConfig.db_type == 'postgresql' + else { + # 数值类型 + 'TINYINT': 'int', + 'SMALLINT': 'int', + 'MEDIUMINT': 'int', + 'INT': 'int', + 'INTEGER': 'int', + 'BIGINT': 'int', + 'FLOAT': 'float', + 'DOUBLE': 'float', + 'DECIMAL': 'Decimal', + 'BIT': 'int', + # 日期和时间类型 + 'DATE': 'date', + 'TIME': 'time', + 'DATETIME': 'datetime', + 'TIMESTAMP': 'datetime', + 'YEAR': 'int', + # 字符串类型 + 'CHAR': 'str', + 'VARCHAR': 'str', + 'TINYTEXT': 'str', + 'TEXT': 'str', + 'MEDIUMTEXT': 'str', + 'LONGTEXT': 'str', + 'BINARY': 'bytes', + 'VARBINARY': 'bytes', + 'TINYBLOB': 'bytes', + 'BLOB': 'bytes', + 'MEDIUMBLOB': 'bytes', + 'LONGBLOB': 'bytes', + # 枚举和集合类型 + 'ENUM': 'str', + 'SET': 'str', + # JSON 类型 + 'JSON': 'dict', + # 空间数据类型(通常需要特殊处理) + 'GEOMETRY': 'bytes', + 'POINT': 'bytes', + 'LINESTRING': 'bytes', + 'POLYGON': 'bytes', + 'MULTIPOINT': 'bytes', + 'MULTILINESTRING': 'bytes', + 'MULTIPOLYGON': 'bytes', + 'GEOMETRYCOLLECTION': 'bytes', + } + ) diff --git a/ruoyi-fastapi-backend/config/database.py b/ruoyi-fastapi-backend/config/database.py new file mode 100644 index 0000000..006b6a5 --- /dev/null +++ b/ruoyi-fastapi-backend/config/database.py @@ -0,0 +1,30 @@ +from sqlalchemy.ext.asyncio import create_async_engine +from sqlalchemy.ext.asyncio import async_sessionmaker +from sqlalchemy.ext.asyncio import AsyncAttrs +from sqlalchemy.orm import DeclarativeBase +from urllib.parse import quote_plus +from config.env import DataBaseConfig + +ASYNC_SQLALCHEMY_DATABASE_URL = ( + f'mysql+asyncmy://{DataBaseConfig.db_username}:{quote_plus(DataBaseConfig.db_password)}@' + f'{DataBaseConfig.db_host}:{DataBaseConfig.db_port}/{DataBaseConfig.db_database}' +) +if DataBaseConfig.db_type == 'postgresql': + ASYNC_SQLALCHEMY_DATABASE_URL = ( + f'postgresql+asyncpg://{DataBaseConfig.db_username}:{quote_plus(DataBaseConfig.db_password)}@' + f'{DataBaseConfig.db_host}:{DataBaseConfig.db_port}/{DataBaseConfig.db_database}' + ) + +async_engine = create_async_engine( + ASYNC_SQLALCHEMY_DATABASE_URL, + echo=DataBaseConfig.db_echo, + max_overflow=DataBaseConfig.db_max_overflow, + pool_size=DataBaseConfig.db_pool_size, + pool_recycle=DataBaseConfig.db_pool_recycle, + pool_timeout=DataBaseConfig.db_pool_timeout, +) +AsyncSessionLocal = async_sessionmaker(autocommit=False, autoflush=False, bind=async_engine) + + +class Base(AsyncAttrs, DeclarativeBase): + pass diff --git a/ruoyi-fastapi-backend/config/enums.py b/ruoyi-fastapi-backend/config/enums.py new file mode 100644 index 0000000..6b62ce7 --- /dev/null +++ b/ruoyi-fastapi-backend/config/enums.py @@ -0,0 +1,78 @@ +from enum import Enum + + +class GuideWordType(Enum): + """ + 引导词类型 + NEWVISITOR 新访客 0 + BOOKEDVISITOR 已预约访客 1 + STAFF 员工 2 + CHAT 聊天关键词 3 + """ + NEWVISITOR = 0 + BOOKEDVISITOR = 1 + STAFF = 2 + CHAT = 3 + +class ExplanationContentStatus(Enum): + """ + 讲解内容状态 + + NOTSTART: 未开始 + READY: 已就绪 + PLAYING: 播放中 + FINISH: 已完成 + """ + NOTSTART = '0' + READY = '1' + PLAYING = '2' + FINISH = '3' + +class BusinessType(Enum): + """ + 业务操作类型 + + OTHER: 其它 + INSERT: 新增 + UPDATE: 修改 + DELETE: 删除 + GRANT: 授权 + EXPORT: 导出 + IMPORT: 导入 + FORCE: 强退 + GENCODE: 生成代码 + CLEAN: 清空数据 + """ + + OTHER = 0 + INSERT = 1 + UPDATE = 2 + DELETE = 3 + GRANT = 4 + EXPORT = 5 + IMPORT = 6 + FORCE = 7 + GENCODE = 8 + CLEAN = 9 + + +class RedisInitKeyConfig(Enum): + """ + 系统内置Redis键名 + """ + + @property + def key(self): + return self.value.get('key') + + @property + def remark(self): + return self.value.get('remark') + + ACCESS_TOKEN = {'key': 'access_token', 'remark': '登录令牌信息'} + SYS_DICT = {'key': 'sys_dict', 'remark': '数据字典'} + SYS_CONFIG = {'key': 'sys_config', 'remark': '配置信息'} + CAPTCHA_CODES = {'key': 'captcha_codes', 'remark': '图片验证码'} + ACCOUNT_LOCK = {'key': 'account_lock', 'remark': '用户锁定'} + PASSWORD_ERROR_COUNT = {'key': 'password_error_count', 'remark': '密码错误次数'} + SMS_CODE = {'key': 'sms_code', 'remark': '短信验证码'} diff --git a/ruoyi-fastapi-backend/config/env.py b/ruoyi-fastapi-backend/config/env.py new file mode 100644 index 0000000..285e721 --- /dev/null +++ b/ruoyi-fastapi-backend/config/env.py @@ -0,0 +1,463 @@ +import argparse +import os +import sys +from dotenv import load_dotenv +from functools import lru_cache +from pydantic import computed_field +from pydantic_settings import BaseSettings +from typing import Literal, Optional +from pathlib import Path + + +class AppSettings(BaseSettings): + """ + 应用配置 + """ + + app_env: str = 'dev' + app_name: str = 'RuoYi-FasAPI' + app_root_path: str = '/dev-api' + app_host: str = '0.0.0.0' + app_port: int = 9099 + app_version: str = '1.0.0' + app_reload: bool = False + app_ip_location_query: bool = True + app_same_time_login: bool = True + + app_docs_url: Optional[str] = None + app_redoc_url: Optional[str] = None + app_openapi_url: Optional[str] = None + + + +class JwtSettings(BaseSettings): + """ + Jwt配置 + """ + + jwt_secret_key: str = os.getenv('JWT_SECRET_KEY', '') + jwt_algorithm: str = os.getenv('JWT_ALGORITHM', 'HS256') + jwt_expire_minutes: int = int(os.getenv('JWT_EXPIRE_MINUTES', '1440')) + jwt_redis_expire_minutes: int = int(os.getenv('JWT_REDIS_EXPIRE_MINUTES', '30')) + + +class DataBaseSettings(BaseSettings): + """ + 数据库配置 + """ + + db_type: Literal['mysql', 'postgresql'] = os.getenv('DB_TYPE', 'mysql') + db_host: str = os.getenv('DB_HOST', '127.0.0.1') + db_port: int = int(os.getenv('DB_PORT', '3306')) + db_username: str = os.getenv('DB_USERNAME', '') + db_password: str = os.getenv('DB_PASSWORD', '') + db_database: str = os.getenv('DB_DATABASE', '') + db_echo: bool = os.getenv('DB_ECHO', 'True').lower() == 'true' + db_max_overflow: int = int(os.getenv('DB_MAX_OVERFLOW', '10')) + db_pool_size: int = int(os.getenv('DB_POOL_SIZE', '50')) + db_pool_recycle: int = int(os.getenv('DB_POOL_RECYCLE', '3600')) + db_pool_timeout: int = int(os.getenv('DB_POOL_TIMEOUT', '30')) + + @computed_field + @property + def sqlglot_parse_dialect(self) -> str: + if self.db_type == 'postgresql': + return 'postgres' + return self.db_type + + +class RedisSettings(BaseSettings): + """ + Redis配置 + """ + + redis_host: str = os.getenv('REDIS_HOST', '127.0.0.1') + redis_port: int = int(os.getenv('REDIS_PORT', '6379')) + redis_username: str = os.getenv('REDIS_USERNAME', '') + redis_password: str = os.getenv('REDIS_PASSWORD', '') + redis_database: int = int(os.getenv('REDIS_DATABASE', '2')) + + +class GenSettings: + """ + 代码生成配置 + """ + + author = 'insistence' + package_name = 'module_admin.system' + auto_remove_pre = False + table_prefix = 'sys_' + allow_overwrite = False + + GEN_PATH = 'vf_admin/gen_path' + + def __init__(self): + if not os.path.exists(self.GEN_PATH): + os.makedirs(self.GEN_PATH) + + +class UploadSettings: + """ + 上传配置 + """ + + UPLOAD_PREFIX = '/profile' + UPLOAD_PATH = 'vf_admin/upload_path' + UPLOAD_MACHINE = 'A' + DEFAULT_ALLOWED_EXTENSION = [ + # 图片 + 'bmp', + 'gif', + 'jpg', + 'jpeg', + 'png', + # word excel powerpoint + 'doc', + 'docx', + 'xls', + 'xlsx', + 'ppt', + 'pptx', + 'html', + 'htm', + 'txt', + # 压缩文件 + 'rar', + 'zip', + 'gz', + 'bz2', + # 视频格式 + 'mp4', + 'avi', + 'rmvb', + # pdf + 'pdf', + ] + DOWNLOAD_PATH = 'vf_admin/download_path' + + def __init__(self): + if not os.path.exists(self.UPLOAD_PATH): + os.makedirs(self.UPLOAD_PATH) + if not os.path.exists(self.DOWNLOAD_PATH): + os.makedirs(self.DOWNLOAD_PATH) + + +class CachePathConfig: + """ + 缓存目录配置 + """ + + PATH = os.path.join(os.path.abspath(os.getcwd()), 'caches') + PATHSTR = 'caches' + +class ComprefaceSettings: + """ + Compreface配置 + """ + COMPREFACE_BASE_URL = os.getenv("COMPREFACE_BASE_URL", "http://10.0.0.202") + COMPERFACE_BASE_PORT = os.getenv("COMPERFACE_BASE_PORT", "8000") + COMPREFACE_API_KEY = os.getenv("COMPREFACE_API_KEY", "") + COMPREFACE_API_KEY_DETECTION = os.getenv("COMPREFACE_API_KEY_DETECTION", "") + COMPREFACE_FACE_FILE = os.getenv("COMPREFACE_FACE_FILE", "./image_face") + COMPREFACE_SIMILARITY_THRESHOLD = float(os.getenv("COMPREFACE_SIMILARITY_THRESHOLD", "0.995")) + COMPREFACE_POSE_MAX_ANGLE = float(os.getenv("COMPREFACE_POSE_MAX_ANGLE", "10")) + COMPREFACE_MIN_FACE_WIDTH = int(os.getenv("COMPREFACE_MIN_FACE_WIDTH", "200")) + COMPREFACE_MIN_FACE_HEIGHT = int(os.getenv("COMPREFACE_MIN_FACE_HEIGHT", "200")) + COMPREFACE_MIN_DETECTION_PROBABILITY = float(os.getenv("COMPREFACE_MIN_DETECTION_PROBABILITY", "0.9")) + +class HaiKangSettings: + """ + 海康平台配置 + """ + HAIKANG_URL = os.getenv('HAIKANG_URL', 'https://127.0.0.1') + HAIKANG_PORT = int(os.getenv('HAIKANG_PORT', '443')) + + HAIKANG_AK = os.getenv('HAIKANG_AK', '') + HAIKANG_SK = os.getenv('HAIKANG_SK', '') + HAIKANG_ACCESS_TOKEN_URL = os.getenv('HAIKANG_ACCESS_TOKEN_URL', '/api/v1/oauth/token') + # HAIKANG_DOOR_STATES_URL = '/api/v1/door/states' + HAIKANG_DOOR_STATES_URL = os.getenv('HAIKANG_DOOR_STATES_URL', '/api/acs/v1/door/states') + HAIKANG_DOOR_DOCONTROL_URL = os.getenv('HAIKANG_DOOR_DOCONTROL_URL', '/api/acs/v1/door/doControl') + HAIKANG_DOOR_ENVENTS_URL = os.getenv('HAIKANG_DOOR_EVENTS_URL', '/api/acs/v2/door/events') + HAIKANG_DOOR_SEARCH = os.getenv('HAIKANG_DOOR_SEARCH', '/api/resource/v2/door/search') + HAIKANG_DOOR_ONLINE_STATUS = os.getenv('HAIKANG_DOOR_ONLINE_STATUS', '/api/nms/v1/online/acs_device/get') + HAIKANG_APPLICATION_ONETOMANY_URL = os.getenv('HAIKANG_APPLICATION_ONETOMANY_URL', '/api/frs/v1/application/oneToMany') + HAIKANG_PICTURE_CHECK_URL = os.getenv('HAIKANG_PICTURE_CHECK_URL', '/api/frs/v1/face/picture/check') + HAIKANG_FACECAPATURE_SEARCH = os.getenv('HAIKANG_FACE_CAPTURE_SEARCH', '/api/frs/v1/event/face_capture/search') + HAIKANG_FACE_GROUP_URL = os.getenv('HAIKANG_FACE_GROUP_URL', '/api/frs/v1/face/group') + + HAIKANG_VISITOR_RECORD_SEARCH = os.getenv('HAIKANG_VISITOR_RECORD_SEARCH', '/api/visitor/v2/appointment/records') + HAIKANG_VISITOR_RECORD_PICTURES = os.getenv('HAIKANG_VISITOR_RECORD_PICTURES', '/api/visitor/v1/record/pictures') + + HAIKANG_PERSON_LIST = os.getenv('HAIKANG_PERSON_LIST', '/api/resource/v2/person/personList') + HAIKANG_GET_PERSON_PICTURE = os.getenv('HAIKANG_GET_PERSON_PICTURE', '/api/resource/v1/person/picture') + + HAIKANG_FACE_GROUP_ADDITION = os.getenv('HAIKANG_FACE_GROUP_ADDITION', '/api/frs/v1/face/group/single/addition') + + HAIKANG_FACE_SIGLE_ADDITION = os.getenv('HAIKANG_FACE_SINGLE_ADDITION', '/api/frs/v1/face/single/addition') + HAIKANG_FACE_DELETE = os.getenv('HAIKANG_FACE_DELETE', '/api/frs/v1/face/deletion') + + HAIKANG_FACE_SAVE_PATH = os.getenv('HAIKANG_FACE_SAVE_PATH', "./face_images") + HAIKANG_VISITOR_PICTURES_SAVE_PATH = os.getenv('HAIKANG_VISITOR_PICTURES_SAVE_PATH', "./visitor/face_images/") + HAIKANG_VISITOR_RECORD_TXT = os.getenv('HAIKANG_VISITOR_RECORD_TXT', "./visitor/visitorIds.txt") + + + +class RAGFlowSettings(BaseSettings): + """ + RAGFlow 配置 + + 注意:必须在 `.env.*` 加载完成后再实例化读取, + 所以使用 BaseSettings 而不是类常量。 + """ + RAGFLOW_BASE_URL: str = "http://10.0.0.202:82" + RAGFLOW_API_KEY: str = "ragflow-hlMjRmNzE2ODNiNTExZjA4ZTNlMDI0Mm" + +class SearchSettings: + """搜索服务配置""" + + SEARCH_API_BASE = os.getenv("BAIDU_SEARCH_API_BASE", "https://qianfan.baidubce.com/v2/ai_search/chat/completions") + SEARCH_API_KEY = os.getenv("SEARCH_API_KEY", "") + SEARCH_ENGINE = os.getenv("SEARCH_ENGINE", "baidu") + SEARCH_LANG = os.getenv("SEARCH_LANG", "zh-cn") + SEARCH_COUNTRY = os.getenv("SEARCH_COUNTRY", "cn") + SEARCH_NUM_RESULTS = int(os.getenv("SEARCH_NUM_RESULTS", "5")) + SEARCH_CACHE_TTL = int(os.getenv("SEARCH_CACHE_TTL", "1800")) + + # Baidu AppBuilder Configuration + BAIDU_APPBUILDER_API_KEY = os.getenv("BAIDU_APPBUILDER_API_KEY", "bce-v3/ALTAK-rXC12MxT2UeVrJP2S5kMJ/e42eb37b3c345e9bfb3e2eb0dcac8004888592f3") + + # ZhipuAI Configuration + ZHIPUAI_API_KEY = os.getenv("ZHIPUAI_API_KEY", "c86394f9af6f4d4ebecf3f6105d3483e.tn3vZmeVB8IzO6i3") + ZHIPUAI_API_BASE = os.getenv("ZHIPUAI_API_BASE", "https://open.bigmodel.cn/api/paas/v4/chat/completions") + + +class DeepSeekSettings: + """DeepSeek大语言模型配置""" + DEEPSEEK_API_BASE = os.getenv("DEEPSEEK_API_BASE", "https://api.deepseek.com") + DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "sk-56b608b26a6949e4b09b5bf5f11c8f5b") + DEEPSEEK_MODEL = os.getenv("DEEPSEEK_MODEL", "deepseek-chat") + + +class KBSettings(BaseSettings): + """内部知识库 Provider 配置(ragflow / es_bm25) + + 注意:必须在 `.env.*` 加载完成后再实例化读取, + 所以使用 BaseSettings 而不是在 import 时读取环境变量。 + """ + + KB_PROVIDER: Literal['ragflow', 'es_bm25'] = 'ragflow' # type: ignore[assignment] + + # ES/OpenSearch + KB_ES_URL: str = '' + KB_ES_INDEX: str = 'kb_chunks_v1' + KB_ES_USERNAME: str = '' + KB_ES_PASSWORD: str = '' + KB_ES_VERIFY_SSL: bool = True + + # retrieval + KB_TOP_K: int = 8 + KB_MIN_SCORE: float = 0.0 + KB_MIN_COVERAGE: float = 0.2 + KB_CACHE_TTL: int = 1800 # seconds + + # fallback + KB_FALLBACK: Literal['ragflow', 'none'] = 'ragflow' # type: ignore[assignment] + + def model_post_init(self, __context) -> None: # type: ignore[override] + # 规范化:dotenv/环境变量可能带空格/大小写 + provider = (str(self.KB_PROVIDER) if self.KB_PROVIDER is not None else 'ragflow').strip().lower() + self.KB_PROVIDER = provider if provider in ('ragflow', 'es_bm25') else 'ragflow' # type: ignore[assignment] + + es_url = (self.KB_ES_URL or '').strip().rstrip('/') + self.KB_ES_URL = es_url + + fallback = (str(self.KB_FALLBACK) if self.KB_FALLBACK is not None else 'ragflow').strip().lower() + self.KB_FALLBACK = fallback if fallback in ('ragflow', 'none') else 'ragflow' # type: ignore[assignment] + + +class GetConfig: + """ + 获取配置 + """ + + def __init__(self): + self.parse_cli_args() + + @lru_cache() + def get_compreface_config(self): + """ + 获取Compreface配置 + """ + # 获取Compreface配置 + return ComprefaceSettings() + @lru_cache() + def get_app_config(self): + """ + 获取应用配置 + """ + # 实例化应用配置模型 + return AppSettings() + + @lru_cache() + def get_jwt_config(self): + """ + 获取Jwt配置 + """ + # 实例化Jwt配置模型 + return JwtSettings() + + @lru_cache() + def get_database_config(self): + """ + 获取数据库配置 + """ + # 实例化数据库配置模型 + return DataBaseSettings() + + @lru_cache() + def get_redis_config(self): + """ + 获取Redis配置 + """ + # 实例化Redis配置模型 + return RedisSettings() + + @lru_cache() + def get_gen_config(self): + """ + 获取代码生成配置 + """ + # 实例化代码生成配置 + return GenSettings() + + @lru_cache() + def get_upload_config(self): + """ + 获取数据库配置 + """ + # 实例上传配置 + return UploadSettings() + + @lru_cache() + def get_haikang_config(self): + """ + 获取海康平台配置 + """ + return HaiKangSettings() + + @lru_cache() + def get_ragflow_config(self): + """ + 获取RAGFlow配置 + """ + return RAGFlowSettings() + + @lru_cache() + def get_search_config(self): + """获取搜索配置""" + return SearchSettings() + + @lru_cache() + def get_deepseek_config(self): + """获取DeepSeek配置""" + return DeepSeekSettings() + + @lru_cache() + def get_kb_config(self): + """获取内部知识库配置""" + return KBSettings() + + @staticmethod + def parse_cli_args(): + """ + 解析命令行参数 + """ + if 'uvicorn' in sys.argv[0]: + # 使用uvicorn启动时,命令行参数需要按照uvicorn的文档进行配置,无法自定义参数 + pass + else: + # 使用argparse定义命令行参数 + parser = argparse.ArgumentParser(description='命令行参数') + parser.add_argument('--env', type=str, default='dev', help='运行环境') + # 解析命令行参数 + args, _unknown = parser.parse_known_args() + # 设置环境变量,如果未设置命令行参数,默认APP_ENV为dev + os.environ['APP_ENV'] = args.env if args.env else 'dev' + # 读取运行环境 + run_env = os.environ.get('APP_ENV', '') + # 运行环境未指定时默认加载.env.dev + env_file = '.env.dev' + # 运行环境不为空时按命令行参数加载对应.env文件 + if run_env != '': + env_file = f'.env.{run_env}' + # 加载配置:优先从项目根目录读取,避免工作目录不同导致找不到 .env.* + project_root = Path(__file__).resolve().parent.parent + env_path = project_root / env_file + def manual_load_env(path: Path, *, override: bool = True) -> None: + """ + python-dotenv 对某些格式(如 KEY = "value")兼容性不一, + 这里做一个非常宽松的兜底解析,确保关键配置一定能读到。 + """ + try: + with path.open("r", encoding="utf-8-sig") as f: + for raw_line in f: + line = raw_line.strip() + if not line or line.startswith("#"): + continue + if "=" not in line: + continue + key, val = line.split("=", 1) + key = key.strip() + val = val.strip() + if not key: + continue + # 去掉行尾注释(不处理引号内 # 的复杂情况,当前 .env 无此需求) + if "#" in val: + val = val.split("#", 1)[0].strip() + # 去掉首尾引号 + if len(val) >= 2 and val[0] == val[-1] and val[0] in ("'", '"'): + val = val[1:-1] + if override or key not in os.environ: + os.environ[key] = val + except Exception: + # 兜底解析失败时不阻断启动 + return + + if env_path.exists(): + load_dotenv(str(env_path), override=True) + # 再做一次宽松解析兜底 + manual_load_env(env_path, override=True) + else: + # 兜底:保持旧行为(从当前工作目录读取) + load_dotenv(env_file, override=True) + manual_load_env(Path(env_file), override=True) + + +# 实例化获取配置类 +get_config = GetConfig() +# 应用配置 +AppConfig = get_config.get_app_config() +# Jwt配置 +JwtConfig = get_config.get_jwt_config() +# 数据库配置 +DataBaseConfig = get_config.get_database_config() +# Redis配置 +RedisConfig = get_config.get_redis_config() +# 代码生成配置 +GenConfig = get_config.get_gen_config() +# 上传配置 +UploadConfig = get_config.get_upload_config() +# 海康平台配置 +HaiKangConfig = get_config.get_haikang_config() +# RAGFlow配置 +RAGFlowConfig = get_config.get_ragflow_config() +# compreface配置 +ComprefaceConfig = get_config.get_compreface_config() +# 搜索配置 +SearchConfig = get_config.get_search_config() +# DeepSeek配置 +DeepSeekConfig = get_config.get_deepseek_config() + +# KB Provider配置 +KBConfig = get_config.get_kb_config() diff --git a/ruoyi-fastapi-backend/config/get_db.py b/ruoyi-fastapi-backend/config/get_db.py new file mode 100644 index 0000000..20986ae --- /dev/null +++ b/ruoyi-fastapi-backend/config/get_db.py @@ -0,0 +1,24 @@ +from config.database import async_engine, AsyncSessionLocal, Base +from utils.log_util import logger + + +async def get_db(): + """ + 每一个请求处理完毕后会关闭当前连接,不同的请求使用不同的连接 + + :return: + """ + async with AsyncSessionLocal() as current_db: + yield current_db + + +async def init_create_table(): + """ + 应用启动时初始化数据库连接 + + :return: + """ + logger.info('初始化数据库连接...') + async with async_engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + logger.info('数据库连接成功') diff --git a/ruoyi-fastapi-backend/config/get_redis.py b/ruoyi-fastapi-backend/config/get_redis.py new file mode 100644 index 0000000..9d78cad --- /dev/null +++ b/ruoyi-fastapi-backend/config/get_redis.py @@ -0,0 +1,77 @@ +from redis import asyncio as aioredis +from redis.exceptions import AuthenticationError, TimeoutError, RedisError +from config.database import AsyncSessionLocal +from config.env import RedisConfig +from module_admin.service.config_service import ConfigService +from module_admin.service.dict_service import DictDataService +from utils.log_util import logger + + +class RedisUtil: + """ + Redis相关方法 + """ + + @classmethod + async def create_redis_pool(cls) -> aioredis.Redis: + """ + 应用启动时初始化redis连接 + + :return: Redis连接对象 + """ + logger.info('开始连接redis...') + redis = await aioredis.from_url( + url=f'redis://{RedisConfig.redis_host}', + port=RedisConfig.redis_port, + username=RedisConfig.redis_username, + password=RedisConfig.redis_password, + db=RedisConfig.redis_database, + encoding='utf-8', + decode_responses=True, + ) + try: + connection = await redis.ping() + if connection: + logger.info('redis连接成功') + else: + logger.error('redis连接失败') + except AuthenticationError as e: + logger.error(f'redis用户名或密码错误,详细错误信息:{e}') + except TimeoutError as e: + logger.error(f'redis连接超时,详细错误信息:{e}') + except RedisError as e: + logger.error(f'redis连接错误,详细错误信息:{e}') + return redis + + @classmethod + async def close_redis_pool(cls, app): + """ + 应用关闭时关闭redis连接 + + :param app: fastapi对象 + :return: + """ + await app.state.redis.close() + logger.info('关闭redis连接成功') + + @classmethod + async def init_sys_dict(cls, redis): + """ + 应用启动时缓存字典表 + + :param redis: redis对象 + :return: + """ + async with AsyncSessionLocal() as session: + await DictDataService.init_cache_sys_dict_services(session, redis) + + @classmethod + async def init_sys_config(cls, redis): + """ + 应用启动时缓存参数配置表 + + :param redis: redis对象 + :return: + """ + async with AsyncSessionLocal() as session: + await ConfigService.init_cache_sys_config_services(session, redis) diff --git a/ruoyi-fastapi-backend/config/get_scheduler.py b/ruoyi-fastapi-backend/config/get_scheduler.py new file mode 100644 index 0000000..4b0c298 --- /dev/null +++ b/ruoyi-fastapi-backend/config/get_scheduler.py @@ -0,0 +1,279 @@ +import json +from apscheduler.events import EVENT_ALL +from apscheduler.executors.asyncio import AsyncIOExecutor +from apscheduler.executors.pool import ProcessPoolExecutor +from apscheduler.jobstores.memory import MemoryJobStore +from apscheduler.jobstores.redis import RedisJobStore +from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from apscheduler.triggers.combining import OrTrigger +from apscheduler.triggers.cron import CronTrigger +from apscheduler.triggers.date import DateTrigger +from asyncio import iscoroutinefunction +from datetime import datetime, timedelta +from sqlalchemy.engine import create_engine +from sqlalchemy.orm import sessionmaker +from typing import Union +from config.database import AsyncSessionLocal, quote_plus +from config.env import DataBaseConfig, RedisConfig +from module_admin.dao.job_dao import JobDao +from module_admin.entity.vo.job_vo import JobLogModel, JobModel +from module_admin.service.job_log_service import JobLogService +from utils.log_util import logger +import module_task # noqa: F401 + + +# 重写Cron定时 +class MyCronTrigger(CronTrigger): + @classmethod + def from_crontab(cls, expr: str, timezone=None): + values = expr.split() + if len(values) != 6 and len(values) != 7: + raise ValueError('Wrong number of fields; got {}, expected 6 or 7'.format(len(values))) + + second = values[0] + minute = values[1] + hour = values[2] + if '?' in values[3]: + day = None + elif 'L' in values[5]: + day = f"last {values[5].replace('L', '')}" + elif 'W' in values[3]: + day = cls.__find_recent_workday(int(values[3].split('W')[0])) + else: + day = values[3].replace('L', 'last') + month = values[4] + if '?' in values[5] or 'L' in values[5]: + week = None + elif '#' in values[5]: + week = int(values[5].split('#')[1]) + else: + week = values[5] + if '#' in values[5]: + day_of_week = int(values[5].split('#')[0]) - 1 + else: + day_of_week = None + year = values[6] if len(values) == 7 else None + return cls( + second=second, + minute=minute, + hour=hour, + day=day, + month=month, + week=week, + day_of_week=day_of_week, + year=year, + timezone=timezone, + ) + + @classmethod + def __find_recent_workday(cls, day: int): + now = datetime.now() + date = datetime(now.year, now.month, day) + if date.weekday() < 5: + return date.day + else: + diff = 1 + while True: + previous_day = date - timedelta(days=diff) + if previous_day.weekday() < 5: + return previous_day.day + else: + diff += 1 + + +SQLALCHEMY_DATABASE_URL = ( + f'mysql+pymysql://{DataBaseConfig.db_username}:{quote_plus(DataBaseConfig.db_password)}@' + f'{DataBaseConfig.db_host}:{DataBaseConfig.db_port}/{DataBaseConfig.db_database}' +) +if DataBaseConfig.db_type == 'postgresql': + SQLALCHEMY_DATABASE_URL = ( + f'postgresql+psycopg2://{DataBaseConfig.db_username}:{quote_plus(DataBaseConfig.db_password)}@' + f'{DataBaseConfig.db_host}:{DataBaseConfig.db_port}/{DataBaseConfig.db_database}' + ) +engine = create_engine( + SQLALCHEMY_DATABASE_URL, + echo=DataBaseConfig.db_echo, + max_overflow=DataBaseConfig.db_max_overflow, + pool_size=DataBaseConfig.db_pool_size, + pool_recycle=DataBaseConfig.db_pool_recycle, + pool_timeout=DataBaseConfig.db_pool_timeout, +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +job_stores = { + 'default': MemoryJobStore(), + 'sqlalchemy': SQLAlchemyJobStore(url=SQLALCHEMY_DATABASE_URL, engine=engine), + 'redis': RedisJobStore( + **dict( + host=RedisConfig.redis_host, + port=RedisConfig.redis_port, + username=RedisConfig.redis_username, + password=RedisConfig.redis_password, + db=RedisConfig.redis_database, + ) + ), +} +executors = {'default': AsyncIOExecutor(), 'processpool': ProcessPoolExecutor(5)} +job_defaults = {'coalesce': False, 'max_instance': 1} +scheduler = AsyncIOScheduler() +scheduler.configure(jobstores=job_stores, executors=executors, job_defaults=job_defaults) + + +class SchedulerUtil: + """ + 定时任务相关方法 + """ + + @classmethod + async def init_system_scheduler(cls): + """ + 应用启动时初始化定时任务 + + :return: + """ + logger.info('开始启动定时任务...') + scheduler.start() + async with AsyncSessionLocal() as session: + job_list = await JobDao.get_job_list_for_scheduler(session) + for item in job_list: + cls.remove_scheduler_job(job_id=str(item.job_id)) + cls.add_scheduler_job(item) + scheduler.add_listener(cls.scheduler_event_listener, EVENT_ALL) + logger.info('系统初始定时任务加载成功') + + @classmethod + async def close_system_scheduler(cls): + """ + 应用关闭时关闭定时任务 + + :return: + """ + scheduler.shutdown() + logger.info('关闭定时任务成功') + + @classmethod + def get_scheduler_job(cls, job_id: Union[str, int]): + """ + 根据任务id获取任务对象 + + :param job_id: 任务id + :return: 任务对象 + """ + query_job = scheduler.get_job(job_id=str(job_id)) + + return query_job + + @classmethod + def add_scheduler_job(cls, job_info: JobModel): + """ + 根据输入的任务对象信息添加任务 + + :param job_info: 任务对象信息 + :return: + """ + job_func = eval(job_info.invoke_target) + job_executor = job_info.job_executor + if iscoroutinefunction(job_func): + job_executor = 'default' + scheduler.add_job( + func=eval(job_info.invoke_target), + trigger=MyCronTrigger.from_crontab(job_info.cron_expression), + args=job_info.job_args.split(',') if job_info.job_args else None, + kwargs=json.loads(job_info.job_kwargs) if job_info.job_kwargs else None, + id=str(job_info.job_id), + name=job_info.job_name, + misfire_grace_time=1000000000000 if job_info.misfire_policy == '3' else None, + coalesce=True if job_info.misfire_policy == '2' else False, + max_instances=3 if job_info.concurrent == '0' else 1, + jobstore=job_info.job_group, + executor=job_executor, + ) + + @classmethod + def execute_scheduler_job_once(cls, job_info: JobModel): + """ + 根据输入的任务对象执行一次任务 + + :param job_info: 任务对象信息 + :return: + """ + job_func = eval(job_info.invoke_target) + job_executor = job_info.job_executor + if iscoroutinefunction(job_func): + job_executor = 'default' + job_trigger = DateTrigger() + if job_info.status == '0': + job_trigger = OrTrigger(triggers=[DateTrigger(), MyCronTrigger.from_crontab(job_info.cron_expression)]) + scheduler.add_job( + func=eval(job_info.invoke_target), + trigger=job_trigger, + args=job_info.job_args.split(',') if job_info.job_args else None, + kwargs=json.loads(job_info.job_kwargs) if job_info.job_kwargs else None, + id=str(job_info.job_id), + name=job_info.job_name, + misfire_grace_time=1000000000000 if job_info.misfire_policy == '3' else None, + coalesce=True if job_info.misfire_policy == '2' else False, + max_instances=3 if job_info.concurrent == '0' else 1, + jobstore=job_info.job_group, + executor=job_executor, + ) + + @classmethod + def remove_scheduler_job(cls, job_id: Union[str, int]): + """ + 根据任务id移除任务 + + :param job_id: 任务id + :return: + """ + query_job = cls.get_scheduler_job(job_id=job_id) + if query_job: + scheduler.remove_job(job_id=str(job_id)) + + @classmethod + def scheduler_event_listener(cls, event): + # 获取事件类型和任务ID + event_type = event.__class__.__name__ + # 获取任务执行异常信息 + status = '0' + exception_info = '' + if event_type == 'JobExecutionEvent' and event.exception: + exception_info = str(event.exception) + status = '1' + if hasattr(event, 'job_id'): + job_id = event.job_id + query_job = cls.get_scheduler_job(job_id=job_id) + if query_job: + query_job_info = query_job.__getstate__() + # 获取任务名称 + job_name = query_job_info.get('name') + # 获取任务组名 + job_group = query_job._jobstore_alias + # 获取任务执行器 + job_executor = query_job_info.get('executor') + # 获取调用目标字符串 + invoke_target = query_job_info.get('func') + # 获取调用函数位置参数 + job_args = ','.join(query_job_info.get('args')) + # 获取调用函数关键字参数 + job_kwargs = json.dumps(query_job_info.get('kwargs')) + # 获取任务触发器 + job_trigger = str(query_job_info.get('trigger')) + # 构造日志消息 + job_message = f"事件类型: {event_type}, 任务ID: {job_id}, 任务名称: {job_name}, 执行于{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + job_log = JobLogModel( + jobName=job_name, + jobGroup=job_group, + jobExecutor=job_executor, + invokeTarget=invoke_target, + jobArgs=job_args, + jobKwargs=job_kwargs, + jobTrigger=job_trigger, + jobMessage=job_message, + status=status, + exceptionInfo=exception_info, + createTime=datetime.now(), + ) + session = SessionLocal() + JobLogService.add_job_log_services(session, job_log) + session.close() diff --git a/ruoyi-fastapi-backend/config/static_qa.json b/ruoyi-fastapi-backend/config/static_qa.json new file mode 100644 index 0000000..b9c296c --- /dev/null +++ b/ruoyi-fastapi-backend/config/static_qa.json @@ -0,0 +1,3197 @@ +{ + "qa_pairs": [ + { + "question": "你叫什么名字?", + "answer": "您好呀!我是厂区专属接待机器人达达,很高兴为您服务~如果您需要了解厂区信息、指引路线或咨询参观流程,都可以跟我说哦!", + "category": "基础问候与身份", + "priority": 10, + "sub_questions": [ + "你是厂区专属接待机器人吗", + "你可以指引路线吗", + "你可以介绍参观流程吗" + ], + "variations": [ + "怎么称呼你", + "你叫啥", + "你的名字是什么", + "你好达达", + "hi", + "hello" + ] + }, + { + "question": "介绍一下康达新材", + "answer": "康达新材于1988年成立, 在深交所主板上市,总部位于上海浦东,布局多地生产及分支机构。公司拥有300余人新材料及军工电子研发团队,百余项专利及软件著作权,集团超1500人,产值近25亿元。业务覆盖装备制造、新能源、航空航天、国防军工等领域,以技术创新为核心,提供系统化解决方案。未来聚焦功能性高分子新材料,深化电子信息、高性能复合材料布局,完善军工战略,打造“新材料+军工电子科技”平台,致力于成为细分领域龙头", + "category": "基础问候与身份", + "priority": 10, + "sub_questions": [ + "康达新材什么时候成立的", + "康达新材总部在哪里", + "康达新材有多少研发人员", + "康达新材业务覆盖哪些领域", + "康达新材未来发展方向是什么" + ], + "variations": [ + "康达新材简介", + "什么是康达新材", + "康达新材公司介绍" + ] + }, + { + "question": "你能帮我做什么?", + "answer": "我能为您提供超多实用帮助呢!比如介绍厂区的发展历史、各功能区域位置指引、参观预约流程讲解", + "category": "基础问候与身份", + "priority": 10, + "sub_questions": [ + "可以介绍厂区历史吗", + "怎么预约参观" + ], + "variations": [ + "你会做什么", + "你能提供什么帮助", + "你有什么功能" + ] + }, + { + "question": "康达新材成立于哪一年?", + "answer": "1988年", + "category": "公司概况", + "priority": 10, + "sub_questions": [], + "variations": [ + "康达新材哪年成立的", + "公司成立时间", + "什么时候创立的" + ] + }, + { + "question": "康达新材的上市时间是什么时候?", + "answer": "2012年4月", + "category": "公司概况", + "priority": 10, + "sub_questions": [], + "variations": [ + "康达新材哪年上市的", + "上市日期是什么时候", + "什么时候IPO的" + ] + }, + { + "question": "康达新材的股票代码是什么?", + "answer": "002669.SZ", + "category": "公司概况", + "priority": 10, + "sub_questions": [], + "variations": [ + "康达新材股票代码", + "证券代码是多少", + "股票代码" + ] + }, + { + "question": "康达新材的实际控制人是谁?", + "answer": "唐山市人民政府国有资产监督管理委员会", + "category": "公司概况", + "priority": 10, + "sub_questions": [], + "variations": [ + "康达新材实控人", + "谁控制着康达新材", + "公司的实际控制人是谁" + ] + }, + { + "question": "康达新材的股权背景链条是怎样的?", + "answer": "唐山市人民政府国有资产监督管理委员会→唐山控股发展集团股份有限公司→唐山工业控股集团有限公司→康达新材料(集团)股份有限公司", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "康达新材的母公司是谁", + "康达新材是唐山控股的下属公司吗" + ], + "variations": [ + "康达新材股权结构", + "公司的股东背景", + "股权关系是怎样的" + ] + }, + { + "question": "康达新材的三大业务板块是什么?", + "answer": "胶粘剂与特种树脂新材料、电子信息材料和电子科技", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "康达新材做电子科技吗", + "胶粘剂是主营业务吗" + ], + "variations": [ + "公司主营业务有哪些", + "康达新材主要做什么", + "业务板块包括什么" + ] + }, + { + "question": "康达新材的业务范围覆盖哪些新兴产业?", + "answer": "装备制造、新能源、轨道交通、航空航天、电子信息及低碳环保等新兴产业", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "康达新材涉及新能源吗", + "康达新材做航空航天材料吗" + ], + "variations": [ + "产品应用在哪些行业", + "业务覆盖哪些领域", + "服务哪些新兴产业" + ] + }, + { + "question": "截至2024年底,康达新材的总资产是多少?", + "answer": "69.40亿元", + "category": "公司概况", + "priority": 10, + "sub_questions": [], + "variations": [ + "2024年总资产多少", + "康达新材资产规模", + "公司现在的总资产" + ] + }, + { + "question": "康达新材2024年的营业收入是多少?", + "answer": "31亿元", + "category": "公司概况", + "priority": 10, + "sub_questions": [], + "variations": [ + "2024年营收多少", + "去年营业收入是多少", + "2024年业绩如何" + ] + }, + { + "question": "截至2024年底,康达新材的员工总人数有多少?", + "answer": "1637人", + "category": "公司概况", + "priority": 10, + "sub_questions": [], + "variations": [ + "公司有多少员工", + "2024年员工总数", + "康达新材团队规模" + ] + }, + { + "question": "康达新材的管理总部位于哪里?", + "answer": "浦东新区张江科学城", + "category": "公司概况", + "priority": 10, + "sub_questions": [], + "variations": [ + "总部地址在哪里", + "康达新材总部位置", + "管理总部在哪" + ] + }, + { + "question": "康达新材在国内哪些地区设有子公司、生产基地或分支机构?", + "answer": "上海、北京、天津、深圳、大连、成都、香港、福建、唐山等地", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "康达新材在唐山有分公司吗", + "康达新材在成都有基地吗" + ], + "variations": [ + "国内布局情况", + "哪些城市有分公司", + "子公司分布在哪里" + ] + }, + { + "question": "康达新材的海外布局有多少处?", + "answer": "3处", + "category": "公司概况", + "priority": 10, + "sub_questions": [], + "variations": [ + "海外有几个点", + "海外布局数量", + "国外有多少分支机构" + ] + }, + { + "question": "康达新材有多少个生产基地?", + "answer": "7个", + "category": "公司概况", + "priority": 10, + "sub_questions": [], + "variations": [ + "生产基地数量", + "一共有几个工厂", + "有几个生产基地" + ] + }, + { + "question": "康达新材的胶粘剂产品牌号数有多少?", + "answer": "500+", + "category": "公司概况", + "priority": 10, + "sub_questions": [], + "variations": [ + "有多少种胶粘剂产品", + "产品型号有多少", + "胶粘剂牌号数量" + ] + }, + { + "question": "康达新材在多个细分领域的市场地位如何?", + "answer": "多项细分领域市场地位第一", + "category": "公司概况", + "priority": 10, + "sub_questions": [], + "variations": [ + "市场排名怎么样", + "行业地位如何", + "是否是行业第一" + ] + }, + { + "question": "康达新材的使命是什么?", + "answer": "服务国家战略,引领行业发展", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "康达新材要服务什么", + "康达新材要引领什么" + ], + "variations": [ + "企业使命", + "公司的使命是什么", + "康达新材使命" + ] + }, + { + "question": "康达新材的愿景是什么?", + "answer": "成为具有全球化视野的“新材料+电子科技”集团化企业", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "康达新材要成为什么样的企业", + "公司的战略方向是什么" + ], + "variations": [ + "企业愿景", + "公司未来的愿景", + "康达新材愿景" + ] + }, + { + "question": "康达新材的核心价值观是什么?", + "answer": "融合、协同、创新、超越", + "category": "公司概况", + "priority": 10, + "sub_questions": [], + "variations": [ + "企业价值观", + "公司的核心价值观", + "康达新材价值观" + ] + }, + { + "question": "康达新材的企业作风是什么?", + "answer": "不断学习,立即行动,没有任何借口", + "category": "公司概况", + "priority": 10, + "sub_questions": [], + "variations": [ + "工作作风", + "企业作风是什么", + "康达新材的行事风格" + ] + }, + { + "question": "康达新材的发展战略核心是什么?", + "answer": "坚守“稳中求进”工作总基调,以科技创新为根本动力,以转型升级、抢占新赛道为基本路径,以高质量发展为主题,立足主业,打造“新材料+电子科技”国家级“专精特新”小巨人企业集群", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "康达新材发展的根本动力是什么", + "公司要打造什么样的企业集群" + ], + "variations": [ + "公司发展战略", + "战略核心内容", + "未来发展规划" + ] + }, + { + "question": "康达新材“1+2+3+1+N”战略布局中的“1个产业研究院”具体定位是什么?", + "answer": "产业发展智库,科技成果研发、转化合作平台,技术信息共享中心", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "产业研究院是智库吗", + "产业研究院有什么功能" + ], + "variations": [ + "产业研究院是做什么的", + "研究院的定位", + "产业研究院职能" + ] + }, + { + "question": "康达新材“1+2+3+1+N”战略布局中的“2个战略新兴产业方向”是什么?", + "answer": "新材料+电子科技", + "category": "公司概况", + "priority": 10, + "sub_questions": [], + "variations": [ + "哪两个新兴产业方向", + "战略新兴产业是哪两个", + "重点发展的两个方向" + ] + }, + { + "question": "康达新材“1+2+3+1+N”战略布局中的“3大业务板块”具体指什么?", + "answer": "胶粘剂与特种树脂新材料+电子信息材料+电子科技", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "电子信息材料是三大板块之一吗", + "胶粘剂属于哪个板块" + ], + "variations": [ + "三大业务板块是什么", + "业务布局包括哪些", + "哪三大板块" + ] + }, + { + "question": "康达新材“1+2+3+1+N”战略布局中的“1+N个产业园(基地)”已在哪些地区建立?", + "answer": "上海、北京、成都、天津、大连、深圳、唐山、福建邵武、香港、泰国等地", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "康达新材在泰国有基地吗", + "康达新材在福建有产业园吗" + ], + "variations": [ + "产业园分布在哪里", + "基地都在哪些城市", + "产业园布局情况" + ] + }, + { + "question": "康达新材在新材料领域的进口替代目标涉及哪些领域?", + "answer": "半导体材料、显示材料、生物医用材料、新能源材料、高性能纤维、高性能膜材料、先进高分子材料及其他领域", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "半导体材料是替代目标吗", + "新能源材料涉及进口替代吗" + ], + "variations": [ + "哪些领域做进口替代", + "进口替代涉及的行业", + "新材料替代目标" + ] + }, + { + "question": "康达新材在半导体材料领域的进口依赖材料有哪些?", + "answer": "大尺寸硅材料、大尺寸碳化硅单晶、SOI、高饱和度光刻胶、高性能靶材、电子特种气体、湿电子化学品、氮化镓单晶/氮化镓单晶衬底、化学机械抛光(CMP)材料、封装基板、高密度陶瓷材料等", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "大尺寸硅材料依赖进口吗", + "光刻胶是进口依赖材料吗" + ], + "variations": [ + "半导体领域哪些材料依赖进口", + "半导体进口替代产品有哪些", + "半导体卡脖子材料" + ] + }, + { + "question": "康达新材在显示材料领域的进口依赖材料有哪些?", + "answer": "OLED发光材料、超薄玻璃、高世代线玻璃基板、精细金属掩模板(FMM)、光学膜、柔性PI膜、偏光片、高性能水汽阻隔膜、异方性导电胶膜(ACF)、特种光学聚氨酯膜(PET)、OCA光学胶、微球、抗指纹涂层(AFC)涂料等", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "OLED发光材料依赖进口吗", + "偏光片是进口依赖材料吗" + ], + "variations": [ + "显示材料领域哪些依赖进口", + "显示屏相关进口替代材料", + "显示领域卡脖子材料" + ] + }, + { + "question": "康达新材在生物医用材料领域的进口依赖材料有哪些?", + "answer": "医用级钛粉与镍钛合金粉、苯乙烯类热塑性弹性体、医用级聚乳酸、碲锌镉晶体、人工晶状体等", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "医用级聚乳酸依赖进口吗", + "人工晶状体是进口依赖材料吗" + ], + "variations": [ + "生物医用哪些材料依赖进口", + "医用材料进口替代", + "生物医用领域卡脖子材料" + ] + }, + { + "question": "康达新材在新能源材料领域的进口依赖材料有哪些?", + "answer": "硅碳负极材料、电解铜箔、电解液添加剂、铝塑膜、质子交换膜、氢燃料电池催化剂、气体扩散层材料等", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "硅碳负极材料依赖进口吗", + "氢燃料电池催化剂是进口依赖吗" + ], + "variations": [ + "新能源哪些材料依赖进口", + "新能源进口替代产品", + "新能源领域卡脖子材料" + ] + }, + { + "question": "康达新材在高性能纤维领域的进口依赖材料有哪些?", + "answer": "高性能碳纤维及其复合材料、高性能对位芳纶纤维及其复合材料、超高分子量聚乙烯纤维等", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "高性能碳纤维依赖进口吗", + "芳纶纤维是进口依赖吗" + ], + "variations": [ + "高性能纤维哪些依赖进口", + "纤维材料进口替代", + "高性能纤维领域卡脖子材料" + ] + }, + { + "question": "康达新材在高性能膜材料领域的进口依赖材料有哪些?", + "answer": "海水淡化反渗透膜、陶瓷膜、离子交换膜、中空纤维膜、高导热石墨膜等", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "反渗透膜依赖进口吗", + "陶瓷膜是进口依赖吗" + ], + "variations": [ + "膜材料哪些依赖进口", + "高性能膜进口替代", + "膜材料领域卡脖子材料" + ] + }, + { + "question": "康达新材在先进高分子材料领域的进口依赖材料有哪些?", + "answer": "聚苯硫醚(PPS)、聚砜(PSF)、聚醚醚酮(PEEK)、聚偏氯乙烯(PVDF)、聚甲醛(POM)、有机硅等", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "PEEK材料依赖进口吗", + "有机硅是进口依赖吗" + ], + "variations": [ + "高分子材料哪些依赖进口", + "先进高分子进口替代", + "高分子领域卡脖子材料" + ] + }, + { + "question": "康达新材在“其他”领域的进口依赖材料有哪些?", + "answer": "高频覆铜板基材、液晶高分子聚合物(LCP)等", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "高频覆铜板基材依赖进口吗", + "LCP是进口依赖吗" + ], + "variations": [ + "还有哪些材料依赖进口", + "其他领域的进口替代", + "其他卡脖子材料" + ] + }, + { + "question": "康达新材及下属子公司中获得国家级“专精特新”小巨人企业认定的有哪些?", + "answer": "大连齐化新材料有限公司(有效期:2022年7月1日至2025年6月30日)、成都必控科技有限责任公司、上海晶材新材料科技有限公司、康达新材", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "大连齐化是国家级专精特新吗", + "成都必控是小巨人企业吗" + ], + "variations": [ + "哪些是国家级专精特新小巨人", + "获得小巨人认定的子公司有哪些", + "公司有多少个国家级小巨人" + ] + }, + { + "question": "北京力源兴达科技有限公司获得了什么级别的“专精特新”相关认定?", + "answer": "北京市专精特新“小巨人”及中小企业", + "category": "公司概况", + "priority": 10, + "sub_questions": [], + "variations": [ + "北京力源兴达是专精特新吗", + "力源兴达获得了什么认定", + "北京力源兴达的资质" + ] + }, + { + "question": "成都赛英科技有限公司获得了什么级别的“专精特新”相关认定?有效期是多久?", + "answer": "四川省专精特新中小企业,有效期为2022年11月1日-2025年10月31日", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "成都赛英是四川省专精特新吗", + "成都赛英认定的有效期" + ], + "variations": [ + "成都赛英的专精特新级别", + "赛英科技获得什么认定", + "成都赛英资质情况" + ] + }, + { + "question": "上海理日新材料科技有限公司获得了什么级别的“专精特新”相关认定?", + "answer": "上海市专精特新中小企业", + "category": "公司概况", + "priority": 10, + "sub_questions": [], + "variations": [ + "上海理日是专精特新吗", + "上海理日获得了什么认定", + "上海理日的资质" + ] + }, + { + "question": "大连齐化获得了哪些资质认证?", + "answer": "2020年12月被科技部火炬中心认定为国家级“高新技术企业”,被辽宁省科技厅认定为“雏鹰企业”;2023年5月,质检中心被中国石油和化学工业联合会认定为“A级”石油和化工企业质量检验机构;通过ISO9001、ISO14001、ISO45001三体系认证", + "category": "公司概况", + "priority": 10, + "sub_questions": [ + "大连齐化是高新技术企业吗", + "大连齐化通过了ISO认证吗" + ], + "variations": [ + "大连齐化有什么证书", + "大连齐化的企业资质", + "大连齐化获得过什么荣誉" + ] + }, + { + "question": "河北惟新科技有限公司获得了什么资质?认定编号和有效期是多少?", + "answer": "河北省科技型中小企业,认定编号:K23016712,有效期3年", + "category": "公司概况", + "priority": 10, + "sub_questions": [], + "variations": [ + "河北惟新是什么资质", + "河北惟新的认定编号", + "惟新科技的资质有效期" + ] + }, + { + "question": "康达新材在1988年发生了什么关键事件?", + "answer": "成立上海康达化工实验厂", + "category": "历史发展", + "priority": 10, + "sub_questions": [], + "variations": [ + "1988年发生了什么", + "88年的大事记", + "公司1988年的历史" + ] + }, + { + "question": "康达新材在2000年发生了什么关键事件?", + "answer": "成立上海康达化工技术研究所", + "category": "历史发展", + "priority": 10, + "sub_questions": [], + "variations": [ + "2000年发生了什么", + "2000年的大事记", + "公司2000年的历史" + ] + }, + { + "question": "康达新材在2002年完成了哪两项重要事件?", + "answer": "引入战略投资者,完成股份制改革,整体变更设立为上海康达化工新材料股份有限公司;获得中国合格评定国家认可委员会(CNAS)认可的实验室资质", + "category": "历史发展", + "priority": 10, + "sub_questions": [ + "哪年完成股份制改革的", + "哪年获得CNAS认可的" + ], + "variations": [ + "2002年发生了什么", + "2002年的大事记", + "公司2002年的历史" + ] + }, + { + "question": "康达新材在2008年发生了什么关键事件?", + "answer": "成立上海康达化工有限责任公司;被认定为国家高新技术企业", + "category": "历史发展", + "priority": 10, + "sub_questions": [ + "哪年被认定为高新技术企业的", + "2008年成立了什么公司" + ], + "variations": [ + "2008年发生了什么", + "2008年的大事记", + "公司2008年的历史" + ] + }, + { + "question": "康达新材在2012年发生了什么关键事件?", + "answer": "成功在深圳交易所上市", + "category": "历史发展", + "priority": 10, + "sub_questions": [], + "variations": [ + "2012年发生了什么", + "2012年的大事记", + "公司2012年的历史" + ] + }, + { + "question": "康达新材在2016年发生了什么关键事件?", + "answer": "非公开发行股票融资8.5亿元,助推康达新材高质量发展", + "category": "历史发展", + "priority": 10, + "sub_questions": [], + "variations": [ + "2016年融资了多少", + "2016年发生了什么", + "公司2016年的历史" + ] + }, + { + "question": "2018年康达新材收购了成都赛英科技有限公司,此次收购的目的是什么?", + "answer": "完善电子科技领域产业链布局", + "category": "历史发展", + "priority": 10, + "sub_questions": [], + "variations": [ + "为什么要收购成都赛英", + "收购赛英科技的意义", + "2018年收购赛英的目的" + ] + }, + { + "question": "2018年康达新材收购了上海晶材新材料科技有限公司,此次收购的目的是什么?", + "answer": "布局先进陶瓷新材料领域,打造“专精特新”小巨人企业集群", + "category": "历史发展", + "priority": 10, + "sub_questions": [], + "variations": [ + "为什么要收购上海晶材", + "收购晶材科技的意义", + "2018年收购上海晶材的目的" + ] + }, + { + "question": "2018年康达新材收购了大连齐化新材料有限公司,此次收购的目的是什么?", + "answer": "向产业链上游战略延伸,打造环氧树脂产业闭环“生态圈”", + "category": "历史发展", + "priority": 10, + "sub_questions": [], + "variations": [ + "为什么要收购大连齐化", + "收购大连齐化的意义", + "2018年收购大连齐化的目的" + ] + }, + { + "question": "2019年康达新材收购了成都必控科技有限责任公司,此次收购的意义是什么?", + "answer": "成功迈入电子科技领域", + "category": "历史发展", + "priority": 10, + "sub_questions": [], + "variations": [ + "为什么要收购成都必控", + "收购必控科技的目的", + "2019年收购必控的意义" + ] + }, + { + "question": "康达新材的研发中心被认定为国家企业技术中心是在哪一年?", + "answer": "2019年", + "category": "历史发展", + "priority": 10, + "sub_questions": [], + "variations": [ + "哪年成为国家企业技术中心的", + "技术中心什么时候升级的", + "2019年获得的研发认定" + ] + }, + { + "question": "康达新材的控股股东变更为唐山工业控股集团有限公司是在哪一年?", + "answer": "2019年", + "category": "历史发展", + "priority": 10, + "sub_questions": [], + "variations": [ + "哪年变更控股股东的", + "什么时候变成唐山控股子公司的", + "2019年股权变更情况" + ] + }, + { + "question": "康达新材更名为“康达新材”是在什么时间?", + "answer": "2021年", + "category": "历史发展", + "priority": 10, + "sub_questions": [], + "variations": [ + "哪年改名的", + "什么时候叫康达新材的", + "更名时间" + ] + }, + { + "question": "康达新材在2021年进行集团化战略布局的范围是什么?", + "answer": "全国区域", + "category": "历史发展", + "priority": 10, + "sub_questions": [], + "variations": [ + "2021年战略布局范围", + "集团化布局区域", + "战略布局涉及哪里" + ] + }, + { + "question": "康达新材被认定为国家级“专精特新”小巨人企业是在哪一年?", + "answer": "2021年", + "category": "历史发展", + "priority": 10, + "sub_questions": [], + "variations": [ + "哪年评上专精特新小巨人的", + "小巨人认定时间", + "什么时候成为国家级小巨人的" + ] + }, + { + "question": "2022年康达新材收购了西安彩晶光电科技股份有限公司,此次收购进入了哪些领域?", + "answer": "电子信息材料、新能源材料、医药领域,打造新材料“第二增长极”", + "category": "历史发展", + "priority": 10, + "sub_questions": [ + "收购彩晶进入了医药领域吗", + "收购彩晶涉及新能源吗" + ], + "variations": [ + "收购西安彩晶的目的是什么", + "彩晶光电涉及哪些领域", + "2022年收购彩晶的意义" + ] + }, + { + "question": "康达新材收购河北惟新科技有限公司是在哪一年?", + "answer": "2023年", + "category": "历史发展", + "priority": 10, + "sub_questions": [], + "variations": [ + "哪年收购河北惟新的", + "收购惟新科技的时间", + "2023年收购了谁" + ] + }, + { + "question": "康达新材收购河北惟新科技有限公司解决了什么“卡脖子”问题?", + "answer": "大尺寸ITO靶材“卡脖子”问题", + "category": "历史发展", + "priority": 10, + "sub_questions": [], + "variations": [ + "收购河北惟新解决了什么难题", + "惟新科技解决了什么卡脖子技术", + "ITO靶材是卡脖子问题吗" + ] + }, + { + "question": "奉贤研发及生产基地(上海)的位置在哪里?", + "answer": "上海奉贤杭州湾经济技术开发区", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [], + "variations": [ + "奉贤基地地址", + "上海基地的位置", + "奉贤工厂在哪里" + ] + }, + { + "question": "奉贤研发及生产基地(上海)的占地面积是多少?", + "answer": "205亩", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [], + "variations": [ + "奉贤基地有多大", + "上海基地占地面积", + "奉贤工厂面积" + ] + }, + { + "question": "奉贤研发及生产基地(上海)的建设历程是怎样的?", + "answer": "包含一期、二期、三期项目,2009年一期建成投产,2015年二期建成投产,2019年三期建成投产", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [ + "奉贤一期哪年投产", + "奉贤三期什么时候建成的" + ], + "variations": [ + "奉贤基地建设过程", + "上海基地的建设历史", + "奉贤工厂分几期建设" + ] + }, + { + "question": "奉贤研发及生产基地(上海)的定位是什么?", + "answer": "遵循高端化、数字化、一体化、绿色生态的发展理念,打造胶粘剂、复合材料等新材料产业链;奉贤基地二期全面启用后,形成集科研开发、生产、服务、贸易为一体的科研产业综合实体", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [ + "奉贤基地生产胶粘剂吗", + "奉贤基地做复合材料吗" + ], + "variations": [ + "奉贤基地是做什么的", + "上海基地的职能", + "奉贤研发中心的定位" + ] + }, + { + "question": "奉贤研发及生产基地(上海)的胶粘剂产品产能是多少?", + "answer": "环氧结构胶、环氧基体树脂、丙烯酸胶、聚氨酯胶、丁基胶等胶粘剂产品产能80000吨", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [ + "奉贤基地能生产多少胶水", + "上海基地胶粘剂年产量" + ], + "variations": [ + "奉贤基地胶粘剂产能", + "上海工厂产能多少", + "奉贤基地年产量" + ] + }, + { + "question": "奉贤研发及生产基地(上海)的复合材料产能是多少?", + "answer": "环氧基、聚氨酯基复合材料10000吨", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [], + "variations": [ + "奉贤基地复材产能", + "上海基地复合材料产量", + "奉贤工厂复材年产量" + ] + }, + { + "question": "唐山丰南生产基地(河北)的位置在哪里?", + "answer": "河北省唐山市丰南经济开发区化工产业园区", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [], + "variations": [ + "唐山基地地址", + "河北基地的位置", + "唐山工厂在哪里" + ] + }, + { + "question": "唐山丰南生产基地(河北)的占地面积是多少?", + "answer": "114亩", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [], + "variations": [ + "唐山基地有多大", + "河北基地占地面积", + "唐山工厂面积" + ] + }, + { + "question": "唐山丰南生产基地(河北)的主要产品有哪些?", + "answer": "水性聚氨酯丙烯酸酯分散体、无溶剂复膜胶、改性型溶剂聚氨酯复膜胶、改性橡胶树脂溶剂胶、高性能丙烯酸酯结构胶、硅胶、环氧树脂胶", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [ + "唐山基地生产复膜胶吗", + "唐山基地生产硅胶吗" + ], + "variations": [ + "唐山基地生产什么", + "河北基地主要产品", + "唐山工厂的产品有哪些" + ] + }, + { + "question": "唐山丰南生产基地(河北)辐射哪些市场?", + "answer": "北方市场", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [], + "variations": [ + "唐山基地主要卖给谁", + "河北基地覆盖哪些区域", + "唐山基地的市场定位" + ] + }, + { + "question": "唐山丰南生产基地(河北)的产品应用于哪些领域?", + "answer": "风电叶片制造、包装材料、轨道交通、汽车、电子电器、机械设备、建筑等领域(含在建基地)", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [ + "唐山产品用于风电吗", + "唐山产品用于轨道交通吗" + ], + "variations": [ + "唐山基地产品用途", + "河北基地服务哪些行业", + "唐山工厂应用领域" + ] + }, + { + "question": "福建邵武生产基地的位置在哪里?", + "answer": "福建省邵武市金塘省级化工园区", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [], + "variations": [ + "邵武基地地址", + "福建基地的位置", + "邵武工厂在哪里" + ] + }, + { + "question": "福建邵武生产基地的占地面积是多少?", + "answer": "95亩", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [], + "variations": [ + "邵武基地有多大", + "福建基地占地面积", + "邵武工厂面积" + ] + }, + { + "question": "福建邵武生产基地的主要产品有哪些?", + "answer": "功能型复膜胶、改性弹性树脂胶黏剂、高性能结构胶、功能性聚酯及反应型热熔胶、特种树脂中间体", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [ + "邵武基地生产热熔胶吗", + "邵武基地生产结构胶吗" + ], + "variations": [ + "邵武基地生产什么", + "福建基地主要产品", + "邵武工厂的产品有哪些" + ] + }, + { + "question": "福建邵武生产基地辐射哪些市场?", + "answer": "南方市场", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [], + "variations": [ + "邵武基地主要卖给谁", + "福建基地覆盖哪些区域", + "邵武基地的市场定位" + ] + }, + { + "question": "福建邵武生产基地的产品应用于哪些领域?", + "answer": "包装材料、电子电器、机械设备、建筑装饰及工业维修等领域", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [], + "variations": [ + "邵武基地产品用途", + "福建基地服务哪些行业", + "邵武工厂应用领域" + ] + }, + { + "question": "福建邵武生产基地的战略意义是什么?", + "answer": "为核心产品提供重要原材料(含在建基地)", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [], + "variations": [ + "建设邵武基地的目的", + "福建基地的作用", + "邵武工厂的战略地位" + ] + }, + { + "question": "大连齐化产业园区的位置在哪里?", + "answer": "辽宁省大连经济技术开发区", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [], + "variations": [ + "大连齐化地址", + "大连基地的位置", + "大连园区在哪里" + ] + }, + { + "question": "大连齐化产业园区的主要产品系列有哪些?", + "answer": "双酚A型环氧树脂、耐热型环氧树脂和特种环氧树脂三大系列", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [ + "大连齐化生产双酚A环氧树脂吗", + "大连齐化做特种树脂吗" + ], + "variations": [ + "大连齐化生产什么", + "大连基地主要产品系列", + "大连园区的产品有哪些" + ] + }, + { + "question": "大连齐化产业园区的具体产品包含哪些?", + "answer": "双酚A型液体环氧树脂、双酚A型固体环氧树脂、溴化环氧树脂、邻甲酚醛环氧树脂、苯酚酚醛环氧树脂、双酚F型环氧树脂、苯氧基树脂、环氧固化剂、环氧稀释剂等多个品种", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [ + "大连齐化生产固化剂吗", + "大连齐化生产稀释剂吗" + ], + "variations": [ + "大连齐化详细产品清单", + "大连基地具体产品", + "大连园区的树脂种类" + ] + }, + { + "question": "大连齐化产业园区液态环氧树脂的装置数量、产能和实际产能分别是多少?", + "answer": "装置数量1套,产能3.4万吨,实际产能4.3万吨", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [ + "液态环氧树脂产能多少", + "液态环氧树脂实际产能" + ], + "variations": [ + "大连齐化液态树脂产能", + "液态环氧树脂装置情况", + "大连液态树脂产量" + ] + }, + { + "question": "大连齐化产业园区固态环氧树脂的装置数量、产能和实际产能分别是多少?", + "answer": "装置数量1套,产能0.5万吨,实际产能1.5万吨", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [ + "固态环氧树脂产能多少", + "固态环氧树脂实际产能" + ], + "variations": [ + "大连齐化固态树脂产能", + "固态环氧树脂装置情况", + "大连固态树脂产量" + ] + }, + { + "question": "大连齐化产业园区邻甲酚醛环氧树脂的装置数量和产能分别是多少?", + "answer": "装置数量1套,产能0.5万吨", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [], + "variations": [ + "邻甲酚醛环氧树脂产能", + "邻甲酚醛树脂装置情况", + "大连邻甲酚醛树脂产量" + ] + }, + { + "question": "大连齐化产业园区特种树脂中试装置的数量和产能分别是多少?", + "answer": "装置数量1套,产能1000吨", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [], + "variations": [ + "特种树脂中试产能", + "中试装置情况", + "大连特种树脂中试规模" + ] + }, + { + "question": "大连齐化产业园区的战略定位是什么?", + "answer": "重点布局下游产业链,打造环氧树脂产业闭环“生态圈”,以大连齐化的低溴环氧树脂技术为依托,开展覆铜板领域的战略布局", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [ + "大连齐化布局覆铜板吗", + "大连齐化要打造生态圈吗" + ], + "variations": [ + "大连齐化的发展方向", + "大连园区的战略目标", + "大连基地的定位" + ] + }, + { + "question": "成都康达电子科技产业园的位置在哪里?", + "answer": "四川省成都市高新区未来科技城", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [], + "variations": [ + "成都产业园地址", + "成都基地的位置", + "康达成都园区在哪" + ] + }, + { + "question": "成都康达电子科技产业园的占地面积是多少?", + "answer": "64亩", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [], + "variations": [ + "成都园区有多大", + "成都基地占地面积", + "成都产业园面积" + ] + }, + { + "question": "成都康达电子科技产业园的定位是什么?", + "answer": "打造电子科技领域系统级产品研发、生产科技创新园区,使之成为四川乃至西南地区具有比较优势的电子信息特色产业园,推动地方战略性新兴产业的发展,助力公司电子科技核心业务做强做精", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [ + "成都园区做电子科技吗", + "成都园区是研发中心吗" + ], + "variations": [ + "成都产业园是做什么的", + "成都基地的职能", + "成都园区的战略定位" + ] + }, + { + "question": "康达新材的胶粘剂研发中心拥有哪些资质平台?", + "answer": "国家级的企业技术中心、上海胶粘剂工程技术研究中心、上海市企业技术中心、企业博士后科研工作总站、国家认定博士后科研工作站(人力资源和社会保障部、全国博士后管委会制发)、国家认可委认可的CNAS国家实验室、德国劳氏船级社(GL)中国认可检测中心", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [ + "研发中心有CNAS认证吗", + "有博士后工作站吗" + ], + "variations": [ + "胶粘剂研发中心的资质", + "研发平台有哪些认证", + "技术中心的级别" + ] + }, + { + "question": "康达新材胶粘剂研发中心的技术型研发占比多少?主要内容是什么?", + "answer": "占比25%,主要内容是引进项目的消化、吸收", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [], + "variations": [ + "技术型研发的情况", + "技术引进占比多少", + "研发中心的吸收消化能力" + ] + }, + { + "question": "康达新材胶粘剂研发中心的产品型研发占比多少?主要内容是什么?", + "answer": "占比50%,主要内容是现有产品的再创新;中试生产及工艺优化", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [], + "variations": [ + "产品型研发的情况", + "产品创新占比多少", + "现有产品优化情况" + ] + }, + { + "question": "康达新材胶粘剂研发中心的关键技术研发占比多少?主要内容是什么?", + "answer": "占比20%,主要内容是关键原材料及高端技术研发,为产品持续的市场竞争力提供支持", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [], + "variations": [ + "关键技术研发情况", + "高端研发占比多少", + "核心技术开发比例" + ] + }, + { + "question": "康达新材胶粘剂研发中心的技术服务型研发占比多少?主要内容是什么?", + "answer": "占比5%,主要内容是与目前的板块服务相对应", + "category": "生产基地与研发中心", + "priority": 10, + "sub_questions": [], + "variations": [ + "技术服务研发情况", + "服务型研发占比", + "技术支持比例" + ] + }, + { + "question": "康达新材2021年的营业收入是多少?", + "answer": "27.93亿元", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2021年营收", + "2021年收入多少", + "21年业绩" + ] + }, + { + "question": "康达新材2021年归属于上市公司股东的净利润是多少?", + "answer": "-2.46亿元", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2021年净利润", + "2021年赚了多少钱", + "21年归母净利润" + ] + }, + { + "question": "康达新材2023年的营业收入是多少?", + "answer": "8.77亿元", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2023年营收", + "2023年收入多少", + "23年业绩" + ] + }, + { + "question": "康达新材2023年归属于上市公司股东的净利润是多少?", + "answer": "0.06亿元", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2023年净利润", + "2023年赚了多少钱", + "23年归母净利润" + ] + }, + { + "question": "康达新材2024年的营业收入是多少?", + "answer": "31.01亿元", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2024年营收", + "2024年收入多少", + "24年业绩" + ] + }, + { + "question": "康达新材2024年归属于上市公司股东的净利润是多少?", + "answer": "0.30亿元", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2024年净利润", + "2024年赚了多少钱", + "24年归母净利润" + ] + }, + { + "question": "2022年康达新材的研发人员数量是多少?", + "answer": "376人", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2022年研发人员有多少", + "22年研发团队规模", + "2022年有多少研发" + ] + }, + { + "question": "2023年康达新材的研发人员数量是多少?", + "answer": "378人", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2023年研发人员有多少", + "23年研发团队规模", + "2023年有多少研发" + ] + }, + { + "question": "2024年康达新材的研发人员数量是多少?", + "answer": "486人", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2024年研发人员有多少", + "24年研发团队规模", + "2024年有多少研发" + ] + }, + { + "question": "2022-2024年康达新材的研发人员数量呈现什么趋势?", + "answer": "呈线性增长趋势", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "近三年研发人员变化", + "研发人员增长趋势", + "人员规模变化情况" + ] + }, + { + "question": "2022年康达新材的研发费用是多少?", + "answer": "1.26亿元", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2022年研发投入", + "22年研发花了多少钱", + "2022年研发支出" + ] + }, + { + "question": "2023年康达新材的研发费用是多少?", + "answer": "1.78亿元", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2023年研发投入", + "23年研发花了多少钱", + "2023年研发支出" + ] + }, + { + "question": "2024年康达新材的研发费用是多少?", + "answer": "2.04亿元", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2024年研发投入", + "24年研发花了多少钱", + "2024年研发支出" + ] + }, + { + "question": "2022-2024年康达新材的研发费用呈现什么趋势?", + "answer": "呈线性增长趋势", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "近三年研发费用变化", + "研发投入增长趋势", + "研发支出变化情况" + ] + }, + { + "question": "2024年康达新材的研发人员数量占比是多少?", + "answer": "22.97%", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2024年研发人员比例", + "研发人员占总人数多少", + "24年技术人员占比" + ] + }, + { + "question": "2024年康达新材的非研发人员占比是多少?", + "answer": "77.03%", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2024年非研发人员比例", + "其他人员占比多少", + "24年非技术人员占比" + ] + }, + { + "question": "2024年康达新材的研发支出占营业收入总额的比例是多少?", + "answer": "6.56%", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2024年研发投入占比", + "研发费用占营收比例", + "24年研发营收比" + ] + }, + { + "question": "2024年康达新材的非研发支出占比是多少?", + "answer": "93.44%", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2024年非研发投入占比", + "其他支出占比", + "24年非研发费用比例" + ] + }, + { + "question": "康达新材2021年的总资产是多少?", + "answer": "37.28亿元", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2021年总资产", + "2021年资产规模", + "21年公司资产" + ] + }, + { + "question": "康达新材2022年的总资产是多少?", + "answer": "53.33亿元", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2022年总资产", + "2022年资产规模", + "22年公司资产" + ] + }, + { + "question": "康达新材2023年的总资产是多少?", + "answer": "69.40亿元", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2023年总资产", + "2023年资产规模", + "23年公司资产" + ] + }, + { + "question": "康达新材2024年的总资产是多少?", + "answer": "71.23亿元", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2024年总资产", + "2024年资产规模", + "24年公司资产" + ] + }, + { + "question": "康达新材2021年的净资产是多少?", + "answer": "22.91亿元", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2021年净资产", + "2021年所有者权益", + "21年公司净资产" + ] + }, + { + "question": "康达新材2022年的净资产是多少?", + "answer": "29.06亿元", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2022年净资产", + "2022年所有者权益", + "22年公司净资产" + ] + }, + { + "question": "康达新材2023年的净资产是多少?", + "answer": "31.43亿元", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2023年净资产", + "2023年所有者权益", + "23年公司净资产" + ] + }, + { + "question": "康达新材2024年的净资产是多少?", + "answer": "33.09亿元", + "category": "财务数据", + "priority": 10, + "sub_questions": [], + "variations": [ + "2024年净资产", + "2024年所有者权益", + "24年公司净资产" + ] + }, + { + "question": "康达新材的胶粘剂与特种树脂新材料业务国内布局包含哪些企业?", + "answer": "河北惟新科技、北京康达晟珊、北京力源兴达、唐山南区康达化工新材有限公司、康达国际供应链(天津)有限公司、大连齐化新材、上海康达新材料科技有限公司、上海理日新材、上海晶材科技、福建南平天宇、成都必控科技、福建康达鑫宇新材料有限公司、成都赛英科技、深圳康达电子材料研发有限公司、上海康达新材(香港)有限公司", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "河北惟新科技是子公司吗", + "上海晶材科技包含在内吗" + ], + "variations": [ + "国内胶粘剂业务有哪些公司", + "胶粘剂业务的国内企业名单", + "特种树脂国内布局企业" + ] + }, + { + "question": "康达新材的胶粘剂与特种树脂新材料业务海外研发办事处设在哪些国家?", + "answer": "德国、日本", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "海外研发办事处在哪里", + "在德国有研发吗", + "海外研发机构分布" + ] + }, + { + "question": "康达新材的胶粘剂与特种树脂新材料业务海外营销办事处设在哪个国家?", + "answer": "印尼", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "海外营销办事处在哪里", + "在印尼有营销点吗", + "海外销售机构分布" + ] + }, + { + "question": "康达新材的胶粘剂与特种树脂新材料业务海外生产基地设在何处?", + "answer": "泰国曼谷", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "海外工厂在哪里", + "在泰国有生产基地吗", + "泰国曼谷基地" + ] + }, + { + "question": "康达新材的胶粘剂与特种树脂新材料业务海外覆盖哪些区域?", + "answer": "德国、丹麦、西班牙、日本、东南亚、巴西等", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "业务覆盖欧洲吗", + "业务覆盖南美吗" + ], + "variations": [ + "海外市场覆盖范围", + "胶粘剂出口到哪些国家", + "海外业务区域" + ] + }, + { + "question": "胶粘剂与特种树脂新材料业务的产品体系包含多少大系列?", + "answer": "8大产品系列", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "有多少个产品系列", + "胶粘剂产品分类", + "特种树脂产品系列" + ] + }, + { + "question": "胶粘剂与特种树脂新材料业务的产品体系包含多少种规格型号?", + "answer": "500多种", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "有多少种产品规格", + "产品型号数量", + "规格型号有多少" + ] + }, + { + "question": "胶粘剂与特种树脂新材料业务的核心应用领域有哪些?", + "answer": "风力发电、光伏太阳能、轨道交通、航空航天、海洋船舶工程、软包装复合、橡塑制品、建筑工程、电子电器、汽摩配件、电机电梯、矿业设备、工业维修、新能源汽车、工程维护、高端制造等", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "产品用于风力发电吗", + "产品用于新能源汽车吗" + ], + "variations": [ + "胶粘剂主要用在哪里", + "产品应用行业", + "核心应用领域" + ] + }, + { + "question": "康达新材在风电领域的核心产品有哪些?", + "answer": "风电胶粘剂系列产品(结构胶、灌注树脂、胶条、喷胶)", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "生产风电结构胶吗", + "生产灌注树脂吗" + ], + "variations": [ + "风电领域卖什么产品", + "风电行业的主要产品", + "风电胶粘剂有哪些" + ] + }, + { + "question": "2024年康达新材风电领域的销售量是多少?", + "answer": "近9万吨", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "2024年风电销量", + "风电产品卖了多少吨", + "24年风电销售量" + ] + }, + { + "question": "2024年康达新材风电领域的销售额(不含税)是多少?", + "answer": "约16亿元", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "2024年风电销售额", + "风电产品卖了多少钱", + "24年风电营收" + ] + }, + { + "question": "康达新材风电领域的客户覆盖范围是什么?", + "answer": "国内所有叶片厂商及其基地,同时产品向海外市场推广", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "覆盖所有国内叶片厂吗", + "风电产品出口吗" + ], + "variations": [ + "风电客户有哪些", + "风电市场覆盖情况", + "叶片厂商合作情况" + ] + }, + { + "question": "康达新材风电领域的核心客户有哪些?", + "answer": "三一重能、中国中材、中国国电、艾郎科技、中科宇能、远景能源、金风科技、时代新材、上玻院、双瑞风电、上海电气、天顺风能、LM艾尔姆、明阳电气、MENS Gamesa等", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "金风科技是客户吗", + "远景能源是客户吗" + ], + "variations": [ + "风电主要客户名单", + "和哪些风电企业合作", + "风电领域的大客户" + ] + }, + { + "question": "康达新材在风电领域的市场地位如何?", + "answer": "市场占有率第一,自主产品保持品牌技术领先、行业领导地位", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "风电市场排名", + "风电胶粘剂市场份额", + "风电行业地位" + ] + }, + { + "question": "康达新材在复膜胶领域的市场份额是多少?", + "answer": "25%", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "复膜胶市场占有率", + "复膜胶份额多少", + "复膜胶业务占比" + ] + }, + { + "question": "复膜胶领域中,法国波士胶的市场份额是多少?", + "answer": "25%", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "波士胶市场份额", + "波士胶占有率", + "法国波士胶占比" + ] + }, + { + "question": "复膜胶领域中,德国汉高的市场份额是多少?", + "answer": "20%", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "汉高市场份额", + "汉高占有率", + "德国汉高占比" + ] + }, + { + "question": "复膜胶领域中,其他30余家企业的市场份额总和是多少?", + "answer": "30%", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "其他企业市场份额", + "剩余市场占比", + "小企业占有率" + ] + }, + { + "question": "康达新材在复膜胶领域的市场地位如何?", + "answer": "市场占有率第一", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "复膜胶市场排名", + "复膜胶行业地位", + "是不是复膜胶第一" + ] + }, + { + "question": "2024年康达新材复膜胶领域的国内客户数量是多少?", + "answer": "700家", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "复膜胶客户有多少", + "国内客户数量", + "有多少家复膜胶客户" + ] + }, + { + "question": "2024年康达新材复膜胶领域的销售额是多少?", + "answer": "约3.2亿元", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "复膜胶卖了多少钱", + "2024年复膜胶营收", + "复膜胶销售额" + ] + }, + { + "question": "康达新材复膜胶领域的销售额年增长率是多少?", + "answer": "10-15%", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "复膜胶增长率", + "每年增长多少", + "复膜胶增速" + ] + }, + { + "question": "康达新材复膜胶领域的软包装复合胶包含哪些类型?", + "answer": "溶剂型聚氨酯复膜胶、无溶剂聚氨酯复膜胶", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "有无溶剂复膜胶吗", + "有溶剂型复膜胶吗" + ], + "variations": [ + "软包装胶有哪些", + "软包装复合胶类型", + "软包装产品分类" + ] + }, + { + "question": "康达新材复膜胶领域的硬包装胶包含哪些类型?", + "answer": "外包装热熔胶、标签快递单压敏胶等", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "有快递单压敏胶吗", + "有热熔胶吗" + ], + "variations": [ + "硬包装胶有哪些", + "硬包装复合胶类型", + "硬包装产品分类" + ] + }, + { + "question": "康达新材复膜胶领域的能量固化印刷油墨包含哪些类型?", + "answer": "UV胶印油墨、UV柔版光油等", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "有UV胶印油墨吗", + "有光油产品吗" + ], + "variations": [ + "印刷油墨有哪些", + "固化油墨类型", + "UV油墨产品" + ] + }, + { + "question": "康达新材复膜胶领域的可降解材料包含哪些类型?", + "answer": "酪蛋白标签胶、环保餐具涂层", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "有环保餐具涂层吗", + "有可降解标签胶吗" + ], + "variations": [ + "可降解材料有哪些", + "环保材料类型", + "可降解产品分类" + ] + }, + { + "question": "康达新材复膜胶领域的发展规划有哪些?", + "answer": "传统替代、新品攻关(UV油墨、可降解材料)、专业化服务、节能环保、对接“一带一路”", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "会发展UV油墨吗", + "涉及一带一路吗" + ], + "variations": [ + "复膜胶未来规划", + "复膜胶发展方向", + "复膜胶业务战略" + ] + }, + { + "question": "康达新材在轨道交通领域的核心产品有哪些?", + "answer": "双组份丙烯酸酯结构胶、单组份结构胶、UV胶、PUR、双组份环氧胶、单组份环氧胶、瞬干胶等系列化产品(可根据客户需求个性化调整)", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "有双组份结构胶吗", + "有UV胶吗" + ], + "variations": [ + "轨道交通产品有哪些", + "轨交领域的胶水", + "轨道交通核心产品" + ] + }, + { + "question": "康达新材轨道交通领域重点布局哪些行业?", + "answer": "消费电子产品组装行业、微型扬声器行业、安防行业、动力电池行业", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "布局消费电子吗", + "布局动力电池吗" + ], + "variations": [ + "轨交业务重点行业", + "轨道交通胶粘剂应用领域", + "重点布局的行业" + ] + }, + { + "question": "康达新材轨道交通领域在消费电子行业的核心客户有哪些?", + "answer": "联想、华硕、罗技、韶音", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "联想是客户吗", + "华硕是客户吗" + ], + "variations": [ + "消费电子主要客户", + "给谁提供消费电子胶水", + "消费电子合作企业" + ] + }, + { + "question": "康达新材轨道交通领域的产品在消费电子行业应用于哪些产品组装?", + "answer": "智能手机、笔记本/平板电脑、穿戴设备、无人机、投影仪组装", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "用于手机组装吗", + "用于无人机吗" + ], + "variations": [ + "消费电子应用场景", + "电子产品组装用途", + "胶水用在哪些电子产品上" + ] + }, + { + "question": "康达新材轨道交通领域在微型扬声器行业的核心客户有哪些?", + "answer": "瑞声、歌尔、豪声、上声", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "歌尔是客户吗", + "瑞声科技是客户吗" + ], + "variations": [ + "扬声器主要客户", + "微型扬声器合作企业", + "扬声器行业大客户" + ] + }, + { + "question": "康达新材轨道交通领域在安防行业的核心客户有哪些?", + "answer": "海康威视、大华", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "海康威视是客户吗", + "大华是客户吗" + ], + "variations": [ + "安防主要客户", + "安防行业合作企业", + "安防领域大客户" + ] + }, + { + "question": "康达新材轨道交通领域针对高铁/地铁的产品有哪些?", + "answer": "地板胶、阻尼浆、阻尼贴片", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "有高铁地板胶吗", + "有阻尼浆吗" + ], + "variations": [ + "高铁产品有哪些", + "地铁用什么胶水", + "轨交车辆专用产品" + ] + }, + { + "question": "康达新材轨道交通领域针对高铁/地铁的产品取得了哪些进展?", + "answer": "阻尼贴片材料通过中车系统检测验证,地板胶阻尼浆在中车系统重点客户试装成功", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "通过中车检测了吗", + "试装成功了吗" + ], + "variations": [ + "高铁产品进展", + "地铁产品认证情况", + "轨交产品市场突破" + ] + }, + { + "question": "康达新材通用工业领域的销售渠道以什么为主?", + "answer": "经销商销售渠道", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "通用工业怎么销售", + "主要销售渠道", + "通用工业销售模式" + ] + }, + { + "question": "康达新材通用工业领域重点覆盖哪些市场?", + "answer": "通用工业(工业装配维修、扬声器、电梯、电机、结构胶等各类工业应用)、流通市场(民用及家装DIY、电商)", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "覆盖家装DIY市场吗", + "覆盖电梯行业吗" + ], + "variations": [ + "通用工业覆盖范围", + "通用工业主要市场", + "工业胶水应用领域" + ] + }, + { + "question": "康达新材的汽车胶粘剂用于哪些场景?", + "answer": "汽车厢体、车门的结构粘接及汽车内饰件粘接", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "用于车门粘接吗", + "用于内饰粘接吗" + ], + "variations": [ + "汽车胶水用途", + "汽车胶粘剂应用场景", + "车用胶水用在哪" + ] + }, + { + "question": "康达新材的汽车胶粘剂能为客户提供什么优化方案?", + "answer": "汽车轻量化和减少VOCs排放的优化方案", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "汽车胶水优势", + "提供什么解决方案", + "汽车胶粘剂特点" + ] + }, + { + "question": "康达新材家电用胶粘剂的既定目标客户有哪些?", + "answer": "格力、美的等国内知名家电企业", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "格力是目标客户吗", + "美的是目标客户吗" + ], + "variations": [ + "家电客户有哪些", + "家电行业合作企业", + "家电胶水卖给谁" + ] + }, + { + "question": "康达新材家电用胶粘剂目前已涉及美的哪些产品应用?", + "answer": "美的厨热、冰箱和家用空调等产品", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "用于美的冰箱吗", + "用于美的空调吗" + ], + "variations": [ + "美的产品应用情况", + "在美的使用在哪些产品", + "家电胶水应用实例" + ] + }, + { + "question": "康达新材家电用胶粘剂未来的拓展计划是什么?", + "answer": "拓展至中央空调、洗衣机等其他生活电器", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "家电业务拓展计划", + "未来想进军哪些家电", + "家电胶水发展方向" + ] + }, + { + "question": "大连齐化新材料有限公司的成立时间是什么时候?", + "answer": "起步于2001年", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "大连齐化哪年成立的", + "齐化新材料成立时间", + "大连齐化历史" + ] + }, + { + "question": "大连齐化新材料有限公司的企业定位是什么?", + "answer": "以生产销售高品质环氧树脂为主,集新材料研制、开发、生产、销售、服务、转让为一体的综合性高新技术化工企业", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "大连齐化是高新技术企业吗", + "大连齐化主营环氧树脂吗" + ], + "variations": [ + "大连齐化是做什么的", + "齐化新材料定位", + "大连齐化的业务性质" + ] + }, + { + "question": "大连齐化新材料有限公司的产品体系包含哪些系列?", + "answer": "双酚A型环氧树脂、耐热型环氧树脂和特种环氧树脂三大系列", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "有耐热型环氧树脂吗", + "有特种环氧树脂吗" + ], + "variations": [ + "大连齐化产品系列", + "齐化新材料主要产品", + "大连齐化的树脂分类" + ] + }, + { + "question": "大连齐化新材料有限公司的具体产品有哪些?", + "answer": "双酚A型液体环氧树脂、双酚A型固体环氧树脂、溴化环氧树脂、邻甲酚醛环氧树脂、苯酚酚醛环氧树脂、双酚F型环氧树脂、苯氧基树脂、环氧固化剂、环氧稀释剂等", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "生产溴化环氧树脂吗", + "生产苯氧基树脂吗" + ], + "variations": [ + "大连齐化具体产品", + "齐化新材料产品清单", + "大连齐化卖什么" + ] + }, + { + "question": "大连齐化新材料有限公司的产品质量指标有什么优势?", + "answer": "各项指标均优于中国国家标准GB/T 13657-2011《双酚A型环氧树脂》,通过RoSH和REACH认证,双酚A型环氧树脂被辽宁省工信厅认定为“专精特新”产品并获欧洲化学品管理局(ECHA)“REACH注册证书”", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "产品优于国标吗", + "有REACH认证吗" + ], + "variations": [ + "大连齐化产品质量", + "齐化新材料技术优势", + "大连齐化的认证情况" + ] + }, + { + "question": "大连齐化新材料有限公司拥有多少项实用新型专利?", + "answer": "21项", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "大连齐化专利数量", + "有多少实用新型专利", + "齐化新材料专利情况" + ] + }, + { + "question": "大连齐化新材料有限公司拥有多少项软件著作权?", + "answer": "20项", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "大连齐化软著数量", + "有多少软件著作权", + "齐化新材料软著情况" + ] + }, + { + "question": "大连齐化新材料有限公司正在申请多少项发明专利?", + "answer": "2项", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "大连齐化申请中的专利", + "正在申请的发明专利", + "齐化新材料专利申请" + ] + }, + { + "question": "大连齐化新材料有限公司通过了哪些体系认证?", + "answer": "ISO9001、ISO14001、ISO45001三体系认证", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "大连齐化ISO认证", + "通过了哪些体系认证", + "齐化新材料认证情况" + ] + }, + { + "question": "大连齐化新材料有限公司获得国家级“高新技术企业”认定的时间是什么时候?", + "answer": "2020年12月", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "哪年成为高新技术企业的", + "高企认定时间", + "什么时候获得高新认证" + ] + }, + { + "question": "大连齐化新材料有限公司获得辽宁省科技厅“雏鹰企业”认定的时间是什么时候?", + "answer": "2020年12月", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "哪年成为雏鹰企业的", + "雏鹰企业认定时间", + "什么时候获得雏鹰称号" + ] + }, + { + "question": "大连齐化新材料有限公司获得国家级“专精特新”小巨人企业认定的时间是什么时候?", + "answer": "2021年7月", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "哪年成为小巨人的", + "小巨人认定时间", + "什么时候获得专精特新" + ] + }, + { + "question": "大连齐化新材料有限公司的销售模式是什么?", + "answer": "直销模式", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "大连齐化怎么销售", + "销售模式是直销吗", + "齐化新材料销售方式" + ] + }, + { + "question": "南平天宇实业被康达新材收购的时间是什么时候?", + "answer": "2021年4月", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "哪年收购南平天宇的", + "南平天宇收购时间", + "什么时候合并南平天宇" + ] + }, + { + "question": "南平天宇实业的占地面积是多少?", + "answer": "33000㎡", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "南平天宇有多大", + "天宇实业占地面积", + "南平工厂面积" + ] + }, + { + "question": "南平天宇实业的年产产值是多少?", + "answer": "近6000万元", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "南平天宇年产值", + "天宇实业一年产值多少", + "南平工厂产值" + ] + }, + { + "question": "南平天宇实业的生产工艺技术水平如何?", + "answer": "国际领先水平", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "南平天宇技术水平", + "天宇实业工艺怎么样", + "南平工厂技术实力" + ] + }, + { + "question": "南平天宇实业的产品定位是什么?", + "answer": "亚洲国家“水性商标胶粘剂”生产量最大、生产品种最多、工艺技术最领先的绿色环保水基性商标胶粘剂生产企业", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "南平天宇主打什么产品", + "天宇实业市场地位", + "南平天宇的优势产品" + ] + }, + { + "question": "南平天宇实业的“远洋牌”水性系列商标胶有什么特点?", + "answer": "质量超越国际领先水平,性能稳定", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "远洋牌商标胶怎么样", + "水性商标胶特点", + "天宇实业产品质量" + ] + }, + { + "question": "南平天宇实业的“远洋牌”封箱专用热熔胶有什么特点?", + "answer": "可塑性粘合剂,无毒、无味,属绿色环保型产品,物理状态随温度改变(化学特性不变)", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "是环保产品吗", + "有毒吗" + ], + "variations": [ + "封箱热熔胶特点", + "远洋牌热熔胶怎么样", + "天宇实业封箱胶优势" + ] + }, + { + "question": "南平天宇实业的“远洋牌”水性纸制品胶粘剂的原材料是什么?", + "answer": "优质高分子材料PVA、VAC、改性淀粉共聚合成", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "含有改性淀粉吗", + "含有PVA吗" + ], + "variations": [ + "水性纸制品胶原料", + "远洋牌胶水成分", + "天宇实业胶水原料" + ] + }, + { + "question": "南平天宇实业的“远洋牌”水性纸制品胶粘剂有什么特点?", + "answer": "无毒性、无公害、无腐蚀性及氧化性,符合欧美最新标准,为食品级纸制品行业不可替代的绿色环保产品", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "符合欧美标准吗", + "是食品级产品吗" + ], + "variations": [ + "水性纸制品胶优势", + "远洋牌胶水特点", + "天宇实业环保胶特性" + ] + }, + { + "question": "南平天宇实业的核心客户有哪些?", + "answer": "百威啤酒、珠江啤酒、青岛啤酒、雪花啤酒、达利食品集团等", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "百威啤酒是客户吗", + "青岛啤酒是客户吗" + ], + "variations": [ + "南平天宇主要客户", + "天宇实业合作企业", + "南平工厂的大客户" + ] + }, + { + "question": "上海理日新材料的技术地位如何?", + "answer": "成熟的研发创新能力,技术和产品保持国内领先、国际先进水平,国内市场品牌影响和占有率仅次于德国汉高等顶级跨国公司,产品已通过多家著名跨国公司认证并供货", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "上海理日市场排名", + "理日新材料技术实力", + "上海理日行业地位" + ] + }, + { + "question": "上海理日新材料的应用领域有哪些?", + "answer": "汽车部件、电子电缆、衣饰、制鞋、建材、装饰、包装以及低压注塑等行业", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "应用于汽车部件吗", + "应用于电子电缆吗" + ], + "variations": [ + "上海理日产品用途", + "理日新材料服务哪些行业", + "上海理日应用场景" + ] + }, + { + "question": "上海理日新材料服务多少家客户?", + "answer": "数百家", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "上海理日客户数量", + "有多少家客户", + "理日新材料服务规模" + ] + }, + { + "question": "上海理日新材料的环保型高性能热熔胶包含哪些类型?", + "answer": "聚酯、聚酰胺热熔胶", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "有聚酯热熔胶吗", + "有聚酰胺热熔胶吗" + ], + "variations": [ + "热熔胶产品类型", + "上海理日热熔胶种类", + "理日新材料胶水分类" + ] + }, + { + "question": "上海理日新材料的环保型高性能热熔胶有什么特点?", + "answer": "不含化学溶剂、无污染,粘接强度高,耐油耐化学性好,粘接材料广泛", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "含化学溶剂吗", + "耐油吗" + ], + "variations": [ + "热熔胶特点", + "上海理日胶水优势", + "理日新材料产品特性" + ] + }, + { + "question": "上海理日新材料的环保型高性能热熔胶应用于哪些企业?", + "answer": "汽车滤清器生产企业、制鞋服饰企业、电子电缆生产企业", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "用于汽车滤清器吗", + "用于制鞋企业吗" + ], + "variations": [ + "热熔胶客户群体", + "上海理日胶水卖给谁", + "理日新材料应用企业" + ] + }, + { + "question": "上海理日新材料的环保型低压注塑封装材料的目标市场是什么?", + "answer": "电缆电子行业中的电池板生产企业、线束插头/微型开关生产企业、连接器/传感器生产企业和PCB线路板生产企业", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "用于PCB线路板吗", + "用于传感器生产吗" + ], + "variations": [ + "低压注塑材料市场", + "封装材料目标客户", + "注塑材料应用领域" + ] + }, + { + "question": "四氢呋喃可作为上海理日新材料产品的哪些应用领域?", + "answer": "合成树脂的工业溶剂、合成革的表面处理剂、合成农药等领域", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "四氢呋喃用途", + "四氢呋喃应用场景", + "理日新材料溶剂应用" + ] + }, + { + "question": "康达新材在高性能复合材料方向的战略定位是什么?", + "answer": "聚焦玻璃纤维、碳纤维的环氧树脂、聚氨酯类复合材料领域,计划通过2-3年深化拓展;基于自身工艺和技术积累,通过投资业务契合度企业、技术创新、加大研发投入等方式,切入先进复合材料领域,推动向高分子新材料领域战略转型", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "聚焦碳纤维复材吗", + "会投资相关企业吗" + ], + "variations": [ + "复合材料战略定位", + "复材业务发展方向", + "高性能复材规划" + ] + }, + { + "question": "康达新材落实先进复合材料转型战略的关键举措是什么?", + "answer": "对聚发新材进行增资", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "复材转型措施", + "怎么落实复材战略", + "增资聚发新材的意义" + ] + }, + { + "question": "拉挤树脂(Urepul)的基础材料是什么?", + "answer": "玻纤&碳纤", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "拉挤树脂原料", + "Urepul成分", + "拉挤树脂用什么做的" + ] + }, + { + "question": "拉挤树脂(Urepul)的应用领域有哪些?", + "answer": "桥架、绝缘梯、桥梁、高压管道、景观工程、高铁、轨道、光伏", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "用于高铁吗", + "用于光伏吗" + ], + "variations": [ + "拉挤树脂用途", + "Urepul应用场景", + "拉挤树脂用在哪里" + ] + }, + { + "question": "SMC/预浸料(Urepreg)的基础材料是什么?", + "answer": "玻纤&碳纤", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "预浸料原料", + "Urepreg成分", + "SMC用什么做的" + ] + }, + { + "question": "SMC/预浸料(Urepreg)的应用领域有哪些?", + "answer": "汽车、轨道交通、电子产品", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "用于汽车吗", + "用于电子产品吗" + ], + "variations": [ + "预浸料用途", + "Urepreg应用场景", + "SMC用在哪里" + ] + }, + { + "question": "RTM树脂(Urethan)的基础材料是什么?", + "answer": "玻纤&碳纤", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "RTM树脂原料", + "Urethan成分", + "RTM树脂用什么做的" + ] + }, + { + "question": "RTM树脂(Urethan)的应用领域有哪些?", + "answer": "风电叶片、汽车内外饰、高铁", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "用于风电叶片吗", + "用于汽车内饰吗" + ], + "variations": [ + "RTM树脂用途", + "Urethan应用场景", + "RTM树脂用在哪里" + ] + }, + { + "question": "缠绕树脂(Urecom)的基础材料是什么?", + "answer": "玻纤&碳纤", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "缠绕树脂原料", + "Urecom成分", + "缠绕树脂用什么做的" + ] + }, + { + "question": "缠绕树脂(Urecom)的应用领域有哪些?", + "answer": "电线杆、传动轴", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "缠绕树脂用途", + "Urecom应用场景", + "缠绕树脂用在哪里" + ] + }, + { + "question": "截至2025年8月,康达新材高性能复合材料已实现量产应用的产品有哪些?", + "answer": "电缆桥架、高铁设备舱及裙板、新能源车电池盒骨架", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "电缆桥架量产了吗", + "电池盒骨架量产了吗" + ], + "variations": [ + "哪些复材产品量产了", + "复材量产应用情况", + "已经量产的复合材料" + ] + }, + { + "question": "截至2025年8月,康达新材高性能复合材料技术开发完成的产品有哪些?", + "answer": "汽车型材、托盘、高铁步道板、绝缘臂腕", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "汽车型材开发完成了吗", + "高铁步道板开发完成了吗" + ], + "variations": [ + "哪些复材产品开发完成了", + "技术开发完成的产品", + "复材研发成果" + ] + }, + { + "question": "截至2025年8月,康达新材高性能复合材料处于开发中的产品有哪些?", + "answer": "太阳能边框及支架、铁路鱼尾板", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "正在开发太阳能边框吗", + "正在开发铁路鱼尾板吗" + ], + "variations": [ + "哪些复材产品正在开发", + "开发中的复合材料", + "复材在研项目" + ] + }, + { + "question": "康达新材的高性能复合材料在性能方面有什么优势?", + "answer": "出色的机械性能,与玻纤完美结合,更高的粘接强度,优异的抗疲劳性能", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "抗疲劳性能好吗", + "粘接强度高吗" + ], + "variations": [ + "复材性能优势", + "复合材料有什么优点", + "康达复材性能特点" + ] + }, + { + "question": "康达新材的高性能复合材料在生产效率方面有什么优势?", + "answer": "更快的灌注速度,更快的固化速度", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "固化速度快吗", + "灌注速度快吗" + ], + "variations": [ + "复材生产效率优势", + "生产速度怎么样", + "康达复材加工效率" + ] + }, + { + "question": "康达新材的高性能复合材料在综合成本方面有什么优势?", + "answer": "模具投入少,具有竞争力的树脂价格", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "复材成本优势", + "价格有竞争力吗", + "康达复材成本效益" + ] + }, + { + "question": "惟新科技被康达新材收购的时间是什么时候?", + "answer": "2023年", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "哪年收购惟新科技的", + "惟新科技收购时间", + "什么时候并购惟新" + ] + }, + { + "question": "惟新科技的企业定位是什么?", + "answer": "国家高新技术企业,致力于研发和生产高端ITO(氧化铟锡)靶材及ITO粉体、ITO气氛烧结炉等ITO周边产品,解决大尺寸ITO靶材“卡脖子”问题", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "惟新科技生产ITO靶材吗", + "惟新科技解决了卡脖子问题吗" + ], + "variations": [ + "惟新科技是做什么的", + "惟新科技主营业务", + "河北惟新的定位" + ] + }, + { + "question": "惟新科技的核心技术有哪些?", + "answer": "与清华大学化工系合作,研发微反应器制备高活性纳米ITO粉体技术;自主研发适合超大尺寸靶材制备的湿法注浆成型技术,拥有自主知识产权的超大空间高温烧结炉", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "和清华大学合作了吗", + "拥有自主知识产权吗" + ], + "variations": [ + "惟新科技技术优势", + "惟新科技有什么技术", + "河北惟新核心竞争力" + ] + }, + { + "question": "惟新科技成功制备的大尺寸ITO靶材规格是多少?", + "answer": "1550x250mm", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "ITO靶材最大规格", + "靶材尺寸是多少", + "惟新科技靶材规格" + ] + }, + { + "question": "惟新科技制备的大尺寸ITO靶材性能指标如何?", + "answer": "与国际同类靶材相当,达到国际先进水平,填补国内空白", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "ITO靶材性能怎么样", + "靶材达到国际水平了吗", + "惟新科技产品质量" + ] + }, + { + "question": "惟新科技的战略意义有哪些?", + "answer": "打破技术壁垒,减少高端靶材进口依赖,助力中美贸易战应对;推动我国铟资源高效利用;填补我国ITO靶材在中、高端平板显示器制造领域应用空白;促进ITO靶材平板生产工艺研发与人才培养", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "减少进口依赖了吗", + "填补国内空白了吗" + ], + "variations": [ + "收购惟新的意义", + "惟新科技的价值", + "为什么重视惟新科技" + ] + }, + { + "question": "惟新科技的ITO靶材试验线设备升级改造完成时间是什么时候?", + "answer": "2022年", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "试验线升级时间", + "什么时候完成设备改造", + "惟新科技产线升级" + ] + }, + { + "question": "惟新科技目前的产能提升目标是什么?", + "answer": "推进30T/年产能提升工作", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "惟新科技产能目标", + "未来产能规划", + "计划达到多少产能" + ] + }, + { + "question": "惟新科技的溅射靶材下游覆盖哪些领域?", + "answer": "平板显示器、光伏电池、记录媒体、半导体等领域", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "用于平板显示器吗", + "用于半导体吗" + ], + "variations": [ + "靶材应用领域", + "溅射靶材用在哪里", + "惟新科技下游市场" + ] + }, + { + "question": "平板显示器领域在惟新科技溅射靶材下游应用中占比多少?", + "answer": "33.8%", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "平板显示器占比", + "显示领域应用比例", + "显示器市场份额" + ] + }, + { + "question": "记录媒体领域在惟新科技溅射靶材下游应用中占比多少?", + "answer": "28.6%", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "记录媒体占比", + "媒体领域应用比例", + "记录媒体市场份额" + ] + }, + { + "question": "半导体领域在惟新科技溅射靶材下游应用中占比多少?", + "answer": "18.5%", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "半导体占比", + "半导体领域应用比例", + "芯片市场份额" + ] + }, + { + "question": "晶材科技成立于哪一年?", + "answer": "2016年", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "晶材科技哪年成立的", + "上海晶材成立时间", + "晶材科技历史" + ] + }, + { + "question": "晶材科技被康达新材收购的时间是什么时候?", + "answer": "2018年", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "哪年收购晶材科技的", + "晶材科技收购时间", + "什么时候并购晶材" + ] + }, + { + "question": "晶材科技的企业定位是什么?", + "answer": "专注于高端电子陶瓷材料国产化,集研发与规模化生产于一体,提供先进材料解决方案,代理德国瓦克化学有机硅产品", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "晶材科技做电子陶瓷吗", + "晶材科技代理瓦克产品吗" + ], + "variations": [ + "晶材科技是做什么的", + "晶材科技主营业务", + "上海晶材的定位" + ] + }, + { + "question": "晶材科技的技术路线是什么?", + "answer": "微晶玻璃体系、微晶玻璃/陶瓷复合体系", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "晶材科技技术路线", + "采用什么技术体系", + "晶材核心技术" + ] + }, + { + "question": "晶材科技在行业中的地位如何?", + "answer": "目前市场唯一批量供货的国产生料带供应商,解决“卡脖子”难题", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "可以批量供货生料带吗", + "解决了卡脖子问题吗" + ], + "variations": [ + "晶材科技市场地位", + "晶材科技有什么优势", + "生料带供应情况" + ] + }, + { + "question": "晶材科技的自研产品有哪些?", + "answer": "陶瓷生料带、贵金属浆料、瓷粉、导电胶", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "生产陶瓷生料带吗", + "生产贵金属浆料吗" + ], + "variations": [ + "晶材科技自研产品", + "晶材自主产品有哪些", + "上海晶材卖什么" + ] + }, + { + "question": "晶材科技的自研产品应用于什么技术领域?", + "answer": "低温共烧陶瓷技术(LTCC)的电子元器件、电路模块制造和封装", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "产品用于LTCC吗", + "晶材产品技术应用", + "产品用在什么元器件上" + ] + }, + { + "question": "晶材科技的代理产品是什么?", + "answer": "有机硅水胶(车载显示器光学全贴合重要原材料)", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "晶材科技代理什么", + "有机硅水胶是代理的吗", + "晶材的贸易产品" + ] + }, + { + "question": "晶材科技的产品在军用领域有什么应用?", + "answer": "有源相控阵雷达", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "晶材产品用于军工吗", + "在雷达上有应用吗", + "军用领域应用情况" + ] + }, + { + "question": "晶材科技的产品在民用领域有什么应用?", + "answer": "高端半导体传感器、光通信器件、汽车电子电路模块、5G路由器、5G机房、数据中心等", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "用于5G设备吗", + "用于汽车电子吗" + ], + "variations": [ + "晶材产品民用场景", + "民用领域应用情况", + "产品在民用市场的用途" + ] + }, + { + "question": "晶材科技的生料带现有产能是多少?", + "answer": "240万片/年", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "生料带年产能", + "晶材生料带产量", + "生料带产能情况" + ] + }, + { + "question": "晶材科技的贵金属浆料现有产能是多少?", + "answer": "30吨/年", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "浆料年产能", + "晶材浆料产量", + "贵金属浆料产能情况" + ] + }, + { + "question": "赛英科技被康达新材收购的时间是什么时候?", + "answer": "2018年", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "哪年收购赛英科技的", + "赛英科技收购时间", + "什么时候并购赛英" + ] + }, + { + "question": "赛英科技的地理位置在哪里?", + "answer": "成都龙潭新经济产业功能区", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "赛英科技地址", + "成都赛英在哪里", + "赛英科技的位置" + ] + }, + { + "question": "赛英科技的科研生产基地规模是多少?", + "answer": "8000余平方米", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "赛英科技有多大", + "赛英基地面积", + "成都赛英规模" + ] + }, + { + "question": "赛英科技的核心业务是什么?", + "answer": "科研、生产整机雷达、微波组件、专用仪器仪表、海洋通讯产品", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "赛英科技做雷达吗", + "赛英科技做微波组件吗" + ], + "variations": [ + "赛英科技做什么产品", + "赛英科技主营业务", + "成都赛英主要业务" + ] + }, + { + "question": "赛英科技在安全管控领域的产品有哪些?", + "answer": "近程警戒雷达(地面、海面)、周界安全警戒雷达(系列产品)", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "有警戒雷达吗", + "有周界雷达吗" + ], + "variations": [ + "安防雷达产品", + "赛英安全管控产品", + "警戒雷达类型" + ] + }, + { + "question": "赛英科技在交通监测领域的产品有哪些?", + "answer": "FOD跑道异物监测雷达(塔架、移动)", + "category": "业务介绍", + "priority": 10, + "sub_questions": [], + "variations": [ + "交通监测产品", + "FOD雷达是什么", + "赛英交通雷达" + ] + }, + { + "question": "赛英科技在工业监测领域的产品有哪些?", + "answer": "高炉料面监测雷达、微位移监测雷达(应用于大型钢厂、铁路大坝等重要区域)", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "有高炉监测雷达吗", + "有微位移雷达吗" + ], + "variations": [ + "工业监测雷达", + "赛英工业产品", + "工业雷达类型" + ] + }, + { + "question": "赛英科技获得了哪些企业荣誉?", + "answer": "成都市企业技术中心、成都市成华区2020年度“工业纳税先进单位”、四川省“专精特新”中小企业、成都市科技装备业商会“副会长单位”等", + "category": "业务介绍", + "priority": 10, + "sub_questions": [ + "赛英科技是专精特新吗", + "是纳税先进单位吗" + ], + "variations": [ + "赛英科技有什么荣誉", + "成都赛英的奖项", + "赛英企业资质" + ] + } + ] +} \ No newline at end of file diff --git a/ruoyi-fastapi-backend/doc/api_documentation.md b/ruoyi-fastapi-backend/doc/api_documentation.md new file mode 100644 index 0000000..e7e706b --- /dev/null +++ b/ruoyi-fastapi-backend/doc/api_documentation.md @@ -0,0 +1,3122 @@ +# 康达机器人管理系统 API 接口文档 + +## 1. 文档概述 + +本文档详细描述了康达机器人管理系统的所有API接口,包括接口地址、请求方式、请求参数、响应格式和接口功能说明等。 + +## 2. 技术栈 + +- **后端框架**: FastAPI +- **数据库**: MySQL + SQLAlchemy (异步) +- **认证方式**: JWT Token +- **缓存**: Redis +- **异步请求**: httpx +- **人脸识别**: CompreFace API +- **门禁控制**: 海康威视平台 + +## 3. 接口认证 + +系统采用 JWT Token 认证机制,所有需要认证的接口都需要在请求头中添加 `Authorization: Bearer {token}`。 + +### 获取 Token + +``` +POST /login +``` + +## 4. 接口分类 + +### 4.1 认证模块 + +#### 4.1.1 用户登录 + +**接口地址**: `/login` +**请求方式**: POST +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| username | String | 是 | 用户名 | +| password | String | 是 | 密码 | +| code | String | 否 | 验证码(当前未启用) | +| uuid | String | 否 | 验证码UUID(当前未启用) | +| login_info | String | 否 | 登录信息 | + +**响应格式**: +```json +{ + "code": 200, + "message": "登录成功", + "data": { + "access_token": "{jwt_token}", + "token_type": "bearer" + } +} +``` + +#### 4.1.2 获取用户信息 + +**接口地址**: `/system/user/getInfo` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "user": { + "user_id": 1, + "user_name": "admin", + "dept_id": 1, + "status": "0", + "email": "admin@example.com", + "phonenumber": "13800138000", + "sex": "1", + "avatar": null + }, + "roles": ["admin"], + "permissions": ["*:*:*"] + } +} +``` + +#### 4.1.3 获取用户路由 + +**接口地址**: `/system/user/getRouters` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + { + "name": "System", + "path": "/system", + "component": "Layout", + "redirect": "/system/user", + "meta": { + "title": "系统管理", + "icon": "system", + "roles": ["admin"] + }, + "children": [ + { + "name": "User", + "path": "user", + "component": "system/user/index", + "meta": { + "title": "用户管理", + "icon": "user", + "roles": ["admin"] + } + } + ] + } + ] +} +``` + +### 4.2 人脸识别与门禁模块 + +#### 4.2.1 人脸检测 + +**接口地址**: `/system/compreface/face_detection` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +- 表单数据或字节流:图片文件 + +**响应格式**: +```json +{ + "code": 200, + "message": "操作成功", + "data": { + "result": [ + { + "box": { + "x_max": 100, + "y_max": 100, + "x_min": 50, + "y_min": 50 + }, + "probability": 0.99 + } + ] + } +} +``` + +#### 4.2.2 人脸识别 + +**接口地址**: `/system/compreface/face_recognition` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +- 表单数据或字节流:图片文件 + +**响应格式**: +```json +{ + "code": 200, + "message": "操作成功", + "data": { + "result": [ + { + "box": { + "x_max": 100, + "y_max": 100, + "x_min": 50, + "y_min": 50 + }, + "probability": 0.99, + "subject": "张三", + "similarity": 0.95 + } + ] + } +} +``` + +#### 4.2.3 门禁控制 + +**接口地址**: `/system/door/control_door` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| door_index_code | String | 是 | 门设备索引码 | +| control_type | Integer | 是 | 控制类型(1: 开门, 0: 关门) | + +**响应格式**: +```json +{ + "code": 200, + "message": "门控制成功", + "data": { + "result": "success" + } +} +``` + +#### 4.2.4 获取门禁设备列表 + +**接口地址**: `/system/door/list` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| door_name | String | 否 | 门名称 | +| door_code | String | 否 | 门编号 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total": 10, + "rows": [ + { + "door_id": 1, + "door_name": "前门", + "door_code": "DOOR001", + "door_index_code": "123456", + "status": "1", + "create_time": "2023-01-01 10:00:00" + } + ] + } +} +``` + +#### 4.2.5 添加门禁设备 + +**接口地址**: `/system/door/add` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| door_name | String | 是 | 门名称 | +| door_code | String | 是 | 门编号 | +| door_index_code | String | 是 | 门设备索引码 | +| status | String | 是 | 状态(0: 禁用, 1: 启用) | + +**响应格式**: +```json +{ + "code": 200, + "message": "添加成功", + "data": null +} +``` + +### 4.3 机器人管理模块 + +#### 4.3.1 获取机器人动作列表 + +**接口地址**: `/system/robot_action/list` +**请求方式**: GET +**认证要求**: 是 +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + { + "action_id": 1, + "action_name": "前进", + "action_code": "forward", + "action_desc": "机器人前进", + "status": "1" + } + ] +} +``` + +#### 4.3.2 修改机器人动作 + +**接口地址**: `/system/robot_action` +**请求方式**: PUT +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| action_id | Integer | 是 | 动作ID | +| action_name | String | 否 | 动作名称 | +| action_code | String | 否 | 动作代码 | +| action_desc | String | 否 | 动作描述 | +| status | String | 否 | 状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "修改成功", + "data": null +} +``` + +### 4.4 内容管理模块 + +#### 4.4.1 获取讲解内容列表 + +**接口地址**: `/system/explanation_content/list` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| content_name | String | 否 | 内容名称 | +| content_type | String | 否 | 内容类型 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + { + "content_id": 1, + "content_name": "公司介绍", + "content_type": "text", + "content": "这是公司介绍内容", + "status": "0", + "create_time": "2023-01-01 10:00:00" + } + ] +} +``` + +#### 4.4.2 添加讲解内容 + +**接口地址**: `/system/explanation_content/add_explanation` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| content_name | String | 是 | 内容名称 | +| content_type | String | 是 | 内容类型 | +| content | String | 是 | 内容 | +| status | String | 是 | 状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "添加成功", + "data": null +} +``` + +#### 4.4.3 修改讲解内容 + +**接口地址**: `/system/explanation_content/edit_explanation` +**请求方式**: PUT +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| content_id | Integer | 是 | 内容ID | +| content_name | String | 否 | 内容名称 | +| content_type | String | 否 | 内容类型 | +| content | String | 否 | 内容 | +| status | String | 否 | 状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "修改成功", + "data": null +} +``` + +#### 4.4.4 删除讲解内容 + +**接口地址**: `/system/explanation_content/{explanation_content_ids}` +**请求方式**: DELETE +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| explanation_content_ids | String | 是 | 内容ID,多个用逗号分隔 | + +**响应格式**: +```json +{ + "code": 200, + "message": "删除成功", + "data": null +} +``` + +### 4.5 系统管理模块 + +#### 4.5.1 验证码管理 + +**获取验证码图片** +**接口地址**: `/captchaImage` +**请求方式**: GET +**认证要求**: 否 +**功能说明**: 获取登录验证码图片,包括验证码开关和注册开关信息 + +**响应格式**: +```json +{ + "code": 200, + "message": "操作成功", + "data": { + "captchaEnabled": true, + "registerEnabled": true, + "img": "base64编码的图片数据", + "uuid": "验证码会话ID" + } +} +``` + +#### 4.5.2 通用功能 + +**文件上传** +**接口地址**: `/common/upload` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| file | File | 是 | 要上传的文件 | + +**响应格式**: +```json +{ + "code": 200, + "message": "上传成功", + "data": { + "fileName": "uploaded_file.jpg", + "filePath": "/upload/path/uploaded_file.jpg" + } +} +``` + +**文件下载** +**接口地址**: `/common/download` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| fileName | String | 是 | 文件名 | +| delete | Boolean | 是 | 是否删除文件 | + +**响应格式**: 文件流 + +#### 4.5.3 用户管理 + +**获取用户列表** +**接口地址**: `/system/user/list` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| user_name | String | 否 | 用户名 | +| phonenumber | String | 否 | 手机号 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total": 10, + "rows": [ + { + "user_id": 1, + "user_name": "admin", + "dept_id": 1, + "status": "0", + "email": "admin@example.com", + "phonenumber": "13800138000", + "sex": "1", + "avatar": null + } + ] + } +} +``` + +**获取部门树** +**接口地址**: `/system/user/deptTree` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + { + "dept_id": 1, + "dept_name": "总公司", + "children": [ + { + "dept_id": 2, + "dept_name": "技术部" + } + ] + } + ] +} +``` + +**添加用户** +**接口地址**: `/system/user` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| user_name | String | 是 | 用户名 | +| dept_id | Integer | 是 | 部门ID | +| email | String | 否 | 邮箱 | +| phonenumber | String | 否 | 手机号 | +| status | String | 是 | 状态 | +| password | String | 是 | 密码 | + +**响应格式**: +```json +{ + "code": 200, + "message": "添加成功", + "data": null +} +``` + +#### 4.5.4 部门管理 + +**获取部门列表** +**接口地址**: `/system/dept/list` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + { + "dept_id": 1, + "dept_name": "总公司", + "parent_id": 0, + "order_num": 1, + "status": "1", + "children": [ + { + "dept_id": 2, + "dept_name": "技术部", + "parent_id": 1, + "order_num": 2, + "status": "1" + } + ] + } + ] +} +``` + +**添加部门** +**接口地址**: `/system/dept` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| dept_name | String | 是 | 部门名称 | +| parent_id | Integer | 是 | 父部门ID | +| order_num | Integer | 是 | 排序 | +| status | String | 是 | 状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "添加成功", + "data": null +} +``` + +**修改部门** +**接口地址**: `/system/dept` +**请求方式**: PUT +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| dept_id | Integer | 是 | 部门ID | +| dept_name | String | 否 | 部门名称 | +| parent_id | Integer | 否 | 父部门ID | +| order_num | Integer | 否 | 排序 | +| status | String | 否 | 状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "修改成功", + "data": null +} +``` + +#### 4.5.5 角色管理 + +**获取角色列表** +**接口地址**: `/system/role/list` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| role_name | String | 否 | 角色名称 | +| status | String | 否 | 状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total": 5, + "rows": [ + { + "role_id": 1, + "role_name": "管理员", + "role_key": "admin", + "status": "1", + "create_time": "2023-01-01 10:00:00" + } + ] + } +} +``` + +**添加角色** +**接口地址**: `/system/role` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| role_name | String | 是 | 角色名称 | +| role_key | String | 是 | 角色标识 | +| status | String | 是 | 状态 | +| menu_ids | String | 是 | 菜单权限ID列表 | + +**响应格式**: +```json +{ + "code": 200, + "message": "添加成功", + "data": null +} +``` + +**获取角色部门树** +**接口地址**: `/system/role/deptTree/{role_id}` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "depts": [ + { + "dept_id": 1, + "dept_name": "总公司", + "children": [] + } + ], + "checkedKeys": [1] + } +} +``` + +#### 4.5.6 菜单管理 + +**获取菜单树** +**接口地址**: `/system/menu/treeselect` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + { + "menu_id": 1, + "menu_name": "系统管理", + "parent_id": 0, + "order_num": 1, + "path": "/system", + "component": "Layout", + "is_frame": 1, + "menu_type": "M", + "visible": "0", + "status": "0", + "children": [ + { + "menu_id": 2, + "menu_name": "用户管理", + "parent_id": 1, + "order_num": 2, + "path": "user", + "component": "system/user/index", + "is_frame": 1, + "menu_type": "C", + "visible": "0", + "status": "0" + } + ] + } + ] +} +``` + +**获取菜单列表** +**接口地址**: `/system/menu/list` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + { + "menu_id": 1, + "menu_name": "系统管理", + "parent_id": 0, + "order_num": 1, + "path": "/system", + "component": "Layout", + "is_frame": 1, + "menu_type": "M", + "visible": "0", + "status": "0" + } + ] +} +``` + +**添加菜单** +**接口地址**: `/system/menu` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| menu_name | String | 是 | 菜单名称 | +| parent_id | Integer | 是 | 父菜单ID | +| order_num | Integer | 是 | 排序 | +| path | String | 否 | 路由地址 | +| component | String | 否 | 组件路径 | +| is_frame | Integer | 是 | 是否是外部链接 | +| menu_type | String | 是 | 菜单类型 | +| visible | String | 是 | 显示状态 | +| status | String | 是 | 菜单状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "添加成功", + "data": null +} +``` + +#### 4.5.7 字典管理 + +**获取字典类型列表** +**接口地址**: `/system/dict/type/list` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| dict_name | String | 否 | 字典名称 | +| dict_type | String | 否 | 字典类型 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total": 10, + "rows": [ + { + "dict_id": 1, + "dict_name": "用户状态", + "dict_type": "sys_normal_disable", + "status": "0", + "create_time": "2023-01-01 10:00:00" + } + ] + } +} +``` + +**获取字典数据列表** +**接口地址**: `/system/dict/data/list` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| dict_type | String | 是 | 字典类型 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total": 2, + "rows": [ + { + "dict_code": 1, + "dict_sort": 1, + "dict_label": "正常", + "dict_value": "0", + "dict_type": "sys_normal_disable", + "status": "0" + }, + { + "dict_code": 2, + "dict_sort": 2, + "dict_label": "禁用", + "dict_value": "1", + "dict_type": "sys_normal_disable", + "status": "0" + } + ] + } +} +``` + +#### 4.5.8 系统配置 + +**获取配置列表** +**接口地址**: `/system/config/list` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| config_name | String | 否 | 配置名称 | +| config_key | String | 否 | 配置键名 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total": 10, + "rows": [ + { + "config_id": 1, + "config_name": "系统名称", + "config_key": "sys.name", + "config_value": "康达机器人管理系统", + "status": "0", + "create_time": "2023-01-01 10:00:00" + } + ] + } +} +``` + +**根据键名获取配置** +**接口地址**: `/system/config/configKey/{config_key}` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "康达机器人管理系统", + "data": null +} +``` + +**刷新配置缓存** +**接口地址**: `/system/config/refreshCache` +**请求方式**: DELETE +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "刷新成功", + "data": null +} +``` + +#### 4.5.9 日志管理 + +#### 4.5.10 缓存监控 + +**获取缓存统计信息** +**接口地址**: `/monitor/cache/statistics` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "memory_used": "100MB", + "memory_total": "1GB", + "key_count": 100 + } +} +``` + +**获取缓存名称列表** +**接口地址**: `/monitor/cache/keys` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "keys": ["user:1", "role:2", "menu:3"] + } +} +``` + +**获取缓存键值对** +**接口地址**: `/monitor/cache/values` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| cache_keys | String | 是 | 缓存键(多个用逗号分隔) | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "user:1": {"user_id": 1, "user_name": "admin"}, + "role:2": {"role_id": 2, "role_name": "管理员"} + } +} +``` + +**清除单个缓存** +**接口地址**: `/monitor/cache/clearCache` +**请求方式**: DELETE +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| cache_key | String | 是 | 缓存键 | + +**响应格式**: +```json +{ + "code": 200, + "message": "清除成功", + "data": null +} +``` + +**清除所有缓存** +**接口地址**: `/monitor/cache/clearCacheAll` +**请求方式**: DELETE +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "清除成功", + "data": null +} +``` + +#### 4.5.11 定时任务管理 + +**获取定时任务列表** +**接口地址**: `/monitor/job/list` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| job_name | String | 否 | 任务名称 | +| job_group | String | 否 | 任务组名 | +| status | String | 否 | 任务状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total": 5, + "rows": [ + { + "job_id": 1, + "job_name": "数据备份", + "job_group": "system", + "invoke_target": "com.example.job.BackupJob", + "cron_expression": "0 0 2 * * ?", + "status": "0", + "create_time": "2023-01-01 10:00:00" + } + ] + } +} +``` + +**添加定时任务** +**接口地址**: `/monitor/job` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| job_name | String | 是 | 任务名称 | +| job_group | String | 是 | 任务组名 | +| invoke_target | String | 是 | 调用目标 | +| cron_expression | String | 是 | cron表达式 | +| status | String | 是 | 任务状态 | +| remark | String | 否 | 备注 | + +**响应格式**: +```json +{ + "code": 200, + "message": "添加成功", + "data": null +} +``` + +**修改定时任务状态** +**接口地址**: `/monitor/job/changeStatus` +**请求方式**: PUT +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| job_id | Integer | 是 | 任务ID | +| status | String | 是 | 任务状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "修改成功", + "data": null +} +``` + +#### 4.5.12 系统统计 + +**获取统计数据** +**接口地址**: `/system/sys_statistics/get_data` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total_visitors": 1000, + "today_visitors": 100, + "success_rate": 0.95, + "total_records": 10000 + } +} +``` + +**获取访问趋势** +**接口地址**: `/system/sys_statistics/get_visitor_count/{day}` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| day | Integer | 是 | 天数 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + { + "date": "2023-01-01", + "count": 100 + }, + { + "date": "2023-01-02", + "count": 150 + } + ] +} +``` + +**获取门禁识别成功率** +**接口地址**: `/system/sys_statistics/access_control_success_rate/{day}` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| day | Integer | 是 | 天数 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "date": "2023-01-01", + "success_count": 95, + "total_count": 100, + "success_rate": 0.95 + } +} +``` + +#### 4.5.13 通知公告管理 + +**获取通知列表** +**接口地址**: `/system/notice/list` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| notice_title | String | 否 | 公告标题 | +| status | String | 否 | 公告状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total": 5, + "rows": [ + { + "notice_id": 1, + "notice_title": "系统更新通知", + "notice_type": "1", + "status": "0", + "create_by": "admin", + "create_time": "2023-01-01 10:00:00" + } + ] + } +} +``` + +**添加通知公告** +**接口地址**: `/system/notice` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| notice_title | String | 是 | 公告标题 | +| notice_type | String | 是 | 公告类型 | +| notice_content | String | 是 | 公告内容 | +| status | String | 是 | 公告状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "添加成功", + "data": null +} +``` + +**获取通知详情** +**接口地址**: `/system/notice/{notice_id}` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "notice_id": 1, + "notice_title": "系统更新通知", + "notice_type": "1", + "notice_content": "系统将于2023-01-01进行更新", + "status": "0", + "create_by": "admin", + "create_time": "2023-01-01 10:00:00" + } +} +``` + +#### 4.5.14 识别记录管理 + +**获取识别记录列表** +**接口地址**: `/system/identification_record/list` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| device_id | String | 否 | 设备ID | +| visitor_name | String | 否 | 访客姓名 | +| start_time | String | 否 | 开始时间 | +| end_time | String | 否 | 结束时间 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total": 10, + "rows": [ + { + "record_id": 1, + "device_id": "device_001", + "visitor_name": "张三", + "visitor_type": "0", + "identification_time": "2023-01-01 10:00:00", + "status": "0" + } + ] + } +} +``` + +**添加识别记录** +**接口地址**: `/system/identification_record` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| device_id | String | 是 | 设备ID | +| visitor_name | String | 是 | 访客姓名 | +| visitor_type | String | 是 | 访客类型 | +| identification_time | String | 是 | 识别时间 | +| status | String | 是 | 状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "添加成功", + "data": null +} +``` + +**导出识别记录** +**接口地址**: `/system/identification_record/export` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| device_id | String | 否 | 设备ID | +| visitor_name | String | 否 | 访客姓名 | +| start_time | String | 否 | 开始时间 | +| end_time | String | 否 | 结束时间 | + +**响应格式**: 文件流 + +#### 4.5.15 讲解内容管理 + +**获取讲解内容列表** +**接口地址**: `/system/explanation_content/list` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| content_name | String | 否 | 内容名称 | +| status | String | 否 | 状态 | +| type_id | Integer | 否 | 类型ID | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + { + "content_id": 1, + "content_name": "展厅介绍", + "type_id": 1, + "content_text": "这是展厅的详细介绍...", + "status": "0", + "create_time": "2023-01-01 10:00:00" + } + ] +} +``` + +**添加讲解内容** +**接口地址**: `/system/explanation_content/add_explanation` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| content_name | String | 是 | 内容名称 | +| type_id | Integer | 是 | 类型ID | +| content_text | String | 是 | 内容文本 | +| status | String | 是 | 状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "添加成功", + "data": null +} +``` + +**编辑讲解内容** +**接口地址**: `/system/explanation_content/edit_explanation` +**请求方式**: PUT +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| content_id | Integer | 是 | 内容ID | +| content_name | String | 是 | 内容名称 | +| type_id | Integer | 是 | 类型ID | +| content_text | String | 是 | 内容文本 | +| status | String | 是 | 状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "编辑成功", + "data": null +} +``` + +**删除讲解内容** +**接口地址**: `/system/explanation_content/{explanation_content_ids}` +**请求方式**: DELETE +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "删除成功", + "data": null +} +``` + +**获取讲解内容类型列表** +**接口地址**: `/system/explanation_content/type_list` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + { + "type_id": 1, + "type_name": "展厅介绍", + "create_time": "2023-01-01 10:00:00" + } + ] +} +``` + +#### 4.5.16 机器人动作管理 + +**获取机器人动作列表** +**接口地址**: `/system/robot_action/list` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| action_name | String | 否 | 动作名称 | +| robot_id | Integer | 否 | 机器人ID | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + { + "action_id": 1, + "action_name": "欢迎动作", + "robot_id": 1, + "action_data": "{...}", + "create_time": "2023-01-01 10:00:00" + } + ] +} +``` + +**编辑机器人动作** +**接口地址**: `/system/robot_action` +**请求方式**: PUT +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| action_id | Integer | 是 | 动作ID | +| action_name | String | 是 | 动作名称 | +| robot_id | Integer | 是 | 机器人ID | +| action_data | String | 是 | 动作数据 | + +**响应格式**: +```json +{ + "code": 200, + "message": "编辑成功", + "data": null +} +``` + +#### 4.5.17 门禁控制管理 + +**机器人控制开门** +**接口地址**: `/system/door/control_door` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| door_index_code | String | 是 | 门禁设备索引码 | +| control_type | Integer | 是 | 控制类型(0-开,1-关) | + +**响应格式**: +```json +{ + "code": 200, + "message": "门控制成功", + "data": null +} +``` + +**获取门禁设备列表** +**接口地址**: `/system/door/list` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| device_name | String | 否 | 设备名称 | +| device_code | String | 否 | 设备编码 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total": 5, + "rows": [ + { + "device_id": 1, + "device_name": "前门", + "device_code": "DOOR_001", + "door_index_code": "123456", + "status": "0", + "create_time": "2023-01-01 10:00:00" + } + ] + } +} +``` + +**添加门禁设备** +**接口地址**: `/system/door/add` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| device_name | String | 是 | 设备名称 | +| device_code | String | 是 | 设备编码 | +| door_index_code | String | 是 | 门禁设备索引码 | +| status | String | 是 | 设备状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "添加成功", + "data": null +} +``` + +**编辑门禁设备** +**接口地址**: `/system/door/edit` +**请求方式**: PUT +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| device_id | Integer | 是 | 设备ID | +| device_name | String | 是 | 设备名称 | +| device_code | String | 是 | 设备编码 | +| door_index_code | String | 是 | 门禁设备索引码 | +| status | String | 是 | 设备状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "编辑成功", + "data": null +} +``` + +#### 4.5.18 海康设备管理 + +**查询访客预约记录** +**接口地址**: `/system/haikang/get_visitor_list` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| visitor_name | String | 否 | 访客姓名 | +| visitor_phone | String | 否 | 访客电话 | +| start_time | String | 否 | 开始时间 | +| end_time | String | 否 | 结束时间 | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | + +**响应格式**: +```json +{ + "code": 200, + "message": "查询成功", + "data": { + "total": 3, + "rows": [ + { + "reservation_id": "123456", + "visitor_name": "张三", + "visitor_phone": "13800138000", + "visit_time": "2023-01-01 10:00:00", + "status": "0" + } + ] + } +} +``` + +#### 4.5.19 告警消息管理 + +**查看告警类型分布** +**接口地址**: `/system/message/alarm_type_distribution` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "type1": 10, + "type2": 5, + "type3": 3 + } +} +``` + +**一键处理未处理的告警消息** +**接口地址**: `/system/message/handle_all` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "处理成功", + "data": null +} +``` + +**查询告警数量信息** +**接口地址**: `/system/message/count_message` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total": 18, + "unhandled": 5, + "handled": 13 + } +} +``` + +**获取告警消息列表** +**接口地址**: `/system/message/alert_message` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total": 18, + "rows": [ + { + "message_id": 1, + "message_title": "设备异常", + "message_type": "1", + "message_content": "前门门禁设备离线", + "status": "0", + "create_time": "2023-01-01 10:00:00" + } + ] + } +} +``` + +#### 4.5.20 人脸识别管理 + +**人脸检测** +**接口地址**: `/system/compreface/face_detection` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: 文件流/字节流形式的图片数据 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "faces": [ + { + "box": [100, 100, 200, 200], + "probability": 0.99 + } + ], + "image_id": "abc123", + "time_used": 100 + } +} +``` + +**人脸识别** +**接口地址**: `/system/compreface/face_recognition` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: 文件流/字节流形式的图片数据 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "faces": [ + { + "box": [100, 100, 200, 200], + "probability": 0.99, + "identity": { + "id": "person_001", + "name": "张三", + "score": 0.95 + } + } + ], + "image_id": "abc123", + "time_used": 150 + } +} +``` + +#### 4.5.21 验证码管理 + +**获取验证码图片** +**接口地址**: `/captchaImage` +**请求方式**: GET +**认证要求**: 否 +**功能说明**: 获取登录验证码图片,包括验证码开关和注册开关信息 + +**响应格式**: +```json +{ + "code": 200, + "message": "操作成功", + "data": { + "captchaEnabled": true, + "registerEnabled": true, + "img": "base64编码的图片数据", + "uuid": "验证码会话ID" + } +} +``` + +#### 4.5.22 通用功能 + +**文件上传** +**接口地址**: `/common/upload` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| file | File | 是 | 要上传的文件 | + +**响应格式**: +```json +{ + "code": 200, + "message": "上传成功", + "data": { + "fileName": "uploaded_file.jpg", + "filePath": "/upload/path/uploaded_file.jpg" + } +} +``` + +**文件下载** +**接口地址**: `/common/download` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| fileName | String | 是 | 文件名 | +| delete | Boolean | 是 | 是否删除文件 | + +**响应格式**: 文件流 + +**获取操作日志列表** +**接口地址**: `/monitor/operlog/list` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| title | String | 否 | 日志标题 | +| business_type | Integer | 否 | 业务类型 | +| oper_name | String | 否 | 操作用户 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total": 100, + "rows": [ + { + "oper_id": 1, + "title": "用户管理", + "business_type": 1, + "oper_name": "admin", + "oper_url": "/system/user/list", + "oper_ip": "127.0.0.1", + "oper_time": "2023-01-01 10:00:00" + } + ] + } +} +``` + +**获取登录日志列表** +**接口地址**: `/monitor/logininfor/list` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| user_name | String | 否 | 用户名 | +| status | String | 否 | 登录状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total": 50, + "rows": [ + { + "info_id": 1, + "user_name": "admin", + "ipaddr": "127.0.0.1", + "login_location": "本地", + "browser": "Chrome", + "os": "Windows 10", + "status": "0", + "msg": "登录成功", + "login_time": "2023-01-01 10:00:00" + } + ] + } +} +``` + +**解锁用户** +**接口地址**: `/monitor/logininfor/unlock/{user_name}` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "解锁成功", + "data": null +} +``` + +#### 4.5.10 缓存监控 + +**获取缓存统计信息** +**接口地址**: `/monitor/cache` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "name": "Redis", + "uptime": "1000000", + "connected_clients": 10, + "used_memory": "1024000", + "db_num": 16 + } +} +``` + +**获取缓存名称列表** +**接口地址**: `/monitor/cache/getNames` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + { + "cacheName": "system", + "cacheType": "Redis" + } + ] +} +``` + +**获取缓存键列表** +**接口地址**: `/monitor/cache/getKeys/{cache_name}` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + "sys_config:sys.name", + "sys_config:sys.version" + ] +} +``` + +**获取缓存值** +**接口地址**: `/monitor/cache/getValue/{cache_name}/{cache_key}` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "cacheName": "system", + "cacheKey": "sys_config:sys.name", + "cacheValue": "康达机器人管理系统", + "cacheTime": "2023-01-01 10:00:00" + } +} +``` + +**清除指定名称缓存** +**接口地址**: `/monitor/cache/clearCacheName/{cache_name}` +**请求方式**: DELETE +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "清除成功", + "data": null +} +``` + +**清除所有缓存** +**接口地址**: `/monitor/cache/clearCacheAll` +**请求方式**: DELETE +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "清除成功", + "data": null +} +``` + +#### 4.5.11 定时任务管理 + +**获取定时任务列表** +**接口地址**: `/monitor/job/list` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| job_name | String | 否 | 任务名称 | +| job_group | String | 否 | 任务组名 | +| status | String | 否 | 任务状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total": 5, + "rows": [ + { + "job_id": 1, + "job_name": "数据备份", + "job_group": "system", + "invoke_target": "com.example.job.BackupJob", + "cron_expression": "0 0 2 * * ?", + "status": "0", + "create_time": "2023-01-01 10:00:00" + } + ] + } +} +``` + +**添加定时任务** +**接口地址**: `/monitor/job` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| job_name | String | 是 | 任务名称 | +| job_group | String | 是 | 任务组名 | +| invoke_target | String | 是 | 调用目标 | +| cron_expression | String | 是 | cron表达式 | +| status | String | 是 | 任务状态 | +| remark | String | 否 | 备注 | + +**响应格式**: +```json +{ + "code": 200, + "message": "添加成功", + "data": null +} +``` + +**修改定时任务状态** +**接口地址**: `/monitor/job/changeStatus` +**请求方式**: PUT +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| job_id | Integer | 是 | 任务ID | +| status | String | 是 | 任务状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "修改成功", + "data": null +} +``` + +#### 4.5.12 系统统计 + +**获取统计数据** +**接口地址**: `/system/sys_statistics/get_data` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total_visitors": 1000, + "today_visitors": 100, + "success_rate": 0.95, + "total_records": 10000 + } +} +``` + +**获取访问趋势** +**接口地址**: `/system/sys_statistics/get_visitor_count/{day}` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| day | Integer | 是 | 天数 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + { + "date": "2023-01-01", + "count": 100 + }, + { + "date": "2023-01-02", + "count": 150 + } + ] +} +``` + +**获取门禁识别成功率** +**接口地址**: `/system/sys_statistics/access_control_success_rate/{day}` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| day | Integer | 是 | 天数 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "date": "2023-01-01", + "success_count": 95, + "total_count": 100, + "success_rate": 0.95 + } +} +``` + +#### 4.5.13 通知公告管理 + +**获取通知列表** +**接口地址**: `/system/notice/list` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| notice_title | String | 否 | 公告标题 | +| status | String | 否 | 公告状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total": 5, + "rows": [ + { + "notice_id": 1, + "notice_title": "系统更新通知", + "notice_type": "1", + "status": "0", + "create_by": "admin", + "create_time": "2023-01-01 10:00:00" + } + ] + } +} +``` + +**添加通知公告** +**接口地址**: `/system/notice` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| notice_title | String | 是 | 公告标题 | +| notice_type | String | 是 | 公告类型 | +| notice_content | String | 是 | 公告内容 | +| status | String | 是 | 公告状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "添加成功", + "data": null +} +``` + +**获取通知详情** +**接口地址**: `/system/notice/{notice_id}` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "notice_id": 1, + "notice_title": "系统更新通知", + "notice_type": "1", + "notice_content": "系统将于2023-01-01进行更新", + "status": "0", + "create_by": "admin", + "create_time": "2023-01-01 10:00:00" + } +} +``` + +#### 4.5.11 识别记录管理 + +**获取识别记录列表** +**接口地址**: `/system/identification_record/list` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| device_id | String | 否 | 设备ID | +| visitor_name | String | 否 | 访客姓名 | +| start_time | String | 否 | 开始时间 | +| end_time | String | 否 | 结束时间 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total": 10, + "rows": [ + { + "record_id": 1, + "device_id": "device_001", + "visitor_name": "张三", + "visitor_type": "0", + "identification_time": "2023-01-01 10:00:00", + "status": "0" + } + ] + } +} +``` + +**添加识别记录** +**接口地址**: `/system/identification_record` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| device_id | String | 是 | 设备ID | +| visitor_name | String | 是 | 访客姓名 | +| visitor_type | String | 是 | 访客类型 | +| identification_time | String | 是 | 识别时间 | +| status | String | 是 | 状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "添加成功", + "data": null +} +``` + +**导出识别记录** +**接口地址**: `/system/identification_record/export` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| device_id | String | 否 | 设备ID | +| visitor_name | String | 否 | 访客姓名 | +| start_time | String | 否 | 开始时间 | +| end_time | String | 否 | 结束时间 | + +**响应格式**: 文件流 + +#### 4.5.12 讲解内容管理 + +**获取讲解内容列表** +**接口地址**: `/system/explanation_content/list` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| content_name | String | 否 | 内容名称 | +| status | String | 否 | 状态 | +| type_id | Integer | 否 | 类型ID | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + { + "content_id": 1, + "content_name": "展厅介绍", + "type_id": 1, + "content_text": "这是展厅的详细介绍...", + "status": "0", + "create_time": "2023-01-01 10:00:00" + } + ] +} +``` + +**添加讲解内容** +**接口地址**: `/system/explanation_content/add_explanation` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| content_name | String | 是 | 内容名称 | +| type_id | Integer | 是 | 类型ID | +| content_text | String | 是 | 内容文本 | +| status | String | 是 | 状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "添加成功", + "data": null +} +``` + +**编辑讲解内容** +**接口地址**: `/system/explanation_content/edit_explanation` +**请求方式**: PUT +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| content_id | Integer | 是 | 内容ID | +| content_name | String | 是 | 内容名称 | +| type_id | Integer | 是 | 类型ID | +| content_text | String | 是 | 内容文本 | +| status | String | 是 | 状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "编辑成功", + "data": null +} +``` + +**删除讲解内容** +**接口地址**: `/system/explanation_content/{explanation_content_ids}` +**请求方式**: DELETE +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "删除成功", + "data": null +} +``` + +**获取讲解内容类型列表** +**接口地址**: `/system/explanation_content/type_list` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + { + "type_id": 1, + "type_name": "展厅介绍", + "create_time": "2023-01-01 10:00:00" + } + ] +} +``` + +#### 4.5.13 机器人动作管理 + +**获取机器人动作列表** +**接口地址**: `/system/robot_action/list` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| action_name | String | 否 | 动作名称 | +| robot_id | Integer | 否 | 机器人ID | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + { + "action_id": 1, + "action_name": "欢迎动作", + "robot_id": 1, + "action_data": "{...}", + "create_time": "2023-01-01 10:00:00" + } + ] +} +``` + +**编辑机器人动作** +**接口地址**: `/system/robot_action` +**请求方式**: PUT +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| action_id | Integer | 是 | 动作ID | +| action_name | String | 是 | 动作名称 | +| robot_id | Integer | 是 | 机器人ID | +| action_data | String | 是 | 动作数据 | + +**响应格式**: +```json +{ + "code": 200, + "message": "编辑成功", + "data": null +} +``` + +#### 4.5.15 门禁控制管理 + +**机器人控制开门** +**接口地址**: `/system/door/control_door` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| door_index_code | String | 是 | 门禁设备索引码 | +| control_type | Integer | 是 | 控制类型(0-开,1-关) | + +**响应格式**: +```json +{ + "code": 200, + "message": "门控制成功", + "data": null +} +``` + +**获取门禁设备列表** +**接口地址**: `/system/door/list` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| device_name | String | 否 | 设备名称 | +| device_code | String | 否 | 设备编码 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total": 5, + "rows": [ + { + "device_id": 1, + "device_name": "前门", + "device_code": "DOOR_001", + "door_index_code": "123456", + "status": "0", + "create_time": "2023-01-01 10:00:00" + } + ] + } +} +``` + +**添加门禁设备** +**接口地址**: `/system/door/add` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| device_name | String | 是 | 设备名称 | +| device_code | String | 是 | 设备编码 | +| door_index_code | String | 是 | 门禁设备索引码 | +| status | String | 是 | 设备状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "添加成功", + "data": null +} +``` + +**编辑门禁设备** +**接口地址**: `/system/door/edit` +**请求方式**: PUT +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| device_id | Integer | 是 | 设备ID | +| device_name | String | 是 | 设备名称 | +| device_code | String | 是 | 设备编码 | +| door_index_code | String | 是 | 门禁设备索引码 | +| status | String | 是 | 设备状态 | + +**响应格式**: +```json +{ + "code": 200, + "message": "编辑成功", + "data": null +} +``` + +#### 4.5.16 海康设备管理 + +**查询访客预约记录** +**接口地址**: `/system/haikang/get_visitor_list` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| visitor_name | String | 否 | 访客姓名 | +| visitor_phone | String | 否 | 访客电话 | +| start_time | String | 否 | 开始时间 | +| end_time | String | 否 | 结束时间 | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | + +**响应格式**: +```json +{ + "code": 200, + "message": "查询成功", + "data": { + "total": 3, + "rows": [ + { + "reservation_id": "123456", + "visitor_name": "张三", + "visitor_phone": "13800138000", + "visit_time": "2023-01-01 10:00:00", + "status": "0" + } + ] + } +} +``` + +#### 4.5.17 告警消息管理 + +**查看告警类型分布** +**接口地址**: `/system/message/alarm_type_distribution` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "type1": 10, + "type2": 5, + "type3": 3 + } +} +``` + +**一键处理未处理的告警消息** +**接口地址**: `/system/message/handle_all` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "处理成功", + "data": null +} +``` + +**查询告警数量信息** +**接口地址**: `/system/message/count_message` +**请求方式**: GET +**认证要求**: 是 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total": 18, + "unhandled": 5, + "handled": 13 + } +} +``` + +**获取告警消息列表** +**接口地址**: `/system/message/alert_message` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "total": 18, + "rows": [ + { + "message_id": 1, + "message_title": "设备异常", + "message_type": "1", + "message_content": "前门门禁设备离线", + "status": "0", + "create_time": "2023-01-01 10:00:00" + } + ] + } +} +``` + +### 4.6 搜索服务模块 + +#### 4.6.1 与聊天助手对话(支持搜索增强) + +**接口地址**: `/system/ragflow/converse_with_chat_assistant` +**请求方式**: POST +**认证要求**: 是 +**功能说明**: 与聊天助手进行对话,支持根据关键词自动触发搜索增强功能 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| chat_id | String | 是 | 聊天助手ID | +| question | String | 是 | 用户问题 | +| stream | Boolean | 否 | 是否使用流式响应(默认:false) | + +**响应格式**: +- 非流式响应: +```json +{ + "code": 200, + "message": "操作成功", + "data": { + "answer": "搜索结果或对话回复" + } +} +``` +- 流式响应: +``` +data: {"answer": "这是"} +data: {"answer": "搜索结果"} +data: {"answer": "的一部分"} +``` + +**搜索触发条件**: +当用户问题包含以下关键词时,会自动触发搜索功能: +- 中文关键词:搜索、查找、新闻、时事、最新、今天、现在、当前、天气、温度、气温、下雨、晴、多云 +- 英文关键词:search、find、news、latest、today、current、now、weather、temperature、rain、sunny、cloudy + +### 4.7 RAGFlow 文档管理模块 + +#### 4.7.1 获取数据集列表 + +**接口地址**: `/system/ragflow/dataset_list` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| name | String | 否 | 数据集名称 | + +**响应格式**: +```json +{ + "code": 200, + "message": "操作成功", + "data": { + "total": 5, + "rows": [ + { + "dataset_id": "12345", + "name": "技术文档", + "description": "系统技术文档", + "create_time": "2023-01-01 10:00:00" + } + ] + } +} +``` + +#### 4.7.2 创建数据集 + +**接口地址**: `/system/ragflow/create_dataset` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| name | String | 是 | 数据集名称 | +| description | String | 否 | 数据集描述 | +| engine_name | String | 否 | 搜索引擎名称 | + +**响应格式**: +```json +{ + "code": 200, + "message": "操作成功", + "data": { + "dataset_id": "12345" + } +} +``` + +#### 4.7.3 更新数据集 + +**接口地址**: `/system/ragflow/update_dataset/{dataset_id}` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| name | String | 是 | 数据集名称 | +| description | String | 否 | 数据集描述 | +| engine_name | String | 否 | 搜索引擎名称 | + +**响应格式**: +```json +{ + "code": 200, + "message": "操作成功", + "data": null +} +``` + +#### 4.7.4 获取数据集中文档列表 + +**接口地址**: `/system/ragflow/list_documents/{dataset_id}` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| name | String | 否 | 文档名称 | + +**响应格式**: +```json +{ + "code": 200, + "message": "操作成功", + "data": { + "total": 10, + "rows": [ + { + "document_id": "67890", + "name": "API文档.pdf", + "size": 1024000, + "status": "parsed" + } + ] + } +} +``` + +#### 4.7.5 上传文件到数据集 + +**接口地址**: `/system/ragflow/upload_file/{dataset_id}` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| files | File[] | 是 | 要上传的文件列表 | + +**响应格式**: +```json +{ + "code": 200, + "message": "操作成功", + "data": { + "success_count": 2, + "failed_count": 0 + } +} +``` + +#### 4.7.6 更新文档 + +**接口地址**: `/system/ragflow/update_file/{dataset_id}/{document_id}` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| name | String | 否 | 文档名称 | +| description | String | 否 | 文档描述 | + +**响应格式**: +```json +{ + "code": 200, + "message": "操作成功", + "data": null +} +``` + +#### 4.7.7 解析文档 + +**接口地址**: `/system/ragflow/parse_documents/{dataset_id}` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| document_ids | String[] | 是 | 要解析的文档ID列表 | + +**响应格式**: +```json +{ + "code": 200, + "message": "操作成功", + "data": null +} +``` + +#### 4.7.8 停止解析文档 + +**接口地址**: `/system/ragflow/stop_parse_documents/{dataset_id}` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| document_ids | String[] | 是 | 要停止解析的文档ID列表 | + +**响应格式**: +```json +{ + "code": 200, + "message": "操作成功", + "data": null +} +``` + +#### 4.7.9 删除文档 + +**接口地址**: `/system/ragflow/delete_file/{dataset_id}` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| document_ids | String[] | 是 | 要删除的文档ID列表 | + +**响应格式**: +```json +{ + "code": 200, + "message": "操作成功", + "data": null +} +``` + +#### 4.7.10 删除数据集 + +**接口地址**: `/system/ragflow/delete_datasets` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| dataset_ids | String[] | 是 | 要删除的数据集ID列表 | + +**响应格式**: +```json +{ + "code": 200, + "message": "操作成功", + "data": null +} +``` + +#### 4.7.11 获取聊天助手列表 + +**接口地址**: `/system/ragflow/get_chat_assistant_list` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页条数 | +| name | String | 否 | 助手名称 | + +**响应格式**: +```json +{ + "code": 200, + "message": "操作成功", + "data": { + "total": 3, + "rows": [ + { + "chat_id": "chat123", + "name": "技术助手", + "description": "提供技术支持", + "create_time": "2023-01-01 10:00:00" + } + ] + } +} +``` + +#### 4.7.12 更新聊天助手 + +**接口地址**: `/system/ragflow/update_chat_assistant` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| chat_id | String | 是 | 聊天助手ID | +| name | String | 否 | 助手名称 | +| description | String | 否 | 助手描述 | +| dataset_ids | String[] | 否 | 关联的数据集ID列表 | + +**响应格式**: +```json +{ + "code": 200, + "message": "操作成功", + "data": null +} +``` + +#### 4.7.13 创建聊天会话 + +**接口地址**: `/system/ragflow/create_session_with_chat` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| chat_id | String | 是 | 聊天助手ID | + +**响应格式**: +```json +{ + "code": 200, + "message": "操作成功", + "data": { + "session_id": "session123" + } +} +``` + + + +#### 4.7.14 与聊天助手对话 + +**接口地址**: `/system/ragflow/converse_with_chat_assistant` +**请求方式**: POST +**认证要求**: 是 +**请求参数**: +| 参数名 | 类型 | 必填 | 描述 | +| ------ | ---- | ---- | ---- | +| chat_id | String | 是 | 聊天助手ID | +| question | String | 是 | 问题内容 | +| stream | Boolean | 否 | 是否使用流式响应(默认:false) | + +**响应格式**: +- 非流式响应: +```json +{ + "code": 200, + "message": "操作成功", + "data": { + "answer": "这是一个示例回答" + } +} +``` +- 流式响应: +``` +data: {"answer": "这是"} +data: {"answer": "一个示例"} +data: {"answer": "回答"} +``` + +#### 4.7.15 获取用户权限 + +**接口地址**: `/system/ragflow/get_user_permission` +**请求方式**: GET +**认证要求**: 是 +**请求参数**: 无 + +**响应格式**: +```json +{ + "code": 200, + "message": "操作成功", + "data": [ + "access:device:list", + "access:record:list" + ] +} +``` + +## 5. 通用响应格式 + +所有接口的响应格式统一为 JSON 格式,包含以下字段: + +| 字段名 | 类型 | 描述 | +| ------ | ---- | ---- | +| code | Integer | 响应码(200表示成功,其他表示失败) | +| message | String | 响应消息 | +| data | Object | 响应数据 | + +## 6. 错误码说明 + +| 错误码 | 描述 | +| ------ | ---- | +| 200 | 成功 | +| 400 | 请求参数错误 | +| 401 | 未授权 | +| 403 | 权限不足 | +| 404 | 接口不存在 | +| 500 | 服务器内部错误 | + +## 7. 附录 + +### 7.1 数据类型说明 + +- **String**: 字符串类型 +- **Integer**: 整数类型 +- **Boolean**: 布尔类型(true/false) +- **Object**: 对象类型 +- **Array**: 数组类型 + +### 7.2 状态码说明 + +- **0**: 禁用 +- **1**: 启用 + +### 7.3 权限标识说明 + +系统使用权限标识控制接口访问,部分接口需要特定权限标识才能访问: + +| 权限标识 | 描述 | +| -------- | ---- | +| access:device:list | 设备列表访问权限 | +| access:record:list | 识别记录访问权限 | +| system:user:list | 用户列表访问权限 | \ No newline at end of file diff --git a/ruoyi-fastapi-backend/doc/architecture_documentation.md b/ruoyi-fastapi-backend/doc/architecture_documentation.md new file mode 100644 index 0000000..855c03e --- /dev/null +++ b/ruoyi-fastapi-backend/doc/architecture_documentation.md @@ -0,0 +1,345 @@ +# 康达机器人系统架构文档 + +## 1. 系统概述 + +康达机器人系统是一个集机器人控制、人脸识别、门禁管理、文档检索和智能对话于一体的综合性管理平台。系统采用前后端分离架构,集成了多种外部服务和AI能力,为用户提供全面的机器人管理和控制解决方案。 + +## 2. 系统架构图 + +```mermaid +flowchart TD + subgraph 客户端层 + Web控制台["Web控制台\n(管理界面)"] + Pad控制端["Pad控制端\n(现场操作)"] + end + + subgraph 后端服务层 + FastAPI["FastAPI后端服务\n(核心业务逻辑)"] + Redis["Redis缓存\n(会话、数据缓存)"] + Database[("数据库\n(MySQL/PostgreSQL)")] + end + + subgraph 外部服务层 + RAGFlow["RAGFlow\n(文档检索与智能对话)"] + SearchAPI["搜索API\n(SerpAPI)"] + Compreface["Compreface\n(人脸识别)"] + HaiKang["海康平台\n(门禁控制)"] + end + + subgraph 设备层 + Robot["机器人\n(执行动作、采集数据)"] + Door["门禁设备\n(开关控制)"] + Camera["摄像头\n(图像采集)"] + end + + %% 连接关系 + Web控制台 -->|HTTP请求| FastAPI + Pad控制端 -->|HTTP请求| FastAPI + FastAPI -->|读写数据| Database + FastAPI -->|缓存操作| Redis + FastAPI -->|调用接口| RAGFlow + FastAPI -->|搜索增强| SearchAPI + FastAPI -->|人脸识别| Compreface + FastAPI -->|门禁控制| HaiKang + FastAPI -->|控制指令| Robot + HaiKang -->|控制| Door + Camera -->|图像数据| Compreface + Robot -->|状态反馈| FastAPI + Door -->|状态反馈| HaiKang + HaiKang -->|状态更新| FastAPI + + %% 大模型服务 + RAGFlow -->|调用| LLM["大语言模型\n(云上服务)"] + SearchAPI -->|调用| SearchEngine["搜索引擎\n(第三方)"] +``` + +## 3. 系统组件说明 + +### 3.1 客户端层 + +#### Web控制台 +- **功能**:提供系统管理、用户管理、角色管理、机器人配置、讲解内容管理等功能 +- **技术**:Vue.js + Element Plus +- **访问方式**:浏览器访问 + +#### Pad控制端 +- **功能**:现场控制机器人、查看实时状态、处理识别记录 +- **技术**:Web应用(适配Pad设备) +- **访问方式**:Pad浏览器或专用应用 + +### 3.2 后端服务层 + +#### FastAPI后端服务 +- **核心功能**: + - 提供RESTful API接口 + - 处理业务逻辑 + - 集成各种外部服务 + - 权限管理与认证 + - 系统监控与日志 +- **主要模块**: + - 用户管理模块 + - 角色管理模块 + - 机器人信息管理模块 + - 机器人动作管理模块 + - 门禁设备管理模块 + - 识别记录管理模块 + - RAGFlow文档管理模块 + - 人脸识别管理模块 + - 系统统计模块 + +#### Redis缓存 +- **功能**: + - 会话管理 + - 数据缓存 + - 分布式锁 + - 消息队列(可选) + +#### 数据库 +- **功能**:存储系统配置、用户信息、机器人数据、识别记录等持久化数据 +- **支持类型**:MySQL、PostgreSQL + +### 3.3 外部服务层 + +#### RAGFlow +- **功能**: + - 文档检索与管理 + - 智能对话(基于大语言模型) + - 知识库管理 +- **集成方式**:API调用 + +#### 搜索API +- **功能**: + - 增强智能对话的搜索能力 + - 提供实时信息查询 +- **技术**:SerpAPI + +#### Compreface +- **功能**: + - 人脸识别 + - 人脸比对 + - 人脸检测 +- **集成方式**:API调用 + +#### 海康平台 +- **功能**: + - 门禁设备管理 + - 门禁状态查询 + - 门禁控制(开关门) + - 门禁事件记录 +- **集成方式**:API调用 + +### 3.4 设备层 + +#### 机器人 +- **功能**: + - 执行预设动作 + - 采集环境数据 + - 接收并执行控制指令 + - 反馈执行状态 + +#### 门禁设备 +- **功能**: + - 开关门控制 + - 状态反馈 + - 事件记录 + +#### 摄像头 +- **功能**: + - 图像采集 + - 视频监控 + - 人脸数据采集 + +## 4. 核心工作流程 + +### 4.1 用户认证流程 + +```mermaid +sequenceDiagram + participant 用户 as 用户 + participant Web as Web控制台/Pad控制端 + participant FastAPI as FastAPI后端 + participant Redis as Redis缓存 + participant DB as 数据库 + + 用户->>Web: 输入用户名密码 + Web->>FastAPI: POST /system/user/login + FastAPI->>DB: 查询用户信息 + DB-->>FastAPI: 返回用户数据 + FastAPI->>FastAPI: 验证密码 + FastAPI->>Redis: 生成并存储token + Redis-->>FastAPI: 返回token + FastAPI-->>Web: 返回token和用户信息 + Web->>Web: 存储token +``` + +### 4.2 门禁控制流程 + +```mermaid +sequenceDiagram + participant Pad as Pad控制端 + participant FastAPI as FastAPI后端 + participant HaiKang as 海康平台API + participant Door as 门禁设备 + + Pad->>FastAPI: POST /system/door/control_door + FastAPI->>HaiKang: 请求门禁控制 + HaiKang->>Door: 发送开关指令 + Door-->>HaiKang: 执行结果 + HaiKang-->>FastAPI: 返回控制结果 + FastAPI-->>Pad: 返回操作状态 +``` + +### 4.3 人脸识别流程 + +```mermaid +sequenceDiagram + participant Camera as 摄像头 + participant Compreface as Compreface + participant FastAPI as FastAPI后端 + participant DB as 数据库 + participant Robot as 机器人 + + Camera->>Compreface: 上传人脸图像 + Compreface->>Compreface: 检测人脸 + Compreface->>Compreface: 提取特征 + Compreface->>FastAPI: 返回识别结果 + FastAPI->>DB: 保存识别记录 + FastAPI->>Robot: 发送响应指令 + Robot-->>FastAPI: 执行结果 +``` + +### 4.4 智能对话流程 + +```mermaid +sequenceDiagram + participant User as 用户 + participant Web as Web/Pad客户端 + participant FastAPI as FastAPI后端 + participant Redis as Redis缓存 + participant RAGFlow as RAGFlow + participant Search as 搜索API + participant LLM as 大语言模型 + + User->>Web: 输入问题 + Web->>FastAPI: POST /system/ragflow/converse_with_chat_assistant + FastAPI->>Redis: 检查缓存 + alt 缓存命中 + Redis-->>FastAPI: 返回缓存结果 + else 缓存未命中 + FastAPI->>RAGFlow: 发送对话请求 + RAGFlow->>Search: 检查是否需要搜索增强 + alt 需要搜索增强 + Search->>SearchEngine: 执行搜索 + SearchEngine-->>Search: 返回搜索结果 + Search-->>RAGFlow: 返回搜索内容 + end + RAGFlow->>LLM: 调用大语言模型 + LLM-->>RAGFlow: 返回生成内容 + RAGFlow-->>FastAPI: 返回对话结果 + FastAPI->>Redis: 存储结果到缓存 + end + FastAPI-->>Web: 返回对话结果 + Web->>User: 显示回答 +``` + +## 5. 系统集成与依赖 + +### 5.1 内部依赖 + +| 依赖名称 | 用途 | 版本 | +|---------|------|------| +| FastAPI | 后端框架 | 最新 | +| SQLAlchemy | ORM框架 | 最新 | +| Redis | 缓存服务 | 最新 | +| Pydantic | 数据验证 | 最新 | +| python-jose | JWT认证 | 最新 | + +### 5.2 外部服务依赖 + +| 服务名称 | 用途 | 集成方式 | +|---------|------|---------| +| RAGFlow | 文档检索与智能对话 | API调用 | +| Compreface | 人脸识别 | API调用 | +| 海康平台 | 门禁控制 | API调用 | +| SerpAPI | 搜索增强 | API调用 | + +## 6. 系统特性与优势 + +1. **模块化设计**:系统采用模块化架构,各功能模块独立,便于维护和扩展 +2. **前后端分离**:前端使用Vue.js,后端使用FastAPI,提高开发效率和系统性能 +3. **多端支持**:支持Web控制台和Pad控制端,满足不同场景的使用需求 +4. **AI能力集成**:集成了人脸识别、智能对话和搜索增强等AI能力,提升系统智能化水平 +5. **丰富的外部接口**:与门禁系统、机器人硬件等多种外部设备和服务进行集成 +6. **完善的权限管理**:支持基于角色的权限控制,确保系统安全 +7. **实时监控与统计**:提供系统统计和识别记录管理,便于数据分析和决策 + +## 7. 系统部署架构 + +```mermaid +flowchart TD + subgraph 服务器集群 + LoadBalancer["负载均衡器\n(Nginx)"] + Backend1["FastAPI节点1"] + Backend2["FastAPI节点2"] + Backend3["FastAPI节点3"] + end + + subgraph 数据层 + DB[("主数据库\n(MySQL/PostgreSQL)")] + DB2[("从数据库\n(MySQL/PostgreSQL)")] + RedisCluster["Redis集群\n(主从架构)"] + end + + subgraph 外部服务 + RAGFlow["RAGFlow服务"] + Compreface["Compreface服务"] + HaiKang["海康平台"] + end + + 用户["用户"] -->|HTTP请求| LoadBalancer + LoadBalancer -->|分发请求| Backend1 + LoadBalancer -->|分发请求| Backend2 + LoadBalancer -->|分发请求| Backend3 + + Backend1 -->|读写| DB + Backend2 -->|读写| DB + Backend3 -->|读写| DB + DB -->|同步| DB2 + + Backend1 -->|缓存操作| RedisCluster + Backend2 -->|缓存操作| RedisCluster + Backend3 -->|缓存操作| RedisCluster + + Backend1 -->|API调用| RAGFlow + Backend2 -->|API调用| RAGFlow + Backend3 -->|API调用| RAGFlow + + Backend1 -->|API调用| Compreface + Backend2 -->|API调用| Compreface + Backend3 -->|API调用| Compreface + + Backend1 -->|API调用| HaiKang + Backend2 -->|API调用| HaiKang + Backend3 -->|API调用| HaiKang +``` + +## 8. 系统扩展性设计 + +1. **微服务架构支持**:系统可扩展为微服务架构,将不同功能模块拆分为独立服务 +2. **插件化设计**:支持通过插件方式扩展系统功能 +3. **API网关集成**:可集成API网关,统一管理API访问 +4. **容器化部署**:支持Docker容器化部署,便于水平扩展 +5. **云原生支持**:支持在Kubernetes等容器编排平台上部署 + +## 9. 系统安全设计 + +1. **身份认证与授权**:采用JWT令牌机制,支持基于角色的访问控制 +2. **数据加密**:敏感数据加密存储,传输过程使用HTTPS +3. **接口安全**:所有API接口均需认证,部分接口需额外权限验证 +4. **日志审计**:记录所有用户操作和系统事件,便于审计和追踪 +5. **防SQL注入**:使用ORM框架和参数化查询,防止SQL注入攻击 +6. **防XSS攻击**:对输入输出进行过滤,防止跨站脚本攻击 + +## 10. 总结 + +康达机器人系统采用现代化的技术架构,集成了多种AI能力和外部服务,为用户提供全面的机器人管理和控制解决方案。系统具有良好的扩展性、安全性和可维护性,能够满足不同规模和场景的使用需求。 \ No newline at end of file diff --git a/ruoyi-fastapi-backend/doc/qa.txt b/ruoyi-fastapi-backend/doc/qa.txt new file mode 100644 index 0000000..8c05cc2 --- /dev/null +++ b/ruoyi-fastapi-backend/doc/qa.txt @@ -0,0 +1,587 @@ +一、公司概况类 +1.康达新材料(集团)股份有限公司成立于哪一年? +•答案:1988年 +2.康达新材料(集团)股份有限公司的上市时间是什么时候? +•答案:2012年4月 +3.康达新材料(集团)股份有限公司的股票代码是什么? +•答案:002669.SZ +4.康达新材的实际控制人是谁? +•答案:唐山市人民政府国有资产监督管理委员会 +5.康达新材的股权背景链条是怎样的? +•答案:唐山市人民政府国有资产监督管理委员会→唐山控股发展集团股份有限公司→唐山工业控股集团有限公司→康达新材料(集团)股份有限公司 +6.康达新材的三大业务板块是什么? +•答案:胶粘剂与特种树脂新材料、电子信息材料和电子科技 +7.康达新材的业务范围覆盖哪些新兴产业? +•答案:装备制造、新能源、轨道交通、航空航天、电子信息及低碳环保等新兴产业 +8.截至2024年底,康达新材的总资产是多少? +•答案:69.40亿元 +9.康达新材2024年的营业收入是多少? +•答案:31亿元 +10.截至2024年底,康达新材的员工总人数有多少? +•答案:1637人 +11.康达新材的管理总部位于哪里? +•答案:浦东新区张江科学城 +12.康达新材在国内哪些地区设有子公司、生产基地或分支机构? +•答案:上海、北京、天津、深圳、大连、成都、香港、福建、唐山等地 +13.康达新材的海外布局有多少处? +•答案:3处 +14.康达新材有多少个生产基地? +•答案:7个 +15.康达新材的胶粘剂产品牌号数有多少? +•答案:500+ +16.康达新材在多个细分领域的市场地位如何? +•答案:多项细分领域市场地位第一 +17.康达新材的使命是什么? +•答案:服务国家战略,引领行业发展 +18.康达新材的愿景是什么? +•答案:成为具有全球化视野的“新材料+电子科技”集团化企业 +19.康达新材的核心价值观是什么? +•答案:融合、协同、创新、超越 +20.康达新材的企业作风是什么? +•答案:不断学习,立即行动,没有任何借口 +21.康达新材的发展战略核心是什么? +•答案:坚守“稳中求进”工作总基调,以科技创新为根本动力,以转型升级、抢占新赛道为基本路径,以高质量发展为主题,立足主业,打造“新材料+电子科技”国家级“专精特新”小巨人企业集群 +22.康达新材“1+2+3+1+N”战略布局中的“1个产业研究院”具体定位是什么? +•答案:产业发展智库,科技成果研发、转化合作平台,技术信息共享中心 +23.康达新材“1+2+3+1+N”战略布局中的“2个战略新兴产业方向”是什么? +•答案:新材料+电子科技 +24.康达新材“1+2+3+1+N”战略布局中的“3大业务板块”具体指什么? +•答案:胶粘剂与特种树脂新材料+电子信息材料+电子科技 +25.康达新材“1+2+3+1+N”战略布局中的“1+N个产业园(基地)”已在哪些地区建立? +•答案:上海、北京、成都、天津、大连、深圳、唐山、福建邵武、香港、泰国等地 +26.康达新材在新材料领域的进口替代目标涉及哪些领域? +•答案:半导体材料、显示材料、生物医用材料、新能源材料、高性能纤维、高性能膜材料、先进高分子材料及其他领域 +27.康达新材在半导体材料领域的进口依赖材料有哪些? +•答案:大尺寸硅材料、大尺寸碳化硅单晶、SOI、高饱和度光刻胶、高性能靶材、电子特种气体、湿电子化学品、氮化镓单晶/氮化镓单晶衬底、化学机械抛光(CMP)材料、封装基板、高密度陶瓷材料等 +28.康达新材在显示材料领域的进口依赖材料有哪些? +•答案:OLED发光材料、超薄玻璃、高世代线玻璃基板、精细金属掩模板(FMM)、光学膜、柔性PI膜、偏光片、高性能水汽阻隔膜、异方性导电胶膜(ACF)、特种光学聚氨酯膜(PET)、OCA光学胶、微球、抗指纹涂层(AFC)涂料等 +29.康达新材在生物医用材料领域的进口依赖材料有哪些? +•答案:医用级钛粉与镍钛合金粉、苯乙烯类热塑性弹性体、医用级聚乳酸、碲锌镉晶体、人工晶状体等 +30.康达新材在新能源材料领域的进口依赖材料有哪些? +•答案:硅碳负极材料、电解铜箔、电解液添加剂、铝塑膜、质子交换膜、氢燃料电池催化剂、气体扩散层材料等 +31.康达新材在高性能纤维领域的进口依赖材料有哪些? +•答案:高性能碳纤维及其复合材料、高性能对位芳纶纤维及其复合材料、超高分子量聚乙烯纤维等 +32.康达新材在高性能膜材料领域的进口依赖材料有哪些? +•答案:海水淡化反渗透膜、陶瓷膜、离子交换膜、中空纤维膜、高导热石墨膜等 +33.康达新材在先进高分子材料领域的进口依赖材料有哪些? +•答案:聚苯硫醚(PPS)、聚砜(PSF)、聚醚醚酮(PEEK)、聚偏氯乙烯(PVDF)、聚甲醛(POM)、有机硅等 +34.康达新材在“其他”领域的进口依赖材料有哪些? +•答案:高频覆铜板基材、液晶高分子聚合物(LCP)等 +35.康达新材及下属子公司中获得国家级“专精特新”小巨人企业认定的有哪些? +•答案:大连齐化新材料有限公司(有效期:2022年7月1日至2025年6月30日)、成都必控科技有限责任公司、上海晶材新材料科技有限公司、康达新材料(集团)股份有限公司 +36.北京力源兴达科技有限公司获得了什么级别的“专精特新”相关认定? +•答案:北京市专精特新“小巨人”及中小企业 +37.成都赛英科技有限公司获得了什么级别的“专精特新”相关认定?有效期是多久? +•答案:四川省专精特新中小企业,有效期为2022年11月1日-2025年10月31日 +38.上海理日新材料科技有限公司获得了什么级别的“专精特新”相关认定? +•答案:上海市专精特新中小企业 +39.大连齐化获得了哪些资质认证? +•答案:2020年12月被科技部火炬中心认定为国家级“高新技术企业”,被辽宁省科技厅认定为“雏鹰企业”;2023年5月,质检中心被中国石油和化学工业联合会认定为“A级”石油和化工企业质量检验机构;通过ISO9001、ISO14001、ISO45001三体系认证 +40.河北惟新科技有限公司获得了什么资质?认定编号和有效期是多少? +•答案:河北省科技型中小企业,认定编号:K23016712,有效期3年 +二、历史发展类 +1.康达新材在1988年发生了什么关键事件? +•答案:成立上海康达化工实验厂 +2.康达新材在2000年发生了什么关键事件? +•答案:成立上海康达化工技术研究所 +3.康达新材在2002年完成了哪两项重要事件? +•答案:引入战略投资者,完成股份制改革,整体变更设立为上海康达化工新材料股份有限公司;获得中国合格评定国家认可委员会(CNAS)认可的实验室资质 +4.康达新材在2008年发生了什么关键事件? +•答案:成立上海康达化工有限责任公司;被认定为国家高新技术企业 +5.康达新材在2012年发生了什么关键事件? +•答案:成功在深圳交易所上市 +6.康达新材在2016年发生了什么关键事件? +•答案:非公开发行股票融资8.5亿元,助推康达新材高质量发展 +7.2018年康达新材收购了成都赛英科技有限公司,此次收购的目的是什么? +•答案:完善电子科技领域产业链布局 +8.2018年康达新材收购了上海晶材新材料科技有限公司,此次收购的目的是什么? +•答案:布局先进陶瓷新材料领域,打造“专精特新”小巨人企业集群 +9.2018年康达新材收购了大连齐化新材料有限公司,此次收购的目的是什么? +•答案:向产业链上游战略延伸,打造环氧树脂产业闭环“生态圈” +10.2019年康达新材收购了成都必控科技有限责任公司,此次收购的意义是什么? +•答案:成功迈入电子科技领域 +11.康达新材的研发中心被认定为国家企业技术中心是在哪一年? +•答案:2019年 +12.康达新材的控股股东变更为唐山工业控股集团有限公司是在哪一年? +•答案:2019年 +13.康达新材更名为“康达新材料(集团)股份有限公司”是在什么时间? +•答案:2021年 +14.康达新材在2021年进行集团化战略布局的范围是什么? +•答案:全国区域 +15.康达新材被认定为国家级“专精特新”小巨人企业是在哪一年? +•答案:2021年 +16.2022年康达新材收购了西安彩晶光电科技股份有限公司,此次收购进入了哪些领域? +•答案:电子信息材料、新能源材料、医药领域,打造新材料“第二增长极” +17.康达新材收购河北惟新科技有限公司是在哪一年? +•答案:2023年 +18.康达新材收购河北惟新科技有限公司解决了什么“卡脖子”问题? +•答案:大尺寸ITO靶材“卡脖子”问题 +三、生产基地与研发中心类 +1.奉贤研发及生产基地(上海)的位置在哪里? +•答案:上海奉贤杭州湾经济技术开发区 +2.奉贤研发及生产基地(上海)的占地面积是多少? +•答案:205亩 +3.奉贤研发及生产基地(上海)的建设历程是怎样的? +•答案:包含一期、二期、三期项目,2009年一期建成投产,2015年二期建成投产,2019年三期建成投产 +4.奉贤研发及生产基地(上海)的定位是什么? +•答案:遵循高端化、数字化、一体化、绿色生态的发展理念,打造胶粘剂、复合材料等新材料产业链;奉贤基地二期全面启用后,形成集科研开发、生产、服务、贸易为一体的科研产业综合实体 +5.奉贤研发及生产基地(上海)的胶粘剂产品产能是多少? +•答案:环氧结构胶、环氧基体树脂、丙烯酸胶、聚氨酯胶、丁基胶等胶粘剂产品产能80000吨 +6.奉贤研发及生产基地(上海)的复合材料产能是多少? +•答案:环氧基、聚氨酯基复合材料10000吨 +7.唐山丰南生产基地(河北)的位置在哪里? +•答案:河北省唐山市丰南经济开发区化工产业园区 +8.唐山丰南生产基地(河北)的占地面积是多少? +•答案:114亩 +9.唐山丰南生产基地(河北)的主要产品有哪些? +•答案:水性聚氨酯丙烯酸酯分散体、无溶剂复膜胶、改性型溶剂聚氨酯复膜胶、改性橡胶树脂溶剂胶、高性能丙烯酸酯结构胶、硅胶、环氧树脂胶 +10.唐山丰南生产基地(河北)辐射哪些市场? +•答案:北方市场 +11.唐山丰南生产基地(河北)的产品应用于哪些领域? +•答案:风电叶片制造、包装材料、轨道交通、汽车、电子电器、机械设备、建筑等领域(含在建基地) +12.福建邵武生产基地的位置在哪里? +•答案:福建省邵武市金塘省级化工园区 +13.福建邵武生产基地的占地面积是多少? +•答案:95亩 +14.福建邵武生产基地的主要产品有哪些? +•答案:功能型复膜胶、改性弹性树脂胶黏剂、高性能结构胶、功能性聚酯及反应型热熔胶、特种树脂中间体 +15.福建邵武生产基地辐射哪些市场? +•答案:南方市场 +16.福建邵武生产基地的产品应用于哪些领域? +•答案:包装材料、电子电器、机械设备、建筑装饰及工业维修等领域 +17.福建邵武生产基地的战略意义是什么? +•答案:为核心产品提供重要原材料(含在建基地) +18.大连齐化产业园区的位置在哪里? +•答案:辽宁省大连经济技术开发区 +19.大连齐化产业园区的主要产品系列有哪些? +•答案:双酚A型环氧树脂、耐热型环氧树脂和特种环氧树脂三大系列 +20.大连齐化产业园区的具体产品包含哪些? +•答案:双酚A型液体环氧树脂、双酚A型固体环氧树脂、溴化环氧树脂、邻甲酚醛环氧树脂、苯酚酚醛环氧树脂、双酚F型环氧树脂、苯氧基树脂、环氧固化剂、环氧稀释剂等多个品种 +21.大连齐化产业园区液态环氧树脂的装置数量、产能和实际产能分别是多少? +•答案:装置数量1套,产能3.4万吨,实际产能4.3万吨 +22.大连齐化产业园区固态环氧树脂的装置数量、产能和实际产能分别是多少? +•答案:装置数量1套,产能0.5万吨,实际产能1.5万吨 +23.大连齐化产业园区邻甲酚醛环氧树脂的装置数量和产能分别是多少? +•答案:装置数量1套,产能0.5万吨 +24.大连齐化产业园区特种树脂中试装置的数量和产能分别是多少? +•答案:装置数量1套,产能1000吨 +25.大连齐化产业园区的战略定位是什么? +•答案:重点布局下游产业链,打造环氧树脂产业闭环“生态圈”,以大连齐化的低溴环氧树脂技术为依托,开展覆铜板领域的战略布局 +26.成都康达电子科技产业园的位置在哪里? +•答案:四川省成都市高新区未来科技城 +27.成都康达电子科技产业园的占地面积是多少? +•答案:64亩 +28.成都康达电子科技产业园的定位是什么? +•答案:打造电子科技领域系统级产品研发、生产科技创新园区,使之成为四川乃至西南地区具有比较优势的电子信息特色产业园,推动地方战略性新兴产业的发展,助力公司电子科技核心业务做强做精 +29.康达新材的胶粘剂研发中心拥有哪些资质平台? +•答案:国家级的企业技术中心、上海胶粘剂工程技术研究中心、上海市企业技术中心、企业博士后科研工作总站、国家认定博士后科研工作站(人力资源和社会保障部、全国博士后管委会制发)、国家认可委认可的CNAS国家实验室、德国劳氏船级社(GL)中国认可检测中心 +30.康达新材胶粘剂研发中心的技术型研发占比多少?主要内容是什么? +•答案:占比25%,主要内容是引进项目的消化、吸收 +31.康达新材胶粘剂研发中心的产品型研发占比多少?主要内容是什么? +•答案:占比50%,主要内容是现有产品的再创新;中试生产及工艺优化 +32.康达新材胶粘剂研发中心的关键技术研发占比多少?主要内容是什么? +•答案:占比20%,主要内容是关键原材料及高端技术研发,为产品持续的市场竞争力提供支持 +33.康达新材胶粘剂研发中心的技术服务型研发占比多少?主要内容是什么? +•答案:占比5%,主要内容是与目前的板块服务相对应 +四、财务数据类 +1.康达新材2021年的营业收入是多少? +•答案:27.93亿元 +2.康达新材2021年归属于上市公司股东的净利润是多少? +•答案:-2.46亿元 +3.康达新材2023年的营业收入是多少? +•答案:8.77亿元 +4.康达新材2023年归属于上市公司股东的净利润是多少? +•答案:0.06亿元 +5.康达新材2024年的营业收入是多少? +•答案:31.01亿元 +6.康达新材2024年归属于上市公司股东的净利润是多少? +•答案:0.30亿元 +7.2022年康达新材的研发人员数量是多少? +•答案:376人 +8.2023年康达新材的研发人员数量是多少? +•答案:378人 +9.2024年康达新材的研发人员数量是多少? +•答案:486人 +10.2022-2024年康达新材的研发人员数量呈现什么趋势? +•答案:呈线性增长趋势 +11.2022年康达新材的研发费用是多少? +•答案:1.26亿元 +12.2023年康达新材的研发费用是多少? +•答案:1.78亿元 +13.2024年康达新材的研发费用是多少? +•答案:2.04亿元 +14.2022-2024年康达新材的研发费用呈现什么趋势? +•答案:呈线性增长趋势 +15.2024年康达新材的研发人员数量占比是多少? +•答案:22.97% +16.2024年康达新材的非研发人员占比是多少? +•答案:77.03% +17.2024年康达新材的研发支出占营业收入总额的比例是多少? +•答案:6.56% +18.2024年康达新材的非研发支出占比是多少? +•答案:93.44% +19.康达新材2021年的总资产是多少? +•答案:37.28亿元 +20.康达新材2022年的总资产是多少? +•答案:53.33亿元 +21.康达新材2023年的总资产是多少? +•答案:69.40亿元 +22.康达新材2024年的总资产是多少? +•答案:71.23亿元 +23.康达新材2021年的净资产是多少? +•答案:22.91亿元 +24.康达新材2022年的净资产是多少? +•答案:29.06亿元 +25.康达新材2023年的净资产是多少? +•答案:31.43亿元 +26.康达新材2024年的净资产是多少? +•答案:33.09亿元 +五、业务介绍类 +(一)胶粘剂与特种树脂新材料 +1.康达新材的胶粘剂与特种树脂新材料业务国内布局包含哪些企业? +•答案:河北惟新科技、北京康达晟珊、北京力源兴达、唐山南区康达化工新材有限公司、康达国际供应链(天津)有限公司、大连齐化新材、上海康达新材料科技有限公司、上海理日新材、上海晶材科技、福建南平天宇、成都必控科技、福建康达鑫宇新材料有限公司、成都赛英科技、深圳康达电子材料研发有限公司、上海康达新材(香港)有限公司 +2.康达新材的胶粘剂与特种树脂新材料业务海外研发办事处设在哪些国家? +•答案:德国、日本 +3.康达新材的胶粘剂与特种树脂新材料业务海外营销办事处设在哪个国家? +•答案:印尼 +4.康达新材的胶粘剂与特种树脂新材料业务海外生产基地设在何处? +•答案:泰国曼谷 +5.康达新材的胶粘剂与特种树脂新材料业务海外覆盖哪些区域? +•答案:德国、丹麦、西班牙、日本、东南亚、巴西等 +6.胶粘剂与特种树脂新材料业务的产品体系包含多少大系列? +•答案:8大产品系列 +7.胶粘剂与特种树脂新材料业务的产品体系包含多少种规格型号? +•答案:500多种 +8.胶粘剂与特种树脂新材料业务的核心应用领域有哪些? +•答案:风力发电、光伏太阳能、轨道交通、航空航天、海洋船舶工程、软包装复合、橡塑制品、建筑工程、电子电器、汽摩配件、电机电梯、矿业设备、工业维修、新能源汽车、工程维护、高端制造等 +9.康达新材在风电领域的核心产品有哪些? +•答案:风电胶粘剂系列产品(结构胶、灌注树脂、胶条、喷胶) +10.2024年康达新材风电领域的销售量是多少? +•答案:近9万吨 +11.2024年康达新材风电领域的销售额(不含税)是多少? +•答案:约16亿元 +12.康达新材风电领域的客户覆盖范围是什么? +•答案:国内所有叶片厂商及其基地,同时产品向海外市场推广 +13.康达新材风电领域的核心客户有哪些? +•答案:三一重能、中国中材、中国国电、艾郎科技、中科宇能、远景能源、金风科技、时代新材、上玻院、双瑞风电、上海电气、天顺风能、LM艾尔姆、明阳电气、MENS Gamesa等 +14.康达新材在风电领域的市场地位如何? +•答案:市场占有率第一,自主产品保持品牌技术领先、行业领导地位 +15.康达新材在复膜胶领域的市场份额是多少? +•答案:25% +16.复膜胶领域中,法国波士胶的市场份额是多少? +•答案:25% +17.复膜胶领域中,德国汉高的市场份额是多少? +•答案:20% +18.复膜胶领域中,其他30余家企业的市场份额总和是多少? +•答案:30% +19.康达新材在复膜胶领域的市场地位如何? +•答案:市场占有率第一 +20.2024年康达新材复膜胶领域的国内客户数量是多少? +•答案:700家 +21.2024年康达新材复膜胶领域的销售额是多少? +•答案:约3.2亿元 +22.康达新材复膜胶领域的销售额年增长率是多少? +•答案:10-15% +23.康达新材复膜胶领域的软包装复合胶包含哪些类型? +•答案:溶剂型聚氨酯复膜胶、无溶剂聚氨酯复膜胶 +24.康达新材复膜胶领域的硬包装胶包含哪些类型? +•答案:外包装热熔胶、标签快递单压敏胶等 +25.康达新材复膜胶领域的能量固化印刷油墨包含哪些类型? +•答案:UV胶印油墨、UV柔版光油等 +26.康达新材复膜胶领域的可降解材料包含哪些类型? +•答案:酪蛋白标签胶、环保餐具涂层 +27.康达新材复膜胶领域的发展规划有哪些? +•答案:传统替代、新品攻关(UV油墨、可降解材料)、专业化服务、节能环保、对接“一带一路” +28.康达新材在轨道交通领域的核心产品有哪些? +•答案:双组份丙烯酸酯结构胶、单组份结构胶、UV胶、PUR、双组份环氧胶、单组份环氧胶、瞬干胶等系列化产品(可根据客户需求个性化调整) +29.康达新材轨道交通领域重点布局哪些行业? +•答案:消费电子产品组装行业、微型扬声器行业、安防行业、动力电池行业 +30.康达新材轨道交通领域在消费电子行业的核心客户有哪些? +•答案:联想、华硕、罗技、韶音 +31.康达新材轨道交通领域的产品在消费电子行业应用于哪些产品组装? +•答案:智能手机、笔记本/平板电脑、穿戴设备、无人机、投影仪组装 +32.康达新材轨道交通领域在微型扬声器行业的核心客户有哪些? +•答案:瑞声、歌尔、豪声、上声 +33.康达新材轨道交通领域在安防行业的核心客户有哪些? +•答案:海康威视、大华 +34.康达新材轨道交通领域针对高铁/地铁的产品有哪些? +•答案:地板胶、阻尼浆、阻尼贴片 +35.康达新材轨道交通领域针对高铁/地铁的产品取得了哪些进展? +•答案:阻尼贴片材料通过中车系统检测验证,地板胶阻尼浆在中车系统重点客户试装成功 +36.康达新材通用工业领域的销售渠道以什么为主? +•答案:经销商销售渠道 +37.康达新材通用工业领域重点覆盖哪些市场? +•答案:通用工业(工业装配维修、扬声器、电梯、电机、结构胶等各类工业应用)、流通市场(民用及家装DIY、电商) +38.康达新材的汽车胶粘剂用于哪些场景? +•答案:汽车厢体、车门的结构粘接及汽车内饰件粘接 +39.康达新材的汽车胶粘剂能为客户提供什么优化方案? +•答案:汽车轻量化和减少VOCs排放的优化方案 +40.康达新材家电用胶粘剂的既定目标客户有哪些? +•答案:格力、美的等国内知名家电企业 +41.康达新材家电用胶粘剂目前已涉及美的哪些产品应用? +•答案:美的厨热、冰箱和家用空调等产品 +42.康达新材家电用胶粘剂未来的拓展计划是什么? +•答案:拓展至中央空调、洗衣机等其他生活电器 +(二)核心子公司业务详情(胶粘剂与特种树脂新材料板块) +1.大连齐化新材料有限公司的成立时间是什么时候? +•答案:起步于2001年 +2.大连齐化新材料有限公司的企业定位是什么? +•答案:以生产销售高品质环氧树脂为主,集新材料研制、开发、生产、销售、服务、转让为一体的综合性高新技术化工企业 +3.大连齐化新材料有限公司的产品体系包含哪些系列? +•答案:双酚A型环氧树脂、耐热型环氧树脂和特种环氧树脂三大系列 +4.大连齐化新材料有限公司的具体产品有哪些? +•答案:双酚A型液体环氧树脂、双酚A型固体环氧树脂、溴化环氧树脂、邻甲酚醛环氧树脂、苯酚酚醛环氧树脂、双酚F型环氧树脂、苯氧基树脂、环氧固化剂、环氧稀释剂等 +5.大连齐化新材料有限公司的产品质量指标有什么优势? +•答案:各项指标均优于中国国家标准GB/T 13657-2011《双酚A型环氧树脂》,通过RoSH和REACH认证,双酚A型环氧树脂被辽宁省工信厅认定为“专精特新”产品并获欧洲化学品管理局(ECHA)“REACH注册证书” +6.大连齐化新材料有限公司拥有多少项实用新型专利? +•答案:21项 +7.大连齐化新材料有限公司拥有多少项软件著作权? +•答案:20项 +8.大连齐化新材料有限公司正在申请多少项发明专利? +•答案:2项 +9.大连齐化新材料有限公司通过了哪些体系认证? +•答案:ISO9001、ISO14001、ISO45001三体系认证 +10.大连齐化新材料有限公司获得国家级“高新技术企业”认定的时间是什么时候? +•答案:2020年12月 +11.大连齐化新材料有限公司获得辽宁省科技厅“雏鹰企业”认定的时间是什么时候? +•答案:2020年12月 +12.大连齐化新材料有限公司获得国家级“专精特新”小巨人企业认定的时间是什么时候? +•答案:2021年7月 +13.大连齐化新材料有限公司的销售模式是什么? +•答案:直销模式 +14.南平天宇实业被康达新材收购的时间是什么时候? +•答案:2021年4月 +15.南平天宇实业的占地面积是多少? +•答案:33000㎡ +16.南平天宇实业的年产产值是多少? +•答案:近6000万元 +17.南平天宇实业的生产工艺技术水平如何? +•答案:国际领先水平 +18.南平天宇实业的产品定位是什么? +•答案:亚洲国家“水性商标胶粘剂”生产量最大、生产品种最多、工艺技术最领先的绿色环保水基性商标胶粘剂生产企业 +19.南平天宇实业的“远洋牌”水性系列商标胶有什么特点? +•答案:质量超越国际领先水平,性能稳定 +20.南平天宇实业的“远洋牌”封箱专用热熔胶有什么特点? +•答案:可塑性粘合剂,无毒、无味,属绿色环保型产品,物理状态随温度改变(化学特性不变) +21.南平天宇实业的“远洋牌”水性纸制品胶粘剂的原材料是什么? +•答案:优质高分子材料PVA、VAC、改性淀粉共聚合成 +22.南平天宇实业的“远洋牌”水性纸制品胶粘剂有什么特点? +•答案:无毒性、无公害、无腐蚀性及氧化性,符合欧美最新标准,为食品级纸制品行业不可替代的绿色环保产品 +23.南平天宇实业的核心客户有哪些? +•答案:百威啤酒、珠江啤酒、青岛啤酒、雪花啤酒、达利食品集团等 +24.上海理日新材料的技术地位如何? +•答案:成熟的研发创新能力,技术和产品保持国内领先、国际先进水平,国内市场品牌影响和占有率仅次于德国汉高等顶级跨国公司,产品已通过多家著名跨国公司认证并供货 +25.上海理日新材料的应用领域有哪些? +•答案:汽车部件、电子电缆、衣饰、制鞋、建材、装饰、包装以及低压注塑等行业 +26.上海理日新材料服务多少家客户? +•答案:数百家 +27.上海理日新材料的环保型高性能热熔胶包含哪些类型? +•答案:聚酯、聚酰胺热熔胶 +28.上海理日新材料的环保型高性能热熔胶有什么特点? +•答案:不含化学溶剂、无污染,粘接强度高,耐油耐化学性好,粘接材料广泛 +29.上海理日新材料的环保型高性能热熔胶应用于哪些企业? +•答案:汽车滤清器生产企业、制鞋服饰企业、电子电缆生产企业 +30.上海理日新材料的环保型低压注塑封装材料的目标市场是什么? +•答案:电缆电子行业中的电池板生产企业、线束插头/微型开关生产企业、连接器/传感器生产企业和PCB线路板生产企业 +31.四氢呋喃可作为上海理日新材料产品的哪些应用领域? +•答案:合成树脂的工业溶剂、合成革的表面处理剂、合成农药等领域 +(三)高性能复合材料方向 +1.康达新材在高性能复合材料方向的战略定位是什么? +•答案:聚焦玻璃纤维、碳纤维的环氧树脂、聚氨酯类复合材料领域,计划通过2-3年深化拓展;基于自身工艺和技术积累,通过投资业务契合度企业、技术创新、加大研发投入等方式,切入先进复合材料领域,推动向高分子新材料领域战略转型 +2.康达新材落实先进复合材料转型战略的关键举措是什么? +•答案:对聚发新材进行增资 +3.拉挤树脂(Urepul)的基础材料是什么? +•答案:玻纤&碳纤 +4.拉挤树脂(Urepul)的应用领域有哪些? +•答案:桥架、绝缘梯、桥梁、高压管道、景观工程、高铁、轨道、光伏 +5.SMC/预浸料(Urepreg)的基础材料是什么? +•答案:玻纤&碳纤 +6.SMC/预浸料(Urepreg)的应用领域有哪些? +•答案:汽车、轨道交通、电子产品 +7.RTM树脂(Urethan)的基础材料是什么? +•答案:玻纤&碳纤 +8.RTM树脂(Urethan)的应用领域有哪些? +•答案:风电叶片、汽车内外饰、高铁 +9.缠绕树脂(Urecom)的基础材料是什么? +•答案:玻纤&碳纤 +10.缠绕树脂(Urecom)的应用领域有哪些? +•答案:电线杆、传动轴 +11.截至2025年8月,康达新材高性能复合材料已实现量产应用的产品有哪些? +•答案:电缆桥架、高铁设备舱及裙板、新能源车电池盒骨架 +12.截至2025年8月,康达新材高性能复合材料技术开发完成的产品有哪些? +•答案:汽车型材、托盘、高铁步道板、绝缘臂腕 +13.截至2025年8月,康达新材高性能复合材料处于开发中的产品有哪些? +•答案:太阳能边框及支架、铁路鱼尾板 +14.康达新材的高性能复合材料在性能方面有什么优势? +•答案:出色的机械性能,与玻纤完美结合,更高的粘接强度,优异的抗疲劳性能 +15.康达新材的高性能复合材料在生产效率方面有什么优势? +•答案:更快的灌注速度,更快的固化速度 +16.康达新材的高性能复合材料在综合成本方面有什么优势? +•答案:模具投入少,具有竞争力的树脂价格 +(四)电子信息材料 +1.惟新科技被康达新材收购的时间是什么时候? +•答案:2023年 +2.惟新科技的企业定位是什么? +•答案:国家高新技术企业,致力于研发和生产高端ITO(氧化铟锡)靶材及ITO粉体、ITO气氛烧结炉等ITO周边产品,解决大尺寸ITO靶材“卡脖子”问题 +3.惟新科技的核心技术有哪些? +•答案:与清华大学化工系合作,研发微反应器制备高活性纳米ITO粉体技术;自主研发适合超大尺寸靶材制备的湿法注浆成型技术,拥有自主知识产权的超大空间高温烧结炉 +4.惟新科技成功制备的大尺寸ITO靶材规格是多少? +•答案:1550x250mm +5.惟新科技制备的大尺寸ITO靶材性能指标如何? +•答案:与国际同类靶材相当,达到国际先进水平,填补国内空白 +6.惟新科技的战略意义有哪些? +•答案:打破技术壁垒,减少高端靶材进口依赖,助力中美贸易战应对;推动我国铟资源高效利用;填补我国ITO靶材在中、高端平板显示器制造领域应用空白;促进ITO靶材平板生产工艺研发与人才培养 +7.惟新科技的ITO靶材试验线设备升级改造完成时间是什么时候? +•答案:2022年 +8.惟新科技目前的产能提升目标是什么? +•答案:推进30T/年产能提升工作 +9.惟新科技的溅射靶材下游覆盖哪些领域? +•答案:平板显示器、光伏电池、记录媒体、半导体等领域 +10.平板显示器领域在惟新科技溅射靶材下游应用中占比多少? +•答案:33.8% +11.记录媒体领域在惟新科技溅射靶材下游应用中占比多少? +•答案:28.6% +12.半导体领域在惟新科技溅射靶材下游应用中占比多少? +•答案:18.5% +13.晶材科技成立于哪一年? +•答案:2016年 +14.晶材科技被康达新材收购的时间是什么时候? +•答案:2018年 +15.晶材科技的企业定位是什么? +•答案:专注于高端电子陶瓷材料国产化,集研发与规模化生产于一体,提供先进材料解决方案,代理德国瓦克化学有机硅产品 +16.晶材科技的技术路线是什么? +•答案:微晶玻璃体系、微晶玻璃/陶瓷复合体系 +17.晶材科技在行业中的地位如何? +•答案:目前市场唯一批量供货的国产生料带供应商,解决“卡脖子”难题 +18.晶材科技的自研产品有哪些? +•答案:陶瓷生料带、贵金属浆料、瓷粉、导电胶 +19.晶材科技的自研产品应用于什么技术领域? +•答案:低温共烧陶瓷技术(LTCC)的电子元器件、电路模块制造和封装 +20.晶材科技的代理产品是什么? +•答案:有机硅水胶(车载显示器光学全贴合重要原材料) +21.晶材科技的产品在军用领域有什么应用? +•答案:有源相控阵雷达 +22.晶材科技的产品在民用领域有什么应用? +•答案:高端半导体传感器、光通信器件、汽车电子电路模块、5G路由器、5G机房、数据中心等 +23.晶材科技的生料带现有产能是多少? +•答案:240万片/年 +24.晶材科技的贵金属浆料现有产能是多少? +•答案:30吨/年 +(五)电子科技——赛英科技 +1.赛英科技被康达新材收购的时间是什么时候? +•答案:2018年 +2.赛英科技的地理位置在哪里? +•答案:成都龙潭新经济产业功能区 +3.赛英科技的科研生产基地规模是多少? +•答案:8000余平方米 +4.赛英科技的核心业务是什么? +•答案:科研、生产整机雷达、微波组件、专用仪器仪表、海洋通讯产品 +5.赛英科技在安全管控领域的产品有哪些? +•答案:近程警戒雷达(地面、海面)、周界安全警戒雷达(系列产品) +6.赛英科技在交通监测领域的产品有哪些? +•答案:FOD跑道异物监测雷达(塔架、移动) +7.赛英科技在工业监测领域的产品有哪些? +•答案:高炉料面监测雷达、微位移监测雷达(应用于大型钢厂、铁路大坝等重要区域) +8.赛英科技获得了哪些企业荣誉? +•答案:成都市企业技术中心、成都市成华区2020年度“工业纳税先进单位”、四川省“专精特新”中小企业、成都市科技装备业商会“副会长单位”等 +六、康达新材日常聊天类 +1.你能给康达新材创作一首诗吗? +•答案:匠心三十载耕耘,新材科技破卡颈胶黏万物兴百业,引领制造向前行 +2.如果用三个关键词概括康达新材的核心气质,你觉得会是什么?为什么? +•答案:硬核(深耕“卡脖子”技术)、创新(研发投入持续增长)、多元(覆盖多业务板块) +3.假设你要给朋友介绍康达新材,会用一段轻松有趣的话怎么说? +•答案:这家1988年成立的国企超牛!能做粘风电叶片的“超级胶水”,还搞定高端ITO靶材,业务横跨新材料和电子科技,妥妥的科技实力派~ +4.要是给康达新材设计一句接地气的宣传语,不局限于官方口号,你会怎么写? +•答案:康达新材,粘得牢、造得硬,让中国制造更靠谱! +5.想象康达新材要举办一场员工家庭日活动,你觉得可以设计哪些有趣的互动环节? +•答案:①亲子胶粘剂组装模型;②VR参观生产基地答题;③家庭新材料元素手作赛 +6.如果你是康达新材的一名新员工,入职第一天最想了解公司的哪三个方面? +•答案:①岗位与核心业务的关联;②研发创新支持政策;③培训与晋升路径 +7.用拟人化的手法描述康达新材,你会把它比作哪种形象?理由是什么? +•答案:比作“实干工匠”,30余年坚守行业,身怀多元技能,持续突破技术难题 +8.康达新材涉及多个业务板块,要是用一种美食比喻它的业务布局,你觉得像什么?为什么? +•答案:像“什锦火锅”,胶粘剂是锅底,各业务板块是食材,协同发力丰富产业生态 +9.要是给康达新材的研发人员写一句鼓励的话,你会怎么表达? +•答案:以匠心攻技术,以创新破壁垒,为中国智造添力! +10.假设康达新材要出一款周边产品送给客户,你觉得什么类型的产品既实用又能体现公司特色? +•答案:定制多功能粘接工具套装,含迷你环保胶粘剂、ITO靶材图案书签 +11.用一句话形容康达新材从1988年成立到现在的发展历程,你会怎么说? +•答案:从化工实验厂到“新材料+电子科技”集团,30余年坚守创新,领跑细分领域 +12.如果你要在社交平台分享康达新材的一则动态,配文会怎么写才能吸引关注? +•答案:国企硬核!粘风电叶片、破ITO靶材“卡脖子”,覆盖新能源、电子科技,为中国制造打call~#康达新材 #国货之光 +13.康达新材在多个地区有生产基地,要是选一个基地推荐给朋友参观,你会选哪个?理由是什么? +•答案:选上海奉贤基地,集研发生产于一体,能直观看到产品从研发到量产的全过程 +14.要是给康达新材设计一个吉祥物,你觉得它的外形和寓意应该是什么样的? +•答案:蓝色分子造型小精灵,带翅膀(象征创新),头顶ITO靶材,寓意科技赋能 +15.想象康达新材要拍摄一支企业宣传片,你觉得开篇画面应该是什么场景? +•答案:高空俯瞰全球生产基地,快速切换研发实验室、生产线、风电应用场景,定格企业口号 +16.如果你是康达新材的客服,接到客户咨询时,第一句暖心的开场白会怎么说? +•答案:您好!康达新材客服为您服务,有任何产品或合作问题,我都会全力解答~ +17.用三个 emoji 代表康达新材的业务板块,你会选哪三个?分别对应什么业务? +•答案:🧪(胶粘剂与特种树脂)、💻(电子科技)、📱(电子信息材料) +18.康达新材有不少“专精特新”子公司,要是给这些子公司办一场表彰会,颁奖词可以怎么写? +•答案:深耕细分领域,以硬科技破壁垒、填空白,为产业生态注入强劲动力,致敬每一份坚守! +19.假设你要给康达新材的员工设计一件文化衫,正面和背面会印什么图案或文字? +•答案:正面:康达新材logo+“以新材赋能”;背面:三大业务图标+“融合 协同 创新 超越” +20.用简单的语言给小朋友解释康达新材生产的胶粘剂是做什么用的,你会怎么说? +•答案:这是“超级胶水”,能把风电叶片、汽车零件粘得牢牢的,让它们更结实哦~ +21.要是康达新材要发起一个公益活动,结合它的业务特点,你觉得可以开展什么主题的活动? +•答案:“绿色新材助环保”,用环保胶粘剂、可降解材料为乡村学校修补课桌椅 +22.如果你参加康达新材的年会,最期待看到哪个环节的表演?为什么? +•答案:业务板块创意秀,能通过表演直观了解各部门工作,还能感受团队风采 +23.用对比的方式说说康达新材和你印象中的传统化工企业有什么不同? +•答案:传统化工多单一业务、低技术,康达聚焦新兴产业,研发投入高、业务多元且绿色环保 +24.康达新材的使命是“服务国家战略,引领行业发展”,要是你是员工,会怎么理解这句话并落实到工作中? +•答案:紧跟国家需求,研发时聚焦“卡脖子”技术,生产时严把质量关,助力产业升级 +25.假设你要给康达新材的官网设计一个新栏目,你会建议增加什么内容? +•答案:“新材科普”栏目,用动画、图文讲解产品原理和应用,开设互动问答 +26.用一句幽默的话调侃康达新材的“硬核”业务(比如胶粘剂、ITO靶材),你会怎么说? +•答案:康达业务太硬核,万物皆可粘、皆可造,就没有搞不定的技术难题? +27.要是康达新材要和一所高校开展合作,你觉得可以在哪些领域合作共赢? +•答案:①联合研发“卡脖子”技术;②开设定制班培养人才;③加速科研成果转化 +28.如果你是康达新材的hr,招聘时会用什么独特的理由吸引人才加入? +•答案:参与热门领域技术攻关,国企保障+清晰晋升,与行业顶尖人才共事 +29.用比喻的手法说说康达新材的研发团队像什么?为什么? +•答案:像“产业探路者”,探索前沿技术,开辟新路径,指引产业发展方向 +30.康达新材海外有布局,要是你负责海外业务推广,会先给海外客户推荐哪款产品? +•答案:风电胶粘剂,市场占有率第一,技术领先,契合全球新能源需求 +31.假设康达新材要举办一场客户答谢会,你觉得现场布置可以融入哪些业务元素? +•答案:①产品模型展示区;②分子结构装饰;③产品拼图、技术问答互动 +32.用一句话总结康达新材对国家新材料产业的意义,你会怎么说? +•答案:打破进口依赖,树立国产标杆,为新材料产业高质量发展提供支撑 +33.要是你是康达新材的一名司机,开车运送产品时,会怎么跟别人介绍车上的货物? +•答案:这是国产硬核科技产品,有风电专用胶粘剂、电子设备核心材料,助力多个行业发展~ +34.康达新材有很多生产基地,要是把这些基地比作一串珍珠,你觉得哪颗“珍珠”最亮眼? +•答案:成都电子科技产业园,聚焦战略新兴产业,是西南地区电子信息特色产业园 +35.用简单的手势或动作比划康达新材某款产品的用途,你会怎么比划? +•答案:比划胶粘剂:双手模拟“叶片”,合拢按压示意粘贴牢固 +36.假设康达新材要出一本企业内刊,你会给内刊起什么名字?设置哪些栏目? +•答案:名字《康达新声》,栏目:总裁寄语、技术前沿、业务动态、员工风采 +37.用一句话形容康达新材员工的工作状态,你会怎么描述? +•答案:勤学实干、协同创新,在各自岗位上全力冲刺目标 +38.要是康达新材要开展员工技能比赛,你觉得可以设置哪些比赛项目? +•答案:①技术攻关挑战赛;②生产精准操作大赛;③产品推广创意赛 +39.用对比的方式说说康达新材20年前和现在的变化,你会怎么总结? +•答案:20年前业务单一,如今成长为“新材料+电子科技”集团,技术、布局全面升级 +40.康达新材的核心价值观是“融合、协同、创新、超越”,要是你用生活中的例子解释“协同”,会选什么例子? +•答案:像家庭做饭,分工不同但目标一致,相互配合才能快速做出丰盛饭菜 +41.假设你要给康达新材的会议室起名字,结合业务特色,你会起什么名字? +•答案:粘合力厅、靶材阁、科创堂、协同馆 +42.用一句鼓励的话送给正在攻克“卡脖子”技术的康达新材研发团队,你会怎么说? +•答案:坚守初心攻难关,国产技术定发光,为你们加油! +43.要是康达新材要拍一支员工纪录片,你觉得应该采访哪些岗位的员工? +•答案:研发人员、生产线工人、销售人员、子公司负责人、行政后勤人员 +44.用比喻的手法说说康达新材的产品像什么?比如胶粘剂像“工业胶水”,再具体描述一下 +•答案:①胶粘剂像“工业纽带”,连接各类部件;②ITO靶材像“电子基石”,支撑电子设备运行 +45.康达新材涉及电子科技领域,要是你给长辈解释什么是“微波组件”,会怎么说? +•答案:这是电子设备的“信号中转站”,能让雷达、通讯设备精准收发信号,保障安全和监测 +46.假设康达新材要参加一场行业展会,你觉得展位设计要突出什么亮点才能吸引观众? +•答案:突出科技感(分子结构造型、LED大屏)和互动性(产品体验、VR参观) +47.用一句话说说你对康达新材未来5年发展的期待,你会怎么说? +•答案:期待成为全球领先的“新材料+电子科技”集团,让更多国产硬核科技走向世界 +48.要是你是康达新材的食堂阿姨,会怎么用食堂菜品比喻公司的业务多元化? +•答案:食堂菜像公司业务,有“核心硬菜”(胶粘剂)、“新鲜时蔬”(电子信息材料),搭配均衡、营养丰富~ +49.用简单的画画形式表现康达新材的业务,你会画哪些元素? +•答案:画大圆圈代表公司,内部画风电叶片、手机、芯片、分子结构,用箭头连接体现协同 +50.康达新材有不少荣誉资质,要是你把这些荣誉比作公司的“勋章”,最想给别人展示哪枚“勋章”?为什么? +•答案:国家级“专精特新”小巨人企业,体现公司技术实力和细分领域领先地位 \ No newline at end of file diff --git a/ruoyi-fastapi-backend/doc/qa_spec.md b/ruoyi-fastapi-backend/doc/qa_spec.md new file mode 100644 index 0000000..0565567 --- /dev/null +++ b/ruoyi-fastapi-backend/doc/qa_spec.md @@ -0,0 +1,13 @@ +### QA问答的修改原则 +1. 子问题(sub_questions)原则 : + + - 子问题的答案必须能在主答案中找到 + - 子问题不是对主问题的扩展或深入,而是从主答案中提取的、用户可能问的相关问题 + - 如果主答案内容简单(如只有年份、数字等),通常不需要子问题 + - 每个问题保留2-3个子问题(如果适用) + +2. 变体(variations)原则 : + + - 变体是主问题的不同表达方式 + - 只保留3个代表性的变体 + - 变体应覆盖常见的问法变化(如:去掉疑问词、简化表达、换种说法) \ No newline at end of file diff --git a/ruoyi-fastapi-backend/exceptions/__init__.py b/ruoyi-fastapi-backend/exceptions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ruoyi-fastapi-backend/exceptions/exception.py b/ruoyi-fastapi-backend/exceptions/exception.py new file mode 100644 index 0000000..b86f50d --- /dev/null +++ b/ruoyi-fastapi-backend/exceptions/exception.py @@ -0,0 +1,58 @@ +class LoginException(Exception): + """ + 自定义登录异常LoginException + """ + + def __init__(self, data: str = None, message: str = None): + self.data = data + self.message = message + + +class AuthException(Exception): + """ + 自定义令牌异常AuthException + """ + + def __init__(self, data: str = None, message: str = None): + self.data = data + self.message = message + + +class PermissionException(Exception): + """ + 自定义权限异常PermissionException + """ + + def __init__(self, data: str = None, message: str = None): + self.data = data + self.message = message + + +class ServiceException(Exception): + """ + 自定义服务异常ServiceException + """ + + def __init__(self, data: str = None, message: str = None): + self.data = data + self.message = message + + +class ServiceWarning(Exception): + """ + 自定义服务警告ServiceWarning + """ + + def __init__(self, data: str = None, message: str = None): + self.data = data + self.message = message + + +class ModelValidatorException(Exception): + """ + 自定义模型校验异常ModelValidatorException + """ + + def __init__(self, data: str = None, message: str = None): + self.data = data + self.message = message diff --git a/ruoyi-fastapi-backend/exceptions/handle.py b/ruoyi-fastapi-backend/exceptions/handle.py new file mode 100644 index 0000000..dec516a --- /dev/null +++ b/ruoyi-fastapi-backend/exceptions/handle.py @@ -0,0 +1,71 @@ +from fastapi import FastAPI, Request +from fastapi.exceptions import HTTPException +from pydantic_validation_decorator import FieldValidationError +from exceptions.exception import ( + AuthException, + LoginException, + ModelValidatorException, + PermissionException, + ServiceException, + ServiceWarning, +) +from utils.log_util import logger +from utils.response_util import jsonable_encoder, JSONResponse, ResponseUtil + + +def handle_exception(app: FastAPI): + """ + 全局异常处理 + """ + + # 自定义token检验异常 + @app.exception_handler(AuthException) + async def auth_exception_handler(request: Request, exc: AuthException): + return ResponseUtil.unauthorized(data=exc.data, msg=exc.message) + + # 自定义登录检验异常 + @app.exception_handler(LoginException) + async def login_exception_handler(request: Request, exc: LoginException): + return ResponseUtil.failure(data=exc.data, msg=exc.message) + + # 自定义模型检验异常 + @app.exception_handler(ModelValidatorException) + async def model_validator_exception_handler(request: Request, exc: ModelValidatorException): + logger.warning(exc.message) + return ResponseUtil.failure(data=exc.data, msg=exc.message) + + # 自定义字段检验异常 + @app.exception_handler(FieldValidationError) + async def field_validation_error_handler(request: Request, exc: FieldValidationError): + logger.warning(exc.message) + return ResponseUtil.failure(msg=exc.message) + + # 自定义权限检验异常 + @app.exception_handler(PermissionException) + async def permission_exception_handler(request: Request, exc: PermissionException): + return ResponseUtil.forbidden(data=exc.data, msg=exc.message) + + # 自定义服务异常 + @app.exception_handler(ServiceException) + async def service_exception_handler(request: Request, exc: ServiceException): + logger.error(exc.message) + return ResponseUtil.error(data=exc.data, msg=exc.message) + + # 自定义服务警告 + @app.exception_handler(ServiceWarning) + async def service_warning_handler(request: Request, exc: ServiceWarning): + logger.warning(exc.message) + return ResponseUtil.failure(data=exc.data, msg=exc.message) + + # 处理其他http请求异常 + @app.exception_handler(HTTPException) + async def http_exception_handler(request: Request, exc: HTTPException): + return JSONResponse( + content=jsonable_encoder({'code': exc.status_code, 'msg': exc.detail}), status_code=exc.status_code + ) + + # 处理其他异常 + @app.exception_handler(Exception) + async def exception_handler(request: Request, exc: Exception): + logger.exception(exc) + return ResponseUtil.error(msg=str(exc)) diff --git a/ruoyi-fastapi-backend/md/2.0康达新材Q&A清单 251228.md b/ruoyi-fastapi-backend/md/2.0康达新材Q&A清单 251228.md new file mode 100644 index 0000000..40a08e2 --- /dev/null +++ b/ruoyi-fastapi-backend/md/2.0康达新材Q&A清单 251228.md @@ -0,0 +1,1175 @@ +**康达新材Q&A清单** + +**一、公司概况类** + +1. 康达新材料(集团)股份有限公司成立于哪一年? + +* 答案:1988年 + +1. 康达新材料(集团)股份有限公司的上市时间是什么时候? + +* 答案:2012年4月 + +1. 康达新材料(集团)股份有限公司的股票代码是什么? + +* 答案:002669.SZ + +1. 康达新材的实际控制人是谁? + +* 答案:唐山市人民政府国有资产监督管理委员会 + +1. 康达新材的股权背景链条是怎样的? + +* 答案:唐山市人民政府国有资产监督管理委员会→唐山控股发展集团股份有限公司→唐山工业控股集团有限公司→康达新材料(集团)股份有限公司 + +1. 康达新材的三大业务板块是什么? + +* 答案:胶粘剂与特种树脂新材料、电子信息材料和电子科技 + +1. 康达新材的业务范围覆盖哪些新兴产业? + +* 答案:装备制造、新能源、轨道交通、航空航天、电子信息及低碳环保等新兴产业 + +1. 截至2024年底,康达新材的总资产是多少? + +* 答案:69.40亿元 + +1. 康达新材2024年的营业收入是多少? + +* 答案:31亿元 + +1. 截至2024年底,康达新材的员工总人数有多少? + +* 答案:1637人 + +1. 康达新材的管理总部位于哪里? + +* 答案:浦东新区张江科学城 + +1. 康达新材在国内哪些地区设有子公司、生产基地或分支机构? + +* 答案:上海、北京、天津、深圳、大连、成都、香港、福建、唐山等地 + +1. 康达新材的海外布局有多少处? + +* 答案:3处 + +1. 康达新材有多少个生产基地? + +* 答案:7个 + +1. 康达新材的胶粘剂产品牌号数有多少? + +* 答案:500+ + +1. 康达新材在多个细分领域的市场地位如何? + +* 答案:多项细分领域市场地位第一 + +1. 康达新材的使命是什么? + +* 答案:服务国家战略,引领行业发展 + +1. 康达新材的愿景是什么? + +* 答案:成为具有全球化视野的“新材料+电子科技”集团化企业 + +1. 康达新材的核心价值观是什么? + +* 答案:融合、协同、创新、超越 + +1. 康达新材的企业作风是什么? + +* 答案:不断学习,立即行动,没有任何借口 + +1. 康达新材的发展战略核心是什么? + +* 答案:坚守“稳中求进”工作总基调,以科技创新为根本动力,以转型升级、抢占新赛道为基本路径,以高质量发展为主题,立足主业,打造“新材料+电子科技”国家级“专精特新”小巨人企业集群 + +1. 康达新材“1+2+3+1+N”战略布局中的“1个产业研究院”具体定位是什么? + +* 答案:产业发展智库,科技成果研发、转化合作平台,技术信息共享中心 + +1. 康达新材“1+2+3+1+N”战略布局中的“2个战略新兴产业方向”是什么? + +* 答案:新材料+电子科技 + +1. 康达新材“1+2+3+1+N”战略布局中的“3大业务板块”具体指什么? + +* 答案:胶粘剂与特种树脂新材料+电子信息材料+电子科技 + +1. 康达新材“1+2+3+1+N”战略布局中的“1+N个产业园(基地)”已在哪些地区建立? + +* 答案:上海、北京、成都、天津、大连、深圳、唐山、福建邵武、香港、泰国等地 + +1. 康达新材在新材料领域的进口替代目标涉及哪些领域? + +* 答案:半导体材料、显示材料、生物医用材料、新能源材料、高性能纤维、高性能膜材料、先进高分子材料及其他领域 + +1. 康达新材在半导体材料领域的进口依赖材料有哪些? + +* 答案:大尺寸硅材料、大尺寸碳化硅单晶、SOI、高饱和度光刻胶、高性能靶材、电子特种气体、湿电子化学品、氮化镓单晶/氮化镓单晶衬底、化学机械抛光(CMP)材料、封装基板、高密度陶瓷材料等 + +1. 康达新材在显示材料领域的进口依赖材料有哪些? + +* 答案:OLED发光材料、超薄玻璃、高世代线玻璃基板、精细金属掩模板(FMM)、光学膜、柔性PI膜、偏光片、高性能水汽阻隔膜、异方性导电胶膜(ACF)、特种光学聚氨酯膜(PET)、OCA光学胶、微球、抗指纹涂层(AFC)涂料等 + +1. 康达新材在生物医用材料领域的进口依赖材料有哪些? + +* 答案:医用级钛粉与镍钛合金粉、苯乙烯类热塑性弹性体、医用级聚乳酸、碲锌镉晶体、人工晶状体等 + +1. 康达新材在新能源材料领域的进口依赖材料有哪些? + +* 答案:硅碳负极材料、电解铜箔、电解液添加剂、铝塑膜、质子交换膜、氢燃料电池催化剂、气体扩散层材料等 + +1. 康达新材在高性能纤维领域的进口依赖材料有哪些? + +* 答案:高性能碳纤维及其复合材料、高性能对位芳纶纤维及其复合材料、超高分子量聚乙烯纤维等 + +1. 康达新材在高性能膜材料领域的进口依赖材料有哪些? + +* 答案:海水淡化反渗透膜、陶瓷膜、离子交换膜、中空纤维膜、高导热石墨膜等 + +1. 康达新材在先进高分子材料领域的进口依赖材料有哪些? + +* 答案:聚苯硫醚(PPS)、聚砜(PSF)、聚醚醚酮(PEEK)、聚偏氯乙烯(PVDF)、聚甲醛(POM)、有机硅等 + +1. 康达新材在“其他”领域的进口依赖材料有哪些? + +* 答案:高频覆铜板基材、液晶高分子聚合物(LCP)等 + +1. 康达新材及下属子公司中获得国家级“专精特新”小巨人企业认定的有哪些? + +* 答案:大连齐化新材料有限公司(有效期:2022年7月1日至2025年6月30日)、成都必控科技有限责任公司、上海晶材新材料科技有限公司、康达新材料(集团)股份有限公司 + +1. 北京力源兴达科技有限公司获得了什么级别的“专精特新”相关认定? + +* 答案:北京市专精特新“小巨人”及中小企业 + +1. 成都赛英科技有限公司获得了什么级别的“专精特新”相关认定?有效期是多久? + +* 答案:四川省专精特新中小企业,有效期为2022年11月1日-2025年10月31日 + +1. 上海理日新材料科技有限公司获得了什么级别的“专精特新”相关认定? + +* 答案:上海市专精特新中小企业 + +1. 大连齐化获得了哪些资质认证? + +* 答案:2020年12月被科技部火炬中心认定为国家级“高新技术企业”,被辽宁省科技厅认定为“雏鹰企业”;2023年5月,质检中心被中国石油和化学工业联合会认定为“A级”石油和化工企业质量检验机构;通过ISO9001、ISO14001、ISO45001三体系认证 + +1. 河北惟新科技有限公司获得了什么资质?认定编号和有效期是多少? + +* 答案:河北省科技型中小企业,认定编号:K23016712,有效期3年 + +**二、历史发展类** + +1. 康达新材在1988年发生了什么关键事件? + +* 答案:成立上海康达化工实验厂 + +1. 康达新材在2000年发生了什么关键事件? + +* 答案:成立上海康达化工技术研究所 + +1. 康达新材在2002年完成了哪两项重要事件? + +* 答案:引入战略投资者,完成股份制改革,整体变更设立为上海康达化工新材料股份有限公司;获得中国合格评定国家认可委员会(CNAS)认可的实验室资质 + +1. 康达新材在2008年发生了什么关键事件? + +* 答案:成立上海康达化工有限责任公司;被认定为国家高新技术企业 + +1. 康达新材在2012年发生了什么关键事件? + +* 答案:成功在深圳交易所上市 + +1. 康达新材在2016年发生了什么关键事件? + +* 答案:非公开发行股票融资8.5亿元,助推康达新材高质量发展 + +1. 2018年康达新材收购了成都赛英科技有限公司,此次收购的目的是什么? + +* 答案:完善电子科技领域产业链布局 + +1. 2018年康达新材收购了上海晶材新材料科技有限公司,此次收购的目的是什么? + +* 答案:布局先进陶瓷新材料领域,打造“专精特新”小巨人企业集群 + +1. 2018年康达新材收购了大连齐化新材料有限公司,此次收购的目的是什么? + +* 答案:向产业链上游战略延伸,打造环氧树脂产业闭环“生态圈” + +1. 2019年康达新材收购了成都必控科技有限责任公司,此次收购的意义是什么? + +* 答案:成功迈入电子科技领域 + +1. 康达新材的研发中心被认定为国家企业技术中心是在哪一年? + +* 答案:2019年 + +1. 康达新材的控股股东变更为唐山工业控股集团有限公司是在哪一年? + +* 答案:2019年 + +1. 康达新材更名为“康达新材料(集团)股份有限公司”是在什么时间? + +* 答案:2021年 + +1. 康达新材在2021年进行集团化战略布局的范围是什么? + +* 答案:全国区域 + +1. 康达新材被认定为国家级“专精特新”小巨人企业是在哪一年? + +* 答案:2021年 + +1. 2022年康达新材收购了西安彩晶光电科技股份有限公司,此次收购进入了哪些领域? + +* 答案:电子信息材料、新能源材料、医药领域,打造新材料“第二增长极” + +1. 康达新材收购河北惟新科技有限公司是在哪一年? + +* 答案:2023年 + +1. 康达新材收购河北惟新科技有限公司解决了什么“卡脖子”问题? + +* 答案:大尺寸ITO靶材“卡脖子”问题 + +**三、生产基地与研发中心类** + +1. 奉贤研发及生产基地(上海)的位置在哪里? + +* 答案:上海奉贤杭州湾经济技术开发区 + +1. 奉贤研发及生产基地(上海)的占地面积是多少? + +* 答案:205亩 + +1. 奉贤研发及生产基地(上海)的建设历程是怎样的? + +* 答案:包含一期、二期、三期项目,2009年一期建成投产,2015年二期建成投产,2019年三期建成投产 + +1. 奉贤研发及生产基地(上海)的定位是什么? + +* 答案:遵循高端化、数字化、一体化、绿色生态的发展理念,打造胶粘剂、复合材料等新材料产业链;奉贤基地二期全面启用后,形成集科研开发、生产、服务、贸易为一体的科研产业综合实体 + +1. 奉贤研发及生产基地(上海)的胶粘剂产品产能是多少? + +* 答案:环氧结构胶、环氧基体树脂、丙烯酸胶、聚氨酯胶、丁基胶等胶粘剂产品产能80000吨 + +1. 奉贤研发及生产基地(上海)的复合材料产能是多少? + +* 答案:环氧基、聚氨酯基复合材料10000吨 + +1. 唐山丰南生产基地(河北)的位置在哪里? + +* 答案:河北省唐山市丰南经济开发区化工产业园区 + +1. 唐山丰南生产基地(河北)的占地面积是多少? + +* 答案:114亩 + +1. 唐山丰南生产基地(河北)的主要产品有哪些? + +* 答案:水性聚氨酯丙烯酸酯分散体、无溶剂复膜胶、改性型溶剂聚氨酯复膜胶、改性橡胶树脂溶剂胶、高性能丙烯酸酯结构胶、硅胶、环氧树脂胶 + +1. 唐山丰南生产基地(河北)辐射哪些市场? + +* 答案:北方市场 + +1. 唐山丰南生产基地(河北)的产品应用于哪些领域? + +* 答案:风电叶片制造、包装材料、轨道交通、汽车、电子电器、机械设备、建筑等领域(含在建基地) + +1. 福建邵武生产基地的位置在哪里? + +* 答案:福建省邵武市金塘省级化工园区 + +1. 福建邵武生产基地的占地面积是多少? + +* 答案:95亩 + +1. 福建邵武生产基地的主要产品有哪些? + +* 答案:功能型复膜胶、改性弹性树脂胶黏剂、高性能结构胶、功能性聚酯及反应型热熔胶、特种树脂中间体 + +1. 福建邵武生产基地辐射哪些市场? + +* 答案:南方市场 + +1. 福建邵武生产基地的产品应用于哪些领域? + +* 答案:包装材料、电子电器、机械设备、建筑装饰及工业维修等领域 + +1. 福建邵武生产基地的战略意义是什么? + +* 答案:为核心产品提供重要原材料(含在建基地) + +1. 大连齐化产业园区的位置在哪里? + +* 答案:辽宁省大连经济技术开发区 + +1. 大连齐化产业园区的主要产品系列有哪些? + +* 答案:双酚A型环氧树脂、耐热型环氧树脂和特种环氧树脂三大系列 + +1. 大连齐化产业园区的具体产品包含哪些? + +* 答案:双酚A型液体环氧树脂、双酚A型固体环氧树脂、溴化环氧树脂、邻甲酚醛环氧树脂、苯酚酚醛环氧树脂、双酚F型环氧树脂、苯氧基树脂、环氧固化剂、环氧稀释剂等多个品种 + +1. 大连齐化产业园区液态环氧树脂的装置数量、产能和实际产能分别是多少? + +* 答案:装置数量1套,产能3.4万吨,实际产能4.3万吨 + +1. 大连齐化产业园区固态环氧树脂的装置数量、产能和实际产能分别是多少? + +* 答案:装置数量1套,产能0.5万吨,实际产能1.5万吨 + +1. 大连齐化产业园区邻甲酚醛环氧树脂的装置数量和产能分别是多少? + +* 答案:装置数量1套,产能0.5万吨 + +1. 大连齐化产业园区特种树脂中试装置的数量和产能分别是多少? + +* 答案:装置数量1套,产能1000吨 + +1. 大连齐化产业园区的战略定位是什么? + +* 答案:重点布局下游产业链,打造环氧树脂产业闭环“生态圈”,以大连齐化的低溴环氧树脂技术为依托,开展覆铜板领域的战略布局 + +1. 成都康达电子科技产业园的位置在哪里? + +* 答案:四川省成都市高新区未来科技城 + +1. 成都康达电子科技产业园的占地面积是多少? + +* 答案:64亩 + +1. 成都康达电子科技产业园的定位是什么? + +* 答案:打造电子科技领域系统级产品研发、生产科技创新园区,使之成为四川乃至西南地区具有比较优势的电子信息特色产业园,推动地方战略性新兴产业的发展,助力公司电子科技核心业务做强做精 + +1. 康达新材的胶粘剂研发中心拥有哪些资质平台? + +* 答案:国家级的企业技术中心、上海胶粘剂工程技术研究中心、上海市企业技术中心、企业博士后科研工作总站、国家认定博士后科研工作站(人力资源和社会保障部、全国博士后管委会制发)、国家认可委认可的CNAS国家实验室、德国劳氏船级社(GL)中国认可检测中心 + +1. 康达新材胶粘剂研发中心的技术型研发占比多少?主要内容是什么? + +* 答案:占比25%,主要内容是引进项目的消化、吸收 + +1. 康达新材胶粘剂研发中心的产品型研发占比多少?主要内容是什么? + +* 答案:占比50%,主要内容是现有产品的再创新;中试生产及工艺优化 + +1. 康达新材胶粘剂研发中心的关键技术研发占比多少?主要内容是什么? + +* 答案:占比20%,主要内容是关键原材料及高端技术研发,为产品持续的市场竞争力提供支持 + +1. 康达新材胶粘剂研发中心的技术服务型研发占比多少?主要内容是什么? + +* 答案:占比5%,主要内容是与目前的板块服务相对应 + +**四、财务数据类** + +1. 康达新材2021年的营业收入是多少? + +* 答案:27.93亿元 + +1. 康达新材2021年归属于上市公司股东的净利润是多少? + +* 答案:-2.46亿元 + +1. 康达新材2023年的营业收入是多少? + +* 答案:8.77亿元 + +1. 康达新材2023年归属于上市公司股东的净利润是多少? + +* 答案:0.06亿元 + +1. 康达新材2024年的营业收入是多少? + +* 答案:31.01亿元 + +1. 康达新材2024年归属于上市公司股东的净利润是多少? + +* 答案:0.30亿元 + +1. 2022年康达新材的研发人员数量是多少? + +* 答案:376人 + +1. 2023年康达新材的研发人员数量是多少? + +* 答案:378人 + +1. 2024年康达新材的研发人员数量是多少? + +* 答案:486人 + +1. 2022-2024年康达新材的研发人员数量呈现什么趋势? + +* 答案:呈线性增长趋势 + +1. 2022年康达新材的研发费用是多少? + +* 答案:1.26亿元 + +1. 2023年康达新材的研发费用是多少? + +* 答案:1.78亿元 + +1. 2024年康达新材的研发费用是多少? + +* 答案:2.04亿元 + +1. 2022-2024年康达新材的研发费用呈现什么趋势? + +* 答案:呈线性增长趋势 + +1. 2024年康达新材的研发人员数量占比是多少? + +* 答案:22.97% + +1. 2024年康达新材的非研发人员占比是多少? + +* 答案:77.03% + +1. 2024年康达新材的研发支出占营业收入总额的比例是多少? + +* 答案:6.56% + +1. 2024年康达新材的非研发支出占比是多少? + +* 答案:93.44% + +1. 康达新材2021年的总资产是多少? + +* 答案:37.28亿元 + +1. 康达新材2022年的总资产是多少? + +* 答案:53.33亿元 + +1. 康达新材2023年的总资产是多少? + +* 答案:69.40亿元 + +1. 康达新材2024年的总资产是多少? + +* 答案:71.23亿元 + +1. 康达新材2021年的净资产是多少? + +* 答案:22.91亿元 + +1. 康达新材2022年的净资产是多少? + +* 答案:29.06亿元 + +1. 康达新材2023年的净资产是多少? + +* 答案:31.43亿元 + +1. 康达新材2024年的净资产是多少? + +* 答案:33.09亿元 + +**五、业务介绍类** + +**(一)胶粘剂与特种树脂新材料** + +1. 康达新材的胶粘剂与特种树脂新材料业务国内布局包含哪些企业? + +* 答案:河北惟新科技、北京康达晟珊、北京力源兴达、唐山南区康达化工新材有限公司、康达国际供应链(天津)有限公司、大连齐化新材、上海康达新材料科技有限公司、上海理日新材、上海晶材科技、福建南平天宇、成都必控科技、福建康达鑫宇新材料有限公司、成都赛英科技、深圳康达电子材料研发有限公司、上海康达新材(香港)有限公司 + +1. 康达新材的胶粘剂与特种树脂新材料业务海外研发办事处设在哪些国家? + +* 答案:德国、日本 + +1. 康达新材的胶粘剂与特种树脂新材料业务海外营销办事处设在哪个国家? + +* 答案:印尼 + +1. 康达新材的胶粘剂与特种树脂新材料业务海外生产基地设在何处? + +* 答案:泰国曼谷 + +1. 康达新材的胶粘剂与特种树脂新材料业务海外覆盖哪些区域? + +* 答案:德国、丹麦、西班牙、日本、东南亚、巴西等 + +1. 胶粘剂与特种树脂新材料业务的产品体系包含多少大系列? + +* 答案:8大产品系列 + +1. 胶粘剂与特种树脂新材料业务的产品体系包含多少种规格型号? + +* 答案:500多种 + +1. 胶粘剂与特种树脂新材料业务的核心应用领域有哪些? + +* 答案:风力发电、光伏太阳能、轨道交通、航空航天、海洋船舶工程、软包装复合、橡塑制品、建筑工程、电子电器、汽摩配件、电机电梯、矿业设备、工业维修、新能源汽车、工程维护、高端制造等 + +1. 康达新材在风电领域的核心产品有哪些? + +* 答案:风电胶粘剂系列产品(结构胶、灌注树脂、胶条、喷胶) + +1. 2024年康达新材风电领域的销售量是多少? + +* 答案:近9万吨 + +1. 2024年康达新材风电领域的销售额(不含税)是多少? + +* 答案:约16亿元 + +1. 康达新材风电领域的客户覆盖范围是什么? + +* 答案:国内所有叶片厂商及其基地,同时产品向海外市场推广 + +1. 康达新材风电领域的核心客户有哪些? + +* 答案:三一重能、中国中材、中国国电、艾郎科技、中科宇能、远景能源、金风科技、时代新材、上玻院、双瑞风电、上海电气、天顺风能、LM艾尔姆、明阳电气、MENS Gamesa等 + +1. 康达新材在风电领域的市场地位如何? + +* 答案:市场占有率第一,自主产品保持品牌技术领先、行业领导地位 + +1. 康达新材在复膜胶领域的市场份额是多少? + +* 答案:25% + +1. 复膜胶领域中,法国波士胶的市场份额是多少? + +* 答案:25% + +1. 复膜胶领域中,德国汉高的市场份额是多少? + +* 答案:20% + +1. 复膜胶领域中,其他30余家企业的市场份额总和是多少? + +* 答案:30% + +1. 康达新材在复膜胶领域的市场地位如何? + +* 答案:市场占有率第一 + +1. 2024年康达新材复膜胶领域的国内客户数量是多少? + +* 答案:700家 + +1. 2024年康达新材复膜胶领域的销售额是多少? + +* 答案:约3.2亿元 + +1. 康达新材复膜胶领域的销售额年增长率是多少? + +* 答案:10-15% + +1. 康达新材复膜胶领域的软包装复合胶包含哪些类型? + +* 答案:溶剂型聚氨酯复膜胶、无溶剂聚氨酯复膜胶 + +1. 康达新材复膜胶领域的硬包装胶包含哪些类型? + +* 答案:外包装热熔胶、标签快递单压敏胶等 + +1. 康达新材复膜胶领域的能量固化印刷油墨包含哪些类型? + +* 答案:UV胶印油墨、UV柔版光油等 + +1. 康达新材复膜胶领域的可降解材料包含哪些类型? + +* 答案:酪蛋白标签胶、环保餐具涂层 + +1. 康达新材复膜胶领域的发展规划有哪些? + +* 答案:传统替代、新品攻关(UV油墨、可降解材料)、专业化服务、节能环保、对接“一带一路” + +1. 康达新材在轨道交通领域的核心产品有哪些? + +* 答案:双组份丙烯酸酯结构胶、单组份结构胶、UV胶、PUR、双组份环氧胶、单组份环氧胶、瞬干胶等系列化产品(可根据客户需求个性化调整) + +1. 康达新材轨道交通领域重点布局哪些行业? + +* 答案:消费电子产品组装行业、微型扬声器行业、安防行业、动力电池行业 + +1. 康达新材轨道交通领域在消费电子行业的核心客户有哪些? + +* 答案:联想、华硕、罗技、韶音 + +1. 康达新材轨道交通领域的产品在消费电子行业应用于哪些产品组装? + +* 答案:智能手机、笔记本/平板电脑、穿戴设备、无人机、投影仪组装 + +1. 康达新材轨道交通领域在微型扬声器行业的核心客户有哪些? + +* 答案:瑞声、歌尔、豪声、上声 + +1. 康达新材轨道交通领域在安防行业的核心客户有哪些? + +* 答案:海康威视、大华 + +1. 康达新材轨道交通领域针对高铁/地铁的产品有哪些? + +* 答案:地板胶、阻尼浆、阻尼贴片 + +1. 康达新材轨道交通领域针对高铁/地铁的产品取得了哪些进展? + +* 答案:阻尼贴片材料通过中车系统检测验证,地板胶阻尼浆在中车系统重点客户试装成功 + +1. 康达新材通用工业领域的销售渠道以什么为主? + +* 答案:经销商销售渠道 + +1. 康达新材通用工业领域重点覆盖哪些市场? + +* 答案:通用工业(工业装配维修、扬声器、电梯、电机、结构胶等各类工业应用)、流通市场(民用及家装DIY、电商) + +1. 康达新材的汽车胶粘剂用于哪些场景? + +* 答案:汽车厢体、车门的结构粘接及汽车内饰件粘接 + +1. 康达新材的汽车胶粘剂能为客户提供什么优化方案? + +* 答案:汽车轻量化和减少VOCs排放的优化方案 + +1. 康达新材家电用胶粘剂的既定目标客户有哪些? + +* 答案:格力、美的等国内知名家电企业 + +1. 康达新材家电用胶粘剂目前已涉及美的哪些产品应用? + +* 答案:美的厨热、冰箱和家用空调等产品 + +1. 康达新材家电用胶粘剂未来的拓展计划是什么? + +* 答案:拓展至中央空调、洗衣机等其他生活电器 + +**(二)核心子公司业务详情(胶粘剂与特种树脂新材料板块)** + +1. 大连齐化新材料有限公司的成立时间是什么时候? + +* 答案:起步于2001年 + +1. 大连齐化新材料有限公司的企业定位是什么? + +* 答案:以生产销售高品质环氧树脂为主,集新材料研制、开发、生产、销售、服务、转让为一体的综合性高新技术化工企业 + +1. 大连齐化新材料有限公司的产品体系包含哪些系列? + +* 答案:双酚A型环氧树脂、耐热型环氧树脂和特种环氧树脂三大系列 + +1. 大连齐化新材料有限公司的具体产品有哪些? + +* 答案:双酚A型液体环氧树脂、双酚A型固体环氧树脂、溴化环氧树脂、邻甲酚醛环氧树脂、苯酚酚醛环氧树脂、双酚F型环氧树脂、苯氧基树脂、环氧固化剂、环氧稀释剂等 + +1. 大连齐化新材料有限公司的产品质量指标有什么优势? + +* 答案:各项指标均优于中国国家标准GB/T 13657-2011《双酚A型环氧树脂》,通过RoSH和REACH认证,双酚A型环氧树脂被辽宁省工信厅认定为“专精特新”产品并获欧洲化学品管理局(ECHA)“REACH注册证书” + +1. 大连齐化新材料有限公司拥有多少项实用新型专利? + +* 答案:21项 + +1. 大连齐化新材料有限公司拥有多少项软件著作权? + +* 答案:20项 + +1. 大连齐化新材料有限公司正在申请多少项发明专利? + +* 答案:2项 + +1. 大连齐化新材料有限公司通过了哪些体系认证? + +* 答案:ISO9001、ISO14001、ISO45001三体系认证 + +1. 大连齐化新材料有限公司获得国家级“高新技术企业”认定的时间是什么时候? + +* 答案:2020年12月 + +1. 大连齐化新材料有限公司获得辽宁省科技厅“雏鹰企业”认定的时间是什么时候? + +* 答案:2020年12月 + +1. 大连齐化新材料有限公司获得国家级“专精特新”小巨人企业认定的时间是什么时候? + +* 答案:2021年7月 + +1. 大连齐化新材料有限公司的销售模式是什么? + +* 答案:直销模式 + +1. 南平天宇实业被康达新材收购的时间是什么时候? + +* 答案:2021年4月 + +1. 南平天宇实业的占地面积是多少? + +* 答案:33000㎡ + +1. 南平天宇实业的年产产值是多少? + +* 答案:近6000万元 + +1. 南平天宇实业的生产工艺技术水平如何? + +* 答案:国际领先水平 + +1. 南平天宇实业的产品定位是什么? + +* 答案:亚洲国家“水性商标胶粘剂”生产量最大、生产品种最多、工艺技术最领先的绿色环保水基性商标胶粘剂生产企业 + +1. 南平天宇实业的“远洋牌”水性系列商标胶有什么特点? + +* 答案:质量超越国际领先水平,性能稳定 + +1. 南平天宇实业的“远洋牌”封箱专用热熔胶有什么特点? + +* 答案:可塑性粘合剂,无毒、无味,属绿色环保型产品,物理状态随温度改变(化学特性不变) + +1. 南平天宇实业的“远洋牌”水性纸制品胶粘剂的原材料是什么? + +* 答案:优质高分子材料PVA、VAC、改性淀粉共聚合成 + +1. 南平天宇实业的“远洋牌”水性纸制品胶粘剂有什么特点? + +* 答案:无毒性、无公害、无腐蚀性及氧化性,符合欧美最新标准,为食品级纸制品行业不可替代的绿色环保产品 + +1. 南平天宇实业的核心客户有哪些? + +* 答案:百威啤酒、珠江啤酒、青岛啤酒、雪花啤酒、达利食品集团等 + +1. 上海理日新材料的技术地位如何? + +* 答案:成熟的研发创新能力,技术和产品保持国内领先、国际先进水平,国内市场品牌影响和占有率仅次于德国汉高等顶级跨国公司,产品已通过多家著名跨国公司认证并供货 + +1. 上海理日新材料的应用领域有哪些? + +* 答案:汽车部件、电子电缆、衣饰、制鞋、建材、装饰、包装以及低压注塑等行业 + +1. 上海理日新材料服务多少家客户? + +* 答案:数百家 + +1. 上海理日新材料的环保型高性能热熔胶包含哪些类型? + +* 答案:聚酯、聚酰胺热熔胶 + +1. 上海理日新材料的环保型高性能热熔胶有什么特点? + +* 答案:不含化学溶剂、无污染,粘接强度高,耐油耐化学性好,粘接材料广泛 + +1. 上海理日新材料的环保型高性能热熔胶应用于哪些企业? + +* 答案:汽车滤清器生产企业、制鞋服饰企业、电子电缆生产企业 + +1. 上海理日新材料的环保型低压注塑封装材料的目标市场是什么? + +* 答案:电缆电子行业中的电池板生产企业、线束插头/微型开关生产企业、连接器/传感器生产企业和PCB线路板生产企业 + +1. 四氢呋喃可作为上海理日新材料产品的哪些应用领域? + +* 答案:合成树脂的工业溶剂、合成革的表面处理剂、合成农药等领域 + +**(三)高性能复合材料方向** + +1. 康达新材在高性能复合材料方向的战略定位是什么? + +* 答案:聚焦玻璃纤维、碳纤维的环氧树脂、聚氨酯类复合材料领域,计划通过2-3年深化拓展;基于自身工艺和技术积累,通过投资业务契合度企业、技术创新、加大研发投入等方式,切入先进复合材料领域,推动向高分子新材料领域战略转型 + +1. 康达新材落实先进复合材料转型战略的关键举措是什么? + +* 答案:对聚发新材进行增资 + +1. 拉挤树脂(Urepul)的基础材料是什么? + +* 答案:玻纤&碳纤 + +1. 拉挤树脂(Urepul)的应用领域有哪些? + +* 答案:桥架、绝缘梯、桥梁、高压管道、景观工程、高铁、轨道、光伏 + +1. SMC/预浸料(Urepreg)的基础材料是什么? + +* 答案:玻纤&碳纤 + +1. SMC/预浸料(Urepreg)的应用领域有哪些? + +* 答案:汽车、轨道交通、电子产品 + +1. RTM树脂(Urethan)的基础材料是什么? + +* 答案:玻纤&碳纤 + +1. RTM树脂(Urethan)的应用领域有哪些? + +* 答案:风电叶片、汽车内外饰、高铁 + +1. 缠绕树脂(Urecom)的基础材料是什么? + +* 答案:玻纤&碳纤 + +1. 缠绕树脂(Urecom)的应用领域有哪些? + +* 答案:电线杆、传动轴 + +1. 截至2025年8月,康达新材高性能复合材料已实现量产应用的产品有哪些? + +* 答案:电缆桥架、高铁设备舱及裙板、新能源车电池盒骨架 + +1. 截至2025年8月,康达新材高性能复合材料技术开发完成的产品有哪些? + +* 答案:汽车型材、托盘、高铁步道板、绝缘臂腕 + +1. 截至2025年8月,康达新材高性能复合材料处于开发中的产品有哪些? + +* 答案:太阳能边框及支架、铁路鱼尾板 + +1. 康达新材的高性能复合材料在性能方面有什么优势? + +* 答案:出色的机械性能,与玻纤完美结合,更高的粘接强度,优异的抗疲劳性能 + +1. 康达新材的高性能复合材料在生产效率方面有什么优势? + +* 答案:更快的灌注速度,更快的固化速度 + +1. 康达新材的高性能复合材料在综合成本方面有什么优势? + +* 答案:模具投入少,具有竞争力的树脂价格 + +**(四)电子信息材料** + +1. 惟新科技被康达新材收购的时间是什么时候? + +* 答案:2023年 + +1. 惟新科技的企业定位是什么? + +* 答案:国家高新技术企业,致力于研发和生产高端ITO(氧化铟锡)靶材及ITO粉体、ITO气氛烧结炉等ITO周边产品,解决大尺寸ITO靶材“卡脖子”问题 + +1. 惟新科技的核心技术有哪些? + +* 答案:与清华大学化工系合作,研发微反应器制备高活性纳米ITO粉体技术;自主研发适合超大尺寸靶材制备的湿法注浆成型技术,拥有自主知识产权的超大空间高温烧结炉 + +1. 惟新科技成功制备的大尺寸ITO靶材规格是多少? + +* 答案:1550x250mm + +1. 惟新科技制备的大尺寸ITO靶材性能指标如何? + +* 答案:与国际同类靶材相当,达到国际先进水平,填补国内空白 + +1. 惟新科技的战略意义有哪些? + +* 答案:打破技术壁垒,减少高端靶材进口依赖,助力中美贸易战应对;推动我国铟资源高效利用;填补我国ITO靶材在中、高端平板显示器制造领域应用空白;促进ITO靶材平板生产工艺研发与人才培养 + +1. 惟新科技的ITO靶材试验线设备升级改造完成时间是什么时候? + +* 答案:2022年 + +1. 惟新科技目前的产能提升目标是什么? + +* 答案:推进30T/年产能提升工作 + +1. 惟新科技的溅射靶材下游覆盖哪些领域? + +* 答案:平板显示器、光伏电池、记录媒体、半导体等领域 + +1. 平板显示器领域在惟新科技溅射靶材下游应用中占比多少? + +* 答案:33.8% + +1. 记录媒体领域在惟新科技溅射靶材下游应用中占比多少? + +* 答案:28.6% + +1. 半导体领域在惟新科技溅射靶材下游应用中占比多少? + +* 答案:18.5% + +1. 晶材科技成立于哪一年? + +* 答案:2016年 + +1. 晶材科技被康达新材收购的时间是什么时候? + +* 答案:2018年 + +1. 晶材科技的企业定位是什么? + +* 答案:专注于高端电子陶瓷材料国产化,集研发与规模化生产于一体,提供先进材料解决方案,代理德国瓦克化学有机硅产品 + +1. 晶材科技的技术路线是什么? + +* 答案:微晶玻璃体系、微晶玻璃/陶瓷复合体系 + +1. 晶材科技在行业中的地位如何? + +* 答案:目前市场唯一批量供货的国产生料带供应商,解决“卡脖子”难题 + +1. 晶材科技的自研产品有哪些? + +* 答案:陶瓷生料带、贵金属浆料、瓷粉、导电胶 + +1. 晶材科技的自研产品应用于什么技术领域? + +* 答案:低温共烧陶瓷技术(LTCC)的电子元器件、电路模块制造和封装 + +1. 晶材科技的代理产品是什么? + +* 答案:有机硅水胶(车载显示器光学全贴合重要原材料) + +1. 晶材科技的产品在军用领域有什么应用? + +* 答案:有源相控阵雷达 + +1. 晶材科技的产品在民用领域有什么应用? + +* 答案:高端半导体传感器、光通信器件、汽车电子电路模块、5G路由器、5G机房、数据中心等 + +1. 晶材科技的生料带现有产能是多少? + +* 答案:240万片/年 + +1. 晶材科技的贵金属浆料现有产能是多少? + +* 答案:30吨/年 + +**(五)电子科技——赛英科技** + +1. 赛英科技被康达新材收购的时间是什么时候? + +* 答案:2018年 + +1. 赛英科技的地理位置在哪里? + +* 答案:成都龙潭新经济产业功能区 + +1. 赛英科技的科研生产基地规模是多少? + +* 答案:8000余平方米 + +1. 赛英科技的核心业务是什么? + +* 答案:科研、生产整机雷达、微波组件、专用仪器仪表、海洋通讯产品 + +1. 赛英科技在安全管控领域的产品有哪些? + +* 答案:近程警戒雷达(地面、海面)、周界安全警戒雷达(系列产品) + +1. 赛英科技在交通监测领域的产品有哪些? + +* 答案:FOD跑道异物监测雷达(塔架、移动) + +1. 赛英科技在工业监测领域的产品有哪些? + +* 答案:高炉料面监测雷达、微位移监测雷达(应用于大型钢厂、铁路大坝等重要区域) + +1. 赛英科技获得了哪些企业荣誉? + +* 答案:成都市企业技术中心、成都市成华区2020年度“工业纳税先进单位”、四川省“专精特新”中小企业、成都市科技装备业商会“副会长单位”等 + +**六、康达新材日常聊天类** + +1.你能给康达新材创作一首诗吗? + +* 答案:匠心三十载耕耘,新材科技破卡颈胶黏万物兴百业,引领制造向前行 + +2.如果用三个关键词概括康达新材的核心气质,你觉得会是什么?为什么? + +* 答案:硬核(深耕“卡脖子”技术)、创新(研发投入持续增长)、多元(覆盖多业务板块) + +3.假设你要给朋友介绍康达新材,会用一段轻松有趣的话怎么说? + +* 答案:这家1988年成立的国企超牛!能做粘风电叶片的“超级胶水”,还搞定高端ITO靶材,业务横跨新材料和电子科技,妥妥的科技实力派~ + +4.要是给康达新材设计一句接地气的宣传语,不局限于官方口号,你会怎么写? + +* 答案:康达新材,粘得牢、造得硬,让中国制造更靠谱! + +5.想象康达新材要举办一场员工家庭日活动,你觉得可以设计哪些有趣的互动环节? + +* 答案:①亲子胶粘剂组装模型;②VR参观生产基地答题;③家庭新材料元素手作赛 + +6.如果你是康达新材的一名新员工,入职第一天最想了解公司的哪三个方面? + +* 答案:①岗位与核心业务的关联;②研发创新支持政策;③培训与晋升路径 + +7.用拟人化的手法描述康达新材,你会把它比作哪种形象?理由是什么? + +* 答案:比作“实干工匠”,30余年坚守行业,身怀多元技能,持续突破技术难题 + +8.康达新材涉及多个业务板块,要是用一种美食比喻它的业务布局,你觉得像什么?为什么? + +* 答案:像“什锦火锅”,胶粘剂是锅底,各业务板块是食材,协同发力丰富产业生态 + +9.要是给康达新材的研发人员写一句鼓励的话,你会怎么表达? + +* 答案:以匠心攻技术,以创新破壁垒,为中国智造添力! + +10.假设康达新材要出一款周边产品送给客户,你觉得什么类型的产品既实用又能体现公司特色? + +* 答案:定制多功能粘接工具套装,含迷你环保胶粘剂、ITO靶材图案书签 + +11.用一句话形容康达新材从1988年成立到现在的发展历程,你会怎么说? + +* 答案:从化工实验厂到“新材料+电子科技”集团,30余年坚守创新,领跑细分领域 + +12.如果你要在社交平台分享康达新材的一则动态,配文会怎么写才能吸引关注? + +* 答案:国企硬核!粘风电叶片、破ITO靶材“卡脖子”,覆盖新能源、电子科技,为中国制造打call~#康达新材 #国货之光 + +13.康达新材在多个地区有生产基地,要是选一个基地推荐给朋友参观,你会选哪个?理由是什么? + +* 答案:选上海奉贤基地,集研发生产于一体,能直观看到产品从研发到量产的全过程 + +14.要是给康达新材设计一个吉祥物,你觉得它的外形和寓意应该是什么样的? + +* 答案:蓝色分子造型小精灵,带翅膀(象征创新),头顶ITO靶材,寓意科技赋能 + +15.想象康达新材要拍摄一支企业宣传片,你觉得开篇画面应该是什么场景? + +* 答案:高空俯瞰全球生产基地,快速切换研发实验室、生产线、风电应用场景,定格企业口号 + +16.如果你是康达新材的客服,接到客户咨询时,第一句暖心的开场白会怎么说? + +* 答案:您好!康达新材客服为您服务,有任何产品或合作问题,我都会全力解答~ + +17.用三个 emoji 代表康达新材的业务板块,你会选哪三个?分别对应什么业务? + +* 答案:🧪(胶粘剂与特种树脂)、💻(电子科技)、📱(电子信息材料) + +18.康达新材有不少“专精特新”子公司,要是给这些子公司办一场表彰会,颁奖词可以怎么写? + +* 答案:深耕细分领域,以硬科技破壁垒、填空白,为产业生态注入强劲动力,致敬每一份坚守! + +19.假设你要给康达新材的员工设计一件文化衫,正面和背面会印什么图案或文字? + +* 答案:正面:康达新材logo+“以新材赋能”;背面:三大业务图标+“融合 协同 创新 超越” + +20.用简单的语言给小朋友解释康达新材生产的胶粘剂是做什么用的,你会怎么说? + +* 答案:这是“超级胶水”,能把风电叶片、汽车零件粘得牢牢的,让它们更结实哦~ + +21.要是康达新材要发起一个公益活动,结合它的业务特点,你觉得可以开展什么主题的活动? + +* 答案:“绿色新材助环保”,用环保胶粘剂、可降解材料为乡村学校修补课桌椅 + +22.如果你参加康达新材的年会,最期待看到哪个环节的表演?为什么? + +* 答案:业务板块创意秀,能通过表演直观了解各部门工作,还能感受团队风采 + +23.用对比的方式说说康达新材和你印象中的传统化工企业有什么不同? + +* 答案:传统化工多单一业务、低技术,康达聚焦新兴产业,研发投入高、业务多元且绿色环保 + +24.康达新材的使命是“服务国家战略,引领行业发展”,要是你是员工,会怎么理解这句话并落实到工作中? + +* 答案:紧跟国家需求,研发时聚焦“卡脖子”技术,生产时严把质量关,助力产业升级 + +25.假设你要给康达新材的官网设计一个新栏目,你会建议增加什么内容? + +* 答案:“新材科普”栏目,用动画、图文讲解产品原理和应用,开设互动问答 + +26.用一句幽默的话调侃康达新材的“硬核”业务(比如胶粘剂、ITO靶材),你会怎么说? + +* 答案:康达业务太硬核,万物皆可粘、皆可造,就没有搞不定的技术难题? + +27.要是康达新材要和一所高校开展合作,你觉得可以在哪些领域合作共赢? + +* 答案:①联合研发“卡脖子”技术;②开设定制班培养人才;③加速科研成果转化 + +28.如果你是康达新材的hr,招聘时会用什么独特的理由吸引人才加入? + +* 答案:参与热门领域技术攻关,国企保障+清晰晋升,与行业顶尖人才共事 + +29.用比喻的手法说说康达新材的研发团队像什么?为什么? + +* 答案:像“产业探路者”,探索前沿技术,开辟新路径,指引产业发展方向 + +30.康达新材海外有布局,要是你负责海外业务推广,会先给海外客户推荐哪款产品? + +* 答案:风电胶粘剂,市场占有率第一,技术领先,契合全球新能源需求 + +31.假设康达新材要举办一场客户答谢会,你觉得现场布置可以融入哪些业务元素? + +* 答案:①产品模型展示区;②分子结构装饰;③产品拼图、技术问答互动 + +32.用一句话总结康达新材对国家新材料产业的意义,你会怎么说? + +* 答案:打破进口依赖,树立国产标杆,为新材料产业高质量发展提供支撑 + +33.要是你是康达新材的一名司机,开车运送产品时,会怎么跟别人介绍车上的货物? + +* 答案:这是国产硬核科技产品,有风电专用胶粘剂、电子设备核心材料,助力多个行业发展~ + +34.康达新材有很多生产基地,要是把这些基地比作一串珍珠,你觉得哪颗“珍珠”最亮眼? + +* 答案:成都电子科技产业园,聚焦战略新兴产业,是西南地区电子信息特色产业园 + +35.用简单的手势或动作比划康达新材某款产品的用途,你会怎么比划? + +* 答案:比划胶粘剂:双手模拟“叶片”,合拢按压示意粘贴牢固 + +36.假设康达新材要出一本企业内刊,你会给内刊起什么名字?设置哪些栏目? + +* 答案:名字《康达新声》,栏目:总裁寄语、技术前沿、业务动态、员工风采 + +37.用一句话形容康达新材员工的工作状态,你会怎么描述? + +* 答案:勤学实干、协同创新,在各自岗位上全力冲刺目标 + +38.要是康达新材要开展员工技能比赛,你觉得可以设置哪些比赛项目? + +* 答案:①技术攻关挑战赛;②生产精准操作大赛;③产品推广创意赛 + +39.用对比的方式说说康达新材20年前和现在的变化,你会怎么总结? + +* 答案:20年前业务单一,如今成长为“新材料+电子科技”集团,技术、布局全面升级 + +40.康达新材的核心价值观是“融合、协同、创新、超越”,要是你用生活中的例子解释“协同”,会选什么例子? + +* 答案:像家庭做饭,分工不同但目标一致,相互配合才能快速做出丰盛饭菜 + +41.假设你要给康达新材的会议室起名字,结合业务特色,你会起什么名字? + +* 答案:粘合力厅、靶材阁、科创堂、协同馆 + +42.用一句鼓励的话送给正在攻克“卡脖子”技术的康达新材研发团队,你会怎么说? + +* 答案:坚守初心攻难关,国产技术定发光,为你们加油! + +43.要是康达新材要拍一支员工纪录片,你觉得应该采访哪些岗位的员工? + +* 答案:研发人员、生产线工人、销售人员、子公司负责人、行政后勤人员 + +44.用比喻的手法说说康达新材的产品像什么?比如胶粘剂像“工业胶水”,再具体描述一下 + +* 答案:①胶粘剂像“工业纽带”,连接各类部件;②ITO靶材像“电子基石”,支撑电子设备运行 + +45.康达新材涉及电子科技领域,要是你给长辈解释什么是“微波组件”,会怎么说? + +* 答案:这是电子设备的“信号中转站”,能让雷达、通讯设备精准收发信号,保障安全和监测 + +46.假设康达新材要参加一场行业展会,你觉得展位设计要突出什么亮点才能吸引观众? + +* 答案:突出科技感(分子结构造型、LED大屏)和互动性(产品体验、VR参观) + +47.用一句话说说你对康达新材未来5年发展的期待,你会怎么说? + +* 答案:期待成为全球领先的“新材料+电子科技”集团,让更多国产硬核科技走向世界 + +48.要是你是康达新材的食堂阿姨,会怎么用食堂菜品比喻公司的业务多元化? + +* 答案:食堂菜像公司业务,有“核心硬菜”(胶粘剂)、“新鲜时蔬”(电子信息材料),搭配均衡、营养丰富~ + +49.用简单的画画形式表现康达新材的业务,你会画哪些元素? + +* 答案:画大圆圈代表公司,内部画风电叶片、手机、芯片、分子结构,用箭头连接体现协同 + +50.康达新材有不少荣誉资质,要是你把这些荣誉比作公司的“勋章”,最想给别人展示哪枚“勋章”?为什么? + +* 答案:国家级“专精特新”小巨人企业,体现公司技术实力和细分领域领先地位 diff --git a/ruoyi-fastapi-backend/md/康达新材【对外】-20250815(Q&A版).md b/ruoyi-fastapi-backend/md/康达新材【对外】-20250815(Q&A版).md new file mode 100644 index 0000000..fa6e2d9 --- /dev/null +++ b/ruoyi-fastapi-backend/md/康达新材【对外】-20250815(Q&A版).md @@ -0,0 +1,14 @@ +# 康达新材(002669.SZ)对外介绍(Q&A版) + +> 本文与《2.0康达新材Q&A清单 251228.md》存在大量重合:公司概况/沿革/股权/战略/基地/板块介绍/部分经营与研发数据等。对外统一口径以清单为准。 + +## 仍保留(本文件独有或更适合对外的补充) + +### 1)英文名 +**A:**KangDa New Materials。 + +### 2)对外免责声明/使用说明 +**A:**如与正式公告/披露存在差异,以公司披露为准。 + +### 3)(可选)对外呈现的结构建议 +**A:**对外问答建议直接引用清单中对应条目(公司基本信息→战略→基地→业务板块),避免出现多份口径。 diff --git a/ruoyi-fastapi-backend/md/康达新材介绍文档 250826(Q&A版).md b/ruoyi-fastapi-backend/md/康达新材介绍文档 250826(Q&A版).md new file mode 100644 index 0000000..51ae729 --- /dev/null +++ b/ruoyi-fastapi-backend/md/康达新材介绍文档 250826(Q&A版).md @@ -0,0 +1,11 @@ +# 康达新材介绍文档(250826)(Q&A版) + +> 本文内容与《2.0康达新材Q&A清单 251228.md》高度重合(公司概况/沿革/股权/战略/基地/业务板块/资质等),对外与对内统一口径以清单为准。 + +## 仍保留(本文件独有/更适合作为“介绍文档”的补充) + +### 1)口号 +**A:**康达匠心赋能 引领制造新材。 + +### 2)原文口径不一致提示(用于内部校对) +**A:**原文个别财务数据在不同位置存在不一致;如需对外口径,请以公司披露/正式财报为准。 diff --git a/ruoyi-fastapi-backend/md/康达新材:2022年年度报告(Q&A版).md b/ruoyi-fastapi-backend/md/康达新材:2022年年度报告(Q&A版).md new file mode 100644 index 0000000..54b83d1 --- /dev/null +++ b/ruoyi-fastapi-backend/md/康达新材:2022年年度报告(Q&A版).md @@ -0,0 +1,243 @@ +# 康达新材:2022年年度报告(Q&A版) + +> 说明:本文为对 `md/康达新材:2022年年度报告.md` 的要点抽取与问答化重组,便于检索与知识库问答使用;问题为整理者归纳,答案以原文表述与数据为准。 + +--- + +## 1. 分配政策 + +**Q5:2022年度利润分配(现金分红/送股/转增)安排是什么?** +**A:** 公司计划不派发现金红利、不送红股、不以公积金转增股本。 + +--- + +## 3. 业务板块与产品结构 + +**Q12:公司主要属于哪些行业/业务类型?** +**A:** 胶粘剂新材料行业、军工电子科技行业、电子信息材料(含医药中间体与电子化学品等)相关领域。 + +**Q13:公司胶粘剂新材料板块有哪些主要产品?应用在哪些领域?** +**A:** 产品包括环氧树脂胶、聚氨酯胶、丙烯酸酯胶、SBS胶、热熔胶、水性胶等,以及环氧/聚氨酯复合材料、聚酰亚胺材料等;应用于风电叶片制造、包装材料、轨道交通、船舶工程、汽车、电子电器、机械设备、建筑装饰、工业维修等。 + +**Q14:胶粘剂业务的经营模式是什么?** +**A:** “产品+解决方案”。采购以订单与计划为导向、供应商体系管理;生产以销定产并部分常规备货;销售以直销为主,强调技术服务与客户认证。 + +**Q15:军工电子科技板块主要做什么?** +**A:** 以应用于航空、航天、舰船、兵器等领域的滤波器/滤波组件与电源模块为主要方向,并覆盖电磁兼容相关设备与产品。 + +**Q16:军工电子科技板块的组织架构是什么?** +**A:** 以晟璟科技为管理平台,下辖必控科技、力源兴达两家全资子公司,并参股成都铭瓷、成都立扬、上海汉未。 + +**Q17:必控科技与力源兴达分别主营什么?** +**A:** 必控科技:电磁兼容设备/预测试系统/软件与电磁兼容加固产品,含滤波器、滤波组件、电源滤波模块等;力源兴达:电源变换器模块研发设计生产销售,面向军工与电网输配电/工业控制等。 + +**Q18:电子信息材料及医药中间体板块主要包含哪些方向?** +**A:** 显示材料(液晶混晶/单体/中间体、OLED相关材料、光刻胶感光剂、LCD-PI导向膜单体、ITO靶材等——靶材项目起步阶段暂无规模化销售)、医药中间体、新能源电子化学品(如电解液添加剂、电子清洗剂)。 + +**Q19:彩晶光电的业务定位与销售方式有什么特点?** +**A:** 通过下游认证后按客户材料结构式与指标定制研发与生产,以直销为主;部分日韩地区客户通过指定代理采购商销售。 + +--- + +## 5. 2022年经营回顾(管理层讨论与分析要点) + +**Q24:2022年公司总体经营表现如何?** +**A:** 营业收入246,636.18万元,同比增长8.57%;营业利润5,275.40万元,同比增长186.13%;归母净利润4,791.35万元,同比增长117.83%;研发投入12,641.94万元,占营业收入5.13%;期末资产总额53.33亿元,同比增长约43.04%;归母净资产30.02亿元,同比增长32.66%。 + +**Q25:风电叶片制造领域有哪些进展?** +**A:** 风电胶粘剂保持市占率领先;产品成功应用于11款百米级叶片(100m–126m);海外拓展LM Windpower,批量采购环氧结构胶、环氧树脂、喷胶等多类型产品。 + +**Q26:软包装无溶剂胶粘剂领域有哪些进展?** +**A:** 聚氨酯类胶粘剂销售量/额同比稳定增长,销售量突破11,000吨,覆盖客户超750家,国内无溶剂软包装胶粘剂市场占有率领先;东南亚/中亚销售网络布局推进;与客户联合申报的相关技术列入《国家清洁生产先进技术目录(2022)》。 + +**Q27:消费电子及家电用胶有哪些进展?** +**A:** 形成以双组份丙烯酸酯结构胶、单组份结构胶、UV胶、PUR胶、双/单组份环氧胶等为主线的系列化产品(近200余个型号);与联想、华为、戴尔、华硕、立讯、韶音等合作;单组份环氧胶在柔性线路板保护/灌封/密封等领域进入中高端并打破国外垄断;电子胶粘剂销售收入稳定增长10%以上。 + +**Q28:军工电子板块有哪些经营与产能/模式探索?** +**A:** 必控科技扩充电磁兼容产品型谱,探索“多品种、小批量”柔性化Cell-Line生产模式并开始试生产,获得四川省“专精特新”中小企业称号;力源兴达将民品电源事业部转为军品为重心,新增客户23家、新项目45项。 + +**Q29:研发与标准/专利方面有哪些成果?** +**A:** 研发费用占营收5.13%;新增授权专利16项(发明13项、实用新型3项),受理发明专利10项;参与制定/修订国家标准2项、行业标准1项。 + +**Q30:产能基地建设与项目推进有哪些重点?** +**A:** 募投项目唐山丰南与福建邵武基地推进;天津复材研究中心与军用电源生产基地完成设计并推进审批;成都高新区军工电子科技产业园进入规划并签署土地转让协议;彩晶光电推进液晶显示材料产业化(Ⅰ期二标段)项目(混合液晶30吨/年、液晶单体40吨/年、液晶中间体250吨/年,计划总投资37,350万元,预计2023年底主体建设完成)。 + +**Q31:2022年资本运作有哪些关键事项?** +**A:** 2022年7月15日向特定对象发行股票52,910,052股,发行价13.23元/股,募集资金总额约7亿元;2022年8月收购彩晶光电60.9205%股权,打造“电子信息材料”第二增长极。 + +**Q32:员工激励与股份回购有什么信息?** +**A:** 报告期内使用自有资金5,430.90万元回购股份,并实施第四期员工持股计划,授予股份469.14万股。 + +--- + +## 6. 主要财务指标(合并口径摘要) + +**Q33:2022/2021/2020营业收入分别是多少?** +**A:** 2022年2,466,361,756.29元;2021年2,271,612,976.74元;2020年1,932,135,499.94元。 + +**Q34:2022年归母净利润与扣非归母净利润是多少?同比如何?** +**A:** 归母净利润47,913,477.29元(同比+117.83%);扣非归母净利润33,756,789.24元(同比+520.23%)。 + +**Q35:2022年经营活动现金流净额是多少?** +**A:** -23,646,854.70元(同比-111.38%)。 + +**Q36:2022年基本/稀释每股收益是多少?** +**A:** 基本每股收益0.179元/股;稀释每股收益0.179元/股。 + +**Q37:2022年末总资产与归母净资产是多少?** +**A:** 总资产5,332,562,034.54元;归母净资产3,001,624,183.70元。 + +**Q38:2022年加权平均净资产收益率是多少?** +**A:** 2.10%。 + +--- + +## 7. 分季度表现(合并口径) + +**Q39:2022年分季度营业收入分别是多少?** +**A:** Q1 598,664,229.98元;Q2 487,606,208.15元;Q3 543,442,867.29元;Q4 836,648,450.87元。 + +**Q40:2022年分季度归母净利润分别是多少?** +**A:** Q1 -10,396,127.38元;Q2 -4,275,913.77元;Q3 -19,085,103.43元;Q4 81,670,621.87元。 + +**Q41:2022年分季度经营活动现金流净额分别是多少?** +**A:** Q1 -75,340,459.93元;Q2 156,132,079.25元;Q3 -21,654,884.68元;Q4 -82,783,589.34元。 + +--- + +## 8. 非经常性损益 + +**Q42:2022年非经常性损益合计是多少?主要构成有哪些?** +**A:** 合计14,156,688.05元。主要包括:政府补助、理财收益/公允价值变动、资产处置损益、其他营业外收支等,并扣除所得税影响与少数股东权益影响。 + +**Q43:“其他符合非经常性损益定义的损益项目”是什么?** +**A:** 其他权益工具投资持有期间取得的股利收入237,658.95元。 + +--- + +## 9. 收入结构与毛利(2022年) + +**Q44:按行业看,营业收入构成如何?** +**A:** 胶粘剂15.80亿元(64.06%);电子产品2.76亿元(11.17%);复合材料4.32亿元(17.53%);显示材料0.19亿元(0.76%);医药中间体0.26亿元(1.06%);电子化学品0.35亿元(1.44%);其他业务0.98亿元(3.97%)。 + +**Q45:按产品看,主要产品收入有哪些?** +**A:** 环氧胶类9.76亿元;聚氨酯胶类2.93亿元;轻木套材4.09亿元;滤波器及滤波组件1.38亿元;电源模块0.93亿元;电源滤波模块0.44亿元等。 + +**Q46:胶粘剂/电子产品/复合材料毛利率大致水平如何?** +**A:** 胶粘剂毛利率17.74%;电子产品毛利率56.04%;复合材料毛利率1.69%(报告期披露口径)。 + +**Q47:按销售模式看,直销与代销占比如何?** +**A:** 直销22.08亿元(89.52%);代销1.61亿元(6.51%);其他业务0.98亿元(3.97%)。 + +**Q48:前五大客户/供应商集中度如何?** +**A:** 前五名客户合计销售金额987,551,997.77元,占年度销售总额41.70%;前五名供应商合计采购金额845,819,887.90元,占年度采购总额42.73%。 + +--- + +## 10. 费用与研发 + +**Q49:2022年主要期间费用是多少?同比如何?** +**A:** 销售费用88,155,461.44元(-6.32%);管理费用156,228,970.50元(+14.17%);财务费用39,194,908.67元(+50.23%,主要因融资额增加);研发费用126,419,384.32元(+28.89%)。 + +**Q50:研发人员规模与结构如何?** +**A:** 研发人员378人(同比+32.63%),占比24.77%;学历结构:本科164、硕士57、博士9;年龄:30岁以下110、30–40岁147等。 + +**Q51:研发人员增长的主要原因是什么?** +**A:** 2022年收购彩晶光电完成后,将彩晶光电研发人员纳入统计。 + +--- + +## 11. 现金流与差异解释 + +**Q52:2022年三大现金流净额分别是多少?** +**A:** 经营活动-23,646,854.70元;投资活动-844,050,079.34元;筹资活动+1,049,218,073.24元。 + +**Q53:经营活动现金流为何与净利润差异较大?** +**A:** 报告说明主要由于购买商品、接受劳务支付的现金增加等因素。 + +--- + +## 12. 资产负债与受限资产 + +**Q54:2022年末资产结构的几个大项是多少?** +**A:** 货币资金5.51亿元;应收账款11.54亿元;存货6.70亿元;固定资产7.45亿元;在建工程4.17亿元;商誉6.15亿元等(详见原表)。 + +**Q55:报告期末有哪些主要受限资产?合计多少?** +**A:** 受限资产合计759,059,552.00元,主要包括票据保证金、质押票据、抵押的固定资产/无形资产/在建工程,以及质押的长期股权投资(以持有彩晶光电60.92%股权质押取得长期借款等)。 + +--- + +## 13. 投资并购与合并范围变动 + +**Q56:2022年合并范围有哪些变动?** +**A:** 新设保定康达新材料(2022年1月纳入);出售天津易远通60%股权(2022年2月不再纳入);新设康达锦瑞(2022年8月纳入);收购彩晶光电60.9205%股权(2022年8月纳入)。 + +**Q57:重大股权投资中,彩晶光电收购的关键信息是什么?** +**A:** 通过收购取得彩晶光电60.9205%股权;股权投资金额(披露为)372,530,500.00元;纳入合并报表范围。 + +**Q58:公司是否发生重大资产出售?** +**A:** 报告期未出售重大资产。 + +**Q59:重大股权出售(天津易远通)情况如何?** +**A:** 康达国际供应链以1,815万元向天津唐控科创集团出售天津易远通60%股权,披露为关联交易;对公司整体经营与业绩无重大影响。 + +--- + +## 14. 募集资金要点 + +**Q60:报告期涉及哪些募集资金项目/年份?** +**A:** 2016年非公开发行募集资金8.5亿元;2022年非公开发行募集资金7亿元。 + +**Q61:截至2022年末,尚未使用的募集资金余额与去向是什么?** +**A:** 2016年项目尚未使用余额1,375.01万元,存放专户;2022年项目尚未使用余额34,493.59万元,其中12,000万元用于现金管理、4,000万元用于暂时补充流动资金、其余存放募集资金专户。 + +**Q62:丁基材料项目为何结项并变更投向?** +**A:** 报告说明市场前景不及预期,为优化资源配置与提高资金使用效率,公司对丁基材料项目结项,并将剩余募集资金18,558.08万元投向“高性能环氧结构胶粘剂扩产项目”和“研发中心扩建项目”。 + +--- + +## 15. 子公司/参股公司经营贡献(摘录) + +**Q63:对公司净利润影响较大的子公司有哪些?表现如何?** +**A:** 报告列示: +* 必控科技:报告期营业收入19,642.62万元、净利润4,698.65万元,对整体业绩贡献较大。 +* 力源兴达:报告期营业收入10,059.81万元、净利润-818.43万元,对整体业绩有一定影响。 + +--- + +## 16. 资质与合规(节选) + +**Q64:公司在安全/环保/质量体系方面有哪些主要证照或认证?** +**A:** 报告列示包括安全生产许可证、安全生产标准化证书、危险化学品经营许可证、ISO9001/ISO14001/IATF16949、排污许可证、CNAS实验室认可、GL证书等,并披露有效期(详见原表)。 + +**Q65:报告期是否发生重大安全环保事故或非正常停产?** +**A:** 报告披露全年未发生重大安全环保事故,且不存在非正常停产情形。 + +--- + +## 17. 税收优惠(财务报表附注摘录) + +**Q66:哪些主体适用15%企业所得税优惠税率(高新/西部大开发等)?** +**A:** 报告附注披露:母公司、本公司部分子公司(如必控科技、力源兴达、河北惟新、理日新材、彩晶光电等)适用15%税率(高新技术企业或西部大开发等政策口径)。 + +**Q67:哪些主体适用小型微利企业所得税优惠?** +**A:** 康厦科技、深圳康达电子、康达国际供应链、微相邦、万斯先进新材料、晟宇科技、顺璟投资、璟创投资、天津瑞宏、天津三友、天津康达新材料、瑞贝斯、河北康达新材料、保定康达新材料、惟新半导体、理日新材、康达锦瑞等(以原文列示为准)。 + +--- + +## 18. 资产负债表日后事项(财务报表附注摘录) + +**Q68:报告披露的资产负债表日后事项有哪些类型?** +**A:** 主要包括部分借款信息披露,以及(1)受让成都赛英科技100%股权的进展披露、(2)为参股公司申请银行贷款提供担保、(3)控股子公司与关联方共同对外投资设立合资公司等。 + +--- + +## 19. 快速检索:关键数字一览 + +* 营业收入(2022):2,466,361,756.29 元 +* 归母净利润(2022):47,913,477.29 元 +* 扣非归母净利润(2022):33,756,789.24 元 +* 经营活动现金流净额(2022):-23,646,854.70 元 +* 总资产(2022年末):5,332,562,034.54 元 +* 归母净资产(2022年末):3,001,624,183.70 元 +* 研发费用(2022):126,419,384.32 元(占营收5.13%) diff --git a/ruoyi-fastapi-backend/md/康达新材:2023年年度报告(Q&A版).md b/ruoyi-fastapi-backend/md/康达新材:2023年年度报告(Q&A版).md new file mode 100644 index 0000000..7053d71 --- /dev/null +++ b/ruoyi-fastapi-backend/md/康达新材:2023年年度报告(Q&A版).md @@ -0,0 +1,264 @@ +# 康达新材:2023年年度报告(Q&A版) + +> 说明:本文为对 `md/康达新材:2023年年度报告.md` 的要点抽取与问答化重组,便于检索与知识库问答使用;问题为整理者归纳,答案以原文表述与数据为准。 + +--- + +## 1. 利润分配 + +**Q5:2023年度利润分配预案是什么?** +**A:** 以实施分配方案时股权登记日“总股本扣减回购专用证券账户股份数”为基数,向全体股东每10股派发现金股利0.70元(含税),不送红股,不以资本公积金转增股本。 + +**Q6:本次现金分红的股本基数与现金分红金额是多少?** +**A:** 分配预案股本基数为297,160,573股;现金分红金额为20,801,240.11元(含税)。 + +--- + +## 3. 业务版图与战略定位(管理层讨论与分析摘要) + +**Q12:公司产品主要对应哪些行业?** +**A:** 公司产品根据特性与终端应用领域/业务类型,分别属于胶粘剂新材料行业、合成树脂行业、电子信息材料行业、电子科技行业。 + +**Q13:公司“主链+第二增长极”的模式是什么?** +**A:** 形成“以胶粘剂新材料系列产品为主链、高端电子信息材料为支撑的第二增长极”的发展模式,并在电子科技领域持续完善战略布局与资源联动。 + +**Q14:2023年公司在合成树脂领域有什么布局?** +**A:** 向胶粘剂产业链上游收购并增资大连齐化,进入合成树脂领域;大连齐化以高品质环氧树脂为主,涵盖特种树脂新材料研发、生产、销售、服务。 + +**Q15:2023年公司在电子信息材料领域有哪些延伸?** +**A:** 继2022年收购彩晶光电进入液晶显示材料、医药中间体及电子化学品等领域后,2023年收购晶材科技,业务涵盖陶瓷生料带、贵金属浆料、瓷粉等。 + +**Q16:2023年电子科技板块产业链如何延展?** +**A:** 收购赛英科技后,形成涵盖电磁兼容、电源模块、电容、微波组件、雷达相关整机等元器件级/部件级/系统级/整机级的业务链条。 + +--- + +## 5. 2023年经营回顾(要点) + +**Q22:2023年公司总体经营表现如何?** +**A:** 营业收入2,792,525,024.71元,同比增长13.22%;归母净利润30,315,165.24元,同比下降36.66%;期末资产总额7,122,936,764.42元,同比增长33.57%。 + +**Q23:归母净利润下降的主要原因(报告说明)是什么?** +**A:** 胶粘剂板块风电系列产品规模稳中有升、原材料趋稳后板块盈利能力较上年提升;但电子科技与电子信息材料板块行业环境阶段性变化、调整及订单延后等因素叠加,部分公司计提商誉减值准备,对归母净利润产生影响。 + +**Q24:风电胶粘剂与风电一体化材料供应方面有哪些进展?** +**A:** 风电胶粘剂系列产品继续保持市占率领先,风电环氧结构胶、灌注树脂等各类产品销售总量近6万吨;海外市场在Siemens Gomesa应用稳定,并在LM Windpower批量采购并推进多工厂试用认证。 + +**Q25:包装领域业务表现与海外拓展如何?** +**A:** 聚氨酯类胶粘剂销售量、销售额同比上升,报告期销售量超1万吨,覆盖客户超700家;海外销售范围扩展至东南亚、中亚等25个国家或地区,贸易额增幅达30%。 + +**Q26:消费电子、家电、轨道交通用胶有哪些进展?** +**A:** 消费电子胶形成多产品线并按客户需求个性化调整;与联想、华硕、罗技、韶音等客户协同开发;家电用胶围绕美的、格力、海尔等目标客户开始批量供货;轨道交通领域通过TB/T2975-2018认证并逐步提升市占率。 + +**Q27:电子信息材料板块有哪些进展?** +**A:** 彩晶光电混合液晶围绕京东方高世代产线,多款产品供货,“户外商显”首次小批量出货;医药板块受部分中间体停产与行业降价影响下滑,但推进新品转化与量产并开展高端中间体研发;惟新科技推进大尺寸ITO靶材产线升级扩建及高纯氧化铝靶材项目。 + +**Q28:电子科技板块在型号/研发与产能方面有哪些进展?** +**A:** 赛英科技推进多个项目并于2023年7月彭州雷达测试基地投入使用;必控科技新研项目近190项;力源兴达全年完成项目开发370余项、约50%为国产化改造,并新增大量新需求。 + +**Q29:2023年研发投入与创新成果如何?** +**A:** 研发费用177,605,054.56元,占营收6.36%;新增授权专利29项(发明16项、实用新型13项),受理发明专利30项,参与制定或修订行业标准1项。 + +--- + +## 6. 主要财务指标(合并口径) + +**Q30:2023/2022营业收入分别是多少?** +**A:** 2023年2,792,525,024.71元;2022年2,466,361,756.29元。 + +**Q31:2023年归母净利润与扣非归母净利润是多少?** +**A:** 归母净利润30,315,165.24元;扣非归母净利润-150,073,608.10元。 + +**Q32:2023年经营活动现金流净额是多少?** +**A:** 9,829,020.97元。 + +**Q33:2023年基本/稀释每股收益与加权ROE是多少?** +**A:** 基本每股收益0.100元/股;稀释每股收益0.100元/股;加权平均净资产收益率1.03%。 + +**Q34:2023年末总资产与归母净资产是多少?** +**A:** 总资产7,122,936,764.42元;归属于上市公司股东的净资产2,976,876,747.22元。 + +**Q35:营业收入扣除前后金额是多少?** +**A:** 2023年营业收入扣除金额156,348,055.39元;扣除后金额2,636,176,969.32元。 + +--- + +## 7. 分季度主要财务指标(合并口径) + +**Q36:2023年分季度营业收入分别是多少?** +**A:** Q1 630,942,962.19元;Q2 648,773,198.50元;Q3 705,360,387.19元;Q4 807,448,476.83元。 + +**Q37:2023年分季度归母净利润分别是多少?** +**A:** Q1 -14,847,352.02元;Q2 50,891,195.90元;Q3 -3,752,562.06元;Q4 -1,976,116.58元。 + +**Q38:2023年分季度经营活动现金流净额分别是多少?** +**A:** Q1 -97,845,264.99元;Q2 -12,832,682.78元;Q3 31,779,967.81元;Q4 88,727,000.93元。 + +--- + +## 8. 非经常性损益(2023年) + +**Q39:2023年非经常性损益合计是多少?** +**A:** 180,388,773.34元。 + +**Q40:2023年非经常性损益的主要来源有哪些?** +**A:** 主要包括: +* 公允价值变动损益123,636,796.27元(报告说明:上海晶材原股东未完成业绩承诺所支付业绩补偿等)。 +* 计入当期损益的政府补助14,378,285.97元。 +* 收购赛英科技形成的“负商誉”收益44,946,508.24元。 +* 非流动资产处置损益-21,818.65元等。 + +**Q41:“其他符合非经常性损益定义的损益项目”是什么?** +**A:** 本期注销子公司康达晟宇产生的投资收益,列示为4,186.14元。 + +--- + +## 9. 收入结构与毛利(2023年) + +**Q42:按行业看,2023年营业收入构成如何?** +**A:** 胶粘剂1,971,572,392.42元(70.60%);电子产品261,692,120.15元(9.37%);复合材料100,701,149.72元(3.61%);显示材料136,741,920.89元(4.90%);医药中间体130,962,131.42元(4.69%);电子化学品18,311,999.63元(0.66%);LTCC材料16,195,255.08元(0.58%);其他业务收入156,348,055.40元(5.60%)。 + +**Q43:按产品看,2023年主要产品收入有哪些?** +**A:** 环氧胶类1,317,562,900.69元;聚氨酯胶类294,258,688.11元;电磁兼容产品130,855,841.37元;电源模块62,660,839.65元;显示材料136,741,920.89元;医药中间体130,962,131.42元;轻木套材74,331,868.48元等。 + +**Q44:占比10%以上的业务中,胶粘剂板块毛利率是多少?** +**A:** 胶粘剂板块毛利率19.59%。 + +**Q45:环氧胶类与聚氨酯胶类毛利率分别是多少?** +**A:** 环氧胶类毛利率15.64%;聚氨酯胶类毛利率22.16%。 + +**Q46:按销售模式看,直销与代销占比如何?** +**A:** 直销2,470,109,362.83元(88.45%);代销166,067,606.48元(5.95%);其他业务156,348,055.40元(5.60%)。 + +--- + +## 10. 主要客户与供应商集中度 + +**Q47:前五名客户集中度如何?** +**A:** 前五名客户合计销售金额959,309,115.17元,占年度销售总额比例34.35%。 + +**Q48:前五名供应商集中度如何?** +**A:** 前五名供应商合计采购金额488,605,336.23元,占年度采购总额比例24.91%。 + +--- + +## 11. 费用、研发与人员 + +**Q49:2023年四项期间费用(销售/管理/财务/研发)分别是多少?** +**A:** 销售费用103,138,352.65元;管理费用204,042,488.26元;财务费用68,578,978.79元;研发费用177,605,054.56元。 + +**Q50:上述费用同比大幅增长的主要原因(报告说明)是什么?** +**A:** 主要因工资及福利费、股权激励费用摊销,以及股权投资形成的合并子公司增加、融资额增加等因素。 + +**Q51:2023年研发人员规模与占比是多少?** +**A:** 研发人员486人,占比25.98%。 + +**Q52:研发人员增加的原因是什么?** +**A:** 公司收购赛英科技、上海晶材、大连齐化后,将其研发人员纳入统计。 + +**Q53:2023年员工总数与结构如何?** +**A:** 在职员工合计1,871人(母公司550人、主要子公司1,321人);专业构成含生产人员794人、技术人员547人、销售人员187人等;学历含博士16人、硕士90人、本科522人等(以原表为准)。 + +--- + +## 12. 现金流(合并口径) + +**Q54:2023年三大现金流净额分别是多少?** +**A:** 经营活动+9,829,020.97元;投资活动-871,553,996.98元;筹资活动+807,818,595.07元。 + +**Q55:现金及现金等价物净增加额是多少?主要原因是什么?** +**A:** -53,861,976.40元;报告说明主要因归还贷款和股份回购增加等。 + +**Q56:经营活动现金流净额同比上升的原因(报告说明)是什么?** +**A:** 主要系购买商品、接受劳务支付的现金减少所致。 + +--- + +## 13. 资产负债表要点(合并口径,2023年末) + +**Q57:2023年末主要资产项目有哪些?** +**A:** 货币资金522,558,226.31元;应收账款1,461,666,737.74元;存货792,860,649.81元;固定资产1,048,809,835.47元;在建工程843,036,382.32元;商誉776,599,927.54元;无形资产418,503,945.02元等。 + +**Q58:2023年末主要负债项目有哪些?** +**A:** 短期借款1,017,747,629.74元;一年内到期的非流动负债620,721,942.15元;长期借款823,275,169.86元;应付账款650,273,652.63元;应付票据349,570,289.51元等。 + +--- + +## 14. 资产受限情况(节选) + +**Q59:截至2023年末,受限资产合计是多少?** +**A:** 受限资产合计(账面余额)968,128,955.76元,合计(账面价值)757,235,603.35元(表中同时列示期初对比数)。 + +**Q60:受限资产主要包括哪些类型?** +**A:** 主要包括票据保证金/承兑汇票保证金、应收票据/应收款项融资质押、抵押的固定资产与无形资产、抵押的在建工程、质押的长期股权投资(如上海晶材67%股权、彩晶光电60.92%股权等融资安排)、以及应收账款质押等。 + +--- + +## 15. 投资并购与合并范围变动 + +**Q61:2023年合并范围有哪些重要变动?** +**A:** 报告披露新增纳入:惟新半导体(唐山)(2023年2月起)、南平鑫天宇(2023年4月起)、惟新科技(唐山)(2023年5月起)、赛英科技(2023年5月起)、晟铭建筑新材料(2023年6月起)、裕隆数智(2023年8月起)、上海晶材(2023年9月起)、大连齐化及大连齐化国际物流(2023年11月起);同时晟宇科技注销(2023年6月起不再纳入)。 + +**Q62:报告期内三项重大股权投资(收购)金额与标的是什么?** +**A:** 赛英科技100%股权:178,311,200.00元;上海晶材67%股权:388,600,000.00元;大连齐化51%股权:116,280,000.00元。 + +**Q63:资产减值与商誉相关情况的要点是什么?** +**A:** 报告列示资产减值-161,189,905.31元,主要因计提存货跌价准备以及上海晶材、必控科技、力源兴达、天津瑞宏等的商誉减值损失。 + +--- + +## 16. 募集资金与现金管理(摘要) + +**Q64:公司涉及哪些募集资金年度与规模?** +**A:** 2016年非公开发行:募集资金总额85,000万元、净额82,743.85万元;2022年非公开发行:募集资金总额70,000万元、净额69,125万元。 + +**Q65:截至2023年末,尚未使用募集资金余额与去向是什么?** +**A:** 2016年尚未使用余额559.11万元,存放募集资金专户;2022年尚未使用余额12,308.73万元,其中7,000万元用于暂时补充流动资金,剩余5,308.73万元存放募集资金专户。 + +**Q66:报告期内是否存在募集资金现金管理(委托理财)?** +**A:** 报告披露存在,银行理财产品(资金来源为募集资金)委托理财发生额14,000万元,期末未到期余额为0。 + +**Q67:报告期内是否发生变更募集资金投向的重要事项?** +**A:** 报告披露变更部分募集资金投向11,628万元用于收购大连齐化部分股权并增资,增资完成后合计持有大连齐化51%股权。 + +--- + +## 17. 公司治理与组织(摘录) + +**Q68:2023年公司董事会结构有何变化?** +**A:** 为匹配发展战略与业务需求,将董事会人数由9名增加至13名,并增补新材料、电子信息、战略管理等领域专家。 + +**Q69:2023年公司是否披露ESG相关信息?** +**A:** 报告披露发布《2022年度环境、社会与公司治理报告》,万得ESG评级最新得分提升至A。 + +--- + +## 18. 风险提示(节选) + +**Q70:报告提到的“市场竞争加剧风险”是什么?** +**A:** 胶粘剂行业市场集中度不高、竞争充分,常规型产品竞争加剧,同时国际企业在国内布局;若公司不能保持技术/服务创新与品牌影响力,可能面临客户流失与市场份额下降风险。 + +--- + +## 19. 资产负债表日后事项(节选) + +**Q71:资产负债表日后利润分配如何披露?** +**A:** 披露拟每10股派息0.7元;并以总股本305,402,973股扣减回购专户8,242,400股后,基数297,160,573股测算,拟派发现金红利20,801,240.11元。 + +**Q72:资产负债表日后与上海晶材业绩补偿相关事项是什么?** +**A:** 披露上海晶材2023年度未完成业绩承诺,交易对方补偿金额15,544万元(现金与股权形式,其中股权部分对应上海晶材13.4%股权);公司对相关商誉计提减值准备12,245.90万元,并将补偿款确认为以公允价值计量且其变动计入当期损益的金融资产,确认公允价值变动损益12,188.62万元(作为资产负债表日后调整事项)。 + +--- + +## 20. 快速检索:关键数字一览 + +* 营业收入(2023):2,792,525,024.71 元 +* 归母净利润(2023):30,315,165.24 元 +* 扣非归母净利润(2023):-150,073,608.10 元 +* 非经常性损益合计(2023):180,388,773.34 元 +* 经营活动现金流净额(2023):9,829,020.97 元 +* 投资活动现金流净额(2023):-871,553,996.98 元 +* 筹资活动现金流净额(2023):807,818,595.07 元 +* 总资产(2023年末):7,122,936,764.42 元 +* 归母净资产(2023年末):2,976,876,747.22 元 +* 研发费用(2023):177,605,054.56 元(占营收 6.36%) diff --git a/ruoyi-fastapi-backend/md/康达新材:2024年年度报告(Q&A版).md b/ruoyi-fastapi-backend/md/康达新材:2024年年度报告(Q&A版).md new file mode 100644 index 0000000..ef5a2ab --- /dev/null +++ b/ruoyi-fastapi-backend/md/康达新材:2024年年度报告(Q&A版).md @@ -0,0 +1,317 @@ +# 康达新材:2024年年度报告(Q&A版) + +> 说明:本文为对 `md/康达新材:2024年年度报告.md` 的要点抽取与问答化重组,便于检索与知识库问答使用;问题为整理者归纳,答案以原文表述与数据为准。 + +--- + +## 1. 重要提示与利润分配 + +**Q1:年度报告由谁保证真实、准确、完整?** +**A:** 公司董事会、监事会及董事、监事、高级管理人员保证年度报告内容真实、准确、完整,不存在虚假记载、误导性陈述或重大遗漏,并承担个别和连带的法律责任。 + +**Q2:公司负责人及财务负责人对财务报告作了什么声明?** +**A:** 公司负责人王建祥、主管会计工作负责人宋兆庆及会计机构负责人(会计主管人员)张国强声明:保证本年度报告中财务报告真实、准确、完整。 + +**Q3:董事出席情况如何?** +**A:** 所有董事均已出席审议本报告的董事会会议。 + +**Q4:前瞻性陈述意味着什么?** +**A:** 报告所涉未来计划、发展战略等前瞻性陈述不构成公司对投资者的实质承诺,投资者及相关人士应保持风险认识,并理解计划、预测与承诺之间的差异。 + +**Q5:公司是否提示了经营风险?** +**A:** 报告第三节“管理层讨论与分析”中“公司面临的风险和应对措施”部分描述了公司未来经营中可能面临的风险,提示投资者关注。 + +**Q6:2024年度利润分配(现金分红/送转)计划是什么?** +**A:** 公司计划不派发现金红利,不送红股,不以公积金转增股本。 + +--- + +## 2. 公司概况与信息披露 + +**Q7:公司简称、股票代码与上市交易所是什么?** +**A:** 股票简称:康达新材;股票代码:002669;上市交易所:深圳证券交易所。 + +**Q8:公司名称、法定代表人、注册地址/办公地址是什么?** +**A:** 公司名称:康达新材料(集团)股份有限公司;法定代表人:王建祥;注册地址/办公地址:上海市奉贤区雷州路169号。 + +**Q9:公司网址与电子信箱是什么?** +**A:** 公司网址:;电子信箱:kdxc@shkdchem.com。 + +**Q10:董事会秘书与证券事务代表是谁?联系方式是什么?** +**A:** 董事会秘书:沈一涛;证券事务代表:刘洁;联系地址:上海市浦东新区五星路707弄御河企业公馆A区3号楼;电话:021-50770196 / 021-50779159;传真:021-50770183;邮箱:kdxc@shkdchem.com。 + +**Q11:年度报告披露渠道与备置地点在哪里?** +**A:** 披露网站:深圳证券交易所(https://www.szse.cn)与巨潮资讯网(http://www.cninfo.com.cn);披露媒体:《证券时报》《证券日报》《中国证券报》《上海证券报》;备置地点:上海市浦东新区五星路707弄御河企业公馆A区3号楼董事会办公室。 + +**Q12:公司控股股东与实际控制人是谁?** +**A:** 控股股东为唐山工业控股集团有限公司(唐山工控,原名唐山金控产业发展集团有限公司);实际控制人为唐山市国资委。 + +--- + +## 3. 主营业务与战略定位(摘要) + +**Q13:公司主要业务板块是什么?** +**A:** 公司主要业务分为胶粘剂与特种树脂新材料、电子信息材料、电子科技三大板块。 + +**Q14:胶粘剂业务主要产品及应用领域有哪些?** +**A:** 产品涵盖环氧树脂胶、聚氨酯胶、丙烯酸酯胶、SBS胶、热熔胶、水性胶等,并包括环氧树脂复合材料、聚氨酯复合材料及聚酰亚胺材料等新材料系列,应用于风电叶片制造、包装材料、轨道交通、船舶工程、汽车、电子电器、机械设备、建筑装饰及工业维修等。 + +**Q15:合成树脂业务(大连齐化)主要产品系列是什么?** +**A:** 产品主要分为双酚A型环氧树脂、耐热型环氧树脂和特种环氧树脂三大系列,涵盖双酚A型液体/固体、溴化、邻甲酚醛等品种,可应用于复合材料、涂料与涂层、粘合剂、电子电气等领域。 + +**Q16:电子信息材料业务覆盖哪些产品方向?** +**A:** 电子信息材料领域包含液晶显示材料、医药中间体、陶瓷生料带、贵金属浆料、特种显示材料及电子化学品等研发、生产与销售。 + +**Q17:电子科技业务覆盖哪些产品方向?** +**A:** 电子科技领域主营为电磁兼容、电源模块、微波组件、雷达相关整机等电子产品的研发、生产与销售。 + +**Q18:公司总体战略(报告表述)如何概括?** +**A:** 报告提出以“新材料+电子科技”为主线,打造国家级专精特新“小巨人”企业集群;推动从单一胶粘剂产品向高端新材料、从核心零部件向系统集成、从国内为主向国际化布局的转型。 + +--- + +## 4. 行业环境(摘录) + +**Q19:胶粘剂行业“十四五”目标(报告引用)是什么?** +**A:** 产量年均增长率约4.2%,销售额年均增长率约4.3%;到2025年末行业高附加值产品产值比例达到40%以上,并推动水基、热熔、无溶剂等绿色方向。 + +**Q20:环氧树脂行业的供需与国产替代机会(报告观点)是什么?** +**A:** 电子工业、汽车产业等带动需求增长;适用性强、性能稳定的高品质环氧树脂仍供不应求,部分电子元件封装、覆铜板等领域产品仍依赖进口,具备国产替代机会。 + +**Q21:LTCC材料国产化为何被认为“需求迫切”?** +**A:** LTCC材料在材料一致性、高频损耗率等指标上,上游原材料市场长期被海外厂商主导,具备规模化稳定供货能力并获市场认可的国内企业仍稀缺;在高性能雷达自主化、航天载荷轻量化等战略领域将释放市场空间。 + +**Q22:ITO靶材为何被描述为“卡脖子”技术之一?** +**A:** 溅射靶材行业技术壁垒与客户认证壁垒高,全球高纯溅射靶材市场长期被海外厂商控制;大尺寸高性能ITO靶材被报道为“卡脖子”技术之一,并列入工信部重点新材料首批次应用示范指导目录(2019年版)。 + +**Q23:军工电子行业需求驱动(报告引用)是什么?** +**A:** 国防支出稳步增长为装备发展提供支撑;报告引用“2025年全国一般公共预算安排国防支出1.81万亿元,同比增7.2%”(来源:2024年3月5日公布的预算执行情况与预算草案报告)。 + +--- + +## 5. 2024年经营回顾(要点) + +**Q24:2024年公司营业收入与利润总体表现如何?** +**A:** 营业收入3,101,062,179.20元,同比增长11.05%;归母净利润-246,173,519.93元,同比下降-912.05%;扣非归母净利润-308,264,197.06元。 + +**Q25:公司解释的亏损主要原因有哪些?** +**A:** 报告指出主要原因包括:对存在减值迹象的子公司商誉资产组组合进行减值测试并计提商誉减值;受市场环境影响,电子信息材料、电子科技板块下游客户订单交付减少或延期;新建生产基地处于产能爬坡阶段,对应固定资产折旧及融资贷款利息费用较大等。 + +**Q26:风电叶片制造领域(胶粘剂板块)有哪些关键进展?** +**A:** 报告称国内市场份额第一地位稳固;风电结构胶全年销售量4万吨;高性能胶D-2新品全面推出,销量提升至结构胶整体销量的17%;风电环氧灌注树脂全年销售量4.5万吨,同比上升90%以上,连续三年实现翻倍增长,市场份额提升至12%,晋升行业前四;并推进海外工厂认证/验证(如LM Windpower、GE、Nordex等)。 + +**Q27:包装领域业务有哪些进展?** +**A:** 报告称无溶剂复膜胶销售量超万吨、市场占有率保持领先;组建专业技术服务团队,为客户定制复膜胶解决方案;并持续推进油墨产品研发体系与整体解决方案能力。 + +**Q28:电子信息材料板块在国产替代方面有哪些进展?** +**A:** 惟新科技建成30吨高纯度ITO靶材生产线,完成OLED面板头部企业G6代线ITO靶材生产验证,关键性能指标优于行业同等规格;并完成河北省重点研发计划课题验收;同时推进CMP(氧化铈)抛光液核心技术攻关。 + +**Q29:电子科技板块在“低空经济/安防”等方向有哪些描述?** +**A:** 报告称赛英科技通过“军民协同、双向赋能”推进市场突破;FOD雷达在大兴机场应用多年并形成国家标准,国内机场将逐步列装;4D凝视雷达用于“低慢小”目标监控任务并在反无系统中大量应用。 + +--- + +## 6. 主要会计数据与财务指标(合并口径) + +**Q30:2024/2023营业收入分别是多少?** +**A:** 2024年3,101,062,179.20元;2023年2,792,525,024.71元。 + +**Q31:2024年归母净利润与扣非归母净利润是多少?** +**A:** 归母净利润-246,173,519.93元;扣非归母净利润-308,264,197.06元。 + +**Q32:2024年经营活动现金流净额是多少?** +**A:** 498,667,285.91元。 + +**Q33:2024年基本/稀释每股收益与加权ROE是多少?** +**A:** 基本每股收益-0.824元/股;稀释每股收益-0.824元/股;加权平均净资产收益率-8.59%。 + +**Q34:2024年末总资产与归母净资产是多少?** +**A:** 总资产6,940,150,195.69元;归属于上市公司股东的净资产2,727,894,289.21元。 + +**Q35:2024年营业收入扣除前后金额是多少?** +**A:** 营业收入扣除金额132,825,438.14元;扣除后金额2,968,236,741.06元。 + +--- + +## 7. 分季度主要财务指标(合并口径) + +**Q36:2024年分季度营业收入分别是多少?** +**A:** Q1 535,607,900.26元;Q2 826,777,196.06元;Q3 816,474,450.24元;Q4 922,202,632.64元。 + +**Q37:2024年分季度归母净利润分别是多少?** +**A:** Q1 -24,793,535.64元;Q2 -31,717,467.21元;Q3 -19,966,135.18元;Q4 -169,696,381.90元。 + +**Q38:2024年分季度经营活动现金流净额分别是多少?** +**A:** Q1 73,400,210.80元;Q2 214,356,837.30元;Q3 139,941,585.62元;Q4 70,968,652.19元。 + +--- + +## 8. 非经常性损益(2024年) + +**Q39:2024年非经常性损益合计是多少?** +**A:** 62,090,677.13元。 + +**Q40:2024年非经常性损益主要由哪些项目构成?** +**A:** 主要包括: +* 非流动性资产处置损益40,430,252.31元(主要系报告期内转让彩晶光电产生的处置收益)。 +* 计入当期损益的政府补助25,980,131.98元。 +* 公允价值变动损益等269,346.81元。 +* 单独进行减值测试的应收款项减值准备转回3,177,638.34元。 +* 其他营业外收入和支出-3,759,143.23元。 +* 减:所得税影响额2,303,077.58元;少数股东权益影响额(税后)1,704,471.50元。 + +--- + +## 9. 营业收入结构与毛利(2024年) + +**Q41:按行业看,2024年营业收入构成如何?** +**A:** 胶粘剂2,252,028,435.31元(72.62%);合成树脂189,307,031.23元(6.10%);电子产品262,819,931.17元(8.48%);复合材料15,841,726.18元(0.51%);显示材料90,438,229.45元(2.92%);医药中间体102,507,074.51元(3.31%);电子化学品11,390,131.19元(0.37%);LTCC材料43,904,182.03元(1.42%);其他业务132,825,438.13元(4.28%)。 + +**Q42:按产品看,2024年主要产品收入有哪些?** +**A:** 环氧胶类1,570,432,237.01元(50.64%);聚氨酯胶类312,452,673.98元(10.08%);丙烯酸胶类92,683,079.29元;SBS胶类110,022,477.65元;水性胶类56,581,632.93元;合成树脂189,307,031.23元;电源模块83,375,168.43元;电磁兼容产品86,020,868.14元;微波组件及系统93,423,894.60元;LTCC材料43,904,182.03元等。 + +**Q43:按地区看,2024年收入分布有哪些重点?** +**A:** 华北地区895,953,680.06元(28.89%);华东地区782,609,545.75元(25.24%);华中地区338,200,626.93元(10.91%);国外地区91,915,079.33元(2.96%)等(以原表为准)。 + +**Q44:按销售模式看,直销与分销占比如何?** +**A:** 直销2,809,752,244.68元(90.61%);分销158,484,496.39元(5.11%);其他业务132,825,438.13元(4.28%)。 + +**Q45:占比10%以上的业务/产品毛利率如何?** +**A:** 胶粘剂毛利率17.84%;环氧胶类毛利率15.92%;聚氨酯胶类毛利率27.88%;直销毛利率16.18%(对应表中同比变动亦已披露)。 + +**Q46:报告期主营业务统计口径有什么变化?** +**A:** 2024年将“合成树脂”从胶粘剂行业与环氧胶类产品中单独列示;口径变化原因之一是2024年合并大连齐化全年数据且列示为对外销售形成的营业收入。 + +--- + +## 10. 产销数据(同比大幅变动项) + +**Q47:胶粘剂与合成树脂的销量同比为何变化较大?** +**A:** 胶粘剂产品销量/产量增加主要为风电结构胶、灌注树脂及其他胶种市场开拓所致,库存量减少主要为年底销量增幅较快所致;合成树脂增幅较大主要为2023年11月大连齐化纳入合并范围,而2024年合并大连齐化全年数据所致。 + +--- + +## 11. 主要客户与供应商集中度 + +**Q48:前五名客户集中度如何?** +**A:** 前五名客户合计销售金额1,170,113,577.70元,占年度销售总额比例37.73%。 + +**Q49:前五名供应商集中度与关联采购占比如何?** +**A:** 前五名供应商合计采购金额840,525,802.96元,占年度采购总额比例35.30%;前五名供应商采购额中关联方采购额占年度采购总额比例21.93%。 + +--- + +## 12. 期间费用、研发与人员 + +**Q50:2024年销售/管理/财务/研发费用分别是多少?** +**A:** 销售费用102,000,705.02元;管理费用232,581,720.53元;财务费用95,545,381.14元;研发费用203,525,998.94元。 + +**Q51:研发投入规模与占比如何?** +**A:** 研发投入金额203,525,998.94元,占营业收入比例6.56%。 + +**Q52:研发人员规模与占比是多少?** +**A:** 研发人员376人,占比22.97%。 + +**Q53:研发人员较上年减少的原因是什么?** +**A:** 报告说明:公司出售彩晶光电股权,彩晶光电不再纳入公司合并报表范围,其研发人员未统计入2024年。 + +**Q54:报告期末员工总数与结构如何?** +**A:** 在职员工合计1,637人(母公司546人、主要子公司1,091人);专业构成含生产人员662人、销售人员188人、技术人员444人等;学历含博士12人、硕士70人、本科483人等(以原表为准)。 + +--- + +## 13. 现金流(合并口径) + +**Q55:2024年三大现金流净额分别是多少?** +**A:** 经营活动+498,667,285.91元;投资活动-358,595,995.08元;筹资活动+137,480,803.15元。 + +**Q56:2024年现金及现金等价物净增加额是多少?** +**A:** 276,194,234.34元。 + +**Q57:经营/投资/筹资现金流变化的主要原因(报告说明)是什么?** +**A:** 经营性净流入增加主要系购买商品、接受劳务支付的现金减少;投资活动净流出同比减少主要系处置子公司取得现金收入增加、取得子公司及联营单位支付现金减少;筹资活动净流入同比减少主要系偿还银行借款及利息支出增加。 + +--- + +## 14. 资产负债表要点(合并口径,2024年末) + +**Q58:2024年末主要资产项目有哪些?** +**A:** 货币资金799,834,198.31元;应收账款1,877,790,764.09元;存货545,690,424.46元;固定资产1,116,686,210.01元;在建工程715,274,766.37元;商誉437,307,088.79元;无形资产328,426,810.26元等。 + +**Q59:2024年末主要负债项目有哪些?** +**A:** 短期借款1,042,105,372.47元;应付票据570,729,889.37元;应付账款941,500,280.13元;一年内到期的非流动负债390,026,280.20元;长期借款772,284,891.01元等。 + +**Q60:报告期内重大资产减值情况是什么?** +**A:** 报告列示资产减值损失-194,887,938.64元,并说明主要系计提存货跌价准备以及上海晶材、必控科技、力源兴达的商誉减值损失等。 + +--- + +## 15. 公允价值计量与业绩补偿(摘要) + +**Q61:2024年末交易性金融资产(业绩补偿款)余额是多少?** +**A:** 交易性金融资产121,886,200.00元(业绩承诺补偿款)。 + +**Q62:报告对交易性金融资产“其他变动”的说明是什么?** +**A:** 报告说明:交易性金融资产(不含衍生金融资产)其他变动是上海晶材原股东未完成业绩承诺所支付业绩补偿。 + +--- + +## 16. 资产权利受限情况(摘要) + +**Q63:截至2024年末,受限资产合计是多少?** +**A:** 合计(账面余额)999,166,089.97元;合计(账面价值)848,820,322.87元。 + +**Q64:受限资产主要包括哪些类型?** +**A:** 主要包括:抵押的固定资产/无形资产/在建工程,质押的长期股权投资、应收款项融资质押、应收账款质押,以及票据保证金等(明细见原表“期末说明”)。 + +--- + +## 17. 合并范围变动与重大股权交易 + +**Q65:2024年合并范围发生了哪些主要变动?** +**A:** 报告披露:璟创投资、康达新材料(保定)、裕隆数智、顺璟投资、万斯新材料等注销或退出不再纳入;新增纳入康达新材(泰国);2024年12月增资取得贵州盛顺82%并间接取得贵州麟拓63%纳入合并;并披露出售彩晶光电股权后将自2025年1月起不再纳入。 + +**Q66:出售彩晶光电股权的交易对手、比例与价格是多少?** +**A:** 公司全资子公司新材料科技向唐山裕隆光电科技有限公司出售所持西安彩晶光电科技股份有限公司66.9996%股权,对价为人民币40,588.00万元。 + +**Q67:出售彩晶光电股权对资金用途的影响(报告说明)是什么?** +**A:** 报告说明:不影响正常生产经营活动;所得资金将用于归还银行贷款及补充流动资金,降低管理成本,提高盈利能力。 + +--- + +## 18. 募集资金使用(摘要) + +**Q68:公司涉及哪些募集资金年度与规模?** +**A:** 2016年向特定对象发行股票:募集资金总额85,000万元、净额82,743.85万元;2022年向特定对象发行股票:募集资金总额70,000万元、净额69,125万元。 + +**Q69:截至2024年末,尚未使用募集资金余额与去向是什么?** +**A:** 2016年募集资金已使用完毕;2022年募集资金尚未使用余额4,953.67万元,其中4,500万元用于暂时补充流动资金,剩余453.67万元存放于募集资金专户。 + +--- + +## 19. 公司治理与ESG(摘录) + +**Q70:公司在分红政策与股东回报规划方面有哪些安排?** +**A:** 报告披露:修订《公司章程》中现金分红相关条款,并制定《未来三年股东分红回报规划(2024-2026年)》。 + +**Q71:报告披露的ESG相关信息与奖项有哪些?** +**A:** 报告披露公司发布《2023年度环境、社会与公司治理报告》、万得ESG评级保持A级,并获得“ESG金牛奖百强”等。 + +**Q72:公司总股本在2024年是否发生变化?原因是什么?** +**A:** 报告披露2024年6月完成注销部分回购股份2,002,973股(占注销前总股本0.66%),注销完成后总股本由305,402,973股变更为303,400,000股。 + +--- + +## 20. 快速检索:关键数字一览 + +* 营业收入(2024):3,101,062,179.20 元 +* 归母净利润(2024):-246,173,519.93 元 +* 扣非归母净利润(2024):-308,264,197.06 元 +* 非经常性损益合计(2024):62,090,677.13 元 +* 经营活动现金流净额(2024):498,667,285.91 元 +* 投资活动现金流净额(2024):-358,595,995.08 元 +* 筹资活动现金流净额(2024):137,480,803.15 元 +* 现金及现金等价物净增加额(2024):276,194,234.34 元 +* 总资产(2024年末):6,940,150,195.69 元 +* 归母净资产(2024年末):2,727,894,289.21 元 +* 研发费用(2024):203,525,998.94 元(占营收 6.56%) diff --git a/ruoyi-fastapi-backend/md/康达新材:2025年一季度报告(Q&A版).md b/ruoyi-fastapi-backend/md/康达新材:2025年一季度报告(Q&A版).md new file mode 100644 index 0000000..75d7cda --- /dev/null +++ b/ruoyi-fastapi-backend/md/康达新材:2025年一季度报告(Q&A版).md @@ -0,0 +1,220 @@ +# 康达新材:2025年第一季度报告(Q&A版) + +> 说明:本文为对 `md/康达新材:2025年一季度报告.md` 的要点抽取与问答化重组,便于检索与知识库问答使用;问题为整理者归纳,答案以原文表述与数据为准。 + +--- + +## 1. 重要提示与审计情况 + +**Q1:公司对一季度报告信息披露作了什么承诺?** +**A:** 本公司及董事会全体成员保证信息披露内容真实、准确、完整,没有虚假记载、误导性陈述或重大遗漏。 + +**Q2:谁对季度报告的真实、准确、完整承担责任?** +**A:** 董事会、监事会及董事、监事、高级管理人员保证季度报告真实、准确、完整,不存在虚假记载、误导性陈述或重大遗漏,并承担个别和连带的法律责任。 + +**Q3:公司负责人及财务负责人对财务信息作了什么声明?** +**A:** 公司负责人、主管会计工作负责人及会计机构负责人(会计主管人员)声明:保证季度报告中财务信息真实、准确、完整。 + +**Q4:2025年第一季度报告是否经过审计?** +**A:** 否,第一季度报告未经审计。 + +--- + +## 2. 主要会计数据与财务指标(合并口径) + +**Q5:2025年第一季度营业收入是多少?同比如何?** +**A:** 营业收入877,010,301.56元;上年同期535,607,900.26元;同比增长63.74%。 + +**Q6:2025年第一季度归母净利润是多少?同比如何?** +**A:** 归属于上市公司股东的净利润6,371,842.98元;上年同期-24,793,535.64元;同比125.70%。 + +**Q7:2025年第一季度扣非归母净利润是多少?同比如何?** +**A:** 扣除非经常性损益的归母净利润3,363,744.00元;上年同期-26,427,485.42元;同比112.73%。 + +**Q8:2025年第一季度经营活动现金流净额是多少?同比如何?** +**A:** 经营活动产生的现金流量净额-243,787,510.86元;上年同期73,400,210.80元;同比-432.13%。 + +**Q9:2025年第一季度每股收益(基本/稀释)是多少?** +**A:** 基本每股收益0.021元/股;稀释每股收益0.021元/股(上年同期均为-0.083元/股)。 + +**Q10:2025年第一季度加权平均净资产收益率是多少?** +**A:** 0.24%(上年同期-0.84%)。 + +**Q11:2025年3月31日总资产与归母净资产是多少?较期初如何变化?** +**A:** 总资产6,747,360,816.59元,较2024年末6,940,150,195.69元下降2.78%;归属于上市公司股东的所有者权益2,724,565,896.80元,较2024年末2,727,894,289.21元下降0.12%。 + +--- + +## 3. 非经常性损益(2025年一季度) + +**Q12:本报告期非经常性损益合计是多少?** +**A:** 3,008,098.98元。 + +**Q13:非经常性损益中“非流动性资产处置损益”是多少?** +**A:** 707,479.18元。 + +**Q14:计入当期损益的政府补助是多少?** +**A:** 1,064,112.95元。 + +**Q15:本期金融资产/金融负债相关公允价值变动及处置损益(报告说明)是什么?** +**A:** 2,000,000.00元,报告说明为“收到参股子公司上年度分红款”。 + +**Q16:单独进行减值测试的应收款项减值准备转回是多少?** +**A:** 20,000.00元。 + +**Q17:其他营业外收入和支出净额是多少?** +**A:** -252,725.49元。 + +**Q18:非经常性损益的所得税影响额与少数股东权益影响额(税后)分别是多少?** +**A:** 所得税影响额529,763.22元;少数股东权益影响额(税后)1,004.44元。 + +**Q19:是否存在其他符合非经常性损益定义的项目或将其界定为经常性损益的情形?** +**A:** 不存在。 + +--- + +## 4. 主要指标变动原因(报告披露) + +### 4.1 资产负债表项目变动 + +**Q20:货币资金为何下降?下降多少?** +**A:** 2025年3月31日495,660,929.09元,较2024年末799,834,198.31元下降38.03%;主要系报告期内归还银行贷款和支付原材料采购款增加所致。 + +**Q21:预付款项为何上升?上升多少?** +**A:** 32,868,552.07元,较2024年末20,329,269.33元上升61.68%;主要系预付原材料采购款增加所致。 + +**Q22:其他应收款为何大幅下降?** +**A:** 16,975,655.72元,较2024年末170,367,423.35元下降90.04%;主要系收到出售子公司股权转让款所致。 + +**Q23:投资性房地产为何为0?** +**A:** 期末为0.00元,较2024年末4,606,245.07元下降100.00%;主要系对外出租房屋减少所致。 + +**Q24:固定资产为何上升、在建工程为何下降?** +**A:** 固定资产1,497,341,452.78元,较2024年末1,116,686,210.01元上升34.09%;在建工程318,514,634.19元,较2024年末715,274,766.37元下降55.47%;主要系在建工程转固所致。 + +**Q25:其他非流动资产为何上升?** +**A:** 22,099,433.13元,较2024年末9,721,396.37元上升127.33%;主要系预付设备款、工程款增加所致。 + +**Q26:预收款项为何大幅下降、合同负债为何大幅上升?** +**A:** 预收款项178,176.12元,较2024年末70,466,949.54元下降99.75%,主要系预收客户货款减少(合同未签订);合同负债120,156,064.23元,较2024年末35,733,227.19元上升236.26%,主要系预收客户货款增加(合同已签订)。 + +**Q27:其他应付款为何下降?** +**A:** 9,858,085.28元,较2024年末14,836,889.80元下降33.56%;主要系往来款减少所致。 + +**Q28:库存股为何上升?** +**A:** 36,909,368.73元,较2024年末23,668,633.73元上升55.94%;主要系报告期内公司回购股份增加所致。 + +**Q29:其他综合收益为何上升?** +**A:** 446,738.51元,较2024年末142,776.11元上升212.89%;主要系外币财务报表折算差额增加所致。 + +### 4.2 利润表项目变动 + +**Q30:营业收入同比大幅增长的原因是什么?** +**A:** 主要系胶粘剂产品中风电胶系列产品订单增加等因素影响,公司销售规模有所上升。 + +**Q31:营业成本同比上升的原因是什么?** +**A:** 主要系营业收入增加,营业成本随之增加所致;本期737,923,975.48元,上年同期436,730,929.55元,同比+68.97%。 + +**Q32:信用减值损失为何由正转负?** +**A:** 本期信用减值损失-3,694,467.19元,上年同期6,345,252.90元;主要系期末应收款项增减变动导致计提减值同步变动所致。 + +**Q33:资产处置收益为何增加?** +**A:** 本期资产处置收益707,844.97元,上年同期217,756.26元,同比+225.06%;主要系处置无法使用的资产增加所致。 + +**Q34:所得税费用为何增长?** +**A:** 本期所得税费用8,903,252.97元,上年同期2,562,419.33元,同比+247.45%;主要系利润总额增加导致所得税费用增加。 + +**Q35:归母净利润同比转正的原因是什么?** +**A:** 报告说明主要系营业收入大幅增加,导致净利润同时增加所致。 + +**Q36:其他综合收益的税后净额为何大幅增加?** +**A:** 本期303,962.40元,上年同期286.65元;主要系外币财务报表折算差额增加所致。 + +--- + +## 5. 股东信息(截至2025年3月31日) + +**Q37:报告期末普通股股东总数是多少?** +**A:** 22,893户。 + +**Q38:控股股东是谁?持股比例与数量是多少?是否存在质押?** +**A:** 唐山工业控股集团有限公司;持股87,421,621股,占28.81%;质押39,918,579股。 + +**Q39:报告披露的前10名股东(部分)有哪些?** +**A:** 例如:第四期员工持股计划(4,673,900股,1.54%)、耿殿根(4,578,168股,1.51%)、张立岗(4,324,750股,1.43%)、第五期员工持股计划(4,289,000股,1.41%)、第三期员工持股计划(3,241,100股,1.07%)等(以原表为准)。 + +**Q40:前十名股东之间是否存在关联关系或一致行动关系?** +**A:** 公司未知前十名股东之间是否存在关联关系,也未知是否属于一致行动人。 + +**Q41:是否披露融资融券参与情况?** +**A:** 披露:股东程洁通过信用账户持有1,667,000股;股东周新勇通过信用账户持有1,567,800股。 + +--- + +## 6. 其他重要事项(报告披露) + +**Q42:公司董事会、监事会换届及高管选举的时间节点是什么?** +**A:** 2025年1月24日召开第五届董事会第四十一次会议、第五届监事会第三十三次会议审议换届相关议案,并提交2025年2月10日临时股东大会审议通过;2025年2月11日召开第六届董事会第一次会议、第六届监事会第一次会议选举董事长、监事会主席及高级管理人员。 + +**Q43:第八期股份回购方案的核心参数是什么?** +**A:** 回购价格不超过15.00元/股(含);回购资金总额不低于10,000万元(含)、不高于20,000万元(含);资金来源为自有资金及回购专项贷款。 + +**Q44:首次实施回购发生在何时?回购了多少股、成交金额多少?** +**A:** 2025年3月4日首次实施回购;回购284,000股(占总股本0.0936%),最高9.87元/股、最低9.57元/股;成交金额2,760,130元(不含交易费用)。 + +**Q45:截至2025年3月31日累计回购多少股、金额多少?** +**A:** 累计回购1,372,300股(占总股本0.4523%),最高9.99元/股、最低9.29元/股;成交金额13,240,735元(不含交易费用)。 + +**Q46:报告披露的“回购比例达到2%”事项是什么?** +**A:** 公司于2025年4月15日披露《关于回购股份比例达到2%暨回购进展公告》,累计回购股份数量已超过公司总股本的2%,后续将根据市场情况继续按方案实施。 + +**Q47:控股股东增持计划(2024/2025两段)主要内容是什么?** +**A:** 2024年7月9日至2025年1月8日,唐山工控以集中竞价累计增持3,266,500股,占总股本1.08%,增持金额约3,010.75万元;2025年1月18日起6个月内继续增持,计划金额不低于1,000万元且不超过2,000万元(方式包括集中竞价和大宗交易等);截至目前唐山工控持股87,421,621股,占28.81%。 + +**Q48:报告披露的关联交易/共同投资事项是什么?** +**A:** 2025年1月24日审议通过:全资子公司必控科技与关联方以0元对价共同收购四川忠华智能科技有限公司股权并增资;唐控智能认购注册资本2,800万元持股35%,必控科技认购2,800万元持股35%(其中固定资产作价2,600万元),风范晶樱认购1,600万元持股20%,海南远领与海南可为分别认购400万元各持股5%;截至目前工商变更登记手续已办理完毕。 + +--- + +## 7. 财务报表关键数字速查(合并口径) + +**Q49:合并资产负债表中,期末主要资产项目有哪些关键数值?** +**A:** 货币资金495,660,929.09元;应收票据266,700,534.77元;应收账款2,034,639,856.21元;应收款项融资208,971,494.43元;存货619,751,020.17元;固定资产1,497,341,452.78元;在建工程318,514,634.19元;无形资产320,537,081.21元;商誉437,307,088.79元。 + +**Q50:合并资产负债表中,期末主要负债项目有哪些关键数值?** +**A:** 短期借款1,070,196,811.34元;应付票据449,338,097.20元;应付账款1,080,690,587.96元;合同负债120,156,064.23元;一年内到期的非流动负债359,701,421.02元;长期借款560,844,522.99元。 + +**Q51:期末归母所有者权益合计是多少?少数股东权益是多少?** +**A:** 归属于母公司所有者权益合计2,724,565,896.80元;少数股东权益173,340,070.82元。 + +**Q52:合并利润表中,营业利润、利润总额与净利润分别是多少?** +**A:** 营业利润11,044,437.99元;利润总额10,791,346.71元;净利润1,888,093.74元。 + +**Q53:归属母公司净利润与少数股东损益分别是多少?** +**A:** 归属于母公司所有者的净利润6,371,842.98元;少数股东损益-4,483,749.24元。 + +**Q54:合并现金流量表中,三大现金流净额分别是多少?** +**A:** 经营活动现金流净额-243,787,510.86元;投资活动现金流净额92,049,529.25元;筹资活动现金流净额-164,746,680.98元。 + +**Q55:投资活动现金流中,处置子公司及其他营业单位收到的现金净额是多少?** +**A:** 162,352,000.00元。 + +**Q56:筹资活动现金流中,取得借款收到的现金、偿还债务支付的现金分别是多少?** +**A:** 取得借款收到的现金478,085,262.74元;偿还债务支付的现金660,256,118.51元。 + +**Q57:本期现金及现金等价物净增加额、期末余额分别是多少?** +**A:** 净增加额-316,378,429.93元;期末余额405,856,258.35元。 + +--- + +## 8. 快速检索:关键指标一览 + +* 营业收入(2025Q1):877,010,301.56 元 +* 归母净利润(2025Q1):6,371,842.98 元 +* 扣非归母净利润(2025Q1):3,363,744.00 元 +* 非经常性损益合计(2025Q1):3,008,098.98 元 +* 经营活动现金流净额(2025Q1):-243,787,510.86 元 +* 总资产(2025-03-31):6,747,360,816.59 元 +* 归母净资产(2025-03-31):2,724,565,896.80 元 +* 普通股股东总数:22,893 +* 控股股东持股:87,421,621 股(28.81%) diff --git a/ruoyi-fastapi-backend/middlewares/__init__.py b/ruoyi-fastapi-backend/middlewares/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ruoyi-fastapi-backend/middlewares/cors_middleware.py b/ruoyi-fastapi-backend/middlewares/cors_middleware.py new file mode 100644 index 0000000..55508e7 --- /dev/null +++ b/ruoyi-fastapi-backend/middlewares/cors_middleware.py @@ -0,0 +1,25 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + + +def add_cors_middleware(app: FastAPI): + """ + 添加跨域中间件 + + :param app: FastAPI对象 + :return: + """ + # 前端页面url + origins = [ + 'http://localhost:80', + 'http://127.0.0.1:80', + ] + + # 后台api允许跨域 + app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=['*'], + allow_headers=['*'], + ) diff --git a/ruoyi-fastapi-backend/middlewares/gzip_middleware.py b/ruoyi-fastapi-backend/middlewares/gzip_middleware.py new file mode 100644 index 0000000..4a70a75 --- /dev/null +++ b/ruoyi-fastapi-backend/middlewares/gzip_middleware.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI + + +def add_gzip_middleware(app: FastAPI): + """ + 添加gzip压缩中间件 - 已禁用 + + 注意: 由于SSE流式响应需要实时传输,gzip压缩被禁用 + SSE数据流很小,压缩意义不大,反而会影响实时性 + + :param app: FastAPI对象 + :return: + """ + # 暂时禁用GZip中间件以支持SSE流式响应 + # 如果需要重新启用,请取消下面的注释 + # app.add_middleware(GZipMiddleware, minimum_size=1000, compresslevel=9) + pass \ No newline at end of file diff --git a/ruoyi-fastapi-backend/middlewares/handle.py b/ruoyi-fastapi-backend/middlewares/handle.py new file mode 100644 index 0000000..abb2d0d --- /dev/null +++ b/ruoyi-fastapi-backend/middlewares/handle.py @@ -0,0 +1,16 @@ +from fastapi import FastAPI +from middlewares.cors_middleware import add_cors_middleware +from middlewares.gzip_middleware import add_gzip_middleware +from middlewares.trace_middleware import add_trace_middleware + + +def handle_middleware(app: FastAPI): + """ + 全局中间件处理 + """ + # 加载跨域中间件 + add_cors_middleware(app) + # 加载gzip压缩中间件 + add_gzip_middleware(app) + # 加载trace中间件 + add_trace_middleware(app) diff --git a/ruoyi-fastapi-backend/middlewares/trace_middleware/__init__.py b/ruoyi-fastapi-backend/middlewares/trace_middleware/__init__.py new file mode 100644 index 0000000..76f8d85 --- /dev/null +++ b/ruoyi-fastapi-backend/middlewares/trace_middleware/__init__.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI +from .ctx import TraceCtx +from .middle import TraceASGIMiddleware + +__all__ = ('TraceASGIMiddleware', 'TraceCtx') + +__version__ = '0.1.0' + + +def add_trace_middleware(app: FastAPI): + """ + 添加trace中间件 + + :param app: FastAPI对象 + :return: + """ + app.add_middleware(TraceASGIMiddleware) diff --git a/ruoyi-fastapi-backend/middlewares/trace_middleware/ctx.py b/ruoyi-fastapi-backend/middlewares/trace_middleware/ctx.py new file mode 100644 index 0000000..558a5c9 --- /dev/null +++ b/ruoyi-fastapi-backend/middlewares/trace_middleware/ctx.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +""" +@author: peng +@file: ctx.py +@time: 2025/1/17 16:57 +""" + +import contextvars +from uuid import uuid4 + +CTX_REQUEST_ID: contextvars.ContextVar[str] = contextvars.ContextVar('request-id', default='') + + +class TraceCtx: + @staticmethod + def set_id(): + _id = uuid4().hex + CTX_REQUEST_ID.set(_id) + return _id + + @staticmethod + def get_id(): + return CTX_REQUEST_ID.get() diff --git a/ruoyi-fastapi-backend/middlewares/trace_middleware/middle.py b/ruoyi-fastapi-backend/middlewares/trace_middleware/middle.py new file mode 100644 index 0000000..a071692 --- /dev/null +++ b/ruoyi-fastapi-backend/middlewares/trace_middleware/middle.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +""" +@author: peng +@file: middle.py +@time: 2025/1/17 16:57 +""" + +from functools import wraps +from starlette.types import ASGIApp, Message, Receive, Scope, Send +from .span import get_current_span, Span + + +class TraceASGIMiddleware: + """ + fastapi-example: + app = FastAPI() + app.add_middleware(TraceASGIMiddleware) + """ + + def __init__(self, app: ASGIApp) -> None: + self.app = app + + @staticmethod + async def my_receive(receive: Receive, span: Span): + await span.request_before() + + @wraps(receive) + async def my_receive(): + message = await receive() + await span.request_after(message) + return message + + return my_receive + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope['type'] != 'http': + await self.app(scope, receive, send) + return + + async with get_current_span(scope) as span: + handle_outgoing_receive = await self.my_receive(receive, span) + + async def handle_outgoing_request(message: 'Message') -> None: + await span.response(message) + await send(message) + + await self.app(scope, handle_outgoing_receive, handle_outgoing_request) diff --git a/ruoyi-fastapi-backend/middlewares/trace_middleware/span.py b/ruoyi-fastapi-backend/middlewares/trace_middleware/span.py new file mode 100644 index 0000000..1e38eab --- /dev/null +++ b/ruoyi-fastapi-backend/middlewares/trace_middleware/span.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +""" +@author: peng +@file: span.py +@time: 2025/1/17 16:57 +""" + +from contextlib import asynccontextmanager +from starlette.types import Scope, Message +from .ctx import TraceCtx + + +class Span: + """ + 整个http生命周期: + request(before) --> request(after) --> response(before) --> response(after) + """ + + def __init__(self, scope: Scope): + self.scope = scope + + async def request_before(self): + """ + request_before: 处理header信息等, 如记录请求体信息 + """ + TraceCtx.set_id() + + async def request_after(self, message: Message): + """ + request_after: 处理请求bytes, 如记录请求参数 + + example: + message: {'type': 'http.request', 'body': b'{\r\n "name": "\xe8\x8b\x8f\xe8\x8b\x8f\xe8\x8b\x8f"\r\n}', 'more_body': False} + """ + return message + + async def response(self, message: Message): + """ + if message['type'] == "http.response.start": -----> request-before + pass + if message['type'] == "http.response.body": -----> request-after + message.get('body', b'') + pass + """ + if message['type'] == 'http.response.start': + message['headers'].append((b'request-id', TraceCtx.get_id().encode())) + return message + + +@asynccontextmanager +async def get_current_span(scope: Scope): + yield Span(scope) diff --git a/ruoyi-fastapi-backend/module_admin/__init__.py b/ruoyi-fastapi-backend/module_admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ruoyi-fastapi-backend/module_admin/annotation/__init__.py b/ruoyi-fastapi-backend/module_admin/annotation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py b/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py new file mode 100644 index 0000000..19ac84e --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py @@ -0,0 +1,266 @@ +import inspect +import json +import os +import requests +import time +from datetime import datetime +from fastapi import Request +from fastapi.responses import JSONResponse, ORJSONResponse, UJSONResponse +from functools import lru_cache, wraps +from sqlalchemy.ext.asyncio import AsyncSession +from typing import Any, Callable, Literal, Optional +from user_agents import parse +from config.enums import BusinessType +from config.env import AppConfig +from exceptions.exception import LoginException, ServiceException, ServiceWarning +from module_admin.entity.vo.log_vo import LogininforModel, OperLogModel +from module_admin.service.log_service import LoginLogService, OperationLogService +from module_admin.service.login_service import LoginService +from utils.log_util import logger +from utils.response_util import ResponseUtil + + +class Log: + """ + 日志装饰器 + """ + + def __init__( + self, + title: str, + business_type: BusinessType, + log_type: Optional[Literal['login', 'operation']] = 'operation', + ): + """ + 日志装饰器 + + :param title: 当前日志装饰器装饰的模块标题 + :param business_type: 业务类型(OTHER其它 INSERT新增 UPDATE修改 DELETE删除 GRANT授权 EXPORT导出 IMPORT导入 FORCE强退 GENCODE生成代码 CLEAN清空数据) + :param log_type: 日志类型(login表示登录日志,operation表示为操作日志) + :return: + """ + self.title = title + self.business_type = business_type.value + self.log_type = log_type + + def __call__(self, func): + @wraps(func) + async def wrapper(*args, **kwargs): + start_time = time.time() + # 获取被装饰函数的文件路径 + file_path = inspect.getfile(func) + # 获取项目根路径 + project_root = os.getcwd() + # 处理文件路径,去除项目根路径部分 + relative_path = os.path.relpath(file_path, start=project_root)[0:-2].replace('\\', '.').replace('/', '.') + # 获取当前被装饰函数所在路径 + func_path = f'{relative_path}{func.__name__}()' + # 获取上下文信息 + request_name_list = get_function_parameters_name_by_type(func, Request) + request = get_function_parameters_value_by_name(func, request_name_list[0], *args, **kwargs) + token = request.headers.get('Authorization') + session_name_list = get_function_parameters_name_by_type(func, AsyncSession) + query_db = get_function_parameters_value_by_name(func, session_name_list[0], *args, **kwargs) + request_method = request.method + operator_type = 0 + user_agent = request.headers.get('User-Agent') + if 'Windows' in user_agent or 'Macintosh' in user_agent or 'Linux' in user_agent: + operator_type = 1 + if 'Mobile' in user_agent or 'Android' in user_agent or 'iPhone' in user_agent: + operator_type = 2 + # 获取请求的url + oper_url = request.url.path + # 获取请求的ip及ip归属区域 + oper_ip = request.headers.get('X-Forwarded-For') + oper_location = '内网IP' + if AppConfig.app_ip_location_query: + oper_location = get_ip_location(oper_ip) + # 根据不同的请求类型使用不同的方法获取请求参数 + content_type = request.headers.get('Content-Type') + if content_type and ( + 'multipart/form-data' in content_type or 'application/x-www-form-urlencoded' in content_type + ): + payload = await request.form() + oper_param = '\n'.join([f'{key}: {value}' for key, value in payload.items()]) + else: + payload = await request.body() + # 通过 request.path_params 直接访问路径参数 + path_params = request.path_params + oper_param = {} + if payload: + oper_param.update(json.loads(str(payload, 'utf-8'))) + if path_params: + oper_param.update(path_params) + oper_param = json.dumps(oper_param, ensure_ascii=False) + # 日志表请求参数字段长度最大为2000,因此在此处判断长度 + if len(oper_param) > 2000: + oper_param = '请求参数过长' + + # 获取操作时间 + oper_time = datetime.now() + # 此处在登录之前向原始函数传递一些登录信息,用于监测在线用户的相关信息 + login_log = {} + if self.log_type == 'login': + user_agent_info = parse(user_agent) + browser = f'{user_agent_info.browser.family}' + system_os = f'{user_agent_info.os.family}' + if user_agent_info.browser.version != (): + browser += f' {user_agent_info.browser.version[0]}' + if user_agent_info.os.version != (): + system_os += f' {user_agent_info.os.version[0]}' + login_log = dict( + ipaddr=oper_ip, + loginLocation=oper_location, + browser=browser, + os=system_os, + loginTime=oper_time.strftime('%Y-%m-%d %H:%M:%S'), + ) + kwargs['form_data'].login_info = login_log + try: + # 调用原始函数 + result = await func(*args, **kwargs) + except (LoginException, ServiceWarning) as e: + logger.warning(e.message) + result = ResponseUtil.failure(data=e.data, msg=e.message) + except ServiceException as e: + logger.error(e.message) + result = ResponseUtil.error(data=e.data, msg=e.message) + except Exception as e: + logger.exception(e) + result = ResponseUtil.error(msg=str(e)) + # 获取请求耗时 + cost_time = float(time.time() - start_time) * 100 + # 判断请求是否来自api文档 + request_from_swagger = ( + request.headers.get('referer').endswith('docs') if request.headers.get('referer') else False + ) + request_from_redoc = ( + request.headers.get('referer').endswith('redoc') if request.headers.get('referer') else False + ) + # 根据响应结果的类型使用不同的方法获取响应结果参数 + if ( + isinstance(result, JSONResponse) + or isinstance(result, ORJSONResponse) + or isinstance(result, UJSONResponse) + ): + result_dict = json.loads(str(result.body, 'utf-8')) + else: + if request_from_swagger or request_from_redoc: + result_dict = {} + else: + if result.status_code == 200: + result_dict = {'code': result.status_code, 'message': '获取成功'} + else: + result_dict = {'code': result.status_code, 'message': '获取失败'} + json_result = json.dumps(result_dict, ensure_ascii=False) + # 根据响应结果获取响应状态及异常信息 + status = 1 + error_msg = '' + if result_dict.get('code') == 200: + status = 0 + else: + error_msg = result_dict.get('msg') + # 根据日志类型向对应的日志表插入数据 + if self.log_type == 'login': + # 登录请求来自于api文档时不记录登录日志,其余情况则记录 + if request_from_swagger or request_from_redoc: + pass + else: + user = kwargs.get('form_data') + user_name = user.username + login_log['loginTime'] = oper_time + login_log['userName'] = user_name + login_log['status'] = str(status) + login_log['msg'] = result_dict.get('msg') + + await LoginLogService.add_login_log_services(query_db, LogininforModel(**login_log)) + else: + try: + current_user = await LoginService.get_current_user(request, token, query_db) + oper_name = current_user.user.user_name if current_user.user else None + dept_name = current_user.user.dept.dept_name if current_user.user.dept else None + except: + oper_name = None + dept_name = None + + operation_log = OperLogModel( + title=self.title, + businessType=self.business_type, + method=func_path, + requestMethod=request_method, + operatorType=operator_type, + operName=oper_name, + deptName=dept_name, + operUrl=oper_url, + operIp=oper_ip, + operLocation=oper_location, + operParam=oper_param, + jsonResult=json_result, + status=status, + errorMsg=error_msg, + operTime=oper_time, + costTime=int(cost_time), + ) + await OperationLogService.add_operation_log_services(query_db, operation_log) + + return result + + return wrapper + + +@lru_cache() +def get_ip_location(oper_ip: str): + """ + 查询ip归属区域 + + :param oper_ip: 需要查询的ip + :return: ip归属区域 + """ + oper_location = '内网IP' + try: + if oper_ip != '127.0.0.1' and oper_ip != 'localhost': + oper_location = '未知' + ip_result = requests.get(f'https://qifu-api.baidubce.com/ip/geo/v1/district?ip={oper_ip}') + if ip_result.status_code == 200: + prov = ip_result.json().get('data').get('prov') + city = ip_result.json().get('data').get('city') + if prov or city: + oper_location = f'{prov}-{city}' + except Exception as e: + oper_location = '未知' + print(e) + return oper_location + + +def get_function_parameters_name_by_type(func: Callable, param_type: Any): + """ + 获取函数指定类型的参数名称 + + :param func: 函数 + :param arg_type: 参数类型 + :return: 函数指定类型的参数名称 + """ + # 获取函数的参数信息 + parameters = inspect.signature(func).parameters + # 找到指定类型的参数名称 + parameters_name_list = [] + for name, param in parameters.items(): + if param.annotation == param_type: + parameters_name_list.append(name) + return parameters_name_list + + +def get_function_parameters_value_by_name(func: Callable, name: str, *args, **kwargs): + """ + 获取函数指定参数的值 + + :param func: 函数 + :param name: 参数名 + :return: 参数值 + """ + # 获取参数值 + bound_parameters = inspect.signature(func).bind(*args, **kwargs) + bound_parameters.apply_defaults() + parameters_value = bound_parameters.arguments.get(name) + + return parameters_value diff --git a/ruoyi-fastapi-backend/module_admin/annotation/pydantic_annotation.py b/ruoyi-fastapi-backend/module_admin/annotation/pydantic_annotation.py new file mode 100644 index 0000000..11e8d7f --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/annotation/pydantic_annotation.py @@ -0,0 +1,84 @@ +import inspect +from fastapi import Form, Query +from pydantic import BaseModel +from pydantic.fields import FieldInfo +from typing import Type, TypeVar + + +BaseModelVar = TypeVar('BaseModelVar', bound=BaseModel) + + +def as_query(cls: Type[BaseModelVar]) -> Type[BaseModelVar]: + """ + pydantic模型查询参数装饰器,将pydantic模型用于接收查询参数 + """ + new_parameters = [] + + for field_name, model_field in cls.model_fields.items(): + model_field: FieldInfo # type: ignore + + if not model_field.is_required(): + new_parameters.append( + inspect.Parameter( + model_field.alias, + inspect.Parameter.POSITIONAL_ONLY, + default=Query(default=model_field.default, description=model_field.description), + annotation=model_field.annotation, + ) + ) + else: + new_parameters.append( + inspect.Parameter( + model_field.alias, + inspect.Parameter.POSITIONAL_ONLY, + default=Query(..., description=model_field.description), + annotation=model_field.annotation, + ) + ) + + async def as_query_func(**data): + return cls(**data) + + sig = inspect.signature(as_query_func) + sig = sig.replace(parameters=new_parameters) + as_query_func.__signature__ = sig # type: ignore + setattr(cls, 'as_query', as_query_func) + return cls + + +def as_form(cls: Type[BaseModelVar]) -> Type[BaseModelVar]: + """ + pydantic模型表单参数装饰器,将pydantic模型用于接收表单参数 + """ + new_parameters = [] + + for field_name, model_field in cls.model_fields.items(): + model_field: FieldInfo # type: ignore + + if not model_field.is_required(): + new_parameters.append( + inspect.Parameter( + model_field.alias, + inspect.Parameter.POSITIONAL_ONLY, + default=Form(default=model_field.default, description=model_field.description), + annotation=model_field.annotation, + ) + ) + else: + new_parameters.append( + inspect.Parameter( + model_field.alias, + inspect.Parameter.POSITIONAL_ONLY, + default=Form(..., description=model_field.description), + annotation=model_field.annotation, + ) + ) + + async def as_form_func(**data): + return cls(**data) + + sig = inspect.signature(as_form_func) + sig = sig.replace(parameters=new_parameters) + as_form_func.__signature__ = sig # type: ignore + setattr(cls, 'as_form', as_form_func) + return cls diff --git a/ruoyi-fastapi-backend/module_admin/aspect/__init__.py b/ruoyi-fastapi-backend/module_admin/aspect/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ruoyi-fastapi-backend/module_admin/aspect/data_scope.py b/ruoyi-fastapi-backend/module_admin/aspect/data_scope.py new file mode 100644 index 0000000..5a7afbb --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/aspect/data_scope.py @@ -0,0 +1,75 @@ +from fastapi import Depends +from typing import Optional +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.login_service import LoginService + + +class GetDataScope: + """ + 获取当前用户数据权限对应的查询sql语句 + """ + + DATA_SCOPE_ALL = '1' + DATA_SCOPE_CUSTOM = '2' + DATA_SCOPE_DEPT = '3' + DATA_SCOPE_DEPT_AND_CHILD = '4' + DATA_SCOPE_SELF = '5' + + def __init__( + self, + query_alias: Optional[str] = '', + db_alias: Optional[str] = 'db', + user_alias: Optional[str] = 'user_id', + dept_alias: Optional[str] = 'dept_id', + ): + """ + 获取当前用户数据权限对应的查询sql语句 + + :param query_alias: 所要查询表对应的sqlalchemy模型名称,默认为'' + :param db_alias: orm对象别名,默认为'db' + :param user_alias: 用户id字段别名,默认为'user_id' + :param dept_alias: 部门id字段别名,默认为'dept_id' + """ + self.query_alias = query_alias + self.db_alias = db_alias + self.user_alias = user_alias + self.dept_alias = dept_alias + + def __call__(self, current_user: CurrentUserModel = Depends(LoginService.get_current_user)): + user_id = current_user.user.user_id + dept_id = current_user.user.dept_id + custom_data_scope_role_id_list = [ + item.role_id for item in current_user.user.role if item.data_scope == self.DATA_SCOPE_CUSTOM + ] + param_sql_list = [] + for role in current_user.user.role: + if current_user.user.admin or role.data_scope == self.DATA_SCOPE_ALL: + param_sql_list = ['1 == 1'] + break + elif role.data_scope == self.DATA_SCOPE_CUSTOM: + if len(custom_data_scope_role_id_list) > 1: + param_sql_list.append( + f"{self.query_alias}.{self.dept_alias}.in_(select(SysRoleDept.dept_id).where(SysRoleDept.role_id.in_({custom_data_scope_role_id_list}))) if hasattr({self.query_alias}, '{self.dept_alias}') else 1 == 0" + ) + else: + param_sql_list.append( + f"{self.query_alias}.{self.dept_alias}.in_(select(SysRoleDept.dept_id).where(SysRoleDept.role_id == {role.role_id})) if hasattr({self.query_alias}, '{self.dept_alias}') else 1 == 0" + ) + elif role.data_scope == self.DATA_SCOPE_DEPT: + param_sql_list.append( + f"{self.query_alias}.{self.dept_alias} == {dept_id} if hasattr({self.query_alias}, '{self.dept_alias}') else 1 == 0" + ) + elif role.data_scope == self.DATA_SCOPE_DEPT_AND_CHILD: + param_sql_list.append( + f"{self.query_alias}.{self.dept_alias}.in_(select(SysDept.dept_id).where(or_(SysDept.dept_id == {dept_id}, func.find_in_set({dept_id}, SysDept.ancestors)))) if hasattr({self.query_alias}, '{self.dept_alias}') else 1 == 0" + ) + elif role.data_scope == self.DATA_SCOPE_SELF: + param_sql_list.append( + f"{self.query_alias}.{self.user_alias} == {user_id} if hasattr({self.query_alias}, '{self.user_alias}') else 1 == 0" + ) + else: + param_sql_list.append('1 == 0') + param_sql_list = list(dict.fromkeys(param_sql_list)) + param_sql = f"or_({', '.join(param_sql_list)})" + + return param_sql diff --git a/ruoyi-fastapi-backend/module_admin/aspect/interface_auth.py b/ruoyi-fastapi-backend/module_admin/aspect/interface_auth.py new file mode 100644 index 0000000..8f8349d --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/aspect/interface_auth.py @@ -0,0 +1,68 @@ +from fastapi import Depends +from typing import List, Union +from exceptions.exception import PermissionException +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.login_service import LoginService + + +class CheckUserInterfaceAuth: + """ + 校验当前用户是否具有相应的接口权限 + """ + + def __init__(self, perm: Union[str, List], is_strict: bool = False): + """ + 校验当前用户是否具有相应的接口权限 + + :param perm: 权限标识 + :param is_strict: 当传入的权限标识是list类型时,是否开启严格模式,开启表示会校验列表中的每一个权限标识,所有的校验结果都需要为True才会通过 + """ + self.perm = perm + self.is_strict = is_strict + + def __call__(self, current_user: CurrentUserModel = Depends(LoginService.get_current_user)): + user_auth_list = current_user.permissions + if '*:*:*' in user_auth_list: + return True + if isinstance(self.perm, str): + if self.perm in user_auth_list: + return True + if isinstance(self.perm, list): + if self.is_strict: + if all([perm_str in user_auth_list for perm_str in self.perm]): + return True + else: + if any([perm_str in user_auth_list for perm_str in self.perm]): + return True + raise PermissionException(data='', message='该用户无此接口权限') + + +class CheckRoleInterfaceAuth: + """ + 根据角色校验当前用户是否具有相应的接口权限 + """ + + def __init__(self, role_key: Union[str, List], is_strict: bool = False): + """ + 根据角色校验当前用户是否具有相应的接口权限 + + :param role_key: 角色标识 + :param is_strict: 当传入的角色标识是list类型时,是否开启严格模式,开启表示会校验列表中的每一个角色标识,所有的校验结果都需要为True才会通过 + """ + self.role_key = role_key + self.is_strict = is_strict + + def __call__(self, current_user: CurrentUserModel = Depends(LoginService.get_current_user)): + user_role_list = current_user.user.role + user_role_key_list = [role.role_key for role in user_role_list] + if isinstance(self.role_key, str): + if self.role_key in user_role_key_list: + return True + if isinstance(self.role_key, list): + if self.is_strict: + if all([role_key_str in user_role_key_list for role_key_str in self.role_key]): + return True + else: + if any([role_key_str in user_role_key_list for role_key_str in self.role_key]): + return True + raise PermissionException(data='', message='该用户无此接口权限') diff --git a/ruoyi-fastapi-backend/module_admin/controller/__init__.py b/ruoyi-fastapi-backend/module_admin/controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ruoyi-fastapi-backend/module_admin/controller/cache_controller.py b/ruoyi-fastapi-backend/module_admin/controller/cache_controller.py new file mode 100644 index 0000000..9e72713 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/cache_controller.py @@ -0,0 +1,89 @@ +from fastapi import APIRouter, Depends, Request +from typing import List +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.cache_vo import CacheInfoModel, CacheMonitorModel +from module_admin.service.cache_service import CacheService +from module_admin.service.login_service import LoginService +from utils.log_util import logger +from utils.response_util import ResponseUtil + + +cacheController = APIRouter(prefix='/monitor/cache', dependencies=[Depends(LoginService.get_current_user)]) + + +@cacheController.get( + '', response_model=CacheMonitorModel, dependencies=[Depends(CheckUserInterfaceAuth('monitor:cache:list'))] +) +async def get_monitor_cache_info(request: Request): + # 获取全量数据 + cache_info_query_result = await CacheService.get_cache_monitor_statistical_info_services(request) + logger.info('获取成功') + + return ResponseUtil.success(data=cache_info_query_result) + + +@cacheController.get( + '/getNames', + response_model=List[CacheInfoModel], + dependencies=[Depends(CheckUserInterfaceAuth('monitor:cache:list'))], +) +async def get_monitor_cache_name(request: Request): + # 获取全量数据 + cache_name_list_result = await CacheService.get_cache_monitor_cache_name_services() + logger.info('获取成功') + + return ResponseUtil.success(data=cache_name_list_result) + + +@cacheController.get( + '/getKeys/{cache_name}', + response_model=List[str], + dependencies=[Depends(CheckUserInterfaceAuth('monitor:cache:list'))], +) +async def get_monitor_cache_key(request: Request, cache_name: str): + # 获取全量数据 + cache_key_list_result = await CacheService.get_cache_monitor_cache_key_services(request, cache_name) + logger.info('获取成功') + + return ResponseUtil.success(data=cache_key_list_result) + + +@cacheController.get( + '/getValue/{cache_name}/{cache_key}', + response_model=CacheInfoModel, + dependencies=[Depends(CheckUserInterfaceAuth('monitor:cache:list'))], +) +async def get_monitor_cache_value(request: Request, cache_name: str, cache_key: str): + # 获取全量数据 + cache_value_list_result = await CacheService.get_cache_monitor_cache_value_services(request, cache_name, cache_key) + logger.info('获取成功') + + return ResponseUtil.success(data=cache_value_list_result) + + +@cacheController.delete( + '/clearCacheName/{cache_name}', dependencies=[Depends(CheckUserInterfaceAuth('monitor:cache:list'))] +) +async def clear_monitor_cache_name(request: Request, cache_name: str): + clear_cache_name_result = await CacheService.clear_cache_monitor_cache_name_services(request, cache_name) + logger.info(clear_cache_name_result.message) + + return ResponseUtil.success(msg=clear_cache_name_result.message) + + +@cacheController.delete( + '/clearCacheKey/{cache_key}', dependencies=[Depends(CheckUserInterfaceAuth('monitor:cache:list'))] +) +async def clear_monitor_cache_key(request: Request, cache_key: str): + clear_cache_key_result = await CacheService.clear_cache_monitor_cache_key_services(request, cache_key) + logger.info(clear_cache_key_result.message) + + return ResponseUtil.success(msg=clear_cache_key_result.message) + + +@cacheController.delete('/clearCacheAll', dependencies=[Depends(CheckUserInterfaceAuth('monitor:cache:list'))]) +async def clear_monitor_cache_all(request: Request): + clear_cache_all_result = await CacheService.clear_cache_monitor_all_services(request) + logger.info(clear_cache_all_result.message) + + return ResponseUtil.success(msg=clear_cache_all_result.message) diff --git a/ruoyi-fastapi-backend/module_admin/controller/captcha_controller.py b/ruoyi-fastapi-backend/module_admin/controller/captcha_controller.py new file mode 100644 index 0000000..83d35b8 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/captcha_controller.py @@ -0,0 +1,40 @@ +import uuid +from datetime import timedelta +from fastapi import APIRouter, Request +from config.enums import RedisInitKeyConfig +from module_admin.entity.vo.login_vo import CaptchaCode +from module_admin.service.captcha_service import CaptchaService +from utils.response_util import ResponseUtil +from utils.log_util import logger + + +captchaController = APIRouter() + + +@captchaController.get('/captchaImage') +async def get_captcha_image(request: Request): + captcha_enabled = ( + True + if await request.app.state.redis.get(f'{RedisInitKeyConfig.SYS_CONFIG.key}:sys.account.captchaEnabled') + == 'true' + else False + ) + register_enabled = ( + True + if await request.app.state.redis.get(f'{RedisInitKeyConfig.SYS_CONFIG.key}:sys.account.registerUser') == 'true' + else False + ) + session_id = str(uuid.uuid4()) + captcha_result = await CaptchaService.create_captcha_image_service() + image = captcha_result[0] + computed_result = captcha_result[1] + await request.app.state.redis.set( + f'{RedisInitKeyConfig.CAPTCHA_CODES.key}:{session_id}', computed_result, ex=timedelta(minutes=2) + ) + logger.info(f'编号为{session_id}的会话获取图片验证码成功') + + return ResponseUtil.success( + model_content=CaptchaCode( + captchaEnabled=captcha_enabled, registerEnabled=register_enabled, img=image, uuid=session_id + ) + ) diff --git a/ruoyi-fastapi-backend/module_admin/controller/common_controller.py b/ruoyi-fastapi-backend/module_admin/controller/common_controller.py new file mode 100644 index 0000000..d2fd621 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/common_controller.py @@ -0,0 +1,36 @@ +from fastapi import APIRouter, BackgroundTasks, Depends, File, Query, Request, UploadFile +from module_admin.service.common_service import CommonService +from module_admin.service.login_service import LoginService +from utils.log_util import logger +from utils.response_util import ResponseUtil + +commonController = APIRouter(prefix='/common', dependencies=[Depends(LoginService.get_current_user)]) + + +@commonController.post('/upload') +async def common_upload(request: Request, file: UploadFile = File(...)): + upload_result = await CommonService.upload_service(request, file) + logger.info('上传成功') + + return ResponseUtil.success(model_content=upload_result.result) + + +@commonController.get('/download') +async def common_download( + request: Request, + background_tasks: BackgroundTasks, + file_name: str = Query(alias='fileName'), + delete: bool = Query(), +): + download_result = await CommonService.download_services(background_tasks, file_name, delete) + logger.info(download_result.message) + + return ResponseUtil.streaming(data=download_result.result) + + +@commonController.get('/download/resource') +async def common_download_resource(request: Request, resource: str = Query()): + download_resource_result = await CommonService.download_resource_services(resource) + logger.info(download_resource_result.message) + + return ResponseUtil.streaming(data=download_resource_result.result) diff --git a/ruoyi-fastapi-backend/module_admin/controller/compreface_controller.py b/ruoyi-fastapi-backend/module_admin/controller/compreface_controller.py new file mode 100644 index 0000000..8f08b7a --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/compreface_controller.py @@ -0,0 +1,58 @@ +from fastapi import APIRouter, Depends, Form, Request, Body, UploadFile, File +from module_admin.service.login_service import LoginService +from module_admin.annotation.log_annotation import Log +from module_admin.service.compreface_service import ComprefaceService +from config.enums import BusinessType +from utils.response_util import ResponseUtil +from utils.log_util import logger +import datetime + + + +comprefaceController = APIRouter(prefix='/system/compreface' + , dependencies=[Depends(LoginService.get_current_user)] + ) + +# 人脸检测 +@comprefaceController.post('/face_detection') +async def face_detection(request: Request, file: UploadFile = File(None)) -> dict: + """ + 人脸检测 + + """ + if file: # 说明是表单上传 + image = await file.read() + + else: # 尝试按字节流读取 + image = await request.body() + + result = await ComprefaceService.face_detection_service(image) + print(result) + return ResponseUtil.success(data=result) + +# 人脸识别 +@comprefaceController.post('/face_recognition') +# @Log(title='人脸识别', business_type=BusinessType.OTHER) +async def face_recognition(request: Request, file: UploadFile = File(None)) -> dict: + """ + 人脸识别 + + """ + if file: # 说明是表单上传 + image = await file.read() + + else: # 尝试按字节流读取 + image = await request.body() + + result = await ComprefaceService.face_recognition_service(image) + print(result) + return ResponseUtil.success(data=result) + + +# def my_job(): +# print("任务执行时间:", datetime.datetime.now()) + +# from config.get_scheduler import scheduler +# from apscheduler.triggers.cron import CronTrigger +# trigger = CronTrigger(minute='*', second=0) +# scheduler.add_job(my_job, trigger=trigger) \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_admin/controller/config_controller.py b/ruoyi-fastapi-backend/module_admin/controller/config_controller.py new file mode 100644 index 0000000..9b0184e --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/config_controller.py @@ -0,0 +1,123 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Form, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.config_vo import ConfigModel, ConfigPageQueryModel, DeleteConfigModel +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.config_service import ConfigService +from module_admin.service.login_service import LoginService +from utils.common_util import bytes2file_response, export_list2excel +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +configController = APIRouter(prefix='/system/config', dependencies=[Depends(LoginService.get_current_user)]) + + +@configController.get( + '/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('system:config:list'))] +) +async def get_system_config_list( + request: Request, + config_page_query: ConfigPageQueryModel = Depends(ConfigPageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + # 获取分页数据 + config_page_query_result = await ConfigService.get_config_list_services(query_db, config_page_query, is_page=True) + logger.info('获取成功') + + return ResponseUtil.success(model_content=config_page_query_result) + + +@configController.post('', dependencies=[Depends(CheckUserInterfaceAuth('system:config:add'))]) +@ValidateFields(validate_model='add_config') +@Log(title='参数管理', business_type=BusinessType.INSERT) +async def add_system_config( + request: Request, + add_config: ConfigModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + add_config.create_by = current_user.user.user_name + add_config.create_time = datetime.now() + add_config.update_by = current_user.user.user_name + add_config.update_time = datetime.now() + add_config_result = await ConfigService.add_config_services(request, query_db, add_config) + logger.info(add_config_result.message) + + return ResponseUtil.success(msg=add_config_result.message) + + +@configController.put('', dependencies=[Depends(CheckUserInterfaceAuth('system:config:edit'))]) +@ValidateFields(validate_model='edit_config') +@Log(title='参数管理', business_type=BusinessType.UPDATE) +async def edit_system_config( + request: Request, + edit_config: ConfigModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + edit_config.update_by = current_user.user.user_name + edit_config.update_time = datetime.now() + edit_config_result = await ConfigService.edit_config_services(request, query_db, edit_config) + logger.info(edit_config_result.message) + + return ResponseUtil.success(msg=edit_config_result.message) + + +@configController.delete('/refreshCache', dependencies=[Depends(CheckUserInterfaceAuth('system:config:remove'))]) +@Log(title='参数管理', business_type=BusinessType.UPDATE) +async def refresh_system_config(request: Request, query_db: AsyncSession = Depends(get_db)): + refresh_config_result = await ConfigService.refresh_sys_config_services(request, query_db) + logger.info(refresh_config_result.message) + + return ResponseUtil.success(msg=refresh_config_result.message) + + +@configController.delete('/{config_ids}', dependencies=[Depends(CheckUserInterfaceAuth('system:config:remove'))]) +@Log(title='参数管理', business_type=BusinessType.DELETE) +async def delete_system_config(request: Request, config_ids: str, query_db: AsyncSession = Depends(get_db)): + delete_config = DeleteConfigModel(configIds=config_ids) + delete_config_result = await ConfigService.delete_config_services(request, query_db, delete_config) + logger.info(delete_config_result.message) + + return ResponseUtil.success(msg=delete_config_result.message) + + +@configController.get( + '/{config_id}', response_model=ConfigModel, dependencies=[Depends(CheckUserInterfaceAuth('system:config:query'))] +) +async def query_detail_system_config(request: Request, config_id: int, query_db: AsyncSession = Depends(get_db)): + config_detail_result = await ConfigService.config_detail_services(query_db, config_id) + logger.info(f'获取config_id为{config_id}的信息成功') + + return ResponseUtil.success(data=config_detail_result) + + +@configController.get('/configKey/{config_key}') +async def query_system_config(request: Request, config_key: str): + # 获取全量数据 + config_query_result = await ConfigService.query_config_list_from_cache_services(request.app.state.redis, config_key) + logger.info('获取成功') + + return ResponseUtil.success(msg=config_query_result) + + +@configController.post('/export', dependencies=[Depends(CheckUserInterfaceAuth('system:config:export'))]) +@Log(title='参数管理', business_type=BusinessType.EXPORT) +async def export_system_config_list( + request: Request, + config_page_query: ConfigPageQueryModel = Form(), + query_db: AsyncSession = Depends(get_db), +): + # 获取全量数据 + config_query_result = await ConfigService.get_config_list_services(query_db, config_page_query, is_page=False) + config_export_result = await ConfigService.export_config_list_services(config_query_result) + logger.info('导出成功') + + return ResponseUtil.streaming(data=export_list2excel(config_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/controller/dept_controller.py b/ruoyi-fastapi-backend/module_admin/controller/dept_controller.py new file mode 100644 index 0000000..2943206 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/dept_controller.py @@ -0,0 +1,132 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.data_scope import GetDataScope +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.dept_vo import DeleteDeptModel, DeptModel, DeptQueryModel +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.dept_service import DeptService +from module_admin.service.login_service import LoginService +from utils.log_util import logger +from utils.response_util import ResponseUtil + + +deptController = APIRouter(prefix='/system/dept', dependencies=[Depends(LoginService.get_current_user)]) + + +@deptController.get( + '/list/exclude/{dept_id}', + response_model=List[DeptModel], + dependencies=[Depends(CheckUserInterfaceAuth('system:dept:list'))], +) +async def get_system_dept_tree_for_edit_option( + request: Request, + dept_id: int, + query_db: AsyncSession = Depends(get_db), + data_scope_sql: str = Depends(GetDataScope('SysDept')), +): + dept_query = DeptModel(deptId=dept_id) + dept_query_result = await DeptService.get_dept_for_edit_option_services(query_db, dept_query, data_scope_sql) + logger.info('获取成功') + + return ResponseUtil.success(data=dept_query_result) + + +@deptController.get( + '/list', response_model=List[DeptModel], dependencies=[Depends(CheckUserInterfaceAuth('system:dept:list'))] +) +async def get_system_dept_list( + request: Request, + dept_query: DeptQueryModel = Depends(DeptQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), + data_scope_sql: str = Depends(GetDataScope('SysDept')), +): + dept_query_result = await DeptService.get_dept_list_services(query_db, dept_query, data_scope_sql) + logger.info('获取成功') + + return ResponseUtil.success(data=dept_query_result) + + +@deptController.post('', dependencies=[Depends(CheckUserInterfaceAuth('system:dept:add'))]) +@ValidateFields(validate_model='add_dept') +@Log(title='部门管理', business_type=BusinessType.INSERT) +async def add_system_dept( + request: Request, + add_dept: DeptModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + add_dept.create_by = current_user.user.user_name + add_dept.create_time = datetime.now() + add_dept.update_by = current_user.user.user_name + add_dept.update_time = datetime.now() + add_dept_result = await DeptService.add_dept_services(query_db, add_dept) + logger.info(add_dept_result.message) + + return ResponseUtil.success(data=add_dept_result) + + +@deptController.put('', dependencies=[Depends(CheckUserInterfaceAuth('system:dept:edit'))]) +@ValidateFields(validate_model='edit_dept') +@Log(title='部门管理', business_type=BusinessType.UPDATE) +async def edit_system_dept( + request: Request, + edit_dept: DeptModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), + data_scope_sql: str = Depends(GetDataScope('SysDept')), +): + if not current_user.user.admin: + await DeptService.check_dept_data_scope_services(query_db, edit_dept.dept_id, data_scope_sql) + edit_dept.update_by = current_user.user.user_name + edit_dept.update_time = datetime.now() + edit_dept_result = await DeptService.edit_dept_services(query_db, edit_dept) + logger.info(edit_dept_result.message) + + return ResponseUtil.success(msg=edit_dept_result.message) + + +@deptController.delete('/{dept_ids}', dependencies=[Depends(CheckUserInterfaceAuth('system:dept:remove'))]) +@Log(title='部门管理', business_type=BusinessType.DELETE) +async def delete_system_dept( + request: Request, + dept_ids: str, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), + data_scope_sql: str = Depends(GetDataScope('SysDept')), +): + dept_id_list = dept_ids.split(',') if dept_ids else [] + if dept_id_list: + for dept_id in dept_id_list: + if not current_user.user.admin: + await DeptService.check_dept_data_scope_services(query_db, int(dept_id), data_scope_sql) + delete_dept = DeleteDeptModel(deptIds=dept_ids) + delete_dept.update_by = current_user.user.user_name + delete_dept.update_time = datetime.now() + delete_dept_result = await DeptService.delete_dept_services(query_db, delete_dept) + logger.info(delete_dept_result.message) + + return ResponseUtil.success(msg=delete_dept_result.message) + + +@deptController.get( + '/{dept_id}', response_model=DeptModel, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:query'))] +) +async def query_detail_system_dept( + request: Request, + dept_id: int, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), + data_scope_sql: str = Depends(GetDataScope('SysDept')), +): + if not current_user.user.admin: + await DeptService.check_dept_data_scope_services(query_db, dept_id, data_scope_sql) + detail_dept_result = await DeptService.dept_detail_services(query_db, dept_id) + logger.info(f'获取dept_id为{dept_id}的信息成功') + + return ResponseUtil.success(data=detail_dept_result) diff --git a/ruoyi-fastapi-backend/module_admin/controller/dict_controller.py b/ruoyi-fastapi-backend/module_admin/controller/dict_controller.py new file mode 100644 index 0000000..27fa24c --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/dict_controller.py @@ -0,0 +1,239 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Form, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.dict_vo import ( + DeleteDictDataModel, + DeleteDictTypeModel, + DictDataModel, + DictDataPageQueryModel, + DictTypeModel, + DictTypePageQueryModel, +) +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.dict_service import DictDataService, DictTypeService +from module_admin.service.login_service import LoginService +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +dictController = APIRouter(prefix='/system/dict', dependencies=[Depends(LoginService.get_current_user)]) + + +@dictController.get( + '/type/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:list'))] +) +async def get_system_dict_type_list( + request: Request, + dict_type_page_query: DictTypePageQueryModel = Depends(DictTypePageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + # 获取分页数据 + dict_type_page_query_result = await DictTypeService.get_dict_type_list_services( + query_db, dict_type_page_query, is_page=True + ) + logger.info('获取成功') + + return ResponseUtil.success(model_content=dict_type_page_query_result) + + +@dictController.post('/type', dependencies=[Depends(CheckUserInterfaceAuth('system:dict:add'))]) +@ValidateFields(validate_model='add_dict_type') +@Log(title='字典类型', business_type=BusinessType.INSERT) +async def add_system_dict_type( + request: Request, + add_dict_type: DictTypeModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + add_dict_type.create_by = current_user.user.user_name + add_dict_type.create_time = datetime.now() + add_dict_type.update_by = current_user.user.user_name + add_dict_type.update_time = datetime.now() + add_dict_type_result = await DictTypeService.add_dict_type_services(request, query_db, add_dict_type) + logger.info(add_dict_type_result.message) + + return ResponseUtil.success(msg=add_dict_type_result.message) + + +@dictController.put('/type', dependencies=[Depends(CheckUserInterfaceAuth('system:dict:edit'))]) +@ValidateFields(validate_model='edit_dict_type') +@Log(title='字典类型', business_type=BusinessType.UPDATE) +async def edit_system_dict_type( + request: Request, + edit_dict_type: DictTypeModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + edit_dict_type.update_by = current_user.user.user_name + edit_dict_type.update_time = datetime.now() + edit_dict_type_result = await DictTypeService.edit_dict_type_services(request, query_db, edit_dict_type) + logger.info(edit_dict_type_result.message) + + return ResponseUtil.success(msg=edit_dict_type_result.message) + + +@dictController.delete('/type/refreshCache', dependencies=[Depends(CheckUserInterfaceAuth('system:dict:remove'))]) +@Log(title='字典类型', business_type=BusinessType.UPDATE) +async def refresh_system_dict(request: Request, query_db: AsyncSession = Depends(get_db)): + refresh_dict_result = await DictTypeService.refresh_sys_dict_services(request, query_db) + logger.info(refresh_dict_result.message) + + return ResponseUtil.success(msg=refresh_dict_result.message) + + +@dictController.delete('/type/{dict_ids}', dependencies=[Depends(CheckUserInterfaceAuth('system:dict:remove'))]) +@Log(title='字典类型', business_type=BusinessType.DELETE) +async def delete_system_dict_type(request: Request, dict_ids: str, query_db: AsyncSession = Depends(get_db)): + delete_dict_type = DeleteDictTypeModel(dictIds=dict_ids) + delete_dict_type_result = await DictTypeService.delete_dict_type_services(request, query_db, delete_dict_type) + logger.info(delete_dict_type_result.message) + + return ResponseUtil.success(msg=delete_dict_type_result.message) + + +@dictController.get('/type/optionselect', response_model=List[DictTypeModel]) +async def query_system_dict_type_options(request: Request, query_db: AsyncSession = Depends(get_db)): + dict_type_query_result = await DictTypeService.get_dict_type_list_services( + query_db, DictTypePageQueryModel(**dict()), is_page=False + ) + logger.info('获取成功') + + return ResponseUtil.success(data=dict_type_query_result) + + +@dictController.get( + '/type/{dict_id}', response_model=DictTypeModel, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:query'))] +) +async def query_detail_system_dict_type(request: Request, dict_id: int, query_db: AsyncSession = Depends(get_db)): + dict_type_detail_result = await DictTypeService.dict_type_detail_services(query_db, dict_id) + logger.info(f'获取dict_id为{dict_id}的信息成功') + + return ResponseUtil.success(data=dict_type_detail_result) + + +@dictController.post('/type/export', dependencies=[Depends(CheckUserInterfaceAuth('system:dict:export'))]) +@Log(title='字典类型', business_type=BusinessType.EXPORT) +async def export_system_dict_type_list( + request: Request, + dict_type_page_query: DictTypePageQueryModel = Form(), + query_db: AsyncSession = Depends(get_db), +): + # 获取全量数据 + dict_type_query_result = await DictTypeService.get_dict_type_list_services( + query_db, dict_type_page_query, is_page=False + ) + dict_type_export_result = await DictTypeService.export_dict_type_list_services(dict_type_query_result) + logger.info('导出成功') + + return ResponseUtil.streaming(data=bytes2file_response(dict_type_export_result)) + + +@dictController.get('/data/type/{dict_type}') +async def query_system_dict_type_data(request: Request, dict_type: str, query_db: AsyncSession = Depends(get_db)): + # 获取全量数据 + dict_data_query_result = await DictDataService.query_dict_data_list_from_cache_services( + request.app.state.redis, dict_type + ) + logger.info('获取成功') + + return ResponseUtil.success(data=dict_data_query_result) + + +@dictController.get( + '/data/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:list'))] +) +async def get_system_dict_data_list( + request: Request, + dict_data_page_query: DictDataPageQueryModel = Depends(DictDataPageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + # 获取分页数据 + dict_data_page_query_result = await DictDataService.get_dict_data_list_services( + query_db, dict_data_page_query, is_page=True + ) + logger.info('获取成功') + + return ResponseUtil.success(model_content=dict_data_page_query_result) + + +@dictController.post('/data', dependencies=[Depends(CheckUserInterfaceAuth('system:dict:add'))]) +@ValidateFields(validate_model='add_dict_data') +@Log(title='字典数据', business_type=BusinessType.INSERT) +async def add_system_dict_data( + request: Request, + add_dict_data: DictDataModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + add_dict_data.create_by = current_user.user.user_name + add_dict_data.create_time = datetime.now() + add_dict_data.update_by = current_user.user.user_name + add_dict_data.update_time = datetime.now() + add_dict_data_result = await DictDataService.add_dict_data_services(request, query_db, add_dict_data) + logger.info(add_dict_data_result.message) + + return ResponseUtil.success(msg=add_dict_data_result.message) + + +@dictController.put('/data', dependencies=[Depends(CheckUserInterfaceAuth('system:dict:edit'))]) +@ValidateFields(validate_model='edit_dict_data') +@Log(title='字典数据', business_type=BusinessType.UPDATE) +async def edit_system_dict_data( + request: Request, + edit_dict_data: DictDataModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + edit_dict_data.update_by = current_user.user.user_name + edit_dict_data.update_time = datetime.now() + edit_dict_data_result = await DictDataService.edit_dict_data_services(request, query_db, edit_dict_data) + logger.info(edit_dict_data_result.message) + + return ResponseUtil.success(msg=edit_dict_data_result.message) + + +@dictController.delete('/data/{dict_codes}', dependencies=[Depends(CheckUserInterfaceAuth('system:dict:remove'))]) +@Log(title='字典数据', business_type=BusinessType.DELETE) +async def delete_system_dict_data(request: Request, dict_codes: str, query_db: AsyncSession = Depends(get_db)): + delete_dict_data = DeleteDictDataModel(dictCodes=dict_codes) + delete_dict_data_result = await DictDataService.delete_dict_data_services(request, query_db, delete_dict_data) + logger.info(delete_dict_data_result.message) + + return ResponseUtil.success(msg=delete_dict_data_result.message) + + +@dictController.get( + '/data/{dict_code}', + response_model=DictDataModel, + dependencies=[Depends(CheckUserInterfaceAuth('system:dict:query'))], +) +async def query_detail_system_dict_data(request: Request, dict_code: int, query_db: AsyncSession = Depends(get_db)): + detail_dict_data_result = await DictDataService.dict_data_detail_services(query_db, dict_code) + logger.info(f'获取dict_code为{dict_code}的信息成功') + + return ResponseUtil.success(data=detail_dict_data_result) + + +@dictController.post('/data/export', dependencies=[Depends(CheckUserInterfaceAuth('system:dict:export'))]) +@Log(title='字典数据', business_type=BusinessType.EXPORT) +async def export_system_dict_data_list( + request: Request, + dict_data_page_query: DictDataPageQueryModel = Form(), + query_db: AsyncSession = Depends(get_db), +): + # 获取全量数据 + dict_data_query_result = await DictDataService.get_dict_data_list_services( + query_db, dict_data_page_query, is_page=False + ) + dict_data_export_result = await DictDataService.export_dict_data_list_services(dict_data_query_result) + logger.info('导出成功') + + return ResponseUtil.streaming(data=bytes2file_response(dict_data_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/controller/door_controller.py b/ruoyi-fastapi-backend/module_admin/controller/door_controller.py new file mode 100644 index 0000000..362f500 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/door_controller.py @@ -0,0 +1,162 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Form, Request, Body +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.login_service import LoginService +from module_admin.service.door_service import DoorService +from module_admin.service.haikang_service import HaiKangService +from module_admin.entity.vo.door_vo import DeleteDoorModel, DoorModel, DoorPageQueryModel +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + +import time + + + +doorController = APIRouter(prefix='/system/door', dependencies=[Depends(LoginService.get_current_user)]) + +""" + 实时监控页面权限 access:monitor:view ,dependencies=[Depends(CheckUserInterfaceAuth('access:monitor:view'))] + 识别记录表权限 access:record:list , dependencies=[Depends(CheckUserInterfaceAuth('access:record:list'))] + 门禁设备管理 access:device:list, dependencies=[Depends(CheckUserInterfaceAuth('access:device:list'))] +""" + +# 机器人控制开门 +@doorController.post('/control_door') +async def control_door(request: Request, door_index_code: str = Body(), control_type: int = Body()): + print("door_index_code: ", door_index_code) + print("control_type: ", control_type) + + start_time = time.time() + result = await HaiKangService.door_do_control_service(door_index_code, control_type) + print("end_time: ", time.time() - start_time) + if result[0]: + logger.info('门控制成功') + return ResponseUtil.success(data=result[1]) + logger.error('门控制失败') + return ResponseUtil.error(msg=result[1]) + +# 获取门禁设备列表 +@doorController.get( + '/list', response_model=PageResponseModel + # , dependencies=[Depends(CheckUserInterfaceAuth('system:door:list'))] + , dependencies=[Depends(CheckUserInterfaceAuth('access:device:list'))] +) +async def get_system_door_list( + request: Request, + door_page_query: DoorPageQueryModel = Depends(DoorPageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + + # 获取分页数据 + door_page_query_result = await DoorService.get_door_list_services(query_db, door_page_query, is_page=True) + logger.info('获取成功') + + return ResponseUtil.success(model_content=door_page_query_result) + + +# 添加门禁设备 +@doorController.post('/add' + # , dependencies=[Depends(CheckUserInterfaceAuth('system:door:add'))] + , dependencies=[Depends(CheckUserInterfaceAuth('access:device:list'))] + ) +@ValidateFields(validate_model='add_door') +@Log(title='门禁设备', business_type=BusinessType.INSERT) +async def add_system_door( + request: Request, + add_door: DoorModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + add_door.create_time = datetime.now() + add_door.create_by = current_user.user.user_name + add_door.update_time = datetime.now() + add_door.update_by = current_user.user.user_name + add_door_result = await DoorService.add_door_services(query_db, add_door) + logger.info(add_door_result.message) + + return ResponseUtil.success(msg=add_door_result.message) + + +@doorController.put('/edit' + # , dependencies=[Depends(CheckUserInterfaceAuth('system:door:edit'))] + , dependencies=[Depends(CheckUserInterfaceAuth('access:device:list'))] + ) +@ValidateFields(validate_model='edit_door') +@Log(title='门禁设备', business_type=BusinessType.UPDATE) +async def edit_system_door( + request: Request, + edit_door: DoorModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + edit_door.update_by = current_user.user.user_name + edit_door.update_time = datetime.now() + edit_door_result = await DoorService.edit_door_services(query_db, edit_door) + logger.info(edit_door_result.message) + + return ResponseUtil.success(msg=edit_door_result.message) + + +@doorController.delete('/{ids}' + # , dependencies=[Depends(CheckUserInterfaceAuth('system:door:remove'))] + , dependencies=[Depends(CheckUserInterfaceAuth('access:device:list'))] + ) +@Log(title='门禁设备', business_type=BusinessType.DELETE) +async def delete_system_door(request: Request, ids: str, query_db: AsyncSession = Depends(get_db)): + delete_door = DeleteDoorModel(ids=ids) + delete_door_result = await DoorService.delete_door_services(query_db, delete_door) + logger.info(delete_door_result.message) + + return ResponseUtil.success(msg=delete_door_result.message) + +# 获取设备状态 +@doorController.get('/door_status', dependencies=[Depends(CheckUserInterfaceAuth('access:monitor:view'))]) +async def get_door_status(request: Request, query_db: AsyncSession = Depends(get_db)): + result = await DoorService.get_door_status_service(query_db) + + if result[0]: + return ResponseUtil.success(data=result[1]) + + return ResponseUtil.error(msg=result[1]) + +# 获取视频流地址 +@doorController.get('/video_uri/{robot_id}' ,dependencies=[Depends(CheckUserInterfaceAuth('access:monitor:view'))]) +async def get_video_uri(request: Request + , robot_id: int + , query_db: AsyncSession = Depends(get_db) + ): + result = await DoorService.get_video_uri_service(robot_id, query_db) + return ResponseUtil.success(data=result) + + +# @doorController.get( +# '/{id}', response_model=DoorModel, dependencies=[Depends(CheckUserInterfaceAuth('system:door:query'))] +# ) +# async def query_detail_system_door(request: Request, id: int, query_db: AsyncSession = Depends(get_db)): +# door_detail_result = await DoorService.door_detail_services(query_db, id) +# logger.info(f'获取id为{id}的信息成功') + +# return ResponseUtil.success(data=door_detail_result) + + +# @doorController.post('/export', dependencies=[Depends(CheckUserInterfaceAuth('system:door:export'))]) +# @Log(title='门禁设备', business_type=BusinessType.EXPORT) +# async def export_system_door_list( +# request: Request, +# door_page_query: DoorPageQueryModel = Form(), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取全量数据 +# door_query_result = await DoorService.get_door_list_services(query_db, door_page_query, is_page=False) +# door_export_result = await DoorService.export_door_list_services(door_query_result) +# logger.info('导出成功') + +# return ResponseUtil.streaming(data=bytes2file_response(door_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/controller/explanation_content_controller.py b/ruoyi-fastapi-backend/module_admin/controller/explanation_content_controller.py new file mode 100644 index 0000000..19432e7 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/explanation_content_controller.py @@ -0,0 +1,143 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Form, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.explanation_content_type_vo import Explanation_content_typePageQueryModel, \ + Explanation_content_typeQueryModel +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.explanation_content_type_service import Explanation_content_typeService +from module_admin.service.login_service import LoginService +from module_admin.service.explanation_content_service import Explanation_contentService +from module_admin.entity.vo.explanation_content_vo import DeleteExplanation_contentModel, Explanation_contentModel, Explanation_contentPageQueryModel +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil +from config.enums import ExplanationContentStatus + + +explanationContentController = APIRouter(prefix='/system/explanation_content', dependencies=[Depends(LoginService.get_current_user)]) + +""" + 展厅讲解权限字符 exhibitionhall:guide:query, dependencies=[Depends(CheckUserInterfaceAuth('exhibitionhall:guide:query'))] +""" + +@explanationContentController.get( + '/list', response_model=PageResponseModel + # , dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_content:list'))] +) +async def get_system_explanation_content_list( + request: Request, +explanation_content_page_query: Explanation_contentPageQueryModel = Depends(Explanation_contentPageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + # 获取分页数据 + explanation_content_page_query_result = await Explanation_contentService.get_explanation_content_list_services(query_db, explanation_content_page_query, is_page=False) + logger.info('获取成功') + + return ResponseUtil.success(data=explanation_content_page_query_result) + + +@explanationContentController.post('/add_explanation' + # , dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_content:add'))] + ) +@ValidateFields(validate_model='add_explanation_content') +@Log(title='讲解内容', business_type=BusinessType.INSERT) +async def add_system_explanation_content( + request: Request, + add_explanation_content: Explanation_contentModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + add_explanation_content.create_time = datetime.now() + add_explanation_content.create_by = current_user.user.user_name + add_explanation_content.update_time = datetime.now() + add_explanation_content.update_by = current_user.user.user_name + # 新添加内容状态默认0 + add_explanation_content.status = ExplanationContentStatus.NOTSTART.value + add_explanation_content_result = await Explanation_contentService.add_explanation_content_services(query_db, add_explanation_content) + logger.info(add_explanation_content_result.message) + + return ResponseUtil.success(msg=add_explanation_content_result.message) + + +@explanationContentController.put('/edit_explanation' + # , dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_content:edit'))] + ) +@ValidateFields(validate_model='edit_explanation_content') +@Log(title='讲解内容', business_type=BusinessType.UPDATE) +async def edit_system_explanation_content( + request: Request, + edit_explanation_content: Explanation_contentModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + edit_explanation_content.update_by = current_user.user.user_name + edit_explanation_content.update_time = datetime.now() + edit_explanation_content_result = await Explanation_contentService.edit_explanation_content_services(query_db, edit_explanation_content) + logger.info(edit_explanation_content_result.message) + + return ResponseUtil.success(msg=edit_explanation_content_result.message) + + +@explanationContentController.delete('/{explanation_content_ids}' + # , dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_content:remove'))] + ) +@Log(title='讲解内容', business_type=BusinessType.DELETE) +async def delete_system_explanation_content(request: Request, explanation_content_ids: str, query_db: AsyncSession = Depends(get_db)): + delete_explanation_content = DeleteExplanation_contentModel(explanationContentIds=explanation_content_ids) + delete_explanation_content_result = await Explanation_contentService.delete_explanation_content_services(query_db, delete_explanation_content) + logger.info(delete_explanation_content_result.message) + + return ResponseUtil.success(msg=delete_explanation_content_result.message) + + +@explanationContentController.get( + '/type_list', response_model=PageResponseModel + # , dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_content_type:list'))] +) +async def get_system_explanation_content_type_list( + request: Request, +# explanation_content_type_page_query: Explanation_content_typePageQueryModel = Depends(Explanation_content_typePageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + # 获取分页数据 + # explanation_content_type_page_query_result = await Explanation_content_typeService.get_explanation_content_type_list_services(query_db, explanation_content_type_page_query, is_page=True) + + explanation_content_type_query_result = await Explanation_content_typeService.get_explanation_content_type_list_services(query_db, Explanation_content_typeQueryModel(), is_page=False) + + logger.info('获取成功') + + return ResponseUtil.success(data=explanation_content_type_query_result) + + +# @explanationContentController.get( +# '/{explanation_content_id}', response_model=Explanation_contentModel +# # , dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_content:query'))] +# ) +# async def query_detail_system_explanation_content(request: Request, explanation_content_id: int, query_db: AsyncSession = Depends(get_db)): +# explanation_content_detail_result = await Explanation_contentService.explanation_content_detail_services(query_db, explanation_content_id) +# logger.info(f'获取explanation_content_id为{explanation_content_id}的信息成功') +# +# return ResponseUtil.success(data=explanation_content_detail_result) + + +# @explanationContentController.post('/export' +# # , dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_content:export'))] +# ) +# @Log(title='讲解内容', business_type=BusinessType.EXPORT) +# async def export_system_explanation_content_list( +# request: Request, +# explanation_content_page_query: Explanation_contentPageQueryModel = Form(), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取全量数据 +# explanation_content_query_result = await Explanation_contentService.get_explanation_content_list_services(query_db, explanation_content_page_query, is_page=False) +# explanation_content_export_result = await Explanation_contentService.export_explanation_content_list_services(explanation_content_query_result) +# logger.info('导出成功') +# +# return ResponseUtil.streaming(data=bytes2file_response(explanation_content_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/controller/explanation_content_type_controller.py b/ruoyi-fastapi-backend/module_admin/controller/explanation_content_type_controller.py new file mode 100644 index 0000000..3c2344c --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/explanation_content_type_controller.py @@ -0,0 +1,111 @@ +# from datetime import datetime +# from fastapi import APIRouter, Depends, Form, Request +# from pydantic_validation_decorator import ValidateFields +# from sqlalchemy.ext.asyncio import AsyncSession +# from config.enums import BusinessType +# from config.get_db import get_db +# from module_admin.annotation.log_annotation import Log +# from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +# from module_admin.entity.vo.user_vo import CurrentUserModel +# from module_admin.service.login_service import LoginService +# from module_admin.service.explanation_content_type_service import Explanation_content_typeService +# from module_admin.entity.vo.explanation_content_type_vo import DeleteExplanation_content_typeModel, Explanation_content_typeModel, Explanation_content_typePageQueryModel +# from utils.common_util import bytes2file_response +# from utils.log_util import logger +# from utils.page_util import PageResponseModel +# from utils.response_util import ResponseUtil + + +# explanation_content_typeController = APIRouter(prefix='/system/explanation_content_type' +# , dependencies=[Depends(LoginService.get_current_user)] +# ) +# +# +# @explanation_content_typeController.get( +# '/list', response_model=PageResponseModel +# # , dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_content_type:list'))] +# ) +# async def get_system_explanation_content_type_list( +# request: Request, +# # explanation_content_type_page_query: Explanation_content_typePageQueryModel = Depends(Explanation_content_typePageQueryModel.as_query), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取分页数据 +# # explanation_content_type_page_query_result = await Explanation_content_typeService.get_explanation_content_type_list_services(query_db, explanation_content_type_page_query, is_page=True) +# +# explanation_content_type_query_result = await Explanation_content_typeService.get_explanation_content_type_list_services(query_db, is_page=False) +# +# logger.info('获取成功') +# +# return ResponseUtil.success(model_content=explanation_content_type_query_result) + + +# @explanation_content_typeController.post('', dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_content_type:add'))]) +# @ValidateFields(validate_model='add_explanation_content_type') +# @Log(title='讲解内容类型', business_type=BusinessType.INSERT) +# async def add_system_explanation_content_type( +# request: Request, +# add_explanation_content_type: Explanation_content_typeModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# add_explanation_content_type.create_time = datetime.now() +# add_explanation_content_type.create_by = current_user.user.user_name +# add_explanation_content_type.update_time = datetime.now() +# add_explanation_content_type.update_by = current_user.user.user_name +# add_explanation_content_type_result = await Explanation_content_typeService.add_explanation_content_type_services(query_db, add_explanation_content_type) +# logger.info(add_explanation_content_type_result.message) +# +# return ResponseUtil.success(msg=add_explanation_content_type_result.message) +# +# +# @explanation_content_typeController.put('', dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_content_type:edit'))]) +# @ValidateFields(validate_model='edit_explanation_content_type') +# @Log(title='讲解内容类型', business_type=BusinessType.UPDATE) +# async def edit_system_explanation_content_type( +# request: Request, +# edit_explanation_content_type: Explanation_content_typeModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# edit_explanation_content_type.update_by = current_user.user.user_name +# edit_explanation_content_type.update_time = datetime.now() +# edit_explanation_content_type_result = await Explanation_content_typeService.edit_explanation_content_type_services(query_db, edit_explanation_content_type) +# logger.info(edit_explanation_content_type_result.message) +# +# return ResponseUtil.success(msg=edit_explanation_content_type_result.message) +# +# +# @explanation_content_typeController.delete('/{content_type_ids}', dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_content_type:remove'))]) +# @Log(title='讲解内容类型', business_type=BusinessType.DELETE) +# async def delete_system_explanation_content_type(request: Request, content_type_ids: str, query_db: AsyncSession = Depends(get_db)): +# delete_explanation_content_type = DeleteExplanation_content_typeModel(contentTypeIds=content_type_ids) +# delete_explanation_content_type_result = await Explanation_content_typeService.delete_explanation_content_type_services(query_db, delete_explanation_content_type) +# logger.info(delete_explanation_content_type_result.message) +# +# return ResponseUtil.success(msg=delete_explanation_content_type_result.message) +# +# +# @explanation_content_typeController.get( +# '/{content_type_id}', response_model=Explanation_content_typeModel, dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_content_type:query'))] +# ) +# async def query_detail_system_explanation_content_type(request: Request, content_type_id: int, query_db: AsyncSession = Depends(get_db)): +# explanation_content_type_detail_result = await Explanation_content_typeService.explanation_content_type_detail_services(query_db, content_type_id) +# logger.info(f'获取content_type_id为{content_type_id}的信息成功') +# +# return ResponseUtil.success(data=explanation_content_type_detail_result) +# +# +# @explanation_content_typeController.post('/export', dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_content_type:export'))]) +# @Log(title='讲解内容类型', business_type=BusinessType.EXPORT) +# async def export_system_explanation_content_type_list( +# request: Request, +# explanation_content_type_page_query: Explanation_content_typePageQueryModel = Form(), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取全量数据 +# explanation_content_type_query_result = await Explanation_content_typeService.get_explanation_content_type_list_services(query_db, explanation_content_type_page_query, is_page=False) +# explanation_content_type_export_result = await Explanation_content_typeService.export_explanation_content_type_list_services(explanation_content_type_query_result) +# logger.info('导出成功') +# +# return ResponseUtil.streaming(data=bytes2file_response(explanation_content_type_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/controller/explanation_style_controller.py b/ruoyi-fastapi-backend/module_admin/controller/explanation_style_controller.py new file mode 100644 index 0000000..8cf8454 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/explanation_style_controller.py @@ -0,0 +1,105 @@ +# from datetime import datetime +# from fastapi import APIRouter, Depends, Form, Request +# from pydantic_validation_decorator import ValidateFields +# from sqlalchemy.ext.asyncio import AsyncSession +# from config.enums import BusinessType +# from config.get_db import get_db +# from module_admin.annotation.log_annotation import Log +# from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +# from module_admin.entity.vo.user_vo import CurrentUserModel +# from module_admin.service.login_service import LoginService +# from module_admin.service.explanation_style_service import Explanation_styleService +# from module_admin.entity.vo.explanation_style_vo import DeleteExplanation_styleModel, Explanation_styleModel, Explanation_stylePageQueryModel +# from utils.common_util import bytes2file_response +# from utils.log_util import logger +# from utils.page_util import PageResponseModel +# from utils.response_util import ResponseUtil +# +# +# explanation_styleController = APIRouter(prefix='/system/explanation_style', dependencies=[Depends(LoginService.get_current_user)]) +# +# +# @explanation_styleController.get( +# '/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_style:list'))] +# ) +# async def get_system_explanation_style_list( +# request: Request, +# explanation_style_page_query: Explanation_stylePageQueryModel = Depends(Explanation_stylePageQueryModel.as_query), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取分页数据 +# explanation_style_page_query_result = await Explanation_styleService.get_explanation_style_list_services(query_db, explanation_style_page_query, is_page=True) +# logger.info('获取成功') +# +# return ResponseUtil.success(model_content=explanation_style_page_query_result) +# +# +# @explanation_styleController.post('', dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_style:add'))]) +# @ValidateFields(validate_model='add_explanation_style') +# @Log(title='讲解风格', business_type=BusinessType.INSERT) +# async def add_system_explanation_style( +# request: Request, +# add_explanation_style: Explanation_styleModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# add_explanation_style.create_time = datetime.now() +# add_explanation_style.create_by = current_user.user.user_name +# add_explanation_style.update_time = datetime.now() +# add_explanation_style.update_by = current_user.user.user_name +# add_explanation_style_result = await Explanation_styleService.add_explanation_style_services(query_db, add_explanation_style) +# logger.info(add_explanation_style_result.message) +# +# return ResponseUtil.success(msg=add_explanation_style_result.message) +# +# +# @explanation_styleController.put('', dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_style:edit'))]) +# @ValidateFields(validate_model='edit_explanation_style') +# @Log(title='讲解风格', business_type=BusinessType.UPDATE) +# async def edit_system_explanation_style( +# request: Request, +# edit_explanation_style: Explanation_styleModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# edit_explanation_style.update_by = current_user.user.user_name +# edit_explanation_style.update_time = datetime.now() +# edit_explanation_style_result = await Explanation_styleService.edit_explanation_style_services(query_db, edit_explanation_style) +# logger.info(edit_explanation_style_result.message) +# +# return ResponseUtil.success(msg=edit_explanation_style_result.message) +# +# +# @explanation_styleController.delete('/{explanation_style_ids}', dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_style:remove'))]) +# @Log(title='讲解风格', business_type=BusinessType.DELETE) +# async def delete_system_explanation_style(request: Request, explanation_style_ids: str, query_db: AsyncSession = Depends(get_db)): +# delete_explanation_style = DeleteExplanation_styleModel(explanationStyleIds=explanation_style_ids) +# delete_explanation_style_result = await Explanation_styleService.delete_explanation_style_services(query_db, delete_explanation_style) +# logger.info(delete_explanation_style_result.message) +# +# return ResponseUtil.success(msg=delete_explanation_style_result.message) +# +# +# @explanation_styleController.get( +# '/{explanation_style_id}', response_model=Explanation_styleModel, dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_style:query'))] +# ) +# async def query_detail_system_explanation_style(request: Request, explanation_style_id: int, query_db: AsyncSession = Depends(get_db)): +# explanation_style_detail_result = await Explanation_styleService.explanation_style_detail_services(query_db, explanation_style_id) +# logger.info(f'获取explanation_style_id为{explanation_style_id}的信息成功') +# +# return ResponseUtil.success(data=explanation_style_detail_result) +# +# +# @explanation_styleController.post('/export', dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_style:export'))]) +# @Log(title='讲解风格', business_type=BusinessType.EXPORT) +# async def export_system_explanation_style_list( +# request: Request, +# explanation_style_page_query: Explanation_stylePageQueryModel = Form(), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取全量数据 +# explanation_style_query_result = await Explanation_styleService.get_explanation_style_list_services(query_db, explanation_style_page_query, is_page=False) +# explanation_style_export_result = await Explanation_styleService.export_explanation_style_list_services(explanation_style_query_result) +# logger.info('导出成功') +# +# return ResponseUtil.streaming(data=bytes2file_response(explanation_style_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/controller/explanation_style_robot_pair_controller.py b/ruoyi-fastapi-backend/module_admin/controller/explanation_style_robot_pair_controller.py new file mode 100644 index 0000000..7356a51 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/explanation_style_robot_pair_controller.py @@ -0,0 +1,130 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Form, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.login_service import LoginService +from module_admin.service.explanation_style_robot_pair_service import Explanation_style_robot_pairService +from module_admin.entity.vo.explanation_style_robot_pair_vo import DeleteExplanation_style_robot_pairModel, \ + Explanation_style_robot_pairModel, Explanation_style_robot_pairPageQueryModel, SwitchExplanationStyleModel +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +explanation_style_robot_pairController = APIRouter(prefix='/system/explanation_style_robot_pair', dependencies=[Depends(LoginService.get_current_user)]) + + +# @explanation_style_robot_pairController.get( +# '/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_style_robot_pair:list'))] +# ) +# async def get_system_explanation_style_robot_pair_list( +# request: Request, +# robot_id: int, +# # explanation_style_robot_pair_page_query: Explanation_style_robot_pairPageQueryModel = Depends(Explanation_style_robot_pairPageQueryModel.as_query), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取分页数据 +# # explanation_style_robot_pair_page_query_result = await Explanation_style_robot_pairService.get_explanation_style_robot_pair_list_services(query_db, explanation_style_robot_pair_page_query, is_page=False) + +# explanation_style_robot_pair_query_result = await Explanation_style_robot_pairService.get_style_robot_pair_list_services(query_db, robot_id) +# logger.info('获取成功') + +# return ResponseUtil.success(data=explanation_style_robot_pair_query_result) + +# 切换风格 +@explanation_style_robot_pairController.post('/switch_style') +async def switch_explanation_style( + request: Request, + switch_explanation_style: SwitchExplanationStyleModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + switch_explanation_style.update_by = current_user.user.user_name + switch_explanation_style_message = await Explanation_style_robot_pairService.switch_explanation_style_services(query_db, switch_explanation_style) + + if switch_explanation_style_message.is_success: + logger.info(switch_explanation_style_message.message) + + return ResponseUtil.success(msg=switch_explanation_style_message.message) + else: + return ResponseUtil.error(msg=switch_explanation_style_message.message) + + + +# @explanation_style_robot_pairController.post('', dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_style_robot_pair:add'))]) +# @ValidateFields(validate_model='add_explanation_style_robot_pair') +# @Log(title='讲解风格--机器人配对', business_type=BusinessType.INSERT) +# async def add_system_explanation_style_robot_pair( +# request: Request, +# add_explanation_style_robot_pair: Explanation_style_robot_pairModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# add_explanation_style_robot_pair.create_time = datetime.now() +# add_explanation_style_robot_pair.create_by = current_user.user.user_name +# add_explanation_style_robot_pair.update_time = datetime.now() +# add_explanation_style_robot_pair.update_by = current_user.user.user_name +# add_explanation_style_robot_pair_result = await Explanation_style_robot_pairService.add_explanation_style_robot_pair_services(query_db, add_explanation_style_robot_pair) +# logger.info(add_explanation_style_robot_pair_result.message) +# +# return ResponseUtil.success(msg=add_explanation_style_robot_pair_result.message) + + + + +# @explanation_style_robot_pairController.put('', dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_style_robot_pair:edit'))]) +# @ValidateFields(validate_model='edit_explanation_style_robot_pair') +# @Log(title='讲解风格--机器人配对', business_type=BusinessType.UPDATE) +# async def edit_system_explanation_style_robot_pair( +# request: Request, +# edit_explanation_style_robot_pair: Explanation_style_robot_pairModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# edit_explanation_style_robot_pair.update_by = current_user.user.user_name +# edit_explanation_style_robot_pair.update_time = datetime.now() +# edit_explanation_style_robot_pair_result = await Explanation_style_robot_pairService.edit_explanation_style_robot_pair_services(query_db, edit_explanation_style_robot_pair) +# logger.info(edit_explanation_style_robot_pair_result.message) +# +# return ResponseUtil.success(msg=edit_explanation_style_robot_pair_result.message) + + +# @explanation_style_robot_pairController.delete('/{pairing_ids}', dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_style_robot_pair:remove'))]) +# @Log(title='讲解风格--机器人配对', business_type=BusinessType.DELETE) +# async def delete_system_explanation_style_robot_pair(request: Request, pairing_ids: str, query_db: AsyncSession = Depends(get_db)): +# delete_explanation_style_robot_pair = DeleteExplanation_style_robot_pairModel(pairingIds=pairing_ids) +# delete_explanation_style_robot_pair_result = await Explanation_style_robot_pairService.delete_explanation_style_robot_pair_services(query_db, delete_explanation_style_robot_pair) +# logger.info(delete_explanation_style_robot_pair_result.message) +# +# return ResponseUtil.success(msg=delete_explanation_style_robot_pair_result.message) + + +# @explanation_style_robot_pairController.get( +# '/{pairing_id}', response_model=Explanation_style_robot_pairModel, dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_style_robot_pair:query'))] +# ) +# async def query_detail_system_explanation_style_robot_pair(request: Request, pairing_id: int, query_db: AsyncSession = Depends(get_db)): +# explanation_style_robot_pair_detail_result = await Explanation_style_robot_pairService.explanation_style_robot_pair_detail_services(query_db, pairing_id) +# logger.info(f'获取pairing_id为{pairing_id}的信息成功') +# +# return ResponseUtil.success(data=explanation_style_robot_pair_detail_result) + + +# @explanation_style_robot_pairController.post('/export', dependencies=[Depends(CheckUserInterfaceAuth('system:explanation_style_robot_pair:export'))]) +# @Log(title='讲解风格--机器人配对', business_type=BusinessType.EXPORT) +# async def export_system_explanation_style_robot_pair_list( +# request: Request, +# explanation_style_robot_pair_page_query: Explanation_style_robot_pairPageQueryModel = Form(), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取全量数据 +# explanation_style_robot_pair_query_result = await Explanation_style_robot_pairService.get_explanation_style_robot_pair_list_services(query_db, explanation_style_robot_pair_page_query, is_page=False) +# explanation_style_robot_pair_export_result = await Explanation_style_robot_pairService.export_explanation_style_robot_pair_list_services(explanation_style_robot_pair_query_result) +# logger.info('导出成功') +# +# return ResponseUtil.streaming(data=bytes2file_response(explanation_style_robot_pair_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/controller/haikang_controller.py b/ruoyi-fastapi-backend/module_admin/controller/haikang_controller.py new file mode 100644 index 0000000..dec22b9 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/haikang_controller.py @@ -0,0 +1,81 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Form, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.service.login_service import LoginService +from module_admin.service.haikang_service import HaiKangService +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil +from module_admin.entity.vo.haikang_vo import VisitorReservationQueryModel + +from typing import List + + +haikang_controller = APIRouter(prefix="/system/haikang", dependencies=[Depends(LoginService.get_current_user)]) + + +# # 查询门禁点列表 +# @haikang_controller.get("/door_list" +# # , response_model=PageResponseModel +# # , dependencies=[Depends(CheckUserInterfaceAuth("system:haikang:list"))] +# ) +# async def get_system_haikang_list( +# request: Request, +# pageNo:int, +# pageSize:int, +# ): +# """ +# 查询门禁点列表 +# """ +# result = await HaiKangService.get_door_list_service(pageNo, pageSize) +# return parse_result(result) + +# # 查询门禁点状态 +# @haikang_controller.post( +# "/door_status", +# # dependencies=[Depends(CheckUserInterfaceAuth("system:haikang:list"))] +# ) +# async def get_door_status( +# request: Request, +# door_index_codes: List[str], +# ): +# """ +# 查询门禁点列表 +# """ +# result = await HaiKangService.get_door_status_service(door_index_codes) +# return parse_result(result) + +# # 门禁点反控 +# @haikang_controller.post( +# "/door_do_control", +# # dependencies=[Depends(CheckUserInterfaceAuth("system:haikang:list"))] +# ) +# async def door_do_control( +# request: Request, +# door_index_codes: List[str], +# control_type: int, +# ): +# pass + +# 查询访客预约记录 +@haikang_controller.post( + "/get_visitor_list", + # dependencies=[Depends(CheckUserInterfaceAuth("system:haikang:list"))] +) +async def get_visitor( + request: Request, + visitor_query: VisitorReservationQueryModel +): + result = await HaiKangService.get_visitor_list_service(visitor_query) + return parse_result(result) + + +def parse_result(result): + if result[0]: + return ResponseUtil.success(data=result[1]) + return ResponseUtil.error(msg=f"code:{result[1]} msg:{result[2]}") \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_admin/controller/identification_record_controller.py b/ruoyi-fastapi-backend/module_admin/controller/identification_record_controller.py new file mode 100644 index 0000000..612a71a --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/identification_record_controller.py @@ -0,0 +1,106 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Form, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.login_service import LoginService +from module_admin.service.identification_record_service import Identification_recordService +from module_admin.entity.vo.identification_record_vo import DeleteIdentification_recordModel, Identification_recordModel, Identification_recordPageQueryModel +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +identification_recordController = APIRouter(prefix='/system/identification_record', dependencies=[Depends(LoginService.get_current_user)]) + + +@identification_recordController.get( + '/list', response_model=PageResponseModel + , dependencies=[Depends(CheckUserInterfaceAuth('access:record:list'))] +) +async def get_system_identification_record_list( + request: Request, +identification_record_page_query: Identification_recordPageQueryModel = Depends(Identification_recordPageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + # 获取分页数据 + identification_record_page_query_result = await Identification_recordService.get_identification_record_list_services(query_db, identification_record_page_query, is_page=True) + logger.info('获取成功') + + return ResponseUtil.success(data=identification_record_page_query_result) + + +@identification_recordController.post('', dependencies=[Depends(CheckUserInterfaceAuth('system:identification_record:add'))]) +@ValidateFields(validate_model='add_identification_record') +@Log(title='识别记录', business_type=BusinessType.INSERT) +async def add_system_identification_record( + request: Request, + add_identification_record: Identification_recordModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + add_identification_record.create_time = datetime.now() + add_identification_record.create_by = current_user.user.user_name + add_identification_record_result = await Identification_recordService.add_identification_record_services(query_db, add_identification_record) + logger.info(add_identification_record_result.message) + + return ResponseUtil.success(msg=add_identification_record_result.message) + + +# @identification_recordController.put('', dependencies=[Depends(CheckUserInterfaceAuth('system:identification_record:edit'))]) +# @ValidateFields(validate_model='edit_identification_record') +# @Log(title='识别记录', business_type=BusinessType.UPDATE) +# async def edit_system_identification_record( +# request: Request, +# edit_identification_record: Identification_recordModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# edit_identification_record.update_by = current_user.user.user_name +# edit_identification_record.update_time = datetime.now() +# edit_identification_record_result = await Identification_recordService.edit_identification_record_services(query_db, edit_identification_record) +# logger.info(edit_identification_record_result.message) + +# return ResponseUtil.success(msg=edit_identification_record_result.message) + + +# @identification_recordController.delete('/{ids}', dependencies=[Depends(CheckUserInterfaceAuth('system:identification_record:remove'))]) +# @Log(title='识别记录', business_type=BusinessType.DELETE) +# async def delete_system_identification_record(request: Request, ids: str, query_db: AsyncSession = Depends(get_db)): +# delete_identification_record = DeleteIdentification_recordModel(ids=ids) +# delete_identification_record_result = await Identification_recordService.delete_identification_record_services(query_db, delete_identification_record) +# logger.info(delete_identification_record_result.message) + +# return ResponseUtil.success(msg=delete_identification_record_result.message) + + +# @identification_recordController.get( +# '/{id}', response_model=Identification_recordModel, dependencies=[Depends(CheckUserInterfaceAuth('system:identification_record:query'))] +# ) +# async def query_detail_system_identification_record(request: Request, id: int, query_db: AsyncSession = Depends(get_db)): +# identification_record_detail_result = await Identification_recordService.identification_record_detail_services(query_db, id) +# logger.info(f'获取id为{id}的信息成功') + +# return ResponseUtil.success(data=identification_record_detail_result) + + +@identification_recordController.post('/export' + # , dependencies=[Depends(CheckUserInterfaceAuth('system:identification_record:export'))] + ) +@Log(title='识别记录', business_type=BusinessType.EXPORT) +async def export_system_identification_record_list( + request: Request, + identification_record_page_query: Identification_recordPageQueryModel = Form(), + query_db: AsyncSession = Depends(get_db), +): + # 获取全量数据 + identification_record_query_result = await Identification_recordService.get_identification_record_list_services(query_db, identification_record_page_query, is_page=False) + identification_record_export_result = await Identification_recordService.export_identification_record_list_services(identification_record_query_result) + logger.info('导出成功') + + return ResponseUtil.streaming(data=bytes2file_response(identification_record_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/controller/identification_statistics_controller.py b/ruoyi-fastapi-backend/module_admin/controller/identification_statistics_controller.py new file mode 100644 index 0000000..b0c7314 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/identification_statistics_controller.py @@ -0,0 +1,131 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Form, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.login_service import LoginService +from module_admin.service.identification_statistics_service import Identification_statisticsService +from module_admin.entity.vo.identification_statistics_vo import DeleteIdentification_statisticsModel, Identification_statisticsModel, Identification_statisticsPageQueryModel +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +identification_statisticsController = APIRouter(prefix='/system/identification_statistics', dependencies=[Depends(LoginService.get_current_user)]) + +# 获取今天的识别统计数据 +@identification_statisticsController.get('/today') +async def get_today_identification_statistics(request: Request, query_db: AsyncSession = Depends(get_db)): + """ + 获取今天的识别统计数据 + :param request: + :param query_db: + :return: + """ + logger.info('获取今天的识别统计数据') + identification_statistics = await Identification_statisticsService.get_today_identification_statistics_services(query_db) + return ResponseUtil.success(data = identification_statistics) + +# 获取总的识别统计数据 +@identification_statisticsController.get('/total') +async def get_total_identification_statistics(request: Request, query_db: AsyncSession = Depends(get_db)): + """ + 获取今天的识别统计数据 + :param request: + :param query_db: + :return: + """ + logger.info('获取今天的识别统计数据') + identification_statistics = await Identification_statisticsService.get_total_identification_statistics_services(query_db) + return ResponseUtil.success(data = identification_statistics) + + + + +# @identification_statisticsController.get( +# '/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('system:identification_statistics:list'))] +# ) +# async def get_system_identification_statistics_list( +# request: Request, +# identification_statistics_page_query: Identification_statisticsPageQueryModel = Depends(Identification_statisticsPageQueryModel.as_query), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取分页数据 +# identification_statistics_page_query_result = await Identification_statisticsService.get_identification_statistics_list_services(query_db, identification_statistics_page_query, is_page=True) +# logger.info('获取成功') + +# return ResponseUtil.success(model_content=identification_statistics_page_query_result) + + +# @identification_statisticsController.post('', dependencies=[Depends(CheckUserInterfaceAuth('system:identification_statistics:add'))]) +# @ValidateFields(validate_model='add_identification_statistics') +# @Log(title='识别统计', business_type=BusinessType.INSERT) +# async def add_system_identification_statistics( +# request: Request, +# add_identification_statistics: Identification_statisticsModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# add_identification_statistics.create_time = datetime.now() +# add_identification_statistics.create_by = current_user.user.user_name +# add_identification_statistics_result = await Identification_statisticsService.add_identification_statistics_services(query_db, add_identification_statistics) +# logger.info(add_identification_statistics_result.message) + +# return ResponseUtil.success(msg=add_identification_statistics_result.message) + + +# @identification_statisticsController.put('', dependencies=[Depends(CheckUserInterfaceAuth('system:identification_statistics:edit'))]) +# @ValidateFields(validate_model='edit_identification_statistics') +# @Log(title='识别统计', business_type=BusinessType.UPDATE) +# async def edit_system_identification_statistics( +# request: Request, +# edit_identification_statistics: Identification_statisticsModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# edit_identification_statistics.update_by = current_user.user.user_name +# edit_identification_statistics.update_time = datetime.now() +# edit_identification_statistics_result = await Identification_statisticsService.edit_identification_statistics_services(query_db, edit_identification_statistics) +# logger.info(edit_identification_statistics_result.message) + +# return ResponseUtil.success(msg=edit_identification_statistics_result.message) + + +# @identification_statisticsController.delete('/{ids}', dependencies=[Depends(CheckUserInterfaceAuth('system:identification_statistics:remove'))]) +# @Log(title='识别统计', business_type=BusinessType.DELETE) +# async def delete_system_identification_statistics(request: Request, ids: str, query_db: AsyncSession = Depends(get_db)): +# delete_identification_statistics = DeleteIdentification_statisticsModel(ids=ids) +# delete_identification_statistics_result = await Identification_statisticsService.delete_identification_statistics_services(query_db, delete_identification_statistics) +# logger.info(delete_identification_statistics_result.message) + +# return ResponseUtil.success(msg=delete_identification_statistics_result.message) + + +# @identification_statisticsController.get( +# '/{id}', response_model=Identification_statisticsModel, dependencies=[Depends(CheckUserInterfaceAuth('system:identification_statistics:query'))] +# ) +# async def query_detail_system_identification_statistics(request: Request, id: int, query_db: AsyncSession = Depends(get_db)): +# identification_statistics_detail_result = await Identification_statisticsService.identification_statistics_detail_services(query_db, id) +# logger.info(f'获取id为{id}的信息成功') + +# return ResponseUtil.success(data=identification_statistics_detail_result) + + +# @identification_statisticsController.post('/export', dependencies=[Depends(CheckUserInterfaceAuth('system:identification_statistics:export'))]) +# @Log(title='识别统计', business_type=BusinessType.EXPORT) +# async def export_system_identification_statistics_list( +# request: Request, +# identification_statistics_page_query: Identification_statisticsPageQueryModel = Form(), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取全量数据 +# identification_statistics_query_result = await Identification_statisticsService.get_identification_statistics_list_services(query_db, identification_statistics_page_query, is_page=False) +# identification_statistics_export_result = await Identification_statisticsService.export_identification_statistics_list_services(identification_statistics_query_result) +# logger.info('导出成功') + +# return ResponseUtil.streaming(data=bytes2file_response(identification_statistics_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/controller/info_controller.py b/ruoyi-fastapi-backend/module_admin/controller/info_controller.py new file mode 100644 index 0000000..91469a1 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/info_controller.py @@ -0,0 +1,131 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Form, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.login_service import LoginService +from module_admin.service.info_service import InfoService +from module_admin.entity.vo.info_vo import DeleteInfoModel, InfoModel, InfoPageQueryModel, InfoUpdateModel +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +infoController = APIRouter(prefix='/robot/info', dependencies=[Depends(LoginService.get_current_user)]) + + +@infoController.get( + '/list', response_model=PageResponseModel + # , dependencies=[Depends(CheckUserInterfaceAuth('robot:info:list'))] +) +async def get_system_info_list( + request: Request, +info_page_query: InfoPageQueryModel = Depends(InfoPageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + print('xxxxxxxxx', info_page_query.begin_create_time) + print('xxxxxxxxx',info_page_query.end_create_time) + print(info_page_query.name) + # 获取分页数据 + info_page_query_result = await InfoService.get_info_list_services(query_db, info_page_query, is_page=True) + logger.info('获取成功') + + return ResponseUtil.success(model_content=info_page_query_result) + + +@infoController.post('/add_robot' + # , dependencies=[Depends(CheckUserInterfaceAuth('robot:info:add'))] + ) +@ValidateFields(validate_model='add_info') +@Log(title='机器人信息', business_type=BusinessType.INSERT) +async def add_system_info( + request: Request, + add_info: InfoModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + add_info.create_time = datetime.now() + add_info.create_by = current_user.user.user_name + add_info.update_time = datetime.now() + add_info.update_by = current_user.user.user_name + add_info.del_flag = '0' + add_info.online = '1' + add_info.power = '100' + add_info_result = await InfoService.add_info_services(query_db, add_info) + logger.info(add_info_result.message) + + return ResponseUtil.success(msg=add_info_result.message) + + +@infoController.put('/edit_robot' + # , dependencies=[Depends(CheckUserInterfaceAuth('robot:info:edit'))] + ) +@ValidateFields(validate_model='edit_info') +@Log(title='机器人信息', business_type=BusinessType.UPDATE) +async def edit_system_info( + request: Request, + edit_info: InfoUpdateModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + edit_info.update_by = current_user.user.user_name + edit_info.update_time = datetime.now() + edit_info_result = await InfoService.edit_info_services(query_db, edit_info) + logger.info(edit_info_result.message) + + return ResponseUtil.success(msg=edit_info_result.message) + + +@infoController.delete('/{robot_ids}' + # , dependencies=[Depends(CheckUserInterfaceAuth('robot:info:remove'))] + ) +@Log(title='机器人信息', business_type=BusinessType.DELETE) +async def delete_system_info(request: Request, robot_ids: str, query_db: AsyncSession = Depends(get_db)): + delete_info = DeleteInfoModel(robotIds=robot_ids) + delete_info_result = await InfoService.delete_info_services(query_db, delete_info) + logger.info(delete_info_result.message) + + return ResponseUtil.success(msg=delete_info_result.message) + + +@infoController.get( + '/{robot_id}', response_model=InfoModel + # , dependencies=[Depends(CheckUserInterfaceAuth('robot:info:query'))] +) +async def query_detail_system_info(request: Request, robot_id: int, query_db: AsyncSession = Depends(get_db)): + info_detail_result = await InfoService.info_detail_services(query_db, robot_id) + logger.info(f'获取robot_id为{robot_id}的信息成功') + + return ResponseUtil.success(data=info_detail_result) + +# 根据机器人id刷新机器人信息 +@infoController.get('/refresh_robot/{robot_ids}' + # , dependencies=[Depends(CheckUserInterfaceAuth('system:info:add'))] +) +async def refresh_robot(request: Request,robot_ids: str , query_db: AsyncSession = Depends(get_db)): + refresh_result = await InfoService.refresh_robot_services(query_db, robot_ids) + logger.info(f'刷新机器人信息成功') + + return ResponseUtil.success(data=refresh_result) + + +@infoController.post('/export' + # , dependencies=[Depends(CheckUserInterfaceAuth('system:info:export'))] + ) +@Log(title='机器人信息', business_type=BusinessType.EXPORT) +async def export_system_info_list( + request: Request, + info_page_query: InfoPageQueryModel = Form(), + query_db: AsyncSession = Depends(get_db), +): + # 获取全量数据 + info_query_result = await InfoService.get_info_list_services(query_db, info_page_query, is_page=False) + info_export_result = await InfoService.export_info_list_services(info_query_result) + logger.info('导出成功') + + return ResponseUtil.streaming(data=bytes2file_response(info_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/controller/job_controller.py b/ruoyi-fastapi-backend/module_admin/controller/job_controller.py new file mode 100644 index 0000000..c930569 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/job_controller.py @@ -0,0 +1,194 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Form, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.job_vo import ( + DeleteJobLogModel, + DeleteJobModel, + EditJobModel, + JobLogPageQueryModel, + JobModel, + JobPageQueryModel, +) +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.job_log_service import JobLogService +from module_admin.service.job_service import JobService +from module_admin.service.login_service import LoginService +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +jobController = APIRouter(prefix='/monitor', dependencies=[Depends(LoginService.get_current_user)]) + + +@jobController.get( + '/job/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:list'))] +) +async def get_system_job_list( + request: Request, + job_page_query: JobPageQueryModel = Depends(JobPageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + # 获取分页数据 + notice_page_query_result = await JobService.get_job_list_services(query_db, job_page_query, is_page=True) + logger.info('获取成功') + + return ResponseUtil.success(model_content=notice_page_query_result) + + +@jobController.post('/job', dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:add'))]) +@ValidateFields(validate_model='add_job') +@Log(title='定时任务', business_type=BusinessType.INSERT) +async def add_system_job( + request: Request, + add_job: JobModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + add_job.create_by = current_user.user.user_name + add_job.create_time = datetime.now() + add_job.update_by = current_user.user.user_name + add_job.update_time = datetime.now() + add_job_result = await JobService.add_job_services(query_db, add_job) + logger.info(add_job_result.message) + + return ResponseUtil.success(msg=add_job_result.message) + + +@jobController.put('/job', dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:edit'))]) +@ValidateFields(validate_model='edit_job') +@Log(title='定时任务', business_type=BusinessType.UPDATE) +async def edit_system_job( + request: Request, + edit_job: EditJobModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + edit_job.update_by = current_user.user.user_name + edit_job.update_time = datetime.now() + edit_job_result = await JobService.edit_job_services(query_db, edit_job) + logger.info(edit_job_result.message) + + return ResponseUtil.success(msg=edit_job_result.message) + + +@jobController.put('/job/changeStatus', dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:changeStatus'))]) +@Log(title='定时任务', business_type=BusinessType.UPDATE) +async def change_system_job_status( + request: Request, + change_job: EditJobModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + edit_job = EditJobModel( + jobId=change_job.job_id, + status=change_job.status, + updateBy=current_user.user.user_name, + updateTime=datetime.now(), + type='status', + ) + edit_job_result = await JobService.edit_job_services(query_db, edit_job) + logger.info(edit_job_result.message) + + return ResponseUtil.success(msg=edit_job_result.message) + + +@jobController.put('/job/run', dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:changeStatus'))]) +@Log(title='定时任务', business_type=BusinessType.UPDATE) +async def execute_system_job(request: Request, execute_job: JobModel, query_db: AsyncSession = Depends(get_db)): + execute_job_result = await JobService.execute_job_once_services(query_db, execute_job) + logger.info(execute_job_result.message) + + return ResponseUtil.success(msg=execute_job_result.message) + + +@jobController.delete('/job/{job_ids}', dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:remove'))]) +@Log(title='定时任务', business_type=BusinessType.DELETE) +async def delete_system_job(request: Request, job_ids: str, query_db: AsyncSession = Depends(get_db)): + delete_job = DeleteJobModel(jobIds=job_ids) + delete_job_result = await JobService.delete_job_services(query_db, delete_job) + logger.info(delete_job_result.message) + + return ResponseUtil.success(msg=delete_job_result.message) + + +@jobController.get( + '/job/{job_id}', response_model=JobModel, dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:query'))] +) +async def query_detail_system_job(request: Request, job_id: int, query_db: AsyncSession = Depends(get_db)): + job_detail_result = await JobService.job_detail_services(query_db, job_id) + logger.info(f'获取job_id为{job_id}的信息成功') + + return ResponseUtil.success(data=job_detail_result) + + +@jobController.post('/job/export', dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:export'))]) +@Log(title='定时任务', business_type=BusinessType.EXPORT) +async def export_system_job_list( + request: Request, + job_page_query: JobPageQueryModel = Form(), + query_db: AsyncSession = Depends(get_db), +): + # 获取全量数据 + job_query_result = await JobService.get_job_list_services(query_db, job_page_query, is_page=False) + job_export_result = await JobService.export_job_list_services(request, job_query_result) + logger.info('导出成功') + + return ResponseUtil.streaming(data=bytes2file_response(job_export_result)) + + +@jobController.get( + '/jobLog/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:list'))] +) +async def get_system_job_log_list( + request: Request, + job_log_page_query: JobLogPageQueryModel = Depends(JobLogPageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + # 获取分页数据 + job_log_page_query_result = await JobLogService.get_job_log_list_services( + query_db, job_log_page_query, is_page=True + ) + logger.info('获取成功') + + return ResponseUtil.success(model_content=job_log_page_query_result) + + +@jobController.delete('/jobLog/clean', dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:remove'))]) +@Log(title='定时任务调度日志', business_type=BusinessType.CLEAN) +async def clear_system_job_log(request: Request, query_db: AsyncSession = Depends(get_db)): + clear_job_log_result = await JobLogService.clear_job_log_services(query_db) + logger.info(clear_job_log_result.message) + + return ResponseUtil.success(msg=clear_job_log_result.message) + + +@jobController.delete('/jobLog/{job_log_ids}', dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:remove'))]) +@Log(title='定时任务调度日志', business_type=BusinessType.DELETE) +async def delete_system_job_log(request: Request, job_log_ids: str, query_db: AsyncSession = Depends(get_db)): + delete_job_log = DeleteJobLogModel(jobLogIds=job_log_ids) + delete_job_log_result = await JobLogService.delete_job_log_services(query_db, delete_job_log) + logger.info(delete_job_log_result.message) + + return ResponseUtil.success(msg=delete_job_log_result.message) + + +@jobController.post('/jobLog/export', dependencies=[Depends(CheckUserInterfaceAuth('monitor:job:export'))]) +@Log(title='定时任务调度日志', business_type=BusinessType.EXPORT) +async def export_system_job_log_list( + request: Request, + job_log_page_query: JobLogPageQueryModel = Form(), + query_db: AsyncSession = Depends(get_db), +): + # 获取全量数据 + job_log_query_result = await JobLogService.get_job_log_list_services(query_db, job_log_page_query, is_page=False) + job_log_export_result = await JobLogService.export_job_log_list_services(request, job_log_query_result) + logger.info('导出成功') + + return ResponseUtil.streaming(data=bytes2file_response(job_log_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/controller/log_controller.py b/ruoyi-fastapi-backend/module_admin/controller/log_controller.py new file mode 100644 index 0000000..a01ba94 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/log_controller.py @@ -0,0 +1,150 @@ +from fastapi import APIRouter, Depends, Form, Request +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.log_vo import ( + DeleteLoginLogModel, + DeleteOperLogModel, + LoginLogPageQueryModel, + OperLogPageQueryModel, + UnlockUser, +) +from module_admin.service.log_service import LoginLogService, OperationLogService +from module_admin.service.login_service import LoginService +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +logController = APIRouter(prefix='/monitor', dependencies=[Depends(LoginService.get_current_user)]) + + +@logController.get( + '/operlog/list', + response_model=PageResponseModel, + dependencies=[Depends(CheckUserInterfaceAuth('monitor:operlog:list'))], +) +async def get_system_operation_log_list( + request: Request, + operation_log_page_query: OperLogPageQueryModel = Depends(OperLogPageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + # 获取分页数据 + operation_log_page_query_result = await OperationLogService.get_operation_log_list_services( + query_db, operation_log_page_query, is_page=True + ) + logger.info('获取成功') + + return ResponseUtil.success(model_content=operation_log_page_query_result) + + +@logController.delete('/operlog/clean', dependencies=[Depends(CheckUserInterfaceAuth('monitor:operlog:remove'))]) +@Log(title='操作日志', business_type=BusinessType.CLEAN) +async def clear_system_operation_log(request: Request, query_db: AsyncSession = Depends(get_db)): + clear_operation_log_result = await OperationLogService.clear_operation_log_services(query_db) + logger.info(clear_operation_log_result.message) + + return ResponseUtil.success(msg=clear_operation_log_result.message) + + +@logController.delete('/operlog/{oper_ids}', dependencies=[Depends(CheckUserInterfaceAuth('monitor:operlog:remove'))]) +@Log(title='操作日志', business_type=BusinessType.DELETE) +async def delete_system_operation_log(request: Request, oper_ids: str, query_db: AsyncSession = Depends(get_db)): + delete_operation_log = DeleteOperLogModel(operIds=oper_ids) + delete_operation_log_result = await OperationLogService.delete_operation_log_services( + query_db, delete_operation_log + ) + logger.info(delete_operation_log_result.message) + + return ResponseUtil.success(msg=delete_operation_log_result.message) + + +@logController.post('/operlog/export', dependencies=[Depends(CheckUserInterfaceAuth('monitor:operlog:export'))]) +@Log(title='操作日志', business_type=BusinessType.EXPORT) +async def export_system_operation_log_list( + request: Request, + operation_log_page_query: OperLogPageQueryModel = Form(), + query_db: AsyncSession = Depends(get_db), +): + # 获取全量数据 + operation_log_query_result = await OperationLogService.get_operation_log_list_services( + query_db, operation_log_page_query, is_page=False + ) + operation_log_export_result = await OperationLogService.export_operation_log_list_services( + request, operation_log_query_result + ) + logger.info('导出成功') + + return ResponseUtil.streaming(data=bytes2file_response(operation_log_export_result)) + + +@logController.get( + '/logininfor/list', + response_model=PageResponseModel, + dependencies=[Depends(CheckUserInterfaceAuth('monitor:logininfor:list'))], +) +async def get_system_login_log_list( + request: Request, + login_log_page_query: LoginLogPageQueryModel = Depends(LoginLogPageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + # 获取分页数据 + login_log_page_query_result = await LoginLogService.get_login_log_list_services( + query_db, login_log_page_query, is_page=True + ) + logger.info('获取成功') + + return ResponseUtil.success(model_content=login_log_page_query_result) + + +@logController.delete('/logininfor/clean', dependencies=[Depends(CheckUserInterfaceAuth('monitor:logininfor:remove'))]) +@Log(title='登录日志', business_type=BusinessType.CLEAN) +async def clear_system_login_log(request: Request, query_db: AsyncSession = Depends(get_db)): + clear_login_log_result = await LoginLogService.clear_login_log_services(query_db) + logger.info(clear_login_log_result.message) + + return ResponseUtil.success(msg=clear_login_log_result.message) + + +@logController.delete( + '/logininfor/{info_ids}', dependencies=[Depends(CheckUserInterfaceAuth('monitor:logininfor:remove'))] +) +@Log(title='登录日志', business_type=BusinessType.DELETE) +async def delete_system_login_log(request: Request, info_ids: str, query_db: AsyncSession = Depends(get_db)): + delete_login_log = DeleteLoginLogModel(infoIds=info_ids) + delete_login_log_result = await LoginLogService.delete_login_log_services(query_db, delete_login_log) + logger.info(delete_login_log_result.message) + + return ResponseUtil.success(msg=delete_login_log_result.message) + + +@logController.get( + '/logininfor/unlock/{user_name}', dependencies=[Depends(CheckUserInterfaceAuth('monitor:logininfor:unlock'))] +) +@Log(title='账户解锁', business_type=BusinessType.OTHER) +async def unlock_system_user(request: Request, user_name: str, query_db: AsyncSession = Depends(get_db)): + unlock_user = UnlockUser(userName=user_name) + unlock_user_result = await LoginLogService.unlock_user_services(request, unlock_user) + logger.info(unlock_user_result.message) + + return ResponseUtil.success(msg=unlock_user_result.message) + + +@logController.post('/logininfor/export', dependencies=[Depends(CheckUserInterfaceAuth('monitor:logininfor:export'))]) +@Log(title='登录日志', business_type=BusinessType.EXPORT) +async def export_system_login_log_list( + request: Request, + login_log_page_query: LoginLogPageQueryModel = Form(), + query_db: AsyncSession = Depends(get_db), +): + # 获取全量数据 + login_log_query_result = await LoginLogService.get_login_log_list_services( + query_db, login_log_page_query, is_page=False + ) + login_log_export_result = await LoginLogService.export_login_log_list_services(login_log_query_result) + logger.info('导出成功') + + return ResponseUtil.streaming(data=bytes2file_response(login_log_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/controller/login_controller.py b/ruoyi-fastapi-backend/module_admin/controller/login_controller.py new file mode 100644 index 0000000..9a255ef --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/login_controller.py @@ -0,0 +1,155 @@ +import jwt +import uuid +from datetime import datetime, timedelta +from fastapi import APIRouter, Depends, Request +from sqlalchemy.ext.asyncio import AsyncSession +from typing import Optional +from config.enums import BusinessType, RedisInitKeyConfig +from config.env import AppConfig, JwtConfig +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.entity.vo.login_vo import UserLogin, UserRegister, Token +from module_admin.entity.vo.user_vo import CurrentUserModel, EditUserModel +from module_admin.service.login_service import CustomOAuth2PasswordRequestForm, LoginService, oauth2_scheme +from module_admin.service.user_service import UserService +from utils.log_util import logger +from utils.response_util import ResponseUtil + + +loginController = APIRouter() + + +@loginController.post('/login', response_model=Token) +@Log(title='用户登录', business_type=BusinessType.OTHER, log_type='login') +async def login( + request: Request, form_data: CustomOAuth2PasswordRequestForm = Depends(), query_db: AsyncSession = Depends(get_db) +): + # 是否启用验证码 + # captcha_enabled = ( + # True + # if await request.app.state.redis.get(f'{RedisInitKeyConfig.SYS_CONFIG.key}:sys.account.captchaEnabled') + # == 'true' + # else False + # ) + + captcha_enabled =False + + user = UserLogin( + userName=form_data.username, + password=form_data.password, + code=form_data.code, + uuid=form_data.uuid, + loginInfo=form_data.login_info, + captchaEnabled=captcha_enabled, + ) + result = await LoginService.authenticate_user(request, query_db, user) + access_token_expires = timedelta(minutes=JwtConfig.jwt_expire_minutes) + session_id = str(uuid.uuid4()) + access_token = await LoginService.create_access_token( + data={ + 'user_id': str(result[0].user_id), + 'user_name': result[0].user_name, + 'dept_name': result[1].dept_name if result[1] else None, + 'session_id': session_id, + 'login_info': user.login_info, + }, + expires_delta=access_token_expires, + ) + if AppConfig.app_same_time_login: + await request.app.state.redis.set( + f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}', + access_token, + ex=timedelta(minutes=JwtConfig.jwt_redis_expire_minutes), + ) + else: + # 此方法可实现同一账号同一时间只能登录一次 + await request.app.state.redis.set( + f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{result[0].user_id}', + access_token, + ex=timedelta(minutes=JwtConfig.jwt_redis_expire_minutes), + ) + await UserService.edit_user_services( + query_db, EditUserModel(userId=result[0].user_id, loginDate=datetime.now(), type='status') + ) + logger.info('登录成功') + # 判断请求是否来自于api文档,如果是返回指定格式的结果,用于修复api文档认证成功后token显示undefined的bug + request_from_swagger = request.headers.get('referer').endswith('docs') if request.headers.get('referer') else False + request_from_redoc = request.headers.get('referer').endswith('redoc') if request.headers.get('referer') else False + if request_from_swagger or request_from_redoc: + return {'access_token': access_token, 'token_type': 'Bearer'} + return ResponseUtil.success(msg='登录成功', dict_content={'token': access_token}) + + +@loginController.get('/getInfo', response_model=CurrentUserModel) +async def get_login_user_info( + request: Request, current_user: CurrentUserModel = Depends(LoginService.get_current_user) +): + logger.info('获取成功') + + return ResponseUtil.success(model_content=current_user) + + +@loginController.get('/getRouters') +async def get_login_user_routers( + request: Request, + current_user: CurrentUserModel = Depends(LoginService.get_current_user), + query_db: AsyncSession = Depends(get_db), +): + logger.info('获取成功') + user_routers = await LoginService.get_current_user_routers(current_user.user.user_id, query_db) + + return ResponseUtil.success(data=user_routers) + + +@loginController.post('/register', response_model=CrudResponseModel) +async def register_user(request: Request, user_register: UserRegister, query_db: AsyncSession = Depends(get_db)): + user_register_result = await LoginService.register_user_services(request, query_db, user_register) + logger.info(user_register_result.message) + + return ResponseUtil.success(data=user_register_result, msg=user_register_result.message) + + +# @loginController.post("/getSmsCode", response_model=SmsCode) +# async def get_sms_code(request: Request, user: ResetUserModel, query_db: AsyncSession = Depends(get_db)): +# try: +# sms_result = await LoginService.get_sms_code_services(request, query_db, user) +# if sms_result.is_success: +# logger.info('获取成功') +# return ResponseUtil.success(data=sms_result) +# else: +# logger.warning(sms_result.message) +# return ResponseUtil.failure(msg=sms_result.message) +# except Exception as e: +# logger.exception(e) +# return ResponseUtil.error(msg=str(e)) +# +# +# @loginController.post("/forgetPwd", response_model=CrudResponseModel) +# async def forget_user_pwd(request: Request, forget_user: ResetUserModel, query_db: AsyncSession = Depends(get_db)): +# try: +# forget_user_result = await LoginService.forget_user_services(request, query_db, forget_user) +# if forget_user_result.is_success: +# logger.info(forget_user_result.message) +# return ResponseUtil.success(data=forget_user_result, msg=forget_user_result.message) +# else: +# logger.warning(forget_user_result.message) +# return ResponseUtil.failure(msg=forget_user_result.message) +# except Exception as e: +# logger.exception(e) +# return ResponseUtil.error(msg=str(e)) + + +@loginController.post('/logout') +async def logout(request: Request + # , token: Optional[str] = Depends(oauth2_scheme) + ): + token = request.headers.get('Authorization').split(" ")[-1] + payload = jwt.decode( + token, JwtConfig.jwt_secret_key, algorithms=[JwtConfig.jwt_algorithm], options={'verify_exp': False} + ) + session_id: str = payload.get('session_id') + await LoginService.logout_services(request, session_id) + logger.info('退出成功') + + return ResponseUtil.success(msg='退出成功') diff --git a/ruoyi-fastapi-backend/module_admin/controller/menu_controller.py b/ruoyi-fastapi-backend/module_admin/controller/menu_controller.py new file mode 100644 index 0000000..0e3124e --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/menu_controller.py @@ -0,0 +1,114 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.menu_vo import DeleteMenuModel, MenuModel, MenuQueryModel +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.login_service import LoginService +from module_admin.service.menu_service import MenuService +from utils.log_util import logger +from utils.response_util import ResponseUtil + + +menuController = APIRouter(prefix='/system/menu', dependencies=[Depends(LoginService.get_current_user)]) + + +@menuController.get('/treeselect') +async def get_system_menu_tree( + request: Request, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + menu_query_result = await MenuService.get_menu_tree_services(query_db, current_user) + logger.info('获取成功') + + return ResponseUtil.success(data=menu_query_result) + + +@menuController.get('/roleMenuTreeselect/{role_id}') +async def get_system_role_menu_tree( + request: Request, + role_id: int, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + role_menu_query_result = await MenuService.get_role_menu_tree_services(query_db, role_id, current_user) + logger.info('获取成功') + + return ResponseUtil.success(model_content=role_menu_query_result) + + +@menuController.get( + '/list', response_model=List[MenuModel], dependencies=[Depends(CheckUserInterfaceAuth('system:menu:list'))] +) +async def get_system_menu_list( + request: Request, + menu_query: MenuQueryModel = Depends(MenuQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + menu_query_result = await MenuService.get_menu_list_services(query_db, menu_query, current_user) + logger.info('获取成功') + + return ResponseUtil.success(data=menu_query_result) + + +@menuController.post('', dependencies=[Depends(CheckUserInterfaceAuth('system:menu:add'))]) +@ValidateFields(validate_model='add_menu') +@Log(title='菜单管理', business_type=BusinessType.INSERT) +async def add_system_menu( + request: Request, + add_menu: MenuModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + add_menu.create_by = current_user.user.user_name + add_menu.create_time = datetime.now() + add_menu.update_by = current_user.user.user_name + add_menu.update_time = datetime.now() + add_menu_result = await MenuService.add_menu_services(query_db, add_menu) + logger.info(add_menu_result.message) + + return ResponseUtil.success(msg=add_menu_result.message) + + +@menuController.put('', dependencies=[Depends(CheckUserInterfaceAuth('system:menu:edit'))]) +@ValidateFields(validate_model='edit_menu') +@Log(title='菜单管理', business_type=BusinessType.UPDATE) +async def edit_system_menu( + request: Request, + edit_menu: MenuModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + edit_menu.update_by = current_user.user.user_name + edit_menu.update_time = datetime.now() + edit_menu_result = await MenuService.edit_menu_services(query_db, edit_menu) + logger.info(edit_menu_result.message) + + return ResponseUtil.success(msg=edit_menu_result.message) + + +@menuController.delete('/{menu_ids}', dependencies=[Depends(CheckUserInterfaceAuth('system:menu:remove'))]) +@Log(title='菜单管理', business_type=BusinessType.DELETE) +async def delete_system_menu(request: Request, menu_ids: str, query_db: AsyncSession = Depends(get_db)): + delete_menu = DeleteMenuModel(menuIds=menu_ids) + delete_menu_result = await MenuService.delete_menu_services(query_db, delete_menu) + logger.info(delete_menu_result.message) + + return ResponseUtil.success(msg=delete_menu_result.message) + + +@menuController.get( + '/{menu_id}', response_model=MenuModel, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:query'))] +) +async def query_detail_system_menu(request: Request, menu_id: int, query_db: AsyncSession = Depends(get_db)): + menu_detail_result = await MenuService.menu_detail_services(query_db, menu_id) + logger.info(f'获取menu_id为{menu_id}的信息成功') + + return ResponseUtil.success(data=menu_detail_result) diff --git a/ruoyi-fastapi-backend/module_admin/controller/message_controller.py b/ruoyi-fastapi-backend/module_admin/controller/message_controller.py new file mode 100644 index 0000000..3da00e6 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/message_controller.py @@ -0,0 +1,163 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Form, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.login_service import LoginService +from module_admin.service.message_service import MessageService +from module_admin.entity.vo.message_vo import DeleteMessageModel, MessageModel, MessagePageQueryModel, EditMessageModel +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +messageController = APIRouter(prefix='/system/message', dependencies=[Depends(LoginService.get_current_user)]) + + +# 查看告警类型分布 +@messageController.get( + '/alarm_type_distribution' +) +async def get_alarm_type_distribution( + request: Request, + query_db: AsyncSession = Depends(get_db), +): + result = await MessageService.get_alarm_type_distribution_services(query_db) + return ResponseUtil.success(data=result) + + + +# 一键处理未处理的告警消息 +@messageController.get( + '/handle_all' +) +async def handle_all_message( + query_db: AsyncSession = Depends(get_db), +): + result = await MessageService.handle_all_message_services(query_db) + return ResponseUtil.success(msg= result.message) + + +# 查询告警数量信息 +@messageController.get( + '/count_message', +) +async def count_message(request: Request, query_db: AsyncSession = Depends(get_db)): + """""" + count_message_result = await MessageService.count_message(query_db) + return ResponseUtil.success(data = count_message_result) + + + +# # 获取所有消息列表 +# @messageController.get( +# '/list', response_model=PageResponseModel +# # , dependencies=[Depends(CheckUserInterfaceAuth('system:message:list'))] +# ) +# async def get_system_message_list( +# request: Request, +# message_page_query: MessagePageQueryModel = Depends(MessagePageQueryModel.as_query), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取分页数据 +# message_page_query_result = await MessageService.get_message_list_services(query_db, message_page_query, is_page=True) +# logger.info('获取成功') + +# return ResponseUtil.success(model_content=message_page_query_result) + +# 获取告警消息列表 +@messageController.get('/alert_message') +async def get_alert_message( + request: Request, + pageNum: int = 1, + pageSize: int = 10, + query_db: AsyncSession = Depends(get_db), +): + alert_message = await MessageService.get_alert_message_services(query_db, pageNum, pageSize) + return ResponseUtil.success(model_content=alert_message) + + +# 该接口只做测试用, 并不对外开放 +# @messageController.post('/add_message' +# # , dependencies=[Depends(CheckUserInterfaceAuth('system:message:add'))] +# ) +# @ValidateFields(validate_model='add_message') +# @Log(title='系统消息', business_type=BusinessType.INSERT) +# async def add_system_message( +# request: Request, +# add_message: MessageModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# add_message.create_time = datetime.now() +# add_message.create_by = current_user.user.user_name +# add_message.update_time = datetime.now() +# add_message.update_by = current_user.user.user_name +# add_message.status = '0' +# add_message_result = await MessageService.add_message_services(query_db, add_message) +# logger.info(add_message_result.message) + +# return ResponseUtil.success(msg=add_message_result.message) + + +# @messageController.put('/edit_message' +# # , dependencies=[Depends(CheckUserInterfaceAuth('system:message:edit'))] +# ) +# @ValidateFields(validate_model='edit_message') +# @Log(title='系统消息', business_type=BusinessType.UPDATE) +# async def edit_system_message( +# request: Request, +# edit_message: EditMessageModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# edit_message.update_by = current_user.user.user_name +# edit_message.update_time = datetime.now() +# edit_message_result = await MessageService.edit_message_services(query_db, edit_message) +# logger.info(edit_message_result.message) + +# return ResponseUtil.success(msg=edit_message_result.message) + + +# @messageController.delete('/{message_ids}' +# , dependencies=[Depends(CheckUserInterfaceAuth('system:message:remove'))] +# ) +# @Log(title='系统消息', business_type=BusinessType.DELETE) +# async def delete_system_message(request: Request, message_ids: str, query_db: AsyncSession = Depends(get_db)): +# delete_message = DeleteMessageModel(messageIds=message_ids) +# delete_message_result = await MessageService.delete_message_services(query_db, delete_message) +# logger.info(delete_message_result.message) + +# return ResponseUtil.success(msg=delete_message_result.message) + + +# @messageController.get( +# '/{message_id}', response_model=MessageModel +# # , dependencies=[Depends(CheckUserInterfaceAuth('system:message:query'))] +# ) +# async def query_detail_system_message(request: Request, message_id: int, query_db: AsyncSession = Depends(get_db)): +# message_detail_result = await MessageService.message_detail_services(query_db, message_id) +# logger.info(f'获取message_id为{message_id}的信息成功') + +# return ResponseUtil.success(data=message_detail_result) + + +# @messageController.post('/export', dependencies=[Depends(CheckUserInterfaceAuth('system:message:export'))]) +# @Log(title='系统消息', business_type=BusinessType.EXPORT) +# async def export_system_message_list( +# request: Request, +# message_page_query: MessagePageQueryModel = Form(), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取全量数据 +# message_query_result = await MessageService.get_message_list_services(query_db, message_page_query, is_page=False) +# message_export_result = await MessageService.export_message_list_services(message_query_result) +# logger.info('导出成功') +# +# return ResponseUtil.streaming(data=bytes2file_response(message_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/controller/notice_controller.py b/ruoyi-fastapi-backend/module_admin/controller/notice_controller.py new file mode 100644 index 0000000..e1e4aa1 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/notice_controller.py @@ -0,0 +1,89 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.notice_vo import DeleteNoticeModel, NoticeModel, NoticePageQueryModel +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.login_service import LoginService +from module_admin.service.notice_service import NoticeService +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +noticeController = APIRouter(prefix='/system/notice', dependencies=[Depends(LoginService.get_current_user)]) + + +@noticeController.get( + '/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('system:notice:list'))] +) +async def get_system_notice_list( + request: Request, + notice_page_query: NoticePageQueryModel = Depends(NoticePageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + # 获取分页数据 + notice_page_query_result = await NoticeService.get_notice_list_services(query_db, notice_page_query, is_page=True) + logger.info('获取成功') + + return ResponseUtil.success(model_content=notice_page_query_result) + + +@noticeController.post('', dependencies=[Depends(CheckUserInterfaceAuth('system:notice:add'))]) +@ValidateFields(validate_model='add_notice') +@Log(title='通知公告', business_type=BusinessType.INSERT) +async def add_system_notice( + request: Request, + add_notice: NoticeModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + add_notice.create_by = current_user.user.user_name + add_notice.create_time = datetime.now() + add_notice.update_by = current_user.user.user_name + add_notice.update_time = datetime.now() + add_notice_result = await NoticeService.add_notice_services(query_db, add_notice) + logger.info(add_notice_result.message) + + return ResponseUtil.success(msg=add_notice_result.message) + + +@noticeController.put('', dependencies=[Depends(CheckUserInterfaceAuth('system:notice:edit'))]) +@ValidateFields(validate_model='edit_notice') +@Log(title='通知公告', business_type=BusinessType.UPDATE) +async def edit_system_notice( + request: Request, + edit_notice: NoticeModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + edit_notice.update_by = current_user.user.user_name + edit_notice.update_time = datetime.now() + edit_notice_result = await NoticeService.edit_notice_services(query_db, edit_notice) + logger.info(edit_notice_result.message) + + return ResponseUtil.success(msg=edit_notice_result.message) + + +@noticeController.delete('/{notice_ids}', dependencies=[Depends(CheckUserInterfaceAuth('system:notice:remove'))]) +@Log(title='通知公告', business_type=BusinessType.DELETE) +async def delete_system_notice(request: Request, notice_ids: str, query_db: AsyncSession = Depends(get_db)): + delete_notice = DeleteNoticeModel(noticeIds=notice_ids) + delete_notice_result = await NoticeService.delete_notice_services(query_db, delete_notice) + logger.info(delete_notice_result.message) + + return ResponseUtil.success(msg=delete_notice_result.message) + + +@noticeController.get( + '/{notice_id}', response_model=NoticeModel, dependencies=[Depends(CheckUserInterfaceAuth('system:notice:query'))] +) +async def query_detail_system_post(request: Request, notice_id: int, query_db: AsyncSession = Depends(get_db)): + notice_detail_result = await NoticeService.notice_detail_services(query_db, notice_id) + logger.info(f'获取notice_id为{notice_id}的信息成功') + + return ResponseUtil.success(data=notice_detail_result) diff --git a/ruoyi-fastapi-backend/module_admin/controller/online_controller.py b/ruoyi-fastapi-backend/module_admin/controller/online_controller.py new file mode 100644 index 0000000..bf65c8f --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/online_controller.py @@ -0,0 +1,40 @@ +from fastapi import APIRouter, Depends, Request +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.online_vo import DeleteOnlineModel, OnlineQueryModel +from module_admin.service.login_service import LoginService +from module_admin.service.online_service import OnlineService +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +onlineController = APIRouter(prefix='/monitor/online', dependencies=[Depends(LoginService.get_current_user)]) + + +@onlineController.get( + '/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('monitor:online:list'))] +) +async def get_monitor_online_list( + request: Request, online_page_query: OnlineQueryModel = Depends(OnlineQueryModel.as_query) +): + # 获取全量数据 + online_query_result = await OnlineService.get_online_list_services(request, online_page_query) + logger.info('获取成功') + + return ResponseUtil.success( + model_content=PageResponseModel(rows=online_query_result, total=len(online_query_result)) + ) + + +@onlineController.delete('/{token_ids}', dependencies=[Depends(CheckUserInterfaceAuth('monitor:online:forceLogout'))]) +@Log(title='在线用户', business_type=BusinessType.FORCE) +async def delete_monitor_online(request: Request, token_ids: str, query_db: AsyncSession = Depends(get_db)): + delete_online = DeleteOnlineModel(tokenIds=token_ids) + delete_online_result = await OnlineService.delete_online_services(request, delete_online) + logger.info(delete_online_result.message) + + return ResponseUtil.success(msg=delete_online_result.message) diff --git a/ruoyi-fastapi-backend/module_admin/controller/post_controler.py b/ruoyi-fastapi-backend/module_admin/controller/post_controler.py new file mode 100644 index 0000000..e9c69c4 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/post_controler.py @@ -0,0 +1,105 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Form, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.service.login_service import LoginService +from module_admin.service.post_service import PostService +from module_admin.entity.vo.post_vo import DeletePostModel, PostModel, PostPageQueryModel +from module_admin.entity.vo.user_vo import CurrentUserModel +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +postController = APIRouter(prefix='/system/post', dependencies=[Depends(LoginService.get_current_user)]) + + +@postController.get( + '/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('system:post:list'))] +) +async def get_system_post_list( + request: Request, + post_page_query: PostPageQueryModel = Depends(PostPageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + # 获取分页数据 + post_page_query_result = await PostService.get_post_list_services(query_db, post_page_query, is_page=True) + logger.info('获取成功') + + return ResponseUtil.success(model_content=post_page_query_result) + + +@postController.post('', dependencies=[Depends(CheckUserInterfaceAuth('system:post:add'))]) +@ValidateFields(validate_model='add_post') +@Log(title='岗位管理', business_type=BusinessType.INSERT) +async def add_system_post( + request: Request, + add_post: PostModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + add_post.create_by = current_user.user.user_name + add_post.create_time = datetime.now() + add_post.update_by = current_user.user.user_name + add_post.update_time = datetime.now() + add_post_result = await PostService.add_post_services(query_db, add_post) + logger.info(add_post_result.message) + + return ResponseUtil.success(msg=add_post_result.message) + + +@postController.put('', dependencies=[Depends(CheckUserInterfaceAuth('system:post:edit'))]) +@ValidateFields(validate_model='edit_post') +@Log(title='岗位管理', business_type=BusinessType.UPDATE) +async def edit_system_post( + request: Request, + edit_post: PostModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + edit_post.update_by = current_user.user.user_name + edit_post.update_time = datetime.now() + edit_post_result = await PostService.edit_post_services(query_db, edit_post) + logger.info(edit_post_result.message) + + return ResponseUtil.success(msg=edit_post_result.message) + + +@postController.delete('/{post_ids}', dependencies=[Depends(CheckUserInterfaceAuth('system:post:remove'))]) +@Log(title='岗位管理', business_type=BusinessType.DELETE) +async def delete_system_post(request: Request, post_ids: str, query_db: AsyncSession = Depends(get_db)): + delete_post = DeletePostModel(postIds=post_ids) + delete_post_result = await PostService.delete_post_services(query_db, delete_post) + logger.info(delete_post_result.message) + + return ResponseUtil.success(msg=delete_post_result.message) + + +@postController.get( + '/{post_id}', response_model=PostModel, dependencies=[Depends(CheckUserInterfaceAuth('system:post:query'))] +) +async def query_detail_system_post(request: Request, post_id: int, query_db: AsyncSession = Depends(get_db)): + post_detail_result = await PostService.post_detail_services(query_db, post_id) + logger.info(f'获取post_id为{post_id}的信息成功') + + return ResponseUtil.success(data=post_detail_result) + + +@postController.post('/export', dependencies=[Depends(CheckUserInterfaceAuth('system:post:export'))]) +@Log(title='岗位管理', business_type=BusinessType.EXPORT) +async def export_system_post_list( + request: Request, + post_page_query: PostPageQueryModel = Form(), + query_db: AsyncSession = Depends(get_db), +): + # 获取全量数据 + post_query_result = await PostService.get_post_list_services(query_db, post_page_query, is_page=False) + post_export_result = await PostService.export_post_list_services(post_query_result) + logger.info('导出成功') + + return ResponseUtil.streaming(data=bytes2file_response(post_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/controller/ragflow_controller.py b/ruoyi-fastapi-backend/module_admin/controller/ragflow_controller.py new file mode 100644 index 0000000..0ebdf07 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/ragflow_controller.py @@ -0,0 +1,656 @@ +""" +简化的RAGFlow控制器 - 集成语义缓存 +""" + +import asyncio +import hashlib +import json +import time +from typing import Any, AsyncGenerator, Dict, Generator, Tuple + +from fastapi import APIRouter, Depends, Request +from fastapi.responses import StreamingResponse + +from module_admin.service.login_service import LoginService +from module_admin.service.kb_es_service import KBESService +from module_admin.service.ragflow_service import RAGFlowService +from module_admin.service.search_service import SearchService +from module_admin.entity.vo.ragflow_vo import ( + ConverseWithChatAssistantModel, + CreateSessionWithChatModel, + UpdateChatAssistantModel, + RagflowListQueryModel +) +from utils.log_util import logger +from utils.response_util import ResponseUtil +from utils.semantic_cache_service import get_semantic_cache_service, store_qa_pair, get_question_hash +from utils.static_qa_service import get_static_qa_service +from utils.deepseek_client import DeepSeekAPIClient +from config.env import KBConfig + + +async def _async_store_qa(chat_id: str, question: str, answer: str, redis) -> None: + """ + 异步存储问答对到语义缓存 + """ + store_hash = get_question_hash(question) + logger.info(f"[SemanticCache] 存储QA | chat_id={chat_id} | question={question} | hash={store_hash} | answer_length={len(answer)}") + try: + await store_qa_pair(chat_id, question, answer, redis) + except Exception as e: + logger.warning(f"[SemanticCache] 异步存储失败: {e}") + + +# 使用标准的APIRouter,增加认证依赖 +ragflowController = APIRouter(prefix="/system/ragflow", dependencies=[Depends(LoginService.get_current_user)]) + + +def format_sse(data: dict, event: str | None = None) -> str: + """格式化SSE数据""" + payload = json.dumps(data, ensure_ascii=False) + prefix = f'event: {event}\n' if event else '' + return f'{prefix}data: {payload}\n\n' + + +def parse_result(result: Dict[str, Any]) -> Dict[str, Any]: + """统一结果解析""" + code = result.get('code', 0) + if code != 0: + msg = result.get('message') or result.get('msg') or '接口异常' + return ResponseUtil.error(msg=msg, data=result.get('data', None)) + return ResponseUtil.success(data=result.get('data', None)) + + +def build_chat_cache_key(chat_id: str, question: str) -> str: + """构建聊天缓存键""" + digest = hashlib.sha256(question.encode('utf-8')).hexdigest() + return f'ragflow:chat:{chat_id}:{digest}' + + +def remove_style_hint(question: str) -> Tuple[str, bool]: + """ + 移除语言风格提示词 + + Args: + question: 原始问题 + + Returns: + (处理后的问题, 是否移除了风格提示词) + """ + if not question: + return question, False + + import re + + pattern = r'语言风格\s*[::]\s*[^,。!?\n]*' + match = re.search(pattern, question) + + if match: + style_hint = match.group(0) + cleaned_question = re.sub(pattern, '', question).strip() + cleaned_question = re.sub(r'\s+', ' ', cleaned_question) + logger.info(f'[StyleHint] 移除风格提示词: {style_hint} | 原始问题: {question} | 处理后: {cleaned_question}') + return cleaned_question, True + + return question, False + + +# 非流式对话接口 +@ragflowController.post('/converse_with_chat_assistant') +async def converse_with_chat_assistant( + request: Request, + converse_params: ConverseWithChatAssistantModel, +): + """ + 与聊天助手进行对话 - 集成语义缓存版本(支持流式和非流式) + + 匹配流程: + 1. 移除语言风格提示词 + 2. 静态问答匹配 (threshold=0.70) + 3. 搜索服务判断 + 4. RAG历史缓存匹配 (threshold=0.60) + 5. RAG服务调用(使用原始问题) + """ + start_time = time.perf_counter() + + original_question = converse_params.question + cleaned_question, style_removed = remove_style_hint(original_question) + + if style_removed: + converse_params.question = cleaned_question + + # 获取redis实例 + redis = getattr(request.app.state, 'redis', None) + cached_answer = None + cache_similarity = 0.0 + + # ========== 1. 静态问答匹配 ========== + try: + static_qa_service = get_static_qa_service() + logger.info(f'[StaticQA] 开始匹配 | chat_id={converse_params.chat_id} | question={converse_params.question} | threshold=0.70') + static_match, static_sim = static_qa_service.find_match(converse_params.question, threshold=0.70) + logger.info(f'[StaticQA] 匹配结果 | similarity={static_sim:.2f} | matched={static_match is not None}') + + if static_match: + cached_answer = static_match.get('answer', '') + cache_similarity = static_sim + logger.info(f'[RAG_SOURCE] 命中静态FAQ | chat_id={converse_params.chat_id} | question={converse_params.question} | similarity={cache_similarity:.2f} | answer_length={len(cached_answer)}') + logger.info(f'[StaticQA] 流式响应使用静态问答答案,chat_id={converse_params.chat_id}') + return StreamingResponse( + stream_cached_response(cached_answer, converse_params.chat_id, start_time, cache_source='static_qa'), + media_type='text/event-stream', + headers={ + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'X-Accel-Buffering': 'no', + 'Transfer-Encoding': 'chunked' + } + ) + except Exception as e: + logger.warning(f'[StaticQA] 静态问答匹配失败: {e}') + + # ========== 2. 检查是否应该使用搜索服务 ========== + # 在 KB_PROVIDER=es_bm25 模式下,仍允许联网搜索(SEARCH)分支; + # 非 SEARCH 则走 ES 内部KB。 + try: + intent = await SearchService.classify_intent(converse_params.question) + logger.info(f"[RAG_PROFILE] Intent Router ({converse_params.chat_id}): {intent}") + + if intent == 'SEARCH': + return await SearchService.handle_search_chat(converse_params, redis) + except Exception as e: + logger.warning(f"意图分类失败,跳过搜索服务: {e}") + + # ========== 3. RAG历史缓存查找 ========== + # 注意:当 KB_PROVIDER=es_bm25 时,内部KB答案应直接来源于 ES 检索结果, + # 避免 rag:semantic:cache 抢答导致“curl 与 API 不一致”。 + if KBConfig.KB_PROVIDER != 'es_bm25': + logger.info(f'[SemanticCache] 准备执行RAG历史缓存查找 | redis={redis is not None} | chat_id={converse_params.chat_id}') + if redis: + lookup_hash = get_question_hash(converse_params.question) + logger.info(f'[SemanticCache] 开始查找 | chat_id={converse_params.chat_id} | question={converse_params.question} | hash={lookup_hash} | threshold=0.60') + + service = get_semantic_cache_service() + logger.info(f'[SemanticCache] service实例: {service}') + cache_result = await service.lookup( + converse_params.chat_id, + converse_params.question, + redis + ) + logger.info(f'[SemanticCache] 查找结果 | found={cache_result is not None}') + if cache_result: + cached_answer, cache_similarity = cache_result + logger.info(f'[RAG_SOURCE] 命中RAG会话历史 | chat_id={converse_params.chat_id} | question={converse_params.question} | similarity={cache_similarity:.2f} | answer_length={len(cached_answer)}') + logger.info(f'[SemanticCache] 流式响应使用RAG历史缓存答案,chat_id={converse_params.chat_id}') + return StreamingResponse( + stream_cached_response(cached_answer, converse_params.chat_id, start_time, cache_source='rag_history'), + media_type='text/event-stream', + headers={ + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'X-Accel-Buffering': 'no', + 'Transfer-Encoding': 'chunked' + } + ) + else: + logger.info(f"[SemanticCache] skip lookup because KB_PROVIDER=es_bm25 | chat_id={converse_params.chat_id}") + + # ========== 4. RAG服务调用(在这里根据 KB_PROVIDER 选择 ragflow / es_bm25) ========== + try: + # 恢复原始问题(包含语言风格提示词) + if style_removed: + converse_params.question = original_question + logger.info(f'[StyleHint] 恢复原始问题: {original_question}') + + if KBConfig.KB_PROVIDER == 'es_bm25': + try: + chunks, metrics = await KBESService.retrieve(converse_params.question, redis=redis) + logger.info( + f"[KB_ES] retrieve_ms={metrics.retrieve_ms:.1f} | top1_score={metrics.top1_score:.3f} | score_gap={metrics.score_gap:.3f} | " + f"hit_count={metrics.hit_count} | keyword_coverage={metrics.keyword_coverage:.2f} | cache_hit={metrics.cache_hit} | " + f"chat_id={converse_params.chat_id}" + ) + + if KBESService.is_confident(metrics): + # KB_PROVIDER=es_bm25 时不写入 rag:semantic:cache,避免污染后续命中 + cache_store_func = None + + return StreamingResponse( + stream_kb_es_response( + question=converse_params.question, + chunks=chunks, + chat_id=converse_params.chat_id, + start_time=start_time, + cache_store_func=cache_store_func, + retrieve_ms=metrics.retrieve_ms, + ), + media_type='text/event-stream', + headers={ + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'X-Accel-Buffering': 'no', + 'Transfer-Encoding': 'chunked' + } + ) + + logger.info( + f"[KB_ES] 低置信度,准备fallback | fallback={KBConfig.KB_FALLBACK} | chat_id={converse_params.chat_id}" + ) + if KBConfig.KB_FALLBACK == 'none': + # 低置信度兜底不应标记为 from_cache,避免同问题第二次直接“缓存命中兜底答案”。 + return StreamingResponse( + stream_plain_response( + '我目前不太确定答案。请补充更多关键词或换种问法。', + converse_params.chat_id, + start_time, + source='kb_es_low_confidence' + ), + media_type='text/event-stream', + headers={ + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'X-Accel-Buffering': 'no', + 'Transfer-Encoding': 'chunked' + } + ) + except Exception as e: + logger.warning(f"[KB_ES] 内部KB异常: {e}") + if KBConfig.KB_FALLBACK == 'none': + return StreamingResponse( + stream_cached_response( + '内部知识库检索失败,请稍后再试或补充更多关键词。', + converse_params.chat_id, + start_time, + cache_source='kb_es_error' + ), + media_type='text/event-stream', + headers={ + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'X-Accel-Buffering': 'no', + 'Transfer-Encoding': 'chunked' + } + ) + + logger.info(f'[RAG_SOURCE] 调用原生RAG服务 | chat_id={converse_params.chat_id} | question={converse_params.question}') + result = RAGFlowService.converse_with_chat_assistant_services(converse_params) + + cache_question = cleaned_question if style_removed else converse_params.question + store_hash = get_question_hash(cache_question) + logger.info(f'[RAG_CACHE] 准备存储 | chat_id={converse_params.chat_id} | question={cache_question} | hash={store_hash}') + + async def cache_store_func(answer: str): + if redis and answer and len(answer.strip()) >= 10: + try: + await _async_store_qa(converse_params.chat_id, cache_question, answer, redis) + logger.info(f'[RAG_CACHE] 语义缓存存储成功 | chat_id={converse_params.chat_id} | answer_length={len(answer)}') + except Exception as e: + logger.warning(f'[RAG_CACHE] 语义缓存存储失败: {e}') + + return StreamingResponse( + stream_ragflow_response(result, converse_params.chat_id, start_time, cache_store_func=cache_store_func), + media_type='text/event-stream', + headers={ + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'X-Accel-Buffering': 'no', + 'Transfer-Encoding': 'chunked' + } + ) + + except Exception as e: + logger.exception(f'[RAG_PROFILE] ragflow对话异常: {e}') + return ResponseUtil.error(msg=f'对话服务异常: {str(e)}') + + +async def stream_ragflow_response(result: Generator, chat_id: str, start_time: float, cache_store_func=None) -> AsyncGenerator[str, None]: + """ + 流式处理RAGFlow响应 - 异步版本,修复首token延迟 + """ + import time + + server_stream_start = time.time() + logger.info(f"[RAG_SERVER {server_stream_start:.3f}] 🚀 开始流式响应处理,chat_id: {chat_id}") + + last_answer = "" + first_token_received = False + start_stream_time = time.perf_counter() + + # 立即发送连接建立消息,解决首token延迟 + connection_time = time.time() + ping_message = "event: ping\ndata: {\"status\": \"connected\"}\n\n" + yield ping_message + logger.info(f"[RAG_SERVER {connection_time:.3f}] 📡 连接建立ping发送,耗时: {connection_time - server_stream_start:.3f}s") + + chunk_count = 0 + try: + for chunk in result: + chunk_time = time.time() + chunk_count += 1 + + # 让出控制权,确保流式响应 + await asyncio.sleep(0) + + # 检查第一个token的延迟 + if not first_token_received: + first_token_received = True + latency = time.perf_counter() - start_stream_time + logger.info(f"[RAG_SERVER {chunk_time:.3f}] 🎯 首token到达,耗时: {latency:.3f}s") + + # 处理chunk数据 + payload = chunk.get('data') if isinstance(chunk, dict) else chunk + if not payload: + logger.info(f"[RAG_SERVER {chunk_time:.3f}] ⚠️ 空chunk跳过") + continue + + body = payload if isinstance(payload, dict) else {'data': payload} + + # 处理错误 + if isinstance(chunk, dict) and chunk.get('code') and chunk.get('code') != 0: + logger.error(f"[RAG_SERVER {chunk_time:.3f}] ❌ RAGFlow Stream Error: {chunk}") + error_message = format_sse({'message': chunk.get('message', '流式处理异常')}, event='error') + yield error_message + continue + + # 处理answer字段的增量更新 + if isinstance(body, dict) and 'answer' in body: + current_answer = body['answer'] + + # 安全检查:确保current_answer不为None且为字符串 + if current_answer is None: + logger.warning(f"[RAG_SERVER {chunk_time:.3f}] ⚠️ current_answer为None,跳过处理") + continue + + if not isinstance(current_answer, str): + logger.warning(f"[RAG_SERVER {chunk_time:.3f}] ⚠️ current_answer不是字符串,类型: {type(current_answer)}") + current_answer = str(current_answer) + + # 计算增量内容 + if current_answer.startswith(last_answer): + delta = current_answer[len(last_answer):] + if delta: + body['answer'] = delta + last_answer = current_answer + logger.info(f"[RAG_SERVER {chunk_time:.3f}] 📝 Chunk #{chunk_count} 处理完成,delta长度: {len(delta)}") + yield format_sse(body) + else: + # 上下文重置的备用处理 + last_answer = current_answer + logger.info(f"[RAG_SERVER {chunk_time:.3f}] 🔄 Chunk #{chunk_count} 上下文重置") + yield format_sse(body) + else: + logger.info(f"[RAG_SERVER {chunk_time:.3f}] 📦 Chunk #{chunk_count} 其他数据: {body}") + yield format_sse(body) + + # 流结束 + stream_end_time = time.time() + end_message = format_sse({'status': 'completed'}, event='end') + yield end_message + logger.info(f'[RAG_SERVER {stream_end_time:.3f}] 流式响应完成,chat_id: {chat_id}') + logger.info(f'[RAG_SERVER {stream_end_time:.3f}] 总共处理chunk数量: {chunk_count}') + logger.info(f'[RAG_SOURCE] 原生RAG流式响应完成 | chat_id={chat_id} | total_chunks={chunk_count} | answer_length={len(last_answer)}') + + except Exception as exc: + error_time = time.time() + logger.exception(f'[RAG_SERVER {error_time:.3f}] ragflow流式对话异常: {exc}') + error_message = format_sse({'message': str(exc)}, event='error') + yield error_message + finally: + total_time = time.perf_counter() - start_time + logger.info(f'[RAG_SERVER {time.time():.3f}] ⏱️ Total Stream Duration ({chat_id}): {total_time:.3f}s') + + if cache_store_func and last_answer and len(last_answer.strip()) >= 10: + try: + await cache_store_func(last_answer) + logger.info(f'[RAG_CACHE] 缓存存储完成 | chat_id={chat_id} | answer_length={len(last_answer)}') + except Exception as cache_err: + logger.warning(f'[RAG_CACHE] 缓存存储失败: {cache_err}') + + +# 聊天助手列表 +@ragflowController.post('/get_chat_assistant_list') +async def get_chat_assistant_list( + query_params: RagflowListQueryModel, # 使用正确的Pydantic模型 +): + """获取聊天助手列表""" + result = RAGFlowService.get_chat_assistant_list_services(query_params) + return parse_result(result) + + +# 创建会话 +@ragflowController.post('/create_session_with_chat') +async def create_session_with_chat( + create_params: CreateSessionWithChatModel, # 使用正确的Pydantic模型 +): + """创建聊天会话""" + result = RAGFlowService.create_session_with_chat_services(create_params) + return parse_result(result) + + +# 更新聊天助手 +@ragflowController.post('/update_chat_assistant') +async def update_chat_assistant( + update_params: UpdateChatAssistantModel, # 使用正确的Pydantic模型 +): + """更新聊天助手""" + result = RAGFlowService.update_chat_assistant_services(update_params) + return parse_result(result) + + +# 数据集相关接口 +@ragflowController.post('/dataset_list') +async def get_system_ragflow_list( + request: Request, + ragflow_list_query: Any, # 使用Any避免导入具体模型类 +): + """获取数据集列表""" + result = RAGFlowService.get_ragflow_dataset_list_services(None, ragflow_list_query) + return parse_result(result) + + +@ragflowController.post('/create_dataset') +async def create_dataset( + create_dataset_params: Any, # 使用Any避免导入具体模型类 +): + """创建数据集""" + result = RAGFlowService.create_dataset_services(create_dataset_params) + return parse_result(result) + + +@ragflowController.post('/delete_datasets') +async def delete_datasets( + delete_params: Any, # 使用Any避免导入具体模型类 +): + """删除数据集""" + result = RAGFlowService.delete_datasets_services(delete_params) + return parse_result(result) + + +def stream_cached_response(cached_answer: str, chat_id: str, start_time: float, cache_source: str = 'cache') -> Generator[str, None, None]: + """ + 流式返回缓存的答案 + + Args: + cached_answer: 缓存的答案文本 + chat_id: 会话ID + start_time: 请求开始时间 + cache_source: 缓存来源 ('static_qa' 或 'rag_history') + """ + import time + + server_stream_start = time.time() + logger.info(f"[CACHE_STREAM {server_stream_start:.3f}] 🚀 开始流式返回缓存答案,chat_id: {chat_id}") + + # 立即发送连接建立消息 + connection_time = time.time() + ping_message = "event: ping\ndata: {\"status\": \"connected\"}\n\n" + yield ping_message + logger.info(f"[CACHE_STREAM {connection_time:.3f}] 📡 连接建立ping发送,耗时: {connection_time - server_stream_start:.3f}s") + + # 模拟流式输出效果(可选:如果是完整答案,可以选择立即返回或模拟流式) + # 这里我们选择模拟流式输出,每次发送一小部分,模拟打字效果 + if cached_answer: + # 将答案分块发送,模拟打字效果 + chunk_size = 10 # 每个块10个字符 + answer_chunks = [cached_answer[i:i+chunk_size] for i in range(0, len(cached_answer), chunk_size)] + + for i, chunk in enumerate(answer_chunks): + chunk_time = time.time() + + # 首token延迟 + if i == 0: + latency = time.perf_counter() - start_time + logger.info(f"[CACHE_STREAM {chunk_time:.3f}] 🎯 首token到达(缓存),耗时: {latency:.3f}s") + + body = {'answer': chunk, 'chunk_index': i, 'total_chunks': len(answer_chunks)} + yield format_sse(body) + + # 小延迟模拟打字效果(可调整或移除) + # time.sleep(0.01) + + logger.info(f"[CACHE_STREAM {chunk_time:.3f}] 📝 缓存答案流式发送完成,共 {len(answer_chunks)} 个chunk") + + # 流结束 + stream_end_time = time.time() + end_message = format_sse({ + 'status': 'completed', + 'from_cache': True, + 'source': cache_source, + 'total_time': stream_end_time - server_stream_start + }, event='end') + yield end_message + + logger.info(f"[CACHE_STREAM {stream_end_time:.3f}] 🏁 缓存流式响应完成") + if cached_answer: + logger.info(f"[CACHE_STREAM {stream_end_time:.3f}] 📊 缓存答案长度: {len(cached_answer)} 字符") + + total_time = time.perf_counter() - start_time + logger.info(f'[CACHE_STREAM {time.time():.3f}] ⏱️ Total Cache Stream Duration ({chat_id}): {total_time:.3f}s') + + +def stream_plain_response(answer: str, chat_id: str, start_time: float, source: str = 'plain') -> Generator[str, None, None]: + """流式返回非缓存答案(不输出 from_cache=true)。""" + import time + + server_stream_start = time.time() + logger.info(f"[PLAIN_STREAM {server_stream_start:.3f}] 开始流式返回,chat_id: {chat_id} | source={source}") + + ping_message = 'event: ping\ndata: {"status": "connected"}\n\n' + yield ping_message + + if answer: + chunk_size = 10 + chunks = [answer[i:i + chunk_size] for i in range(0, len(answer), chunk_size)] + for i, chunk in enumerate(chunks): + if i == 0: + latency = time.perf_counter() - start_time + logger.info(f"[PLAIN_STREAM {time.time():.3f}] 首token到达,ttft={latency:.3f}s | chat_id={chat_id}") + yield format_sse({'answer': chunk, 'chunk_index': i, 'total_chunks': len(chunks), 'source': source}) + + yield format_sse({'status': 'completed', 'source': source}, event='end') + logger.info(f"[PLAIN_STREAM {time.time():.3f}] 完成流式返回,chat_id: {chat_id} | source={source}") + + +async def stream_kb_es_response( + *, + question: str, + chunks: list[dict], + chat_id: str, + start_time: float, + cache_store_func=None, + retrieve_ms: float | None = None, +) -> AsyncGenerator[str, None]: + import time + + server_stream_start = time.time() + logger.info(f"[KB_ES_STREAM {server_stream_start:.3f}] 开始流式响应处理,chat_id: {chat_id}") + + ping_message = 'event: ping\ndata: {"status": "connected"}\n\n' + yield ping_message + + # 结构化Q&A命中时,直接返回 answer(最快,不走 LLM) + if chunks: + top = chunks[0] + top_answer = (top.get('answer') or '').strip() + if top_answer: + chunk_size = 12 + for i in range(0, len(top_answer), chunk_size): + if i == 0: + latency = time.perf_counter() - start_time + logger.info( + f"[KB_ES_STREAM {time.time():.3f}] 首token到达(抽取式),ttft={latency:.3f}s | retrieve_ms={retrieve_ms} | chat_id={chat_id}" + ) + yield format_sse({'answer': top_answer[i : i + chunk_size], 'source': 'kb_es'}) + yield format_sse({'status': 'completed', 'source': 'kb_es'}, event='end') + if cache_store_func and len(top_answer.strip()) >= 10: + try: + await cache_store_func(top_answer) + except Exception as e: + logger.warning(f"[KB_ES_STREAM] 缓存存储失败: {e}") + return + + context_parts = [] + for i, c in enumerate(chunks[: KBConfig.KB_TOP_K], 1): + title = (c.get('title') or '').strip() + source = (c.get('source') or '').strip() + content = (c.get('content') or '').strip() + header = f"[{i}]" + if title: + header += f" {title}" + if source: + header += f" ({source})" + context_parts.append(f"{header}\n{content}") + + context = "\n\n".join(context_parts) + system_prompt = ( + "你是公司内部知识库助手。请严格基于【已知资料】回答;如果资料不足以回答,直接说明不知道并提示用户补充关键词。" + "回答要简洁。" + ) + + messages = [ + {"role": "system", "content": f"{system_prompt}\n\n【已知资料】\n{context}"}, + {"role": "user", "content": question}, + ] + + full_answer = '' + first_token_sent = False + + try: + client = DeepSeekAPIClient() + stream = await client.chat_completion( + messages=messages, + temperature=0.2, + max_tokens=512, + stream=True, + ) + + async for chunk in stream: + await asyncio.sleep(0) + try: + delta = chunk.choices[0].delta.content or '' + except Exception: + delta = '' + if not delta: + continue + if not first_token_sent: + first_token_sent = True + latency = time.perf_counter() - start_time + logger.info( + f"[KB_ES_STREAM {time.time():.3f}] 首token到达,ttft={latency:.3f}s | retrieve_ms={retrieve_ms} | chat_id={chat_id}" + ) + full_answer += delta + yield format_sse({'answer': delta, 'source': 'kb_es'}) + + yield format_sse({'status': 'completed', 'source': 'kb_es'}, event='end') + + except Exception as exc: + logger.exception(f"[KB_ES_STREAM] 流式对话异常: {exc}") + yield format_sse({'message': str(exc)}, event='error') + finally: + total_time = time.perf_counter() - start_time + logger.info(f"[KB_ES_STREAM {time.time():.3f}] Total Stream Duration ({chat_id}): {total_time:.3f}s") + if cache_store_func and full_answer and len(full_answer.strip()) >= 10: + try: + await cache_store_func(full_answer) + except Exception as e: + logger.warning(f"[KB_ES_STREAM] 缓存存储失败: {e}") \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_admin/controller/ragflow_controller_backup.py b/ruoyi-fastapi-backend/module_admin/controller/ragflow_controller_backup.py new file mode 100644 index 0000000..04efff6 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/ragflow_controller_backup.py @@ -0,0 +1,376 @@ +# from datetime import datetime +import hashlib +import json +import time +from typing import List +from fastapi import APIRouter, Depends, Request, UploadFile, File +from fastapi.responses import StreamingResponse +# from pydantic_validation_decorator import ValidateFields +# from sqlalchemy.ext.asyncio import AsyncSession +# from config.enums import BusinessType +# from config.get_db import get_db +from module_admin.aspect.interface_auth import CheckRoleInterfaceAuth +# from module_admin.entity.vo.notice_vo import DeleteNoticeModel, NoticeModel, NoticePageQueryModel +# from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.login_service import LoginService +from module_admin.service.ragflow_service import RAGFlowService +from module_admin.service.search_service import SearchService, SearchServiceError + +from utils.log_util import logger +# from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil +from module_admin.entity.vo.ragflow_vo import RagflowListQueryModel, ListDocumentsQueryModel, UpdateFileModel, DeleteFileModel, CreateDatasetModel, DocumentIdsModel, UpdateChatAssistantModel,\ + CreateSessionWithChatModel, ConverseWithChatAssistantModel +# from config.env import RAGFlowConfig +import asyncio + + + +ragflowController = APIRouter(prefix="/system/ragflow", dependencies=[Depends(LoginService.get_current_user)]) + + + +# 查看数据集列表 +@ragflowController.post("/dataset_list" + # , response_model=PageResponseModel + # , dependencies=[Depends(CheckUserInterfaceAuth("system:ragflow:list"))]" + ) +async def get_system_ragflow_list( + request: Request, + rage_flow_dastset_query: RagflowListQueryModel , + # query_db: AsyncSession = Depends(get_db), +): + + result = await RAGFlowService.get_ragflow_dataset_list_services(None, rage_flow_dastset_query) + + return parse_result(result) + +# 创建数据集 +@ragflowController.post('/create_dataset') +async def create_dataset( + request: Request, + create_dataset_params: CreateDatasetModel, +): + + result = await RAGFlowService.create_dataset_services(create_dataset_params) + return parse_result(result) + +# 更新数据集 +@ragflowController.post('/update_dataset/{dataset_id}') +async def update_dataset( + request: Request, + dataset_id: str, + update_dataset_params: CreateDatasetModel, +): + result = await RAGFlowService.update_dataset_services(dataset_id, update_dataset_params) + return parse_result(result) + + +# 列出数据集中文档列表 +@ragflowController.get("/list_documents/{dataset_id}") +async def list_documents_by_dataset_id( + request: Request, + dataset_id: str, + list_documents_query: ListDocumentsQueryModel = Depends(ListDocumentsQueryModel.as_query), + + # query_db: AsyncSession = Depends(get_db), +): + """ + 列出数据集中文档列表 + """ + print(list_documents_query) + result = await RAGFlowService.list_documents_services(None, dataset_id, list_documents_query) + + return parse_result(result) + + +# 上传文件到数据集 +@ragflowController.post("/upload_file/{dataset_id}") +async def upload_file_dataset( + dataset_id: str, + files: List[UploadFile] = File(...), + # query_db: AsyncSession = Depends(get_db), +): + """ + 上传文件到数据集 + """ + # print(file) + result = await RAGFlowService.upload_file_dataset_services(None, dataset_id ,files) + + return parse_result(result) + +# 更新文档 +@ragflowController.post("/update_file/{dataset_id}/{document_id}") +async def update_file_dataset( + dataset_id: str, + document_id: str, + update_params: UpdateFileModel, + # query_db: AsyncSession = Depends(get_db), +): + """ + 更新文件到数据集 + """ + # print(file) + result = await RAGFlowService.update_file_dataset_services(dataset_id ,document_id, update_params) + + return parse_result(result) + +# 开始解析文档 +@ragflowController.post('/parse_documents/{dataset_id}') +async def parse_documents( + dataset_id: str, + parse_params: DocumentIdsModel, + # query_db: AsyncSession = Depends(get_db), +): + result = await RAGFlowService.parse_documents_services(dataset_id, parse_params) + return parse_result(result) + +# 停止解析文档 +@ragflowController.post('/stop_parse_documents/{dataset_id}') +async def stop_parse_documents( + dataset_id: str, + parse_params: DocumentIdsModel, + # query_db: AsyncSession = Depends(get_db), +): + result = await RAGFlowService.stop_parse_documents_services(dataset_id, parse_params) + return parse_result(result) + +# 删除文档 +@ragflowController.post('/delete_file/{dataset_id}') +async def delete_file( + dataset_id: str, + delete_params: DeleteFileModel, + # query_db: AsyncSession = Depends(get_db), +): + """ + 删除文件 + """ + result = await RAGFlowService.delete_file_services(dataset_id, delete_params) + + return parse_result(result) + + +# 删除数据集 +@ragflowController.post('/delete_datasets') +async def delete_datasets( + delete_params: DeleteFileModel, + # query_db: AsyncSession = Depends(get_db), +): + """ + 删除数据集 + """ + result = await RAGFlowService.delete_datasets_services(delete_params) + return parse_result(result) + +# 查看聊天助手列表 +@ragflowController.post('/get_chat_assistant_list') +async def get_chat_assistant_list( + query_params: RagflowListQueryModel, +): + """ + 查看聊天助手列表 + """ + + result = await RAGFlowService.get_chat_assistant_list_services(query_params) + return parse_result(result) + + # pass + +# 更新聊天助手 +@ragflowController.post('/update_chat_assistant') +async def update_chat_assistant( + update_params: UpdateChatAssistantModel, +): + """ + 更新聊天助手 + """ + result = await RAGFlowService.update_chat_assistant_services(update_params) + return parse_result(result) + +# 创建属于聊天助手的会话 +@ragflowController.post('/create_session_with_chat') +async def create_session_with_chat( + create_params: CreateSessionWithChatModel, +): + """ + 创建属于聊天助手的会话 + """ + + result = await RAGFlowService.create_session_with_chat_services(create_params) + return parse_result(result) + +# 与聊天助手进行对话 +@ragflowController.post('/converse_with_chat_assistant') +async def converse_with_chat_assistant( + request: Request, + converse_params: ConverseWithChatAssistantModel, +): + """ + 与聊天助手进行对话 + """ + + start_time = time.perf_counter() + redis = getattr(request.app.state, 'redis', None) + cache_key = None + + # 检查是否应该使用搜索服务 (Router Strategy) + # Using 'glm-4-flash' to classify intent: SEARCH vs RAG + t0 = time.perf_counter() + intent = await SearchService.classify_intent(converse_params.question) + logger.info(f"[RAG_PROFILE] Intent Router ({converse_params.chat_id}): {intent} | Time: {time.perf_counter() - t0:.3f}s | Query: {converse_params.question}") + + if intent == 'SEARCH': + return await SearchService.handle_search_chat(converse_params, redis) + + if not converse_params.stream and redis: + cache_key = build_chat_cache_key(converse_params.chat_id, converse_params.question) + cached = await redis.get(cache_key) + if cached: + logger.info('ragflow对话命中缓存: chat=%s', converse_params.chat_id) + return ResponseUtil.success(json.loads(cached)) + + # Revert to Native RAGFlow Service (OpenAI endpoint failed with Auth error) + t1 = time.perf_counter() + # 使用同步Generator,更简单 + result = RAGFlowService.converse_with_chat_assistant_services(converse_params) + logger.info(f"[RAG_PROFILE] RAG Init/Connect ({converse_params.chat_id}): {time.perf_counter() - t1:.3f}s") + + if converse_params.stream: + async def stream_response(): + last_answer = "" + first_token_received = False + start_stream_time = time.perf_counter() + + try: + # 将同步Generator转换为异步迭代 + loop = asyncio.get_event_loop() + + for chunk in result: + # 在事件循环中运行CPU密集型操作 + await asyncio.sleep(0) # 让出控制权,允许其他协程运行 + + if not first_token_received: + first_token_received = True + latency = time.perf_counter() - start_stream_time + logger.info(f"[RAG_PROFILE] Time to First Token ({converse_params.chat_id}): {latency:.3f}s") + + # Native RAGFlow returns cumulative text in 'answer' field + # Chunk structure: {'data': {'answer': 'Cumulative Text...'}} + payload = chunk.get('data') if isinstance(chunk, dict) else chunk + if not payload: + continue + + body = payload if isinstance(payload, dict) else {'data': payload} + + if isinstance(body, dict) and 'answer' in body: + # Inspect for errors even in stream + if isinstance(chunk, dict) and chunk.get('code') and chunk.get('code') != 0: + logger.error(f"RAGFlow Stream Error: {chunk}") + + current_answer = body['answer'] + # Calculate Delta + if current_answer.startswith(last_answer): + delta = current_answer[len(last_answer):] + if delta: + body['answer'] = delta # Send only the new part + last_answer = current_answer + yield format_sse(body) + else: + # Context reset (unlikely but safe fallback) + last_answer = current_answer + yield format_sse(body) + else: + yield format_sse(body) + + yield format_sse({'status': 'completed'}, event='end') + except Exception as exc: + logger.exception('[RAG_PROFILE] ragflow流式对话异常: %s', exc) + yield format_sse({'message': str(exc)}, event='error') + finally: + total_time = time.perf_counter() - start_time + logger.info(f'[RAG_PROFILE] Total Stream Duration ({converse_params.chat_id}): {total_time:.3f}s') + + return StreamingResponse(stream_response(), media_type='text/event-stream') + + response = parse_result(result) + if redis and cache_key and isinstance(result, dict) and result.get('code') == 0: + await redis.set(cache_key, json.dumps(result.get('data'), ensure_ascii=False), ex=60) + logger.info(f'[RAG_PROFILE] Total Sync Duration ({converse_params.chat_id}): {time.perf_counter() - start_time:.3f}s') + return response + + + + # return parse_result(result) + +# 获取用户权限 +@ragflowController.get('/get_user_permission', dependencies=[Depends(CheckRoleInterfaceAuth('pad'))]) +async def get_user_permission(current_user = Depends(LoginService.get_current_user)): + """ + 获取用户权限 + """ + + user_auth_list = current_user.permissions + print(user_auth_list) + + + return ResponseUtil.success(data=user_auth_list) + +def parse_result(result): + code = result.get('code', 0) + if code != 0: + msg = result.get('message') or result.get('msg') or '接口异常' + return ResponseUtil.error(msg=msg, data=result.get('data', None)) + return ResponseUtil.success(data=result.get('data', None)) + + +def build_chat_cache_key(chat_id: str, question: str) -> str: + digest = hashlib.sha256(question.encode('utf-8')).hexdigest() + return f'ragflow:chat:{chat_id}:{digest}' + + +def format_sse(data: dict, event: str | None = None) -> str: + payload = json.dumps(data, ensure_ascii=False) + prefix = f'event: {event}\n' if event else '' + return f'{prefix}data: {payload}\n\n' + + +async def aiter_sync_generator(sync_gen): + """将同步 Generator 转换为异步迭代器""" + loop = asyncio.get_event_loop() + try: + while True: + # 在线程池中获取下一个值 + value = await loop.run_in_executor(None, next, sync_gen, None) + if value is None: + break + yield value + except StopIteration: + pass +import asyncio +import hashlib +import json +import time +from typing import AsyncIterator, Union + +import aiofiles +import redis +from fastapi import Depends, Request, Response +from fastapi.responses import StreamingResponse +from fastapi_controller.decorators import Controller +from fastapi_controller.dependencies import LoginService +from fastapi_controller.response import ResponseUtil +from sqlalchemy.ext.asyncio import AsyncSession + +from module_admin.service.ragflow_service import RAGFlowService +from module_admin.service.search_service import SearchService +from module_admin.service.login_service import CheckRoleInterfaceAuth +from module_admin.model.ragflow_models import ( + ConverseWithChatAssistantModel, + CreateSessionWithChatModel, + DeleteFileModel, + DocumentIdsModel, + RagflowListQueryModel, + UpdateChatAssistantModel, +) + +import logger as logger_module \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_admin/controller/robot_action_controller.py b/ruoyi-fastapi-backend/module_admin/controller/robot_action_controller.py new file mode 100644 index 0000000..72d7902 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/robot_action_controller.py @@ -0,0 +1,106 @@ +from fastapi import APIRouter, Depends, Form, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth, CheckRoleInterfaceAuth +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.login_service import LoginService +from module_admin.service.robot_action_service import Robot_actionService +from module_admin.entity.vo.robot_action_vo import DeleteRobot_actionModel, Robot_actionModel, Robot_actionPageQueryModel +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + +from datetime import datetime + + +robot_actionController = APIRouter(prefix='/system/robot_action', dependencies=[Depends(LoginService.get_current_user)]) + + +@robot_actionController.get( + '/list', response_model=PageResponseModel + # , dependencies=[Depends(CheckUserInterfaceAuth('system:robot_action:list'))] +) +async def get_system_robot_action_list( + request: Request, + robot_action_page_query: Robot_actionPageQueryModel = Depends(Robot_actionPageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + # 获取分页数据 + robot_action_page_query_result = await Robot_actionService.get_robot_action_list_services(query_db, robot_action_page_query, is_page=False) + logger.info('获取成功') + + return ResponseUtil.success(data=robot_action_page_query_result) + + +# @robot_actionController.post('', dependencies=[Depends(CheckUserInterfaceAuth('system:robot_action:add'))]) +# @ValidateFields(validate_model='add_robot_action') +# @Log(title='机器人动作', business_type=BusinessType.INSERT) +# async def add_system_robot_action( +# request: Request, +# add_robot_action: Robot_actionModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# add_robot_action.update_time = datetime.now() +# add_robot_action_result = await Robot_actionService.add_robot_action_services(query_db, add_robot_action) +# logger.info(add_robot_action_result.message) + +# return ResponseUtil.success(msg=add_robot_action_result.message) + + +@robot_actionController.put('' + # , dependencies=[Depends(CheckUserInterfaceAuth('system:robot_action:edit'))] + ) +@ValidateFields(validate_model='edit_robot_action') +@Log(title='机器人动作', business_type=BusinessType.UPDATE) +async def edit_system_robot_action( + request: Request, + edit_robot_action: Robot_actionModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + edit_robot_action.update_by = current_user.user.user_name + edit_robot_action.update_time = datetime.now() + edit_robot_action_result = await Robot_actionService.edit_robot_action_services(query_db, edit_robot_action) + logger.info(edit_robot_action_result.message) + + return ResponseUtil.success(msg=edit_robot_action_result.message) + + +# @robot_actionController.delete('/{ids}', dependencies=[Depends(CheckUserInterfaceAuth('system:robot_action:remove'))]) +# @Log(title='机器人动作', business_type=BusinessType.DELETE) +# async def delete_system_robot_action(request: Request, ids: str, query_db: AsyncSession = Depends(get_db)): +# delete_robot_action = DeleteRobot_actionModel(ids=ids) +# delete_robot_action_result = await Robot_actionService.delete_robot_action_services(query_db, delete_robot_action) +# logger.info(delete_robot_action_result.message) + +# return ResponseUtil.success(msg=delete_robot_action_result.message) + + +# @robot_actionController.get( +# '/{id}', response_model=Robot_actionModel, dependencies=[Depends(CheckUserInterfaceAuth('system:robot_action:query'))] +# ) +# async def query_detail_system_robot_action(request: Request, id: int, query_db: AsyncSession = Depends(get_db)): +# robot_action_detail_result = await Robot_actionService.robot_action_detail_services(query_db, id) +# logger.info(f'获取id为{id}的信息成功') + +# return ResponseUtil.success(data=robot_action_detail_result) + + +# @robot_actionController.post('/export', dependencies=[Depends(CheckUserInterfaceAuth('system:robot_action:export'))]) +# @Log(title='机器人动作', business_type=BusinessType.EXPORT) +# async def export_system_robot_action_list( +# request: Request, +# robot_action_page_query: Robot_actionPageQueryModel = Form(), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取全量数据 +# robot_action_query_result = await Robot_actionService.get_robot_action_list_services(query_db, robot_action_page_query, is_page=False) +# robot_action_export_result = await Robot_actionService.export_robot_action_list_services(robot_action_query_result) +# logger.info('导出成功') + +# return ResponseUtil.streaming(data=bytes2file_response(robot_action_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/controller/robot_role_controller.py b/ruoyi-fastapi-backend/module_admin/controller/robot_role_controller.py new file mode 100644 index 0000000..2fd2d74 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/robot_role_controller.py @@ -0,0 +1,110 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Form, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.login_service import LoginService +from module_admin.service.role_service import RoleService +from module_admin.entity.vo.role_vo import DeleteRoleModel, RoleModel, RolePageQueryModel +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +""" + 当前用不到这个接口 +""" + + +# roleController = APIRouter(prefix='/system/role', dependencies=[Depends(LoginService.get_current_user)]) +# +# +# @roleController.get( +# '/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('system:role:list'))] +# ) +# async def get_system_role_list( +# request: Request, +# role_page_query: RolePageQueryModel = Depends(RolePageQueryModel.as_query), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取分页数据 +# role_page_query_result = await RoleService.get_role_list_services(query_db, role_page_query, is_page=True) +# logger.info('获取成功') +# +# return ResponseUtil.success(model_content=role_page_query_result) + + +# @roleController.post('', dependencies=[Depends(CheckUserInterfaceAuth('system:role:add'))]) +# @ValidateFields(validate_model='add_role') +# @Log(title='系统角色', business_type=BusinessType.INSERT) +# async def add_system_role( +# request: Request, +# add_role: RoleModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# add_role.create_time = datetime.now() +# add_role.create_by = current_user.user.user_name +# add_role.update_time = datetime.now() +# add_role.update_by = current_user.user.user_name +# add_role_result = await RoleService.add_role_services(query_db, add_role) +# logger.info(add_role_result.message) +# +# return ResponseUtil.success(msg=add_role_result.message) + + +# @roleController.put('', dependencies=[Depends(CheckUserInterfaceAuth('system:role:edit'))]) +# @ValidateFields(validate_model='edit_role') +# @Log(title='系统角色', business_type=BusinessType.UPDATE) +# async def edit_system_role( +# request: Request, +# edit_role: RoleModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# edit_role.update_by = current_user.user.user_name +# edit_role.update_time = datetime.now() +# edit_role_result = await RoleService.edit_role_services(query_db, edit_role) +# logger.info(edit_role_result.message) +# +# return ResponseUtil.success(msg=edit_role_result.message) + + +# @roleController.delete('/{robot_role_ids}', dependencies=[Depends(CheckUserInterfaceAuth('system:role:remove'))]) +# @Log(title='系统角色', business_type=BusinessType.DELETE) +# async def delete_system_role(request: Request, robot_role_ids: str, query_db: AsyncSession = Depends(get_db)): +# delete_role = DeleteRoleModel(robotRoleIds=robot_role_ids) +# delete_role_result = await RoleService.delete_role_services(query_db, delete_role) +# logger.info(delete_role_result.message) +# +# return ResponseUtil.success(msg=delete_role_result.message) + + +# @roleController.get( +# '/{robot_role_id}', response_model=RoleModel, dependencies=[Depends(CheckUserInterfaceAuth('system:role:query'))] +# ) +# async def query_detail_system_role(request: Request, robot_role_id: int, query_db: AsyncSession = Depends(get_db)): +# role_detail_result = await RoleService.role_detail_services(query_db, robot_role_id) +# logger.info(f'获取robot_role_id为{robot_role_id}的信息成功') +# +# return ResponseUtil.success(data=role_detail_result) + + +# @roleController.post('/export', dependencies=[Depends(CheckUserInterfaceAuth('system:role:export'))]) +# @Log(title='系统角色', business_type=BusinessType.EXPORT) +# async def export_system_role_list( +# request: Request, +# role_page_query: RolePageQueryModel = Form(), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取全量数据 +# role_query_result = await RoleService.get_role_list_services(query_db, role_page_query, is_page=False) +# role_export_result = await RoleService.export_role_list_services(role_query_result) +# logger.info('导出成功') +# +# return ResponseUtil.streaming(data=bytes2file_response(role_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/controller/robot_role_pairing_controller.py b/ruoyi-fastapi-backend/module_admin/controller/robot_role_pairing_controller.py new file mode 100644 index 0000000..18a0670 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/robot_role_pairing_controller.py @@ -0,0 +1,155 @@ +from fastapi import APIRouter, Depends, Form, Request, Response +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.dao.robot_role_pairing_dao import PairingDao +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.login_service import LoginService +from module_admin.service.robot_role_pairing_service import PairingService +from module_admin.entity.vo.robot_role_pairing_vo import DeletePairingModel, PairingModel, PairingPageQueryModel, \ + ChangeRobotRoleModel +from utils.common_util import bytes2file_response, export_list2excel +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil +from datetime import datetime + + +pairingController = APIRouter(prefix='/system/robot_role' + # , dependencies=[Depends(LoginService.get_current_user)] + ) + + +# 获取机器人角色 +@pairingController.get( + '/{robot_id}', +) +async def get_system_role_pairing_list( + request: Request, + robot_id: int, + query_db: AsyncSession = Depends(get_db), +): + # 获取分页数据 + pairing_list, style = await PairingService.get_role_pairing_list_services(query_db, robot_id) + + return ResponseUtil.success(data={"role": pairing_list, "style":style}) + +# 获取机器人角色新版 +@pairingController.get( + '/v1/{robot_id}', +) +async def get_system_role_pairing_list_v1( + request: Request, + robot_id: int, + query_db: AsyncSession = Depends(get_db), +): + + pairing_list, style = await PairingService.get_role_pairing_list_services_v1(query_db, robot_id) + + return ResponseUtil.success(data={"role": pairing_list, "style":style}) + + + +@pairingController.post( + '/change', +) +async def change_robot_role( + request: Request, + change_pairing: ChangeRobotRoleModel, + query_db: AsyncSession = Depends(get_db), +): + await PairingService.change_robot_role_services(query_db,change_pairing) + return ResponseUtil.success() + +# @pairingController.get( +# '/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('system:pairing:list'))] +# ) +# async def get_system_pairing_list( +# request: Request, +# pairing_page_query: PairingPageQueryModel = Depends(PairingPageQueryModel.as_query), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取分页数据 +# pairing_page_query_result = await PairingService.get_pairing_list_services(query_db, pairing_page_query, is_page=True) +# logger.info('获取成功') +# +# return ResponseUtil.success(model_content=pairing_page_query_result) +# +# +# @pairingController.post('', dependencies=[Depends(CheckUserInterfaceAuth('system:pairing:add'))]) +# @ValidateFields(validate_model='add_pairing') +# @Log(title='角色-机器人-映射', business_type=BusinessType.INSERT) +# async def add_system_pairing( +# request: Request, +# add_pairing: PairingModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# add_pairing.update_time = datetime.now() +# add_pairing.update_by = current_user.user.user_name +# add_pairing_result = await PairingService.add_pairing_services(query_db, add_pairing) +# logger.info(add_pairing_result.message) +# +# return ResponseUtil.success(msg=add_pairing_result.message) +# +# +# @pairingController.put('', dependencies=[Depends(CheckUserInterfaceAuth('system:pairing:edit'))]) +# @ValidateFields(validate_model='edit_pairing') +# @Log(title='角色-机器人-映射', business_type=BusinessType.UPDATE) +# async def edit_system_pairing( +# request: Request, +# edit_pairing: PairingModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# edit_pairing.update_by = current_user.user.user_name +# edit_pairing.update_time = datetime.now() +# edit_pairing_result = await PairingService.edit_pairing_services(query_db, edit_pairing) +# logger.info(edit_pairing_result.message) +# +# return ResponseUtil.success(msg=edit_pairing_result.message) +# +# +# @pairingController.delete('/{pairing_ids}', dependencies=[Depends(CheckUserInterfaceAuth('system:pairing:remove'))]) +# @Log(title='角色-机器人-映射', business_type=BusinessType.DELETE) +# async def delete_system_pairing(request: Request, pairing_ids: str, query_db: AsyncSession = Depends(get_db)): +# delete_pairing = DeletePairingModel(pairingIds=pairing_ids) +# delete_pairing_result = await PairingService.delete_pairing_services(query_db, delete_pairing) +# logger.info(delete_pairing_result.message) +# +# return ResponseUtil.success(msg=delete_pairing_result.message) +# +# +# @pairingController.get( +# '/{pairing_id}', response_model=PairingModel, dependencies=[Depends(CheckUserInterfaceAuth('system:pairing:query'))] +# ) +# async def query_detail_system_pairing(request: Request, pairing_id: int, query_db: AsyncSession = Depends(get_db)): +# pairing_detail_result = await PairingService.pairing_detail_services(query_db, pairing_id) +# logger.info(f'获取pairing_id为{pairing_id}的信息成功') +# +# return ResponseUtil.success(data=pairing_detail_result) +# +# +# @pairingController.post('/export', dependencies=[Depends(CheckUserInterfaceAuth('system:pairing:export'))]) +# @Log(title='角色-机器人-映射', business_type=BusinessType.EXPORT) +# async def export_system_pairing_list( +# request: Request, +# pairing_page_query: PairingPageQueryModel = Form(), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取全量数据 +# pairing_query_result = await PairingService.get_pairing_list_services(query_db, pairing_page_query, is_page=False) +# pairing_export_result = await PairingService.export_pairing_list_services(pairing_query_result) +# logger.info('导出成功') + +# # return Response( +# # content=pairing_export_result, +# # media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", +# # headers={ +# # "Content-Disposition": "attachment; filename=export.xlsx" +# # } +# # ) +# return ResponseUtil.streaming(data=bytes2file_response(pairing_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/controller/role_controller.py b/ruoyi-fastapi-backend/module_admin/controller/role_controller.py new file mode 100644 index 0000000..d4ab531 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/role_controller.py @@ -0,0 +1,283 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Form, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.data_scope import GetDataScope +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.dept_vo import DeptModel +from module_admin.entity.vo.role_vo import AddRoleModel, DeleteRoleModel, RoleModel, RolePageQueryModel +from module_admin.entity.vo.user_vo import CrudUserRoleModel, CurrentUserModel, UserRolePageQueryModel +from module_admin.service.dept_service import DeptService +from module_admin.service.login_service import LoginService +from module_admin.service.role_service import RoleService +from module_admin.service.user_service import UserService +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +roleController = APIRouter(prefix='/system/role', dependencies=[Depends(LoginService.get_current_user)]) + + +@roleController.get('/deptTree/{role_id}', dependencies=[Depends(CheckUserInterfaceAuth('system:role:query'))]) +async def get_system_role_dept_tree( + request: Request, + role_id: int, + query_db: AsyncSession = Depends(get_db), + data_scope_sql: str = Depends(GetDataScope('SysDept')), +): + dept_query_result = await DeptService.get_dept_tree_services(query_db, DeptModel(**{}), data_scope_sql) + role_dept_query_result = await RoleService.get_role_dept_tree_services(query_db, role_id) + role_dept_query_result.depts = dept_query_result + logger.info('获取成功') + + return ResponseUtil.success(model_content=role_dept_query_result) + + +@roleController.get( + '/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('system:role:list'))] +) +async def get_system_role_list( + request: Request, + role_page_query: RolePageQueryModel = Depends(RolePageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), + data_scope_sql: str = Depends(GetDataScope('SysDept')), +): + role_page_query_result = await RoleService.get_role_list_services( + query_db, role_page_query, data_scope_sql, is_page=True + ) + logger.info('获取成功') + + return ResponseUtil.success(model_content=role_page_query_result) + + +@roleController.post('', dependencies=[Depends(CheckUserInterfaceAuth('system:role:add'))]) +@ValidateFields(validate_model='add_role') +@Log(title='角色管理', business_type=BusinessType.INSERT) +async def add_system_role( + request: Request, + add_role: AddRoleModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + add_role.create_by = current_user.user.user_name + add_role.create_time = datetime.now() + add_role.update_by = current_user.user.user_name + add_role.update_time = datetime.now() + add_role_result = await RoleService.add_role_services(query_db, add_role) + logger.info(add_role_result.message) + + return ResponseUtil.success(msg=add_role_result.message) + + +@roleController.put('', dependencies=[Depends(CheckUserInterfaceAuth('system:role:edit'))]) +@ValidateFields(validate_model='edit_role') +@Log(title='角色管理', business_type=BusinessType.UPDATE) +async def edit_system_role( + request: Request, + edit_role: AddRoleModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), + data_scope_sql: str = Depends(GetDataScope('SysDept')), +): + await RoleService.check_role_allowed_services(edit_role) + if not current_user.user.admin: + await RoleService.check_role_data_scope_services(query_db, str(edit_role.role_id), data_scope_sql) + edit_role.update_by = current_user.user.user_name + edit_role.update_time = datetime.now() + edit_role_result = await RoleService.edit_role_services(query_db, edit_role) + logger.info(edit_role_result.message) + + return ResponseUtil.success(msg=edit_role_result.message) + + +@roleController.put('/dataScope', dependencies=[Depends(CheckUserInterfaceAuth('system:role:edit'))]) +@Log(title='角色管理', business_type=BusinessType.GRANT) +async def edit_system_role_datascope( + request: Request, + role_data_scope: AddRoleModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), + data_scope_sql: str = Depends(GetDataScope('SysDept')), +): + await RoleService.check_role_allowed_services(role_data_scope) + if not current_user.user.admin: + await RoleService.check_role_data_scope_services(query_db, str(role_data_scope.role_id), data_scope_sql) + edit_role = AddRoleModel( + roleId=role_data_scope.role_id, + dataScope=role_data_scope.data_scope, + deptIds=role_data_scope.dept_ids, + deptCheckStrictly=role_data_scope.dept_check_strictly, + updateBy=current_user.user.user_name, + updateTime=datetime.now(), + ) + role_data_scope_result = await RoleService.role_datascope_services(query_db, edit_role) + logger.info(role_data_scope_result.message) + + return ResponseUtil.success(msg=role_data_scope_result.message) + + +@roleController.delete('/{role_ids}', dependencies=[Depends(CheckUserInterfaceAuth('system:role:remove'))]) +@Log(title='角色管理', business_type=BusinessType.DELETE) +async def delete_system_role( + request: Request, + role_ids: str, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), + data_scope_sql: str = Depends(GetDataScope('SysDept')), +): + role_id_list = role_ids.split(',') if role_ids else [] + if role_id_list: + for role_id in role_id_list: + await RoleService.check_role_allowed_services(RoleModel(roleId=int(role_id))) + if not current_user.user.admin: + await RoleService.check_role_data_scope_services(query_db, role_id, data_scope_sql) + delete_role = DeleteRoleModel(roleIds=role_ids, updateBy=current_user.user.user_name, updateTime=datetime.now()) + delete_role_result = await RoleService.delete_role_services(query_db, delete_role) + logger.info(delete_role_result.message) + + return ResponseUtil.success(msg=delete_role_result.message) + + +@roleController.get( + '/{role_id}', response_model=RoleModel, dependencies=[Depends(CheckUserInterfaceAuth('system:role:query'))] +) +async def query_detail_system_role( + request: Request, + role_id: int, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), + data_scope_sql: str = Depends(GetDataScope('SysDept')), +): + if not current_user.user.admin: + await RoleService.check_role_data_scope_services(query_db, str(role_id), data_scope_sql) + role_detail_result = await RoleService.role_detail_services(query_db, role_id) + logger.info(f'获取role_id为{role_id}的信息成功') + + return ResponseUtil.success(data=role_detail_result.model_dump(by_alias=True)) + + +@roleController.post('/export', dependencies=[Depends(CheckUserInterfaceAuth('system:role:export'))]) +@Log(title='角色管理', business_type=BusinessType.EXPORT) +async def export_system_role_list( + request: Request, + role_page_query: RolePageQueryModel = Form(), + query_db: AsyncSession = Depends(get_db), + data_scope_sql: str = Depends(GetDataScope('SysDept')), +): + # 获取全量数据 + role_query_result = await RoleService.get_role_list_services( + query_db, role_page_query, data_scope_sql, is_page=False + ) + role_export_result = await RoleService.export_role_list_services(role_query_result) + logger.info('导出成功') + + return ResponseUtil.streaming(data=bytes2file_response(role_export_result)) + + +@roleController.put('/changeStatus', dependencies=[Depends(CheckUserInterfaceAuth('system:role:edit'))]) +@Log(title='角色管理', business_type=BusinessType.UPDATE) +async def reset_system_role_status( + request: Request, + change_role: AddRoleModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), + data_scope_sql: str = Depends(GetDataScope('SysDept')), +): + await RoleService.check_role_allowed_services(change_role) + if not current_user.user.admin: + await RoleService.check_role_data_scope_services(query_db, str(change_role.role_id), data_scope_sql) + edit_role = AddRoleModel( + roleId=change_role.role_id, + status=change_role.status, + updateBy=current_user.user.user_name, + updateTime=datetime.now(), + type='status', + ) + edit_role_result = await RoleService.edit_role_services(query_db, edit_role) + logger.info(edit_role_result.message) + + return ResponseUtil.success(msg=edit_role_result.message) + + +@roleController.get( + '/authUser/allocatedList', + response_model=PageResponseModel, + dependencies=[Depends(CheckUserInterfaceAuth('system:role:list'))], +) +async def get_system_allocated_user_list( + request: Request, + user_role: UserRolePageQueryModel = Depends(UserRolePageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), + data_scope_sql: str = Depends(GetDataScope('SysUser')), +): + role_user_allocated_page_query_result = await RoleService.get_role_user_allocated_list_services( + query_db, user_role, data_scope_sql, is_page=True + ) + logger.info('获取成功') + + return ResponseUtil.success(model_content=role_user_allocated_page_query_result) + + +@roleController.get( + '/authUser/unallocatedList', + response_model=PageResponseModel, + dependencies=[Depends(CheckUserInterfaceAuth('system:role:list'))], +) +async def get_system_unallocated_user_list( + request: Request, + user_role: UserRolePageQueryModel = Depends(UserRolePageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), + data_scope_sql: str = Depends(GetDataScope('SysUser')), +): + role_user_unallocated_page_query_result = await RoleService.get_role_user_unallocated_list_services( + query_db, user_role, data_scope_sql, is_page=True + ) + logger.info('获取成功') + + return ResponseUtil.success(model_content=role_user_unallocated_page_query_result) + + +@roleController.put('/authUser/selectAll', dependencies=[Depends(CheckUserInterfaceAuth('system:role:edit'))]) +@Log(title='角色管理', business_type=BusinessType.GRANT) +async def add_system_role_user( + request: Request, + add_role_user: CrudUserRoleModel = Depends(CrudUserRoleModel.as_query), + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), + data_scope_sql: str = Depends(GetDataScope('SysDept')), +): + if not current_user.user.admin: + await RoleService.check_role_data_scope_services(query_db, str(add_role_user.role_id), data_scope_sql) + add_role_user_result = await UserService.add_user_role_services(query_db, add_role_user) + logger.info(add_role_user_result.message) + + return ResponseUtil.success(msg=add_role_user_result.message) + + +@roleController.put('/authUser/cancel', dependencies=[Depends(CheckUserInterfaceAuth('system:role:edit'))]) +@Log(title='角色管理', business_type=BusinessType.GRANT) +async def cancel_system_role_user( + request: Request, cancel_user_role: CrudUserRoleModel, query_db: AsyncSession = Depends(get_db) +): + cancel_user_role_result = await UserService.delete_user_role_services(query_db, cancel_user_role) + logger.info(cancel_user_role_result.message) + + return ResponseUtil.success(msg=cancel_user_role_result.message) + + +@roleController.put('/authUser/cancelAll', dependencies=[Depends(CheckUserInterfaceAuth('system:role:edit'))]) +@Log(title='角色管理', business_type=BusinessType.GRANT) +async def batch_cancel_system_role_user( + request: Request, + batch_cancel_user_role: CrudUserRoleModel = Depends(CrudUserRoleModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + batch_cancel_user_role_result = await UserService.delete_user_role_services(query_db, batch_cancel_user_role) + logger.info(batch_cancel_user_role_result.message) + + return ResponseUtil.success(msg=batch_cancel_user_role_result.message) diff --git a/ruoyi-fastapi-backend/module_admin/controller/scheduler_controller.py b/ruoyi-fastapi-backend/module_admin/controller/scheduler_controller.py new file mode 100644 index 0000000..229d923 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/scheduler_controller.py @@ -0,0 +1,79 @@ +""" + 系统定时任务 +""" + +from config.get_scheduler import scheduler +from apscheduler.triggers.cron import CronTrigger +from apscheduler.triggers.interval import IntervalTrigger +from module_admin.service.haikang_service import HaiKangService +from module_admin.service.compreface_service import ComprefaceService +from utils.log_util import logger + + +# 定时获取访客图片并上传compreface +async def download_visitor_image_and_upload_compreface(): + """ + 定时任务:获取海康访客图片并上传到compreface进行人脸识别 + 同时删除已过期的访客人脸数据 + 执行频率:每2分钟一次 + """ + try: + logger.info('开始执行定时任务:获取访客图片并上传compreface') + + # 获取所有有效状态的访客图片(visitorStatus=1表示正常状态) + visitor_name_list = await HaiKangService.get_all_visitor_pictures(visitorStatus=1) + logger.info(f'获取到 {len(visitor_name_list)} 张有效访客图片') + + # 如果有新的访客图片,将访客图片上传到compreface + if visitor_name_list: + result = await ComprefaceService.face_addition_batch_service(visitor_name_list) + logger.info(f'访客图片上传compreface结果: {result}') + else: + logger.info('没有新的访客图片需要处理') + + # 删除过期的访客人脸数据 + # 从visitor_name_list中提取所有访客的名字 + active_visitor_names = [name for _, name in visitor_name_list] if visitor_name_list else [] + + logger.info(f'开始删除过期访客人脸,当前有效访客数量: {len(active_visitor_names)}') + delete_result = await ComprefaceService.delete_expired_visitor_faces_service(active_visitor_names) + + if delete_result.get('error'): + logger.error(f'删除过期访客失败: {delete_result.get("error")}') + else: + logger.info( + f'删除过期访客完成 - 成功删除: {delete_result.get("deleted_count")} 个, ' + f'失败: {delete_result.get("failed_count")} 个' + ) + if delete_result.get('deleted_subjects'): + logger.info(f'已删除的访客: {", ".join(delete_result.get("deleted_subjects"))}') + if delete_result.get('failed_subjects'): + logger.warning(f'删除失败的访客: {delete_result.get("failed_subjects")}') + + logger.info('定时任务执行完成:获取访客图片并上传compreface') + except Exception as e: + logger.error(f'定时任务执行失败:获取访客图片并上传compreface - {str(e)}', exc_info=True) + + +# 初始化定时任务 +def init_scheduled_tasks(): + """ + 初始化所有定时任务 + 在应用启动时调用此方法 + """ + # 添加访客图片上传任务,每2分钟执行一次 + scheduler.add_job( + func=download_visitor_image_and_upload_compreface, + trigger=IntervalTrigger(minutes=2), + id='download_visitor_image_task', + name='定时获取访客图片并上传compreface', + replace_existing=True, + max_instances=1, # 同一时间只允许一个实例运行 + ) + logger.info('定时任务已添加:download_visitor_image_and_upload_compreface (每2分钟执行一次)') + + + + + + diff --git a/ruoyi-fastapi-backend/module_admin/controller/server_controller.py b/ruoyi-fastapi-backend/module_admin/controller/server_controller.py new file mode 100644 index 0000000..f63fdf8 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/server_controller.py @@ -0,0 +1,21 @@ +from fastapi import APIRouter, Depends, Request +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.server_vo import ServerMonitorModel +from module_admin.service.login_service import LoginService +from module_admin.service.server_service import ServerService +from utils.response_util import ResponseUtil +from utils.log_util import logger + + +serverController = APIRouter(prefix='/monitor/server', dependencies=[Depends(LoginService.get_current_user)]) + + +@serverController.get( + '', response_model=ServerMonitorModel, dependencies=[Depends(CheckUserInterfaceAuth('monitor:server:list'))] +) +async def get_monitor_server_info(request: Request): + # 获取全量数据 + server_info_query_result = await ServerService.get_server_monitor_info() + logger.info('获取成功') + + return ResponseUtil.success(data=server_info_query_result) diff --git a/ruoyi-fastapi-backend/module_admin/controller/sys_alert_controller.py b/ruoyi-fastapi-backend/module_admin/controller/sys_alert_controller.py new file mode 100644 index 0000000..bacc616 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/sys_alert_controller.py @@ -0,0 +1,106 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Form, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.login_service import LoginService +from module_admin.service.sys_alert_service import Sys_alertService +from module_admin.entity.vo.sys_alert_vo import DeleteSys_alertModel, Sys_alertModel, Sys_alertPageQueryModel +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +sys_alertController = APIRouter(prefix='/system/sys_alert', dependencies=[Depends(LoginService.get_current_user)]) + + +# @sys_alertController.get( +# '/list', response_model=PageResponseModel +# # , dependencies=[Depends(CheckUserInterfaceAuth('system:sys_alert:list'))] +# ) +# async def get_system_sys_alert_list( +# request: Request, +# sys_alert_page_query: Sys_alertPageQueryModel = Depends(Sys_alertPageQueryModel.as_query), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取分页数据 +# sys_alert_page_query_result = await Sys_alertService.get_sys_alert_list_services(query_db, sys_alert_page_query, is_page=True) +# logger.info('获取成功') + +# return ResponseUtil.success(model_content=sys_alert_page_query_result) + + +# @sys_alertController.post('', dependencies=[Depends(CheckUserInterfaceAuth('system:sys_alert:add'))]) +# @ValidateFields(validate_model='add_sys_alert') +# @Log(title='系统告警', business_type=BusinessType.INSERT) +# async def add_system_sys_alert( +# request: Request, +# add_sys_alert: Sys_alertModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# add_sys_alert.create_time = datetime.now() +# add_sys_alert.create_by = current_user.user.user_name +# add_sys_alert.update_time = datetime.now() +# add_sys_alert.update_by = current_user.user.user_name +# add_sys_alert_result = await Sys_alertService.add_sys_alert_services(query_db, add_sys_alert) +# logger.info(add_sys_alert_result.message) + +# return ResponseUtil.success(msg=add_sys_alert_result.message) + + +# @sys_alertController.put('', dependencies=[Depends(CheckUserInterfaceAuth('system:sys_alert:edit'))]) +# @ValidateFields(validate_model='edit_sys_alert') +# @Log(title='系统告警', business_type=BusinessType.UPDATE) +# async def edit_system_sys_alert( +# request: Request, +# edit_sys_alert: Sys_alertModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# edit_sys_alert.update_by = current_user.user.user_name +# edit_sys_alert.update_time = datetime.now() +# edit_sys_alert_result = await Sys_alertService.edit_sys_alert_services(query_db, edit_sys_alert) +# logger.info(edit_sys_alert_result.message) + +# return ResponseUtil.success(msg=edit_sys_alert_result.message) + + +# @sys_alertController.delete('/{ids}', dependencies=[Depends(CheckUserInterfaceAuth('system:sys_alert:remove'))]) +# @Log(title='系统告警', business_type=BusinessType.DELETE) +# async def delete_system_sys_alert(request: Request, ids: str, query_db: AsyncSession = Depends(get_db)): +# delete_sys_alert = DeleteSys_alertModel(ids=ids) +# delete_sys_alert_result = await Sys_alertService.delete_sys_alert_services(query_db, delete_sys_alert) +# logger.info(delete_sys_alert_result.message) + +# return ResponseUtil.success(msg=delete_sys_alert_result.message) + + +# @sys_alertController.get( +# '/{id}', response_model=Sys_alertModel, dependencies=[Depends(CheckUserInterfaceAuth('system:sys_alert:query'))] +# ) +# async def query_detail_system_sys_alert(request: Request, id: int, query_db: AsyncSession = Depends(get_db)): +# sys_alert_detail_result = await Sys_alertService.sys_alert_detail_services(query_db, id) +# logger.info(f'获取id为{id}的信息成功') + +# return ResponseUtil.success(data=sys_alert_detail_result) + + +# @sys_alertController.post('/export', dependencies=[Depends(CheckUserInterfaceAuth('system:sys_alert:export'))]) +# @Log(title='系统告警', business_type=BusinessType.EXPORT) +# async def export_system_sys_alert_list( +# request: Request, +# sys_alert_page_query: Sys_alertPageQueryModel = Form(), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取全量数据 +# sys_alert_query_result = await Sys_alertService.get_sys_alert_list_services(query_db, sys_alert_page_query, is_page=False) +# sys_alert_export_result = await Sys_alertService.export_sys_alert_list_services(sys_alert_query_result) +# logger.info('导出成功') + +# return ResponseUtil.streaming(data=bytes2file_response(sys_alert_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/controller/sys_statistics_controller.py b/ruoyi-fastapi-backend/module_admin/controller/sys_statistics_controller.py new file mode 100644 index 0000000..ef29d5a --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/sys_statistics_controller.py @@ -0,0 +1,240 @@ +from datetime import datetime, date +from fastapi import APIRouter, Depends, Form, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.login_service import LoginService +from module_admin.service.identification_record_service import Identification_recordService +from module_admin.service.sys_statistics_service import Sys_statisticsService +from module_admin.entity.vo.sys_statistics_vo import DeleteSys_statisticsModel, Sys_statisticsModel, Sys_statisticsPageQueryModel +from module_admin.entity.vo.identification_record_vo import Identification_recordModel +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +sys_statisticsController = APIRouter(prefix='/system/sys_statistics', dependencies=[Depends(LoginService.get_current_user)]) + +# 添加识别记录 +@sys_statisticsController.post( + '/add_identification_record' +) +async def add_system_identification_record( + request: Request, + add_identification_record: Identification_recordModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + try: + # 设置创建时间和创建者 + add_identification_record.create_time = datetime.now() + add_identification_record.create_by = current_user.user.user_name + + # 调用服务层添加识别记录 + result = await Identification_recordService.add_identification_record_services(query_db, add_identification_record) + + if result.is_success: + logger.info(f'添加识别记录成功: {add_identification_record.person_name}') + return ResponseUtil.success(msg=result.message) + else: + logger.error(f'添加识别记录失败: {result.message}') + return ResponseUtil.error(msg=result.message) + except Exception as e: + logger.error(f'添加识别记录异常: {str(e)}') + return ResponseUtil.error(msg=f'添加识别记录失败: {str(e)}') + + +# 获取统计数据 +@sys_statisticsController.get( + '/get_data' +) +async def get_statistics_data( + request: Request, + query_db: AsyncSession = Depends(get_db), +): + data = await Sys_statisticsService.get_statistics_data_services(query_db) + return ResponseUtil.success(data=data) + +# 获取访问趋势 +@sys_statisticsController.get( + '/get_visitor_count/{day}' +) +async def get_visitor_count( + request: Request, + day: int, + query_db: AsyncSession = Depends(get_db), +): + data = await Sys_statisticsService.get_visitor_count_services(query_db, day) + return ResponseUtil.success(data=data) + +# 获取门禁识别成功率 +@sys_statisticsController.get( + '/access_control_success_rate/{day}' +) +async def get_access_control_success_rate( + request: Request, + day: int, + query_db: AsyncSession = Depends(get_db), +): + data = await Sys_statisticsService.get_access_control_success_rate_services(query_db, day) + return ResponseUtil.success(data=data) + +# 大模型调用次数加1 +@sys_statisticsController.get( + '/add_model_call_count' +) +async def add_model_call_count( + request: Request, + query_db: AsyncSession = Depends(get_db), + user: CurrentUserModel = Depends(LoginService.get_current_user), +): + data = await Sys_statisticsService.add_model_call_count_services(query_db, user.user.user_name) + if data.is_success: + logger.info('大模型调用次数加1成功') + return ResponseUtil.success(msg=data.message) + else: + logger.error('大模型调用次数加1失败') + return ResponseUtil.error(msg=data.message) + +# 门禁通行量次数加1 +@sys_statisticsController.get( + '/add_door_pass_count' +) +async def add_door_pass_count( + request: Request, + query_db: AsyncSession = Depends(get_db), + user: CurrentUserModel = Depends(LoginService.get_current_user), +): + data = await Sys_statisticsService.add_door_pass_count_services(query_db, user.user.user_name) + if data.is_success: + logger.info('门禁通行量次数加1成功') + return ResponseUtil.success(msg=data.message) + else: + logger.error('门禁通行量次数加1失败') + return ResponseUtil.error(msg=data.message) +# 访客引导次数加1 +@sys_statisticsController.get( + '/add_visitor_guide_count' +) +async def add_visitor_guide_count( + request: Request, + query_db: AsyncSession = Depends(get_db), + user: CurrentUserModel = Depends(LoginService.get_current_user), +): + data = await Sys_statisticsService.add_visitor_guide_count_services(query_db, user.user.user_name) + if data.is_success: + logger.info('访客引导次数加1成功') + return ResponseUtil.success(msg=data.message) + else: + logger.error('访客引导次数加1失败') + return ResponseUtil.error(msg=data.message) + +# 展厅讲解次数加1 +@sys_statisticsController.get( + '/add_explain_count' +) +async def add_explain_count( + request: Request, + query_db: AsyncSession = Depends(get_db), + user: CurrentUserModel = Depends(LoginService.get_current_user), +): + data = await Sys_statisticsService.add_explain_count_services(query_db, user.user.user_name) + if data.is_success: + logger.info('展厅讲解次数加1成功') + return ResponseUtil.success(msg=data.message) + else: + logger.error('展厅讲解次数加1失败') + return ResponseUtil.error(msg=data.message) + +# @sys_statisticsController.get( +# '/list', response_model=PageResponseModel +# # , dependencies=[Depends(CheckUserInterfaceAuth('system:sys_statistics:list'))] +# ) +# async def get_system_sys_statistics_list( +# request: Request, +# sys_statistics_page_query: Sys_statisticsPageQueryModel = Depends(Sys_statisticsPageQueryModel.as_query), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取分页数据 +# sys_statistics_page_query_result = await Sys_statisticsService.get_sys_statistics_list_services(query_db, sys_statistics_page_query, is_page=True) +# logger.info('获取成功') +# +# return ResponseUtil.success(model_content=sys_statistics_page_query_result) + + +# @sys_statisticsController.post('' +# , dependencies=[Depends(CheckUserInterfaceAuth('system:sys_statistics:add'))] +# ) +# @ValidateFields(validate_model='add_sys_statistics') +# @Log(title='系统统计数据', business_type=BusinessType.INSERT) +# async def add_system_sys_statistics( +# request: Request, +# add_sys_statistics: Sys_statisticsModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# add_sys_statistics.create_time = datetime.now() +# add_sys_statistics.create_by = current_user.user.user_name +# add_sys_statistics.update_time = datetime.now() +# add_sys_statistics.update_by = current_user.user.user_name +# add_sys_statistics_result = await Sys_statisticsService.add_sys_statistics_services(query_db, add_sys_statistics) +# logger.info(add_sys_statistics_result.message) +# +# return ResponseUtil.success(msg=add_sys_statistics_result.message) +# +# +# @sys_statisticsController.put('', dependencies=[Depends(CheckUserInterfaceAuth('system:sys_statistics:edit'))]) +# @ValidateFields(validate_model='edit_sys_statistics') +# @Log(title='系统统计数据', business_type=BusinessType.UPDATE) +# async def edit_system_sys_statistics( +# request: Request, +# edit_sys_statistics: Sys_statisticsModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# edit_sys_statistics.update_by = current_user.user.user_name +# edit_sys_statistics.update_time = datetime.now() +# edit_sys_statistics_result = await Sys_statisticsService.edit_sys_statistics_services(query_db, edit_sys_statistics) +# logger.info(edit_sys_statistics_result.message) +# +# return ResponseUtil.success(msg=edit_sys_statistics_result.message) + + +# @sys_statisticsController.delete('/{statistic_ids}', dependencies=[Depends(CheckUserInterfaceAuth('system:sys_statistics:remove'))]) +# @Log(title='系统统计数据', business_type=BusinessType.DELETE) +# async def delete_system_sys_statistics(request: Request, statistic_ids: str, query_db: AsyncSession = Depends(get_db)): +# delete_sys_statistics = DeleteSys_statisticsModel(statisticIds=statistic_ids) +# delete_sys_statistics_result = await Sys_statisticsService.delete_sys_statistics_services(query_db, delete_sys_statistics) +# logger.info(delete_sys_statistics_result.message) +# +# return ResponseUtil.success(msg=delete_sys_statistics_result.message) +# +# +# @sys_statisticsController.get( +# '/{statistic_id}', response_model=Sys_statisticsModel, dependencies=[Depends(CheckUserInterfaceAuth('system:sys_statistics:query'))] +# ) +# async def query_detail_system_sys_statistics(request: Request, statistic_id: int, query_db: AsyncSession = Depends(get_db)): +# sys_statistics_detail_result = await Sys_statisticsService.sys_statistics_detail_services(query_db, statistic_id) +# logger.info(f'获取statistic_id为{statistic_id}的信息成功') +# +# return ResponseUtil.success(data=sys_statistics_detail_result) +# +# +# @sys_statisticsController.post('/export', dependencies=[Depends(CheckUserInterfaceAuth('system:sys_statistics:export'))]) +# @Log(title='系统统计数据', business_type=BusinessType.EXPORT) +# async def export_system_sys_statistics_list( +# request: Request, +# sys_statistics_page_query: Sys_statisticsPageQueryModel = Form(), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取全量数据 +# sys_statistics_query_result = await Sys_statisticsService.get_sys_statistics_list_services(query_db, sys_statistics_page_query, is_page=False) +# sys_statistics_export_result = await Sys_statisticsService.export_sys_statistics_list_services(sys_statistics_query_result) +# logger.info('导出成功') +# +# return ResponseUtil.streaming(data=bytes2file_response(sys_statistics_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/controller/test_controller.py b/ruoyi-fastapi-backend/module_admin/controller/test_controller.py new file mode 100644 index 0000000..a5b73d0 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/test_controller.py @@ -0,0 +1,58 @@ +from fastapi import APIRouter, Body, Depends, Form, Request +from module_admin.service.login_service import LoginService +from module_admin.entity.vo.test_vo import TestUserName, TestPostForm, TestUserNamePage +from typing import List, Optional +from module_admin.service.test_service import TestService +from config.get_db import get_db +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.annotation.log_annotation import Log +from config.enums import BusinessType, RedisInitKeyConfig +from utils.response_util import ResponseUtil +from utils.log_util import logger + + +testController = APIRouter(prefix='/test') + +@testController.get('/hello', response_model= List[TestUserName] + # ,dependencies=[Depends(LoginService.get_current_user)] + ) +async def hello(request: Request, + name: Optional[str] = None, + + query_db: AsyncSession = Depends(get_db), ): + print("名称", name) + user_name = await TestService.get_user_name(query_db,) + return user_name + +@testController.post('/hello_post_json', response_model=str + ,dependencies=[Depends(LoginService.get_current_user)] + ) +@Log(title='测试Log', business_type=BusinessType.OTHER, log_type='operation') +async def hello_post_json( + request: Request, + login_info: TestUserNamePage, + query_db: AsyncSession = Depends(get_db) + +): + print(login_info.user_name) + print(login_info.page_num) + print(login_info.page_size) + # print(login_info.password) + + user_name = await TestService.get_user_name(query_db, login_info) + + logger.info("测试成功") + + return ResponseUtil.success(model_content=user_name) + +@Log(title='测试post_form_data', business_type=BusinessType.OTHER, log_type='post') +@testController.post('/hello_post_form_data', response_model=str) +async def hello_post_form_data( + request: Request, + user_name: Optional[str] = Form(), + password: Optional[str] = Form() +): + print("*"*100) + print(user_name) + print(password) + return "success" \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_admin/controller/user_controller.py b/ruoyi-fastapi-backend/module_admin/controller/user_controller.py new file mode 100644 index 0000000..ed7cf7a --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/user_controller.py @@ -0,0 +1,399 @@ +import os +from datetime import datetime +from fastapi import APIRouter, Depends, File, Form, Query, Request, UploadFile +from sqlalchemy.ext.asyncio import AsyncSession +from typing import Literal, Optional, Union +from pydantic_validation_decorator import ValidateFields +from config.get_db import get_db +from config.enums import BusinessType +from config.env import UploadConfig +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.data_scope import GetDataScope +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.dept_vo import DeptModel +from module_admin.entity.vo.user_vo import ( + AddUserModel, + CrudUserRoleModel, + CurrentUserModel, + DeleteUserModel, + EditUserModel, + ResetPasswordModel, + ResetUserModel, + UserDetailModel, + UserInfoModel, + UserModel, + UserPageQueryModel, + UserProfileModel, + UserRoleQueryModel, + UserRoleResponseModel, +) +from module_admin.service.login_service import LoginService +from module_admin.service.user_service import UserService +from module_admin.service.role_service import RoleService +from module_admin.service.dept_service import DeptService +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.pwd_util import PwdUtil +from utils.response_util import ResponseUtil +from utils.upload_util import UploadUtil + + +userController = APIRouter(prefix='/system/user', dependencies=[Depends(LoginService.get_current_user)]) + + +@userController.get('/deptTree', dependencies=[Depends(CheckUserInterfaceAuth('system:user:list'))]) +async def get_system_dept_tree( + request: Request, query_db: AsyncSession = Depends(get_db), data_scope_sql: str = Depends(GetDataScope('SysDept')) +): + dept_query_result = await DeptService.get_dept_tree_services(query_db, DeptModel(**{}), data_scope_sql) + logger.info('获取成功') + + return ResponseUtil.success(data=dept_query_result) + + +@userController.get( + '/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('system:user:list'))] +) +async def get_system_user_list( + request: Request, + user_page_query: UserPageQueryModel = Depends(UserPageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), + data_scope_sql: str = Depends(GetDataScope('SysUser')), +): + # 获取分页数据 + user_page_query_result = await UserService.get_user_list_services( + query_db, user_page_query, data_scope_sql, is_page=True + ) + logger.info('获取成功') + + return ResponseUtil.success(model_content=user_page_query_result) + + +@userController.post('', dependencies=[Depends(CheckUserInterfaceAuth('system:user:add'))]) +@ValidateFields(validate_model='add_user') +@Log(title='用户管理', business_type=BusinessType.INSERT) +async def add_system_user( + request: Request, + add_user: AddUserModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), + dept_data_scope_sql: str = Depends(GetDataScope('SysDept')), + role_data_scope_sql: str = Depends(GetDataScope('SysDept')), +): + if not current_user.user.admin: + await DeptService.check_dept_data_scope_services(query_db, add_user.dept_id, dept_data_scope_sql) + await RoleService.check_role_data_scope_services( + query_db, ','.join([str(item) for item in add_user.role_ids]), role_data_scope_sql + ) + add_user.password = PwdUtil.get_password_hash(add_user.password) + add_user.create_by = current_user.user.user_name + add_user.create_time = datetime.now() + add_user.update_by = current_user.user.user_name + add_user.update_time = datetime.now() + add_user_result = await UserService.add_user_services(query_db, add_user) + logger.info(add_user_result.message) + + return ResponseUtil.success(msg=add_user_result.message) + + +@userController.put('', dependencies=[Depends(CheckUserInterfaceAuth('system:user:edit'))]) +@ValidateFields(validate_model='edit_user') +@Log(title='用户管理', business_type=BusinessType.UPDATE) +async def edit_system_user( + request: Request, + edit_user: EditUserModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), + user_data_scope_sql: str = Depends(GetDataScope('SysUser')), + dept_data_scope_sql: str = Depends(GetDataScope('SysDept')), + role_data_scope_sql: str = Depends(GetDataScope('SysDept')), +): + await UserService.check_user_allowed_services(edit_user) + if not current_user.user.admin: + await UserService.check_user_data_scope_services(query_db, edit_user.user_id, user_data_scope_sql) + await DeptService.check_dept_data_scope_services(query_db, edit_user.dept_id, dept_data_scope_sql) + await RoleService.check_role_data_scope_services( + query_db, ','.join([str(item) for item in edit_user.role_ids]), role_data_scope_sql + ) + edit_user.update_by = current_user.user.user_name + edit_user.update_time = datetime.now() + edit_user_result = await UserService.edit_user_services(query_db, edit_user) + logger.info(edit_user_result.message) + + return ResponseUtil.success(msg=edit_user_result.message) + + +@userController.delete('/{user_ids}', dependencies=[Depends(CheckUserInterfaceAuth('system:user:remove'))]) +@Log(title='用户管理', business_type=BusinessType.DELETE) +async def delete_system_user( + request: Request, + user_ids: str, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), + data_scope_sql: str = Depends(GetDataScope('SysUser')), +): + user_id_list = user_ids.split(',') if user_ids else [] + if user_id_list: + if current_user.user.user_id in list(map(int, user_id_list)): + logger.warning('当前登录用户不能删除') + + return ResponseUtil.failure(msg='当前登录用户不能删除') + for user_id in user_id_list: + await UserService.check_user_allowed_services(UserModel(userId=int(user_id))) + if not current_user.user.admin: + await UserService.check_user_data_scope_services(query_db, int(user_id), data_scope_sql) + delete_user = DeleteUserModel(userIds=user_ids, updateBy=current_user.user.user_name, updateTime=datetime.now()) + delete_user_result = await UserService.delete_user_services(query_db, delete_user) + logger.info(delete_user_result.message) + + return ResponseUtil.success(msg=delete_user_result.message) + + +@userController.put('/resetPwd', dependencies=[Depends(CheckUserInterfaceAuth('system:user:resetPwd'))]) +@Log(title='用户管理', business_type=BusinessType.UPDATE) +async def reset_system_user_pwd( + request: Request, + reset_user: EditUserModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), + data_scope_sql: str = Depends(GetDataScope('SysUser')), +): + await UserService.check_user_allowed_services(reset_user) + if not current_user.user.admin: + await UserService.check_user_data_scope_services(query_db, reset_user.user_id, data_scope_sql) + edit_user = EditUserModel( + userId=reset_user.user_id, + password=PwdUtil.get_password_hash(reset_user.password), + updateBy=current_user.user.user_name, + updateTime=datetime.now(), + type='pwd', + ) + edit_user_result = await UserService.edit_user_services(query_db, edit_user) + logger.info(edit_user_result.message) + + return ResponseUtil.success(msg=edit_user_result.message) + + +@userController.put('/changeStatus', dependencies=[Depends(CheckUserInterfaceAuth('system:user:edit'))]) +@Log(title='用户管理', business_type=BusinessType.UPDATE) +async def change_system_user_status( + request: Request, + change_user: EditUserModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), + data_scope_sql: str = Depends(GetDataScope('SysUser')), +): + await UserService.check_user_allowed_services(change_user) + if not current_user.user.admin: + await UserService.check_user_data_scope_services(query_db, change_user.user_id, data_scope_sql) + edit_user = EditUserModel( + userId=change_user.user_id, + status=change_user.status, + updateBy=current_user.user.user_name, + updateTime=datetime.now(), + type='status', + ) + edit_user_result = await UserService.edit_user_services(query_db, edit_user) + logger.info(edit_user_result.message) + + return ResponseUtil.success(msg=edit_user_result.message) + + +@userController.get('/profile', response_model=UserProfileModel) +async def query_detail_system_user_profile( + request: Request, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + profile_user_result = await UserService.user_profile_services(query_db, current_user.user.user_id) + logger.info(f'获取user_id为{current_user.user.user_id}的信息成功') + + return ResponseUtil.success(model_content=profile_user_result) + + +@userController.get( + '/{user_id}', response_model=UserDetailModel, dependencies=[Depends(CheckUserInterfaceAuth('system:user:query'))] +) +@userController.get( + '/', response_model=UserDetailModel, dependencies=[Depends(CheckUserInterfaceAuth('system:user:query'))] +) +async def query_detail_system_user( + request: Request, + user_id: Optional[Union[int, Literal['']]] = '', + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), + data_scope_sql: str = Depends(GetDataScope('SysUser')), +): + if user_id and not current_user.user.admin: + await UserService.check_user_data_scope_services(query_db, user_id, data_scope_sql) + detail_user_result = await UserService.user_detail_services(query_db, user_id) + logger.info(f'获取user_id为{user_id}的信息成功') + + return ResponseUtil.success(model_content=detail_user_result) + + +@userController.post('/profile/avatar') +@Log(title='个人信息', business_type=BusinessType.UPDATE) +async def change_system_user_profile_avatar( + request: Request, + avatarfile: bytes = File(), + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + if avatarfile: + relative_path = ( + f'avatar/{datetime.now().strftime("%Y")}/{datetime.now().strftime("%m")}/{datetime.now().strftime("%d")}' + ) + dir_path = os.path.join(UploadConfig.UPLOAD_PATH, relative_path) + try: + os.makedirs(dir_path) + except FileExistsError: + pass + avatar_name = f'avatar_{datetime.now().strftime("%Y%m%d%H%M%S")}{UploadConfig.UPLOAD_MACHINE}{UploadUtil.generate_random_number()}.png' + avatar_path = os.path.join(dir_path, avatar_name) + with open(avatar_path, 'wb') as f: + f.write(avatarfile) + edit_user = EditUserModel( + userId=current_user.user.user_id, + avatar=f'{UploadConfig.UPLOAD_PREFIX}/{relative_path}/{avatar_name}', + updateBy=current_user.user.user_name, + updateTime=datetime.now(), + type='avatar', + ) + edit_user_result = await UserService.edit_user_services(query_db, edit_user) + logger.info(edit_user_result.message) + + return ResponseUtil.success(dict_content={'imgUrl': edit_user.avatar}, msg=edit_user_result.message) + return ResponseUtil.failure(msg='上传图片异常,请联系管理员') + + +@userController.put('/profile') +@Log(title='个人信息', business_type=BusinessType.UPDATE) +async def change_system_user_profile_info( + request: Request, + user_info: UserInfoModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + edit_user = EditUserModel( + **user_info.model_dump(exclude_unset=True, by_alias=True, exclude={'role_ids', 'post_ids'}), + userId=current_user.user.user_id, + userName=current_user.user.user_name, + updateBy=current_user.user.user_name, + updateTime=datetime.now(), + roleIds=current_user.user.role_ids.split(',') if current_user.user.role_ids else [], + postIds=current_user.user.post_ids.split(',') if current_user.user.post_ids else [], + role=current_user.user.role, + ) + edit_user_result = await UserService.edit_user_services(query_db, edit_user) + logger.info(edit_user_result.message) + + return ResponseUtil.success(msg=edit_user_result.message) + + +@userController.put('/profile/updatePwd') +@Log(title='个人信息', business_type=BusinessType.UPDATE) +async def reset_system_user_password( + request: Request, + reset_password: ResetPasswordModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + reset_user = ResetUserModel( + userId=current_user.user.user_id, + oldPassword=reset_password.old_password, + password=reset_password.new_password, + updateBy=current_user.user.user_name, + updateTime=datetime.now(), + ) + reset_user_result = await UserService.reset_user_services(query_db, reset_user) + logger.info(reset_user_result.message) + + return ResponseUtil.success(msg=reset_user_result.message) + + +@userController.post('/importData', dependencies=[Depends(CheckUserInterfaceAuth('system:user:import'))]) +@Log(title='用户管理', business_type=BusinessType.IMPORT) +async def batch_import_system_user( + request: Request, + file: UploadFile = File(...), + update_support: bool = Query(alias='updateSupport'), + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), + user_data_scope_sql: str = Depends(GetDataScope('SysUser')), + dept_data_scope_sql: str = Depends(GetDataScope('SysDept')), +): + batch_import_result = await UserService.batch_import_user_services( + request, query_db, file, update_support, current_user, user_data_scope_sql, dept_data_scope_sql + ) + logger.info(batch_import_result.message) + + return ResponseUtil.success(msg=batch_import_result.message) + + +@userController.post('/importTemplate', dependencies=[Depends(CheckUserInterfaceAuth('system:user:import'))]) +async def export_system_user_template(request: Request, query_db: AsyncSession = Depends(get_db)): + user_import_template_result = await UserService.get_user_import_template_services() + logger.info('获取成功') + + return ResponseUtil.streaming(data=bytes2file_response(user_import_template_result)) + + +@userController.post('/export', dependencies=[Depends(CheckUserInterfaceAuth('system:user:export'))]) +@Log(title='用户管理', business_type=BusinessType.EXPORT) +async def export_system_user_list( + request: Request, + user_page_query: UserPageQueryModel = Form(), + query_db: AsyncSession = Depends(get_db), + data_scope_sql: str = Depends(GetDataScope('SysUser')), +): + # 获取全量数据 + user_query_result = await UserService.get_user_list_services( + query_db, user_page_query, data_scope_sql, is_page=False + ) + user_export_result = await UserService.export_user_list_services(user_query_result) + logger.info('导出成功') + + return ResponseUtil.streaming(data=bytes2file_response(user_export_result)) + + +@userController.get( + '/authRole/{user_id}', + response_model=UserRoleResponseModel, + dependencies=[Depends(CheckUserInterfaceAuth('system:user:query'))], +) +async def get_system_allocated_role_list(request: Request, user_id: int, query_db: AsyncSession = Depends(get_db)): + user_role_query = UserRoleQueryModel(userId=user_id) + user_role_allocated_query_result = await UserService.get_user_role_allocated_list_services( + query_db, user_role_query + ) + logger.info('获取成功') + + return ResponseUtil.success(model_content=user_role_allocated_query_result) + + +@userController.put( + '/authRole', + response_model=UserRoleResponseModel, + dependencies=[Depends(CheckUserInterfaceAuth('system:user:edit'))], +) +@Log(title='用户管理', business_type=BusinessType.GRANT) +async def update_system_role_user( + request: Request, + user_id: int = Query(alias='userId'), + role_ids: str = Query(alias='roleIds'), + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), + user_data_scope_sql: str = Depends(GetDataScope('SysUser')), + role_data_scope_sql: str = Depends(GetDataScope('SysDept')), +): + if not current_user.user.admin: + await UserService.check_user_data_scope_services(query_db, user_id, user_data_scope_sql) + await RoleService.check_role_data_scope_services(query_db, role_ids, role_data_scope_sql) + add_user_role_result = await UserService.add_user_role_services( + query_db, CrudUserRoleModel(userId=user_id, roleIds=role_ids) + ) + logger.info(add_user_role_result.message) + + return ResponseUtil.success(msg=add_user_role_result.message) diff --git a/ruoyi-fastapi-backend/module_admin/controller/visitor_type_controller.py b/ruoyi-fastapi-backend/module_admin/controller/visitor_type_controller.py new file mode 100644 index 0000000..d73c855 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/visitor_type_controller.py @@ -0,0 +1,105 @@ +# from datetime import datetime +# from fastapi import APIRouter, Depends, Form, Request +# from pydantic_validation_decorator import ValidateFields +# from sqlalchemy.ext.asyncio import AsyncSession +# from config.enums import BusinessType +# from config.get_db import get_db +# from module_admin.annotation.log_annotation import Log +# from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +# from module_admin.entity.vo.user_vo import CurrentUserModel +# from module_admin.service.login_service import LoginService +# from module_admin.service.visitor_type_service import Visitor_typeService +# from module_admin.entity.vo.visitor_type_vo import DeleteVisitor_typeModel, Visitor_typeModel, Visitor_typePageQueryModel +# from utils.common_util import bytes2file_response +# from utils.log_util import logger +# from utils.page_util import PageResponseModel +# from utils.response_util import ResponseUtil +# +# +# visitor_typeController = APIRouter(prefix='/system/visitor_type', dependencies=[Depends(LoginService.get_current_user)]) +# +# +# @visitor_typeController.get( +# '/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('system:visitor_type:list'))] +# ) +# async def get_system_visitor_type_list( +# request: Request, +# visitor_type_page_query: Visitor_typePageQueryModel = Depends(Visitor_typePageQueryModel.as_query), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取分页数据 +# visitor_type_page_query_result = await Visitor_typeService.get_visitor_type_list_services(query_db, visitor_type_page_query, is_page=True) +# logger.info('获取成功') +# +# return ResponseUtil.success(model_content=visitor_type_page_query_result) +# +# +# @visitor_typeController.post('', dependencies=[Depends(CheckUserInterfaceAuth('system:visitor_type:add'))]) +# @ValidateFields(validate_model='add_visitor_type') +# @Log(title='访客类型', business_type=BusinessType.INSERT) +# async def add_system_visitor_type( +# request: Request, +# add_visitor_type: Visitor_typeModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# add_visitor_type.create_time = datetime.now() +# add_visitor_type.create_by = current_user.user.user_name +# add_visitor_type.update_time = datetime.now() +# add_visitor_type.update_by = current_user.user.user_name +# add_visitor_type_result = await Visitor_typeService.add_visitor_type_services(query_db, add_visitor_type) +# logger.info(add_visitor_type_result.message) +# +# return ResponseUtil.success(msg=add_visitor_type_result.message) +# +# +# @visitor_typeController.put('', dependencies=[Depends(CheckUserInterfaceAuth('system:visitor_type:edit'))]) +# @ValidateFields(validate_model='edit_visitor_type') +# @Log(title='访客类型', business_type=BusinessType.UPDATE) +# async def edit_system_visitor_type( +# request: Request, +# edit_visitor_type: Visitor_typeModel, +# query_db: AsyncSession = Depends(get_db), +# current_user: CurrentUserModel = Depends(LoginService.get_current_user), +# ): +# edit_visitor_type.update_by = current_user.user.user_name +# edit_visitor_type.update_time = datetime.now() +# edit_visitor_type_result = await Visitor_typeService.edit_visitor_type_services(query_db, edit_visitor_type) +# logger.info(edit_visitor_type_result.message) +# +# return ResponseUtil.success(msg=edit_visitor_type_result.message) +# +# +# @visitor_typeController.delete('/{type_ids}', dependencies=[Depends(CheckUserInterfaceAuth('system:visitor_type:remove'))]) +# @Log(title='访客类型', business_type=BusinessType.DELETE) +# async def delete_system_visitor_type(request: Request, type_ids: str, query_db: AsyncSession = Depends(get_db)): +# delete_visitor_type = DeleteVisitor_typeModel(typeIds=type_ids) +# delete_visitor_type_result = await Visitor_typeService.delete_visitor_type_services(query_db, delete_visitor_type) +# logger.info(delete_visitor_type_result.message) +# +# return ResponseUtil.success(msg=delete_visitor_type_result.message) +# +# +# @visitor_typeController.get( +# '/{type_id}', response_model=Visitor_typeModel, dependencies=[Depends(CheckUserInterfaceAuth('system:visitor_type:query'))] +# ) +# async def query_detail_system_visitor_type(request: Request, type_id: int, query_db: AsyncSession = Depends(get_db)): +# visitor_type_detail_result = await Visitor_typeService.visitor_type_detail_services(query_db, type_id) +# logger.info(f'获取type_id为{type_id}的信息成功') +# +# return ResponseUtil.success(data=visitor_type_detail_result) +# +# +# @visitor_typeController.post('/export', dependencies=[Depends(CheckUserInterfaceAuth('system:visitor_type:export'))]) +# @Log(title='访客类型', business_type=BusinessType.EXPORT) +# async def export_system_visitor_type_list( +# request: Request, +# visitor_type_page_query: Visitor_typePageQueryModel = Form(), +# query_db: AsyncSession = Depends(get_db), +# ): +# # 获取全量数据 +# visitor_type_query_result = await Visitor_typeService.get_visitor_type_list_services(query_db, visitor_type_page_query, is_page=False) +# visitor_type_export_result = await Visitor_typeService.export_visitor_type_list_services(visitor_type_query_result) +# logger.info('导出成功') +# +# return ResponseUtil.streaming(data=bytes2file_response(visitor_type_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/controller/words_controller.py b/ruoyi-fastapi-backend/module_admin/controller/words_controller.py new file mode 100644 index 0000000..6d9633e --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/controller/words_controller.py @@ -0,0 +1,173 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Form, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.login_service import LoginService +from module_admin.service.visitor_type_service import Visitor_typeService +from module_admin.service.words_service import WordsService +from module_admin.entity.vo.words_vo import DeleteWordsModel, WordsModel, WordsPageQueryModel, WordsUpdateModel +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + + +wordsController = APIRouter(prefix='/guide/words', dependencies=[Depends(LoginService.get_current_user)]) +""" + 访客记录: visitor:record:list, dependencies=[Depends(CheckUserInterfaceAuth('guide:words:list'))] + 引导词管理: visitor:guide:list, dependencies=[Depends(CheckUserInterfaceAuth('visitor:guide:list'))] +""" + + +# 随机获取引导词 +@wordsController.get('/random_words/{type}') +async def get_random_guide_word(request: Request, type: str, query_db: AsyncSession = Depends(get_db)): + + guide_word = await WordsService.get_random_guide_word_services(query_db, type) + return ResponseUtil.success(data=guide_word) + + + +@wordsController.post( + '/list', response_model=PageResponseModel + # , dependencies=[Depends(CheckUserInterfaceAuth('guide:words:list'))] + , dependencies=[Depends(CheckUserInterfaceAuth('visitor:guide:list'))] +) +async def get_guide_words_list( + request: Request, + words_page_query: WordsPageQueryModel = Depends(WordsPageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + print(words_page_query.begin_time) + print(words_page_query.end_time) + # 获取分页数据 + words_page_query_result = await WordsService.get_words_list_services(query_db, words_page_query, is_page=True) + logger.info('获取成功') + + return ResponseUtil.success(model_content=words_page_query_result) + + +#批量启用引导词 +@wordsController.post('/enable_guide/{guide_ids}', dependencies=[Depends(CheckUserInterfaceAuth('visitor:guide:list'))]) +async def enable_guide( + request: Request, + guide_ids: str, + query_db: AsyncSession = Depends(get_db), + +): + edit_del_result = await WordsService.edit_del_words_services(query_db=query_db, guide_ids=guide_ids, del_flg='0') + return ResponseUtil.success(msg=edit_del_result.message) + +#批量禁用引导词 +@wordsController.post('/disable_guide/{guide_ids}', dependencies=[Depends(CheckUserInterfaceAuth('visitor:guide:list'))]) +async def disable_guide( + request: Request, + guide_ids: str, + query_db: AsyncSession = Depends(get_db), + +): + edit_del_result = await WordsService.edit_del_words_services(query_db=query_db, guide_ids=guide_ids, del_flg='2') + return ResponseUtil.success(msg=edit_del_result.message) + +#获取访客类型列表 +@wordsController.get('/visitor_type_list',) +async def get_system_visitor_type_list( + request: Request, + query_db: AsyncSession = Depends(get_db), +): + visitor_type_list = await Visitor_typeService.get_system_visitor_type_list(query_db=query_db) + return ResponseUtil.success(data=visitor_type_list) + + + +@wordsController.post('/add_guide' + # , dependencies=[Depends(CheckUserInterfaceAuth('guide:words:add'))] + , dependencies=[Depends(CheckUserInterfaceAuth('visitor:guide:list'))] + ) +@ValidateFields(validate_model='add_words') +@Log(title='引导词', business_type=BusinessType.INSERT) +async def add_guide_words( + request: Request, + add_words: WordsModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + if add_words.type is None: + return ResponseUtil.failure(msg='引导词类型不能为空') + if add_words.text is None: + return ResponseUtil.failure(msg='引导词内容不能为空') + add_words.create_time = datetime.now() + add_words.update_time = datetime.now() + add_words.create_by = current_user.user.user_name + add_words.del_flag = "0" + add_words_result = await WordsService.add_words_services(query_db, add_words) + logger.info(add_words_result.message) + + return ResponseUtil.success(msg=add_words_result.message) + + +@wordsController.put('/edit_guide' + # ,dependencies=[Depends(CheckUserInterfaceAuth('guide:words:edit'))] + , dependencies=[Depends(CheckUserInterfaceAuth('visitor:guide:list'))] + ) +@ValidateFields(validate_model='edit_words') +@Log(title='引导词', business_type=BusinessType.UPDATE) +async def edit_guide_words( + request: Request, + edit_words: WordsUpdateModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + + edit_words.update_by = current_user.user.user_name + edit_words.update_time = datetime.now() + edit_words_result = await WordsService.edit_words_services(query_db, edit_words) + logger.info(edit_words_result.message) + + return ResponseUtil.success(msg=edit_words_result.message) + + +@wordsController.delete('/{guide_ids}' + # , dependencies=[Depends(CheckUserInterfaceAuth('guide:words:remove'))] + , dependencies=[Depends(CheckUserInterfaceAuth('visitor:guide:list'))] + ) +@Log(title='引导词', business_type=BusinessType.DELETE) +async def delete_guide_words(request: Request, guide_ids: str, query_db: AsyncSession = Depends(get_db)): + delete_words = DeleteWordsModel(guideIds=guide_ids) + delete_words_result = await WordsService.delete_words_services(query_db, delete_words) + logger.info(delete_words_result.message) + + return ResponseUtil.success(msg=delete_words_result.message) + + +@wordsController.get( + '/{guide_id}', response_model=WordsModel + # , dependencies=[Depends(CheckUserInterfaceAuth('guide:words:query'))] + , dependencies=[Depends(CheckUserInterfaceAuth('visitor:guide:list'))] +) +async def query_detail_guide_words(request: Request, guide_id: int, query_db: AsyncSession = Depends(get_db)): + words_detail_result = await WordsService.words_detail_services(query_db, guide_id) + logger.info(f'获取guide_id为{guide_id}的信息成功') + + return ResponseUtil.success(data=words_detail_result) + + +@wordsController.post('/export', dependencies=[Depends(CheckUserInterfaceAuth('visitor:guide:list'))]) +@Log(title='引导词', business_type=BusinessType.EXPORT) +async def export_guide_words_list( + request: Request, + words_page_query: WordsPageQueryModel = Form(), + query_db: AsyncSession = Depends(get_db), +): + # 获取全量数据 + words_query_result = await WordsService.get_words_list_services(query_db, words_page_query, is_page=False) + words_export_result = await WordsService.export_words_list_services(words_query_result) + logger.info('导出成功') + + return ResponseUtil.streaming(data=bytes2file_response(words_export_result)) diff --git a/ruoyi-fastapi-backend/module_admin/dao/__init__.py b/ruoyi-fastapi-backend/module_admin/dao/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ruoyi-fastapi-backend/module_admin/dao/config_dao.py b/ruoyi-fastapi-backend/module_admin/dao/config_dao.py new file mode 100644 index 0000000..4f34a2f --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/config_dao.py @@ -0,0 +1,116 @@ +from datetime import datetime, time +from sqlalchemy import delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.config_do import SysConfig +from module_admin.entity.vo.config_vo import ConfigModel, ConfigPageQueryModel +from utils.page_util import PageUtil + + +class ConfigDao: + """ + 参数配置管理模块数据库操作层 + """ + + @classmethod + async def get_config_detail_by_id(cls, db: AsyncSession, config_id: int): + """ + 根据参数配置id获取参数配置详细信息 + + :param db: orm对象 + :param config_id: 参数配置id + :return: 参数配置信息对象 + """ + config_info = (await db.execute(select(SysConfig).where(SysConfig.config_id == config_id))).scalars().first() + + return config_info + + @classmethod + async def get_config_detail_by_info(cls, db: AsyncSession, config: ConfigModel): + """ + 根据参数配置参数获取参数配置信息 + + :param db: orm对象 + :param config: 参数配置参数对象 + :return: 参数配置信息对象 + """ + config_info = ( + ( + await db.execute( + select(SysConfig).where( + SysConfig.config_key == config.config_key if config.config_key else True, + SysConfig.config_value == config.config_value if config.config_value else True, + ) + ) + ) + .scalars() + .first() + ) + + return config_info + + @classmethod + async def get_config_list(cls, db: AsyncSession, query_object: ConfigPageQueryModel, is_page: bool = False): + """ + 根据查询参数获取参数配置列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 参数配置列表信息对象 + """ + query = ( + select(SysConfig) + .where( + SysConfig.config_name.like(f'%{query_object.config_name}%') if query_object.config_name else True, + SysConfig.config_key.like(f'%{query_object.config_key}%') if query_object.config_key else True, + SysConfig.config_type == query_object.config_type if query_object.config_type else True, + SysConfig.create_time.between( + datetime.combine(datetime.strptime(query_object.begin_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_time and query_object.end_time + else True, + ) + .order_by(SysConfig.config_id) + .distinct() + ) + config_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return config_list + + @classmethod + async def add_config_dao(cls, db: AsyncSession, config: ConfigModel): + """ + 新增参数配置数据库操作 + + :param db: orm对象 + :param config: 参数配置对象 + :return: + """ + db_config = SysConfig(**config.model_dump()) + db.add(db_config) + await db.flush() + + return db_config + + @classmethod + async def edit_config_dao(cls, db: AsyncSession, config: dict): + """ + 编辑参数配置数据库操作 + + :param db: orm对象 + :param config: 需要更新的参数配置字典 + :return: + """ + await db.execute(update(SysConfig), [config]) + + @classmethod + async def delete_config_dao(cls, db: AsyncSession, config: ConfigModel): + """ + 删除参数配置数据库操作 + + :param db: orm对象 + :param config: 参数配置对象 + :return: + """ + await db.execute(delete(SysConfig).where(SysConfig.config_id.in_([config.config_id]))) diff --git a/ruoyi-fastapi-backend/module_admin/dao/dept_dao.py b/ruoyi-fastapi-backend/module_admin/dao/dept_dao.py new file mode 100644 index 0000000..f450210 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/dept_dao.py @@ -0,0 +1,307 @@ +from sqlalchemy import bindparam, func, or_, select, update # noqa: F401 +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.util import immutabledict +from typing import List +from module_admin.entity.do.dept_do import SysDept +from module_admin.entity.do.role_do import SysRoleDept # noqa: F401 +from module_admin.entity.do.user_do import SysUser +from module_admin.entity.vo.dept_vo import DeptModel + + +class DeptDao: + """ + 部门管理模块数据库操作层 + """ + + @classmethod + async def get_dept_by_id(cls, db: AsyncSession, dept_id: int): + """ + 根据部门id获取在用部门信息 + + :param db: orm对象 + :param dept_id: 部门id + :return: 在用部门信息对象 + """ + dept_info = (await db.execute(select(SysDept).where(SysDept.dept_id == dept_id))).scalars().first() + + return dept_info + + @classmethod + async def get_dept_detail_by_id(cls, db: AsyncSession, dept_id: int): + """ + 根据部门id获取部门详细信息 + + :param db: orm对象 + :param dept_id: 部门id + :return: 部门信息对象 + """ + dept_info = ( + (await db.execute(select(SysDept).where(SysDept.dept_id == dept_id, SysDept.del_flag == '0'))) + .scalars() + .first() + ) + + return dept_info + + @classmethod + async def get_dept_detail_by_info(cls, db: AsyncSession, dept: DeptModel): + """ + 根据部门参数获取部门信息 + + :param db: orm对象 + :param dept: 部门参数对象 + :return: 部门信息对象 + """ + dept_info = ( + ( + await db.execute( + select(SysDept).where( + SysDept.parent_id == dept.parent_id if dept.parent_id else True, + SysDept.dept_name == dept.dept_name if dept.dept_name else True, + ) + ) + ) + .scalars() + .first() + ) + + return dept_info + + @classmethod + async def get_dept_info_for_edit_option(cls, db: AsyncSession, dept_info: DeptModel, data_scope_sql: str): + """ + 获取部门编辑对应的在用部门列表信息 + + :param db: orm对象 + :param dept_info: 部门对象 + :param data_scope_sql: 数据权限对应的查询sql语句 + :return: 部门列表信息 + """ + dept_result = ( + ( + await db.execute( + select(SysDept) + .where( + SysDept.dept_id != dept_info.dept_id, + ~SysDept.dept_id.in_( + select(SysDept.dept_id).where(func.find_in_set(dept_info.dept_id, SysDept.ancestors)) + ), + SysDept.del_flag == '0', + SysDept.status == '0', + eval(data_scope_sql), + ) + .order_by(SysDept.order_num) + .distinct() + ) + ) + .scalars() + .all() + ) + + return dept_result + + @classmethod + async def get_children_dept_dao(cls, db: AsyncSession, dept_id: int): + """ + 根据部门id查询当前部门的子部门列表信息 + + :param db: orm对象 + :param dept_id: 部门id + :return: 子部门信息列表 + """ + dept_result = ( + (await db.execute(select(SysDept).where(func.find_in_set(dept_id, SysDept.ancestors)))).scalars().all() + ) + + return dept_result + + @classmethod + async def get_dept_list_for_tree(cls, db: AsyncSession, dept_info: DeptModel, data_scope_sql: str): + """ + 获取所有在用部门列表信息 + + :param db: orm对象 + :param dept_info: 部门对象 + :param data_scope_sql: 数据权限对应的查询sql语句 + :return: 在用部门列表信息 + """ + dept_result = ( + ( + await db.execute( + select(SysDept) + .where( + SysDept.status == '0', + SysDept.del_flag == '0', + SysDept.dept_name.like(f'%{dept_info.dept_name}%') if dept_info.dept_name else True, + eval(data_scope_sql), + ) + .order_by(SysDept.order_num) + .distinct() + ) + ) + .scalars() + .all() + ) + + return dept_result + + @classmethod + async def get_dept_list(cls, db: AsyncSession, page_object: DeptModel, data_scope_sql: str): + """ + 根据查询参数获取部门列表信息 + + :param db: orm对象 + :param page_object: 不分页查询参数对象 + :param data_scope_sql: 数据权限对应的查询sql语句 + :return: 部门列表信息对象 + """ + dept_result = ( + ( + await db.execute( + select(SysDept) + .where( + SysDept.del_flag == '0', + SysDept.dept_id == page_object.dept_id if page_object.dept_id is not None else True, + SysDept.status == page_object.status if page_object.status else True, + SysDept.dept_name.like(f'%{page_object.dept_name}%') if page_object.dept_name else True, + eval(data_scope_sql), + ) + .order_by(SysDept.order_num) + .distinct() + ) + ) + .scalars() + .all() + ) + + return dept_result + + @classmethod + async def add_dept_dao(cls, db: AsyncSession, dept: DeptModel): + """ + 新增部门数据库操作 + + :param db: orm对象 + :param dept: 部门对象 + :return: 新增校验结果 + """ + db_dept = SysDept(**dept.model_dump()) + db.add(db_dept) + await db.flush() + + return db_dept + + @classmethod + async def edit_dept_dao(cls, db: AsyncSession, dept: dict): + """ + 编辑部门数据库操作 + + :param db: orm对象 + :param dept: 需要更新的部门字典 + :return: 编辑校验结果 + """ + await db.execute(update(SysDept), [dept]) + + @classmethod + async def update_dept_children_dao(cls, db: AsyncSession, update_dept: List): + """ + 更新子部门信息 + + :param db: orm对象 + :param update_dept: 需要更新的部门列表 + :return: + """ + await db.execute( + update(SysDept) + .where(SysDept.dept_id == bindparam('dept_id')) + .values( + { + 'dept_id': bindparam('dept_id'), + 'ancestors': bindparam('ancestors'), + } + ), + update_dept, + execution_options=immutabledict({'synchronize_session': None}), + ) + + @classmethod + async def update_dept_status_normal_dao(cls, db: AsyncSession, dept_id_list: List): + """ + 批量更新部门状态为正常 + + :param db: orm对象 + :param dept_id_list: 部门id列表 + :return: + """ + await db.execute(update(SysDept).where(SysDept.dept_id.in_(dept_id_list)).values(status='0')) + + @classmethod + async def delete_dept_dao(cls, db: AsyncSession, dept: DeptModel): + """ + 删除部门数据库操作 + + :param db: orm对象 + :param dept: 部门对象 + :return: + """ + await db.execute( + update(SysDept) + .where(SysDept.dept_id == dept.dept_id) + .values(del_flag='2', update_by=dept.update_by, update_time=dept.update_time) + ) + + @classmethod + async def count_normal_children_dept_dao(cls, db: AsyncSession, dept_id: int): + """ + 根据部门id查询查询所有子部门(正常状态)的数量 + + :param db: orm对象 + :param dept_id: 部门id + :return: 所有子部门(正常状态)的数量 + """ + normal_children_dept_count = ( + await db.execute( + select(func.count('*')) + .select_from(SysDept) + .where(SysDept.status == '0', SysDept.del_flag == '0', func.find_in_set(dept_id, SysDept.ancestors)) + ) + ).scalar() + + return normal_children_dept_count + + @classmethod + async def count_children_dept_dao(cls, db: AsyncSession, dept_id: int): + """ + 根据部门id查询查询所有子部门(所有状态)的数量 + + :param db: orm对象 + :param dept_id: 部门id + :return: 所有子部门(所有状态)的数量 + """ + children_dept_count = ( + await db.execute( + select(func.count('*')) + .select_from(SysDept) + .where(SysDept.del_flag == '0', SysDept.parent_id == dept_id) + .limit(1) + ) + ).scalar() + + return children_dept_count + + @classmethod + async def count_dept_user_dao(cls, db: AsyncSession, dept_id: int): + """ + 根据部门id查询查询部门下的用户数量 + + :param db: orm对象 + :param dept_id: 部门id + :return: 部门下的用户数量 + """ + dept_user_count = ( + await db.execute( + select(func.count('*')).select_from(SysUser).where(SysUser.dept_id == dept_id, SysUser.del_flag == '0') + ) + ).scalar() + + return dept_user_count diff --git a/ruoyi-fastapi-backend/module_admin/dao/dict_dao.py b/ruoyi-fastapi-backend/module_admin/dao/dict_dao.py new file mode 100644 index 0000000..c5a8ed5 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/dict_dao.py @@ -0,0 +1,281 @@ +from datetime import datetime, time +from sqlalchemy import and_, delete, func, select, update +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.dict_do import SysDictType, SysDictData +from module_admin.entity.vo.dict_vo import DictDataModel, DictDataPageQueryModel, DictTypeModel, DictTypePageQueryModel +from utils.page_util import PageUtil +from utils.time_format_util import list_format_datetime + + +class DictTypeDao: + """ + 字典类型管理模块数据库操作层 + """ + + @classmethod + async def get_dict_type_detail_by_id(cls, db: AsyncSession, dict_id: int): + """ + 根据字典类型id获取字典类型详细信息 + + :param db: orm对象 + :param dict_id: 字典类型id + :return: 字典类型信息对象 + """ + dict_type_info = (await db.execute(select(SysDictType).where(SysDictType.dict_id == dict_id))).scalars().first() + + return dict_type_info + + @classmethod + async def get_dict_type_detail_by_info(cls, db: AsyncSession, dict_type: DictTypeModel): + """ + 根据字典类型参数获取字典类型信息 + + :param db: orm对象 + :param dict_type: 字典类型参数对象 + :return: 字典类型信息对象 + """ + dict_type_info = ( + ( + await db.execute( + select(SysDictType).where( + SysDictType.dict_type == dict_type.dict_type if dict_type.dict_type else True, + SysDictType.dict_name == dict_type.dict_name if dict_type.dict_name else True, + ) + ) + ) + .scalars() + .first() + ) + + return dict_type_info + + @classmethod + async def get_all_dict_type(cls, db: AsyncSession): + """ + 获取所有的字典类型信息 + + :param db: orm对象 + :return: 字典类型信息列表对象 + """ + dict_type_info = (await db.execute(select(SysDictType))).scalars().all() + + return list_format_datetime(dict_type_info) + + @classmethod + async def get_dict_type_list(cls, db: AsyncSession, query_object: DictTypePageQueryModel, is_page: bool = False): + """ + 根据查询参数获取字典类型列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 字典类型列表信息对象 + """ + query = ( + select(SysDictType) + .where( + SysDictType.dict_name.like(f'%{query_object.dict_name}%') if query_object.dict_name else True, + SysDictType.dict_type.like(f'%{query_object.dict_type}%') if query_object.dict_type else True, + SysDictType.status == query_object.status if query_object.status else True, + SysDictType.create_time.between( + datetime.combine(datetime.strptime(query_object.begin_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_time and query_object.end_time + else True, + ) + .order_by(SysDictType.dict_id) + .distinct() + ) + dict_type_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return dict_type_list + + @classmethod + async def add_dict_type_dao(cls, db: AsyncSession, dict_type: DictTypeModel): + """ + 新增字典类型数据库操作 + + :param db: orm对象 + :param dict_type: 字典类型对象 + :return: + """ + db_dict_type = SysDictType(**dict_type.model_dump()) + db.add(db_dict_type) + await db.flush() + + return db_dict_type + + @classmethod + async def edit_dict_type_dao(cls, db: AsyncSession, dict_type: dict): + """ + 编辑字典类型数据库操作 + + :param db: orm对象 + :param dict_type: 需要更新的字典类型字典 + :return: + """ + await db.execute(update(SysDictType), [dict_type]) + + @classmethod + async def delete_dict_type_dao(cls, db: AsyncSession, dict_type: DictTypeModel): + """ + 删除字典类型数据库操作 + + :param db: orm对象 + :param dict_type: 字典类型对象 + :return: + """ + await db.execute(delete(SysDictType).where(SysDictType.dict_id.in_([dict_type.dict_id]))) + + +class DictDataDao: + """ + 字典数据管理模块数据库操作层 + """ + + @classmethod + async def get_dict_data_detail_by_id(cls, db: AsyncSession, dict_code: int): + """ + 根据字典数据id获取字典数据详细信息 + + :param db: orm对象 + :param dict_code: 字典数据id + :return: 字典数据信息对象 + """ + dict_data_info = ( + (await db.execute(select(SysDictData).where(SysDictData.dict_code == dict_code))).scalars().first() + ) + + return dict_data_info + + @classmethod + async def get_dict_data_detail_by_info(cls, db: AsyncSession, dict_data: DictDataModel): + """ + 根据字典数据参数获取字典数据信息 + + :param db: orm对象 + :param dict_data: 字典数据参数对象 + :return: 字典数据信息对象 + """ + dict_data_info = ( + ( + await db.execute( + select(SysDictData).where( + SysDictData.dict_type == dict_data.dict_type, + SysDictData.dict_label == dict_data.dict_label, + SysDictData.dict_value == dict_data.dict_value, + ) + ) + ) + .scalars() + .first() + ) + + return dict_data_info + + @classmethod + async def get_dict_data_list(cls, db: AsyncSession, query_object: DictDataPageQueryModel, is_page: bool = False): + """ + 根据查询参数获取字典数据列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 字典数据列表信息对象 + """ + query = ( + select(SysDictData) + .where( + SysDictData.dict_type == query_object.dict_type if query_object.dict_type else True, + SysDictData.dict_label.like(f'%{query_object.dict_label}%') if query_object.dict_label else True, + SysDictData.status == query_object.status if query_object.status else True, + ) + .order_by(SysDictData.dict_sort) + .distinct() + ) + dict_data_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return dict_data_list + + @classmethod + async def query_dict_data_list(cls, db: AsyncSession, dict_type: str): + """ + 根据查询参数获取字典数据列表信息 + + :param db: orm对象 + :param dict_type: 字典类型 + :return: 字典数据列表信息对象 + """ + dict_data_list = ( + ( + await db.execute( + select(SysDictData) + .select_from(SysDictType) + .where(SysDictType.dict_type == dict_type if dict_type else True, SysDictType.status == '0') + .join( + SysDictData, + and_(SysDictType.dict_type == SysDictData.dict_type, SysDictData.status == '0'), + isouter=True, + ) + .order_by(SysDictData.dict_sort) + .distinct() + ) + ) + .scalars() + .all() + ) + + return dict_data_list + + @classmethod + async def add_dict_data_dao(cls, db: AsyncSession, dict_data: DictDataModel): + """ + 新增字典数据数据库操作 + + :param db: orm对象 + :param dict_data: 字典数据对象 + :return: + """ + db_data_type = SysDictData(**dict_data.model_dump()) + db.add(db_data_type) + await db.flush() + + return db_data_type + + @classmethod + async def edit_dict_data_dao(cls, db: AsyncSession, dict_data: dict): + """ + 编辑字典数据数据库操作 + + :param db: orm对象 + :param dict_data: 需要更新的字典数据字典 + :return: + """ + await db.execute(update(SysDictData), [dict_data]) + + @classmethod + async def delete_dict_data_dao(cls, db: AsyncSession, dict_data: DictDataModel): + """ + 删除字典数据数据库操作 + + :param db: orm对象 + :param dict_data: 字典数据对象 + :return: + """ + await db.execute(delete(SysDictData).where(SysDictData.dict_code.in_([dict_data.dict_code]))) + + @classmethod + async def count_dict_data_dao(cls, db: AsyncSession, dict_type: str): + """ + 根据字典类型查询字典类型关联的字典数据数量 + + :param db: orm对象 + :param dict_type: 字典类型 + :return: 字典类型关联的字典数据数量 + """ + dict_data_count = ( + await db.execute(select(func.count('*')).select_from(SysDictData).where(SysDictData.dict_type == dict_type)) + ).scalar() + + return dict_data_count diff --git a/ruoyi-fastapi-backend/module_admin/dao/door_dao.py b/ruoyi-fastapi-backend/module_admin/dao/door_dao.py new file mode 100644 index 0000000..0e5ae6d --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/door_dao.py @@ -0,0 +1,154 @@ +from sqlalchemy import delete, select, update, or_ +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.door_do import Door +from module_admin.entity.vo.door_vo import DoorModel, DoorPageQueryModel +from utils.page_util import PageUtil + + +class DoorDao: + """ + 门禁设备模块数据库操作层 + """ + + @classmethod + async def get_door_door_status(cls, db: AsyncSession): + """获取门禁状态 + """ + stmt = ( + select(Door.indexCode, Door.name).select_from(Door).where(Door.permission=="1") + ) + result = await db.execute(stmt) + + return result.mappings().all() + + @classmethod + async def get_door_index_code_list(cls, db: AsyncSession, permission: str = None): + """ + 获取门禁设备索引编码列表 + """ + query = select(Door.indexCode).where(Door.permission == permission if permission else True) + result = await db.execute(query) + return [row[0] for row in result.all()] + + @classmethod + async def get_door_detail_by_id(cls, db: AsyncSession, id: int): + """ + 根据主键自增获取门禁设备详细信息 + + :param db: orm对象 + :param id: 主键自增 + :return: 门禁设备信息对象 + """ + door_info = ( + ( + await db.execute( + select(Door) + .where( + Door.id == id + ) + ) + ) + .scalars() + .first() + ) + + return door_info + + @classmethod + async def get_door_detail_by_info(cls, db: AsyncSession, door: DoorModel): + """ + 根据门禁设备参数获取门禁设备信息 + + :param db: orm对象 + :param door: 门禁设备参数对象 + :return: 门禁设备信息对象 + """ + door_info = ( + ( + await db.execute( + select(Door).where( + ) + ) + ) + .scalars() + .first() + ) + + return door_info + + @classmethod + async def get_door_list(cls, db: AsyncSession, query_object: DoorPageQueryModel, is_page: bool = False): + """ + 根据查询参数获取门禁设备列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 门禁设备列表信息对象 + """ + query = ( + select(Door) + .where( + # Door.index_code == query_object.index_code if query_object.index_code else True, + or_( + ( + Door.name.like(f"%{query_object.key_word}%") + if query_object.key_word + else True + ), + ( + Door.install_location.like(f"%{query_object.key_word}%") + if query_object.key_word + else True + ), + ), + # Door.status == query_object.status if query_object.status else True, + ( + Door.permission == query_object.permission + if query_object.permission + else True + ), + ) + .order_by(Door.id) + .distinct() + ) + door_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return door_list + + @classmethod + async def add_door_dao(cls, db: AsyncSession, door: DoorModel): + """ + 新增门禁设备数据库操作 + + :param db: orm对象 + :param door: 门禁设备对象 + :return: + """ + db_door = Door(**door.model_dump(exclude={})) + db.add(db_door) + await db.flush() + + return db_door + + @classmethod + async def edit_door_dao(cls, db: AsyncSession, door: dict): + """ + 编辑门禁设备数据库操作 + + :param db: orm对象 + :param door: 需要更新的门禁设备字典 + :return: + """ + await db.execute(update(Door), [door]) + + @classmethod + async def delete_door_dao(cls, db: AsyncSession, door: DoorModel): + """ + 删除门禁设备数据库操作 + + :param db: orm对象 + :param door: 门禁设备对象 + :return: + """ + await db.execute(delete(Door).where(Door.id.in_([door.id]))) diff --git a/ruoyi-fastapi-backend/module_admin/dao/explanation_content_dao.py b/ruoyi-fastapi-backend/module_admin/dao/explanation_content_dao.py new file mode 100644 index 0000000..085aa66 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/explanation_content_dao.py @@ -0,0 +1,135 @@ +from datetime import datetime, time +from sqlalchemy import delete, select, update, desc +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.explanation_content_do import ExplanationContent +from module_admin.entity.vo.explanation_content_vo import Explanation_contentModel, Explanation_contentPageQueryModel +from utils.page_util import PageUtil + + +class Explanation_contentDao: + """ + 讲解内容模块数据库操作层 + """ + + @classmethod + async def get_explanation_content_detail_by_id(cls, db: AsyncSession, explanation_content_id: int): + """ + 根据主键ID获取讲解内容详细信息 + + :param db: orm对象 + :param explanation_content_id: 主键ID + :return: 讲解内容信息对象 + """ + explanation_content_info = ( + ( + await db.execute( + select(ExplanationContent) + .where( + ExplanationContent.explanation_content_id == explanation_content_id + ) + ) + ) + .scalars() + .first() + ) + + return explanation_content_info + + @classmethod + async def get_explanation_content_detail_by_info(cls, db: AsyncSession, explanation_content: Explanation_contentModel): + """ + 根据讲解内容参数获取讲解内容信息 + + :param db: orm对象 + :param explanation_content: 讲解内容参数对象 + :return: 讲解内容信息对象 + """ + explanation_content_info = ( + ( + await db.execute( + select(ExplanationContent).where( + ) + ) + ) + .scalars() + .first() + ) + + return explanation_content_info + + @classmethod + async def get_explanation_content_list(cls, db: AsyncSession, query_object: Explanation_contentPageQueryModel, is_page: bool = False): + """ + 根据查询参数获取讲解内容列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 讲解内容列表信息对象 + """ + query = ( + select(ExplanationContent) + .where( + # ExplanationContent.title == query_object.title if query_object.title else True, + # ExplanationContent.content == query_object.content if query_object.content else True, + ExplanationContent.type == query_object.type if query_object.type else True, + # ExplanationContent.spend_time == query_object.spend_time if query_object.spend_time else True, + ExplanationContent.language == query_object.language if query_object.language else True, + ExplanationContent.status == query_object.status if query_object.status else True, + ExplanationContent.create_time.between( + datetime.combine(datetime.strptime(query_object.begin_create_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_create_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_create_time and query_object.end_create_time + else True, + ExplanationContent.update_time.between( + datetime.combine(datetime.strptime(query_object.begin_update_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_update_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_update_time and query_object.end_update_time + else True, + ) + .order_by(desc(ExplanationContent.update_time)) + .distinct() + ) + explanation_content_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return explanation_content_list + + @classmethod + async def add_explanation_content_dao(cls, db: AsyncSession, explanation_content: Explanation_contentModel): + """ + 新增讲解内容数据库操作 + + :param db: orm对象 + :param explanation_content: 讲解内容对象 + :return: + """ + db_explanation_content = ExplanationContent(**explanation_content.model_dump(exclude={})) + db.add(db_explanation_content) + await db.flush() + + return db_explanation_content + + @classmethod + async def edit_explanation_content_dao(cls, db: AsyncSession, explanation_content: dict): + """ + 编辑讲解内容数据库操作 + + :param db: orm对象 + :param explanation_content: 需要更新的讲解内容字典 + :return: + """ + await db.execute(update(ExplanationContent), [explanation_content]) + + @classmethod + async def delete_explanation_content_dao(cls, db: AsyncSession, explanation_content: Explanation_contentModel): + """ + 删除讲解内容数据库操作 + + :param db: orm对象 + :param explanation_content: 讲解内容对象 + :return: + """ + await db.execute(delete(ExplanationContent).where(ExplanationContent.explanation_content_id.in_([explanation_content.explanation_content_id]))) + diff --git a/ruoyi-fastapi-backend/module_admin/dao/explanation_content_type_dao.py b/ruoyi-fastapi-backend/module_admin/dao/explanation_content_type_dao.py new file mode 100644 index 0000000..cac3fa0 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/explanation_content_type_dao.py @@ -0,0 +1,120 @@ +from sqlalchemy import delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.explanation_content_type_do import ExplanationContentType +from module_admin.entity.vo.explanation_content_type_vo import Explanation_content_typeModel, Explanation_content_typePageQueryModel +from utils.page_util import PageUtil + + +class Explanation_content_typeDao: + """ + 讲解内容类型模块数据库操作层 + """ + + @classmethod + async def get_explanation_content_type_detail_by_id(cls, db: AsyncSession, content_type_id: int): + """ + 根据主键ID获取讲解内容类型详细信息 + + :param db: orm对象 + :param content_type_id: 主键ID + :return: 讲解内容类型信息对象 + """ + explanation_content_type_info = ( + ( + await db.execute( + select(ExplanationContentType) + .where( + ExplanationContentType.content_type_id == content_type_id + ) + ) + ) + .scalars() + .first() + ) + + return explanation_content_type_info + + @classmethod + async def get_explanation_content_type_detail_by_info(cls, db: AsyncSession, explanation_content_type: Explanation_content_typeModel): + """ + 根据讲解内容类型参数获取讲解内容类型信息 + + :param db: orm对象 + :param explanation_content_type: 讲解内容类型参数对象 + :return: 讲解内容类型信息对象 + """ + explanation_content_type_info = ( + ( + await db.execute( + select(ExplanationContentType).where( + ) + ) + ) + .scalars() + .first() + ) + + return explanation_content_type_info + + @classmethod + async def get_explanation_content_type_list(cls, db: AsyncSession, query_object: Explanation_content_typePageQueryModel, is_page: bool = False): + """ + 根据查询参数获取讲解内容类型列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 讲解内容类型列表信息对象 + """ + query = ( + select(ExplanationContentType) + .where( + ExplanationContentType.name.like(f'%{query_object.name}%') if query_object.name else True, + ExplanationContentType.type_value == query_object.type_value if query_object.type_value else True, + ExplanationContentType.create_time.like(f'%{query_object.create_time}%') if query_object.create_time else True, + ExplanationContentType.update_time.like(f'%{query_object.update_time}%') if query_object.update_time else True, + ) + .order_by(ExplanationContentType.content_type_id) + .distinct() + ) + # explanation_content_type_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + explanation_content_type_list = (await db.execute(query)).scalars().all() + return explanation_content_type_list + + @classmethod + async def add_explanation_content_type_dao(cls, db: AsyncSession, explanation_content_type: Explanation_content_typeModel): + """ + 新增讲解内容类型数据库操作 + + :param db: orm对象 + :param explanation_content_type: 讲解内容类型对象 + :return: + """ + db_explanation_content_type = ExplanationContentType(**explanation_content_type.model_dump(exclude={})) + db.add(db_explanation_content_type) + await db.flush() + + return db_explanation_content_type + + @classmethod + async def edit_explanation_content_type_dao(cls, db: AsyncSession, explanation_content_type: dict): + """ + 编辑讲解内容类型数据库操作 + + :param db: orm对象 + :param explanation_content_type: 需要更新的讲解内容类型字典 + :return: + """ + await db.execute(update(ExplanationContentType), [explanation_content_type]) + + @classmethod + async def delete_explanation_content_type_dao(cls, db: AsyncSession, explanation_content_type: Explanation_content_typeModel): + """ + 删除讲解内容类型数据库操作 + + :param db: orm对象 + :param explanation_content_type: 讲解内容类型对象 + :return: + """ + await db.execute(delete(ExplanationContentType).where(ExplanationContentType.content_type_id.in_([explanation_content_type.content_type_id]))) + diff --git a/ruoyi-fastapi-backend/module_admin/dao/explanation_style_dao.py b/ruoyi-fastapi-backend/module_admin/dao/explanation_style_dao.py new file mode 100644 index 0000000..cb922e6 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/explanation_style_dao.py @@ -0,0 +1,131 @@ +from datetime import datetime, time +from sqlalchemy import delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.explanation_style_do import ExplanationStyle +from module_admin.entity.vo.explanation_style_vo import Explanation_styleModel, Explanation_stylePageQueryModel +from utils.page_util import PageUtil + + +class Explanation_styleDao: + """ + 讲解风格模块数据库操作层 + """ + + @classmethod + async def get_explanation_style_detail_by_id(cls, db: AsyncSession, explanation_style_id: int): + """ + 根据主键ID获取讲解风格详细信息 + + :param db: orm对象 + :param explanation_style_id: 主键ID + :return: 讲解风格信息对象 + """ + explanation_style_info = ( + ( + await db.execute( + select(ExplanationStyle) + .where( + ExplanationStyle.explanation_style_id == explanation_style_id + ) + ) + ) + .scalars() + .first() + ) + + return explanation_style_info + + @classmethod + async def get_explanation_style_detail_by_info(cls, db: AsyncSession, explanation_style: Explanation_styleModel): + """ + 根据讲解风格参数获取讲解风格信息 + + :param db: orm对象 + :param explanation_style: 讲解风格参数对象 + :return: 讲解风格信息对象 + """ + explanation_style_info = ( + ( + await db.execute( + select(ExplanationStyle).where( + ) + ) + ) + .scalars() + .first() + ) + + return explanation_style_info + + @classmethod + async def get_explanation_style_list(cls, db: AsyncSession, query_object: Explanation_stylePageQueryModel, is_page: bool = False): + """ + 根据查询参数获取讲解风格列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 讲解风格列表信息对象 + """ + query = ( + select(ExplanationStyle) + .where( + ExplanationStyle.name.like(f'%{query_object.name}%') if query_object.name else True, + ExplanationStyle.detail == query_object.detail if query_object.detail else True, + ExplanationStyle.create_time.between( + datetime.combine(datetime.strptime(query_object.begin_create_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_create_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_create_time and query_object.end_create_time + else True, + ExplanationStyle.update_time.between( + datetime.combine(datetime.strptime(query_object.begin_update_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_update_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_update_time and query_object.end_update_time + else True, + ) + .order_by(ExplanationStyle.explanation_style_id) + .distinct() + ) + explanation_style_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return explanation_style_list + + @classmethod + async def add_explanation_style_dao(cls, db: AsyncSession, explanation_style: Explanation_styleModel): + """ + 新增讲解风格数据库操作 + + :param db: orm对象 + :param explanation_style: 讲解风格对象 + :return: + """ + db_explanation_style = ExplanationStyle(**explanation_style.model_dump(exclude={})) + db.add(db_explanation_style) + await db.flush() + + return db_explanation_style + + @classmethod + async def edit_explanation_style_dao(cls, db: AsyncSession, explanation_style: dict): + """ + 编辑讲解风格数据库操作 + + :param db: orm对象 + :param explanation_style: 需要更新的讲解风格字典 + :return: + """ + await db.execute(update(ExplanationStyle), [explanation_style]) + + @classmethod + async def delete_explanation_style_dao(cls, db: AsyncSession, explanation_style: Explanation_styleModel): + """ + 删除讲解风格数据库操作 + + :param db: orm对象 + :param explanation_style: 讲解风格对象 + :return: + """ + await db.execute(delete(ExplanationStyle).where(ExplanationStyle.explanation_style_id.in_([explanation_style.explanation_style_id]))) + diff --git a/ruoyi-fastapi-backend/module_admin/dao/explanation_style_robot_pair_dao.py b/ruoyi-fastapi-backend/module_admin/dao/explanation_style_robot_pair_dao.py new file mode 100644 index 0000000..be2c316 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/explanation_style_robot_pair_dao.py @@ -0,0 +1,194 @@ +from datetime import datetime, time +from sqlalchemy import delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession + +from module_admin.entity.do.explanation_style_do import ExplanationStyle +from module_admin.entity.do.explanation_style_robot_pair_do import ExplanationStyleRobotPairing +from module_admin.entity.do.explanation_style_do import ExplanationStyle +from module_admin.entity.vo.explanation_style_robot_pair_vo import Explanation_style_robot_pairModel, \ + Explanation_style_robot_pairPageQueryModel, SwitchExplanationStyleModel +from utils.page_util import PageUtil + + +class Explanation_style_robot_pairDao: + """ + 讲解风格--机器人配对模块数据库操作层 + """ + + @classmethod + async def get_robot_role_style(cls, db: AsyncSession, robot_id: int, role_id: int): + """根据机器人id和角色id获取机器人角色样式 + + Args: + db (AsyncSession): 数据库会话 + robot_id (int): 机器人id + role_id (int): 角色id + + Returns: + _type_: _description_ + """ + + stmt = ( + select(ExplanationStyleRobotPairing.pairing_id, ExplanationStyleRobotPairing.explanation_style_id, ExplanationStyle.name, ExplanationStyle.detail, ExplanationStyleRobotPairing.status, ExplanationStyleRobotPairing.robot_role_id) + .select_from(ExplanationStyleRobotPairing) + .join(ExplanationStyle, ExplanationStyle.explanation_style_id == ExplanationStyleRobotPairing.explanation_style_id) + .where(ExplanationStyleRobotPairing.robot_id == robot_id, ExplanationStyleRobotPairing.robot_role_id == role_id) + ) + + result = (await db.execute(stmt)).mappings().all() + return result + + + + @classmethod + async def switch_explanation_style(cls, db: AsyncSession, robot_id: int = None, pairing_id: int = None, robot_role_id: str = None ,status:str = '0', update_by: str = None): + + condition = list() + + if robot_id: + condition.append(ExplanationStyleRobotPairing.robot_id == robot_id) + if pairing_id: + condition.append(ExplanationStyleRobotPairing.pairing_id == pairing_id) + if robot_role_id: + condition.append(ExplanationStyleRobotPairing.robot_role_id == robot_role_id) + + query = ( + update(ExplanationStyleRobotPairing) + .values(status=status) + .where(*condition) + ) + return await db.execute(query) + + @classmethod + async def get_style_robot_pair_list(cls, db: AsyncSession, robot_id: int): + query = ( + select(ExplanationStyleRobotPairing.pairing_id, ExplanationStyle.name, ExplanationStyle.detail, ExplanationStyleRobotPairing.status) + .select_from(ExplanationStyleRobotPairing) + .join(ExplanationStyle, ExplanationStyle.explanation_style_id == ExplanationStyleRobotPairing.explanation_style_id) + .where(ExplanationStyleRobotPairing.robot_id == robot_id) + ) + + result = (await db.execute(query)).mappings().all() + return result + + + @classmethod + async def get_explanation_style_robot_pair_detail_by_id(cls, db: AsyncSession, pairing_id: int): + """ + 根据主键ID获取讲解风格--机器人配对详细信息 + + :param db: orm对象 + :param pairing_id: 主键ID + :return: 讲解风格--机器人配对信息对象 + """ + explanation_style_robot_pair_info = ( + ( + await db.execute( + select(ExplanationStyleRobotPairing) + .where( + ExplanationStyleRobotPairing.pairing_id == pairing_id + ) + ) + ) + .scalars() + .first() + ) + + return explanation_style_robot_pair_info + + @classmethod + async def get_explanation_style_robot_pair_detail_by_info(cls, db: AsyncSession, explanation_style_robot_pair: Explanation_style_robot_pairModel): + """ + 根据讲解风格--机器人配对参数获取讲解风格--机器人配对信息 + + :param db: orm对象 + :param explanation_style_robot_pair: 讲解风格--机器人配对参数对象 + :return: 讲解风格--机器人配对信息对象 + """ + explanation_style_robot_pair_info = ( + ( + await db.execute( + select(ExplanationStyleRobotPairing).where( + ) + ) + ) + .scalars() + .first() + ) + + return explanation_style_robot_pair_info + + @classmethod + async def get_explanation_style_robot_pair_list(cls, db: AsyncSession, query_object: Explanation_style_robot_pairPageQueryModel, is_page: bool = False): + """ + 根据查询参数获取讲解风格--机器人配对列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 讲解风格--机器人配对列表信息对象 + """ + query = ( + select(ExplanationStyleRobotPairing) + .where( + ExplanationStyleRobotPairing.robot_id == query_object.robot_id if query_object.robot_id else True, + ExplanationStyleRobotPairing.explanation_style_id == query_object.explanation_style_id if query_object.explanation_style_id else True, + ExplanationStyleRobotPairing.prompt == query_object.prompt if query_object.prompt else True, + ExplanationStyleRobotPairing.status == query_object.status if query_object.status else True, + ExplanationStyleRobotPairing.create_time.between( + datetime.combine(datetime.strptime(query_object.begin_create_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_create_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_create_time and query_object.end_create_time + else True, + ExplanationStyleRobotPairing.update_time.between( + datetime.combine(datetime.strptime(query_object.begin_update_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_update_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_update_time and query_object.end_update_time + else True, + ) + .order_by(ExplanationStyleRobotPairing.pairing_id) + .distinct() + ) + explanation_style_robot_pair_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return explanation_style_robot_pair_list + + @classmethod + async def add_explanation_style_robot_pair_dao(cls, db: AsyncSession, explanation_style_robot_pair: Explanation_style_robot_pairModel): + """ + 新增讲解风格--机器人配对数据库操作 + + :param db: orm对象 + :param explanation_style_robot_pair: 讲解风格--机器人配对对象 + :return: + """ + db_explanation_style_robot_pair = ExplanationStyleRobotPairing(**explanation_style_robot_pair.model_dump(exclude={})) + db.add(db_explanation_style_robot_pair) + await db.flush() + + return db_explanation_style_robot_pair + + @classmethod + async def edit_explanation_style_robot_pair_dao(cls, db: AsyncSession, explanation_style_robot_pair: dict): + """ + 编辑讲解风格--机器人配对数据库操作 + + :param db: orm对象 + :param explanation_style_robot_pair: 需要更新的讲解风格--机器人配对字典 + :return: + """ + await db.execute(update(ExplanationStyleRobotPairing), [explanation_style_robot_pair]) + + @classmethod + async def delete_explanation_style_robot_pair_dao(cls, db: AsyncSession, explanation_style_robot_pair: Explanation_style_robot_pairModel): + """ + 删除讲解风格--机器人配对数据库操作 + + :param db: orm对象 + :param explanation_style_robot_pair: 讲解风格--机器人配对对象 + :return: + """ + await db.execute(delete(ExplanationStyleRobotPairing).where(ExplanationStyleRobotPairing.pairing_id.in_([explanation_style_robot_pair.pairing_id]))) + diff --git a/ruoyi-fastapi-backend/module_admin/dao/identification_record_dao.py b/ruoyi-fastapi-backend/module_admin/dao/identification_record_dao.py new file mode 100644 index 0000000..d2ae3b4 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/identification_record_dao.py @@ -0,0 +1,129 @@ +from datetime import datetime, time +from sqlalchemy import delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.identification_record_do import IdentificationRecord +from module_admin.entity.vo.identification_record_vo import Identification_recordModel, Identification_recordPageQueryModel +from utils.page_util import PageUtil + + +class Identification_recordDao: + """ + 识别记录模块数据库操作层 + """ + + @classmethod + async def get_identification_record_detail_by_id(cls, db: AsyncSession, id: int): + """ + 根据主键 自增获取识别记录详细信息 + + :param db: orm对象 + :param id: 主键 自增 + :return: 识别记录信息对象 + """ + identification_record_info = ( + ( + await db.execute( + select(IdentificationRecord) + .where( + IdentificationRecord.id == id + ) + ) + ) + .scalars() + .first() + ) + + return identification_record_info + + @classmethod + async def get_identification_record_detail_by_info(cls, db: AsyncSession, identification_record: Identification_recordModel): + """ + 根据识别记录参数获取识别记录信息 + + :param db: orm对象 + :param identification_record: 识别记录参数对象 + :return: 识别记录信息对象 + """ + identification_record_info = ( + ( + await db.execute( + select(IdentificationRecord).where( + ) + ) + ) + .scalars() + .first() + ) + + return identification_record_info + + @classmethod + async def get_identification_record_list(cls, db: AsyncSession, query_object: Identification_recordPageQueryModel, is_page: bool = False): + """ + 根据查询参数获取识别记录列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 识别记录列表信息对象 + """ + query = ( + select(IdentificationRecord) + .where( + IdentificationRecord.person_name.like(f'%{query_object.person_name}%') if query_object.person_name else True, + IdentificationRecord.door_name.like(f'%{query_object.door_name}%') if query_object.door_name else True, + IdentificationRecord.status == query_object.status if query_object.status else True, + IdentificationRecord.source == query_object.source if query_object.source else True, + IdentificationRecord.create_time.between( + datetime.combine(datetime.strptime(query_object.begin_create_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_create_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_create_time and query_object.end_create_time + else True, + ) + .order_by(IdentificationRecord.create_time.desc()) + .distinct() + ) + identification_record_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return identification_record_list + + @classmethod + async def add_identification_record_dao(cls, db: AsyncSession, identification_record: Identification_recordModel): + """ + 新增识别记录数据库操作 + + :param db: orm对象 + :param identification_record: 识别记录对象 + :return: + """ + # 排除 id 字段(自增主键),但保留其他所有字段 + data = identification_record.model_dump(exclude={'id'}, exclude_none=True) + db_identification_record = IdentificationRecord(**data) + db.add(db_identification_record) + await db.flush() + + return db_identification_record + + @classmethod + async def edit_identification_record_dao(cls, db: AsyncSession, identification_record: dict): + """ + 编辑识别记录数据库操作 + + :param db: orm对象 + :param identification_record: 需要更新的识别记录字典 + :return: + """ + await db.execute(update(IdentificationRecord), [identification_record]) + + @classmethod + async def delete_identification_record_dao(cls, db: AsyncSession, identification_record: Identification_recordModel): + """ + 删除识别记录数据库操作 + + :param db: orm对象 + :param identification_record: 识别记录对象 + :return: + """ + await db.execute(delete(IdentificationRecord).where(IdentificationRecord.id.in_([identification_record.id]))) + diff --git a/ruoyi-fastapi-backend/module_admin/dao/identification_statistics_dao.py b/ruoyi-fastapi-backend/module_admin/dao/identification_statistics_dao.py new file mode 100644 index 0000000..ff38f41 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/identification_statistics_dao.py @@ -0,0 +1,200 @@ +from datetime import datetime, time +from sqlalchemy import delete, select, update, func +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.identification_statistics_do import IdentificationStatistics +from module_admin.entity.vo.identification_statistics_vo import Identification_statisticsModel, Identification_statisticsPageQueryModel +from utils.page_util import PageUtil + + +class Identification_statisticsDao: + """ + 识别统计模块数据库操作层 + """ + + @classmethod + async def get_today_identification_statistics(cls, query_db: AsyncSession): + stmt = ( + select(IdentificationStatistics.employ, IdentificationStatistics.stranger, IdentificationStatistics.visitor, IdentificationStatistics.create_time) + .where(IdentificationStatistics.create_time == datetime.now().date()) + ) + + result = await query_db.execute(stmt) + return result.mappings().all() + + @classmethod + async def get_total_identification_statistics(cls, query_db: AsyncSession): + """ + 获取总的识别统计数据 - 返回各个字段的总和 + + :param query_db: orm对象 + :return: 总和统计数据 + """ + stmt = select( + func.sum(IdentificationStatistics.employ).label('employ'), + func.sum(IdentificationStatistics.visitor).label('visitor'), + func.sum(IdentificationStatistics.stranger).label('stranger') + ) + + result = await query_db.execute(stmt) + return result.mappings().first() + + @classmethod + async def get_identification_statistics_detail_by_id(cls, db: AsyncSession, id: int): + """ + 根据主键 自增获取识别统计详细信息 + + :param db: orm对象 + :param id: 主键 自增 + :return: 识别统计信息对象 + """ + identification_statistics_info = ( + ( + await db.execute( + select(IdentificationStatistics) + .where( + IdentificationStatistics.id == id + ) + ) + ) + .scalars() + .first() + ) + + return identification_statistics_info + + @classmethod + async def get_identification_statistics_detail_by_info(cls, db: AsyncSession, identification_statistics: Identification_statisticsModel): + """ + 根据识别统计参数获取识别统计信息 + + :param db: orm对象 + :param identification_statistics: 识别统计参数对象 + :return: 识别统计信息对象 + """ + identification_statistics_info = ( + ( + await db.execute( + select(IdentificationStatistics).where( + ) + ) + ) + .scalars() + .first() + ) + + return identification_statistics_info + + @classmethod + async def get_identification_statistics_list(cls, db: AsyncSession, query_object: Identification_statisticsPageQueryModel, is_page: bool = False): + """ + 根据查询参数获取识别统计列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 识别统计列表信息对象 + """ + query = ( + select(IdentificationStatistics) + .where( + IdentificationStatistics.create_time.between( + datetime.combine(datetime.strptime(query_object.begin_create_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_create_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_create_time and query_object.end_create_time + else True, + ) + .order_by(IdentificationStatistics.id) + .distinct() + ) + identification_statistics_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return identification_statistics_list + + @classmethod + async def add_identification_statistics_dao(cls, db: AsyncSession, identification_statistics: Identification_statisticsModel): + """ + 新增识别统计数据库操作 + + :param db: orm对象 + :param identification_statistics: 识别统计对象 + :return: + """ + db_identification_statistics = IdentificationStatistics(**identification_statistics.model_dump(exclude={})) + db.add(db_identification_statistics) + await db.flush() + + return db_identification_statistics + + @classmethod + async def edit_identification_statistics_dao(cls, db: AsyncSession, identification_statistics: dict): + """ + 编辑识别统计数据库操作 + + :param db: orm对象 + :param identification_statistics: 需要更新的识别统计字典 + :return: + """ + await db.execute(update(IdentificationStatistics), [identification_statistics]) + + @classmethod + async def delete_identification_statistics_dao(cls, db: AsyncSession, identification_statistics: Identification_statisticsModel): + """ + 删除识别统计数据库操作 + + :param db: orm对象 + :param identification_statistics: 识别统计对象 + :return: + """ + await db.execute(delete(IdentificationStatistics).where(IdentificationStatistics.id.in_([identification_statistics.id]))) + + @classmethod + async def update_identification_statistics_dao(cls, db: AsyncSession, person_status: str): + """ + 更新今日识别统计数据库操作 - 根据person_status增加相应字段的计数 + + :param db: orm对象 + :param person_status: 人员状态 (0 陌生人, 1 员工, 2 访客) + :return: + """ + today = datetime.now().date() + + # 查询今日是否已有统计记录 + stmt = select(IdentificationStatistics).where( + func.date(IdentificationStatistics.create_time) == today + ) + result = await db.execute(stmt) + today_statistics = result.scalars().first() + + # 如果没有今日记录,创建新记录 + if not today_statistics: + new_statistics = IdentificationStatistics( + employ=1 if person_status == "1" else 0, + visitor=1 if person_status == "2" else 0, + stranger=1 if person_status == "0" else 0, + create_time=datetime.now(), + create_by="system" + ) + db.add(new_statistics) + await db.flush() + else: + # 根据person_status更新相应字段 + if person_status == "0": # 陌生人 + await db.execute( + update(IdentificationStatistics) + .where(IdentificationStatistics.id == today_statistics.id) + .values(stranger=IdentificationStatistics.stranger + 1) + ) + elif person_status == "1": # 员工 + await db.execute( + update(IdentificationStatistics) + .where(IdentificationStatistics.id == today_statistics.id) + .values(employ=IdentificationStatistics.employ + 1) + ) + elif person_status == "2": # 访客 + await db.execute( + update(IdentificationStatistics) + .where(IdentificationStatistics.id == today_statistics.id) + .values(visitor=IdentificationStatistics.visitor + 1) + ) + diff --git a/ruoyi-fastapi-backend/module_admin/dao/info_dao.py b/ruoyi-fastapi-backend/module_admin/dao/info_dao.py new file mode 100644 index 0000000..0724b95 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/info_dao.py @@ -0,0 +1,173 @@ +from sqlalchemy import delete, select, update, desc +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.info_do import RobotInfo +from module_admin.entity.vo.info_vo import InfoModel, InfoPageQueryModel +from utils.page_util import PageUtil +from datetime import datetime, time + + + +class InfoDao: + """ + 机器人信息模块数据库操作层 + """ + + + @classmethod + async def get_video_uri(cls, robot_id: int, query_db: AsyncSession): + """根据机器人id获取视频流地址 + + Args: + robot_id (int): 机器人地址 + query_db (AsyncSession): _description_ + """ + stmt = ( + select(RobotInfo.video_uri) + .select_from(RobotInfo) + .where(RobotInfo.robot_id == robot_id) + ) + + result = await query_db.execute(stmt) + + return result.scalars().first() + + @classmethod + async def refresh_robot_by_ids(cls, db: AsyncSession, robot_ids: str): + id_list = map(int, robot_ids.strip(" ").split(',')) + refresh_result = ( + ( + await db.execute( + select(RobotInfo) + .where(RobotInfo.robot_id.in_(id_list)) + + ) + + ) + .scalars() + .all() + ) + return refresh_result + + @classmethod + async def get_info_detail_by_id(cls, db: AsyncSession, robot_id: int): + """ + 根据机器人ID获取机器人信息详细信息 + + :param db: orm对象 + :param robot_id: 机器人ID + :return: 机器人信息信息对象 + """ + info_info = ( + ( + await db.execute( + select(RobotInfo) + .where( + RobotInfo.robot_id == robot_id + ) + ) + ) + .scalars() + .first() + ) + + return info_info + + @classmethod + async def get_info_detail_by_info(cls, db: AsyncSession, info: InfoModel): + """ + 根据机器人信息参数获取机器人信息信息 + + :param db: orm对象 + :param info: 机器人信息参数对象 + :return: 机器人信息信息对象 + """ + info_info = ( + ( + await db.execute( + select(RobotInfo).where( + ) + ) + ) + .scalars() + .first() + ) + + return info_info + + @classmethod + async def get_info_list(cls, db: AsyncSession, query_object: InfoPageQueryModel, is_page: bool = False): + """ + 根据查询参数获取机器人信息列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 机器人信息列表信息对象 + """ + query = ( + select(RobotInfo) + .where( + RobotInfo.name.like(f'%{query_object.name}%') if query_object.name else True, + RobotInfo.model == query_object.model if query_object.model else True, + RobotInfo.serial == query_object.serial if query_object.serial else True, + RobotInfo.online == query_object.online if query_object.online else True, + RobotInfo.power == query_object.power if query_object.power else True, + RobotInfo.run_time == query_object.run_time if query_object.run_time else True, + RobotInfo.position == query_object.position if query_object.position else True, + RobotInfo.image == query_object.image if query_object.image else True, + RobotInfo.create_time.between( + datetime.combine(datetime.strptime(query_object.begin_create_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_create_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_create_time and query_object.end_create_time else True, + RobotInfo.update_time.between( + datetime.combine(datetime.strptime(query_object.begin_update_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_update_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_update_time and query_object.end_update_time else True + + ) + .order_by(desc(RobotInfo.create_time)) + .distinct() + ) + info_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return info_list + + @classmethod + async def add_info_dao(cls, db: AsyncSession, info: InfoModel): + """ + 新增机器人信息数据库操作 + + :param db: orm对象 + :param info: 机器人信息对象 + :return: + """ + db_info = RobotInfo(**info.model_dump(exclude={})) + db.add(db_info) + await db.flush() + + return db_info + + @classmethod + async def edit_info_dao(cls, db: AsyncSession, info: dict): + """ + 编辑机器人信息数据库操作 + + :param db: orm对象 + :param info: 需要更新的机器人信息字典 + :return: + """ + await db.execute(update(RobotInfo), [info]) + + @classmethod + async def delete_info_dao(cls, db: AsyncSession, info: InfoModel): + """ + 删除机器人信息数据库操作 + + :param db: orm对象 + :param info: 机器人信息对象 + :return: + """ + await db.execute(delete(RobotInfo).where(RobotInfo.robot_id.in_([info.robot_id]))) + diff --git a/ruoyi-fastapi-backend/module_admin/dao/job_dao.py b/ruoyi-fastapi-backend/module_admin/dao/job_dao.py new file mode 100644 index 0000000..805d460 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/job_dao.py @@ -0,0 +1,126 @@ +from sqlalchemy import delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.job_do import SysJob +from module_admin.entity.vo.job_vo import JobModel, JobPageQueryModel +from utils.page_util import PageUtil + + +class JobDao: + """ + 定时任务管理模块数据库操作层 + """ + + @classmethod + async def get_job_detail_by_id(cls, db: AsyncSession, job_id: int): + """ + 根据定时任务id获取定时任务详细信息 + + :param db: orm对象 + :param job_id: 定时任务id + :return: 定时任务信息对象 + """ + job_info = (await db.execute(select(SysJob).where(SysJob.job_id == job_id))).scalars().first() + + return job_info + + @classmethod + async def get_job_detail_by_info(cls, db: AsyncSession, job: JobModel): + """ + 根据定时任务参数获取定时任务信息 + + :param db: orm对象 + :param job: 定时任务参数对象 + :return: 定时任务信息对象 + """ + job_info = ( + ( + await db.execute( + select(SysJob).where( + SysJob.job_name == job.job_name, + SysJob.job_group == job.job_group, + SysJob.job_executor == job.job_executor, + SysJob.invoke_target == job.invoke_target, + SysJob.job_args == job.job_args, + SysJob.job_kwargs == job.job_kwargs, + SysJob.cron_expression == job.cron_expression, + ) + ) + ) + .scalars() + .first() + ) + + return job_info + + @classmethod + async def get_job_list(cls, db: AsyncSession, query_object: JobPageQueryModel, is_page: bool = False): + """ + 根据查询参数获取定时任务列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 定时任务列表信息对象 + """ + query = ( + select(SysJob) + .where( + SysJob.job_name.like(f'%{query_object.job_name}%') if query_object.job_name else True, + SysJob.job_group == query_object.job_group if query_object.job_group else True, + SysJob.status == query_object.status if query_object.status else True, + ) + .order_by(SysJob.job_id) + .distinct() + ) + job_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return job_list + + @classmethod + async def get_job_list_for_scheduler(cls, db: AsyncSession): + """ + 获取定时任务列表信息 + + :param db: orm对象 + :return: 定时任务列表信息对象 + """ + job_list = (await db.execute(select(SysJob).where(SysJob.status == '0').distinct())).scalars().all() + + return job_list + + @classmethod + async def add_job_dao(cls, db: AsyncSession, job: JobModel): + """ + 新增定时任务数据库操作 + + :param db: orm对象 + :param job: 定时任务对象 + :return: + """ + db_job = SysJob(**job.model_dump()) + db.add(db_job) + await db.flush() + + return db_job + + @classmethod + async def edit_job_dao(cls, db: AsyncSession, job: dict): + """ + 编辑定时任务数据库操作 + + :param db: orm对象 + :param job: 需要更新的定时任务字典 + :return: + """ + await db.execute(update(SysJob), [job]) + + @classmethod + async def delete_job_dao(cls, db: AsyncSession, job: JobModel): + """ + 删除定时任务数据库操作 + + :param db: orm对象 + :param job: 定时任务对象 + :return: + """ + await db.execute(delete(SysJob).where(SysJob.job_id.in_([job.job_id]))) diff --git a/ruoyi-fastapi-backend/module_admin/dao/job_log_dao.py b/ruoyi-fastapi-backend/module_admin/dao/job_log_dao.py new file mode 100644 index 0000000..586a3d6 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/job_log_dao.py @@ -0,0 +1,79 @@ +from datetime import datetime, time +from sqlalchemy import delete, desc, select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import Session +from module_admin.entity.do.job_do import SysJobLog +from module_admin.entity.vo.job_vo import JobLogModel, JobLogPageQueryModel +from utils.page_util import PageUtil + + +class JobLogDao: + """ + 定时任务日志管理模块数据库操作层 + """ + + @classmethod + async def get_job_log_list(cls, db: AsyncSession, query_object: JobLogPageQueryModel, is_page: bool = False): + """ + 根据查询参数获取定时任务日志列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 定时任务日志列表信息对象 + """ + query = ( + select(SysJobLog) + .where( + SysJobLog.job_name.like(f'%{query_object.job_name}%') if query_object.job_name else True, + SysJobLog.job_group == query_object.job_group if query_object.job_group else True, + SysJobLog.status == query_object.status if query_object.status else True, + SysJobLog.create_time.between( + datetime.combine(datetime.strptime(query_object.begin_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_time and query_object.end_time + else True, + ) + .order_by(desc(SysJobLog.create_time)) + .distinct() + ) + job_log_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return job_log_list + + @classmethod + def add_job_log_dao(cls, db: Session, job_log: JobLogModel): + """ + 新增定时任务日志数据库操作 + + :param db: orm对象 + :param job_log: 定时任务日志对象 + :return: + """ + db_job_log = SysJobLog(**job_log.model_dump()) + db.add(db_job_log) + db.flush() + + return db_job_log + + @classmethod + async def delete_job_log_dao(cls, db: AsyncSession, job_log: JobLogModel): + """ + 删除定时任务日志数据库操作 + + :param db: orm对象 + :param job_log: 定时任务日志对象 + :return: + """ + await db.execute(delete(SysJobLog).where(SysJobLog.job_log_id.in_([job_log.job_log_id]))) + + @classmethod + async def clear_job_log_dao(cls, db: AsyncSession): + """ + 清除定时任务日志数据库操作 + + :param db: orm对象 + :return: + """ + await db.execute(delete(SysJobLog)) diff --git a/ruoyi-fastapi-backend/module_admin/dao/log_dao.py b/ruoyi-fastapi-backend/module_admin/dao/log_dao.py new file mode 100644 index 0000000..684f3d7 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/log_dao.py @@ -0,0 +1,171 @@ +from datetime import datetime, time +from sqlalchemy import asc, delete, desc, select +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.log_do import SysLogininfor, SysOperLog +from module_admin.entity.vo.log_vo import LogininforModel, LoginLogPageQueryModel, OperLogModel, OperLogPageQueryModel +from utils.common_util import SnakeCaseUtil +from utils.page_util import PageUtil +from utils.time_format_util import TimeFormatUtil + + +class OperationLogDao: + """ + 操作日志管理模块数据库操作层 + """ + + @classmethod + async def get_operation_log_list(cls, db: AsyncSession, query_object: OperLogPageQueryModel, is_page: bool = False): + """ + 根据查询参数获取操作日志列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 操作日志列表信息对象 + """ + if query_object.is_asc == 'ascending': + order_by_column = asc(getattr(SysOperLog, SnakeCaseUtil.camel_to_snake(query_object.order_by_column), None)) + elif query_object.is_asc == 'descending': + order_by_column = desc( + getattr(SysOperLog, SnakeCaseUtil.camel_to_snake(query_object.order_by_column), None) + ) + else: + order_by_column = desc(SysOperLog.oper_time) + query = ( + select(SysOperLog) + .where( + SysOperLog.title.like(f'%{query_object.title}%') if query_object.title else True, + SysOperLog.oper_name.like(f'%{query_object.oper_name}%') if query_object.oper_name else True, + SysOperLog.business_type == query_object.business_type if query_object.business_type else True, + SysOperLog.status == query_object.status if query_object.status else True, + SysOperLog.oper_time.between( + datetime.combine(TimeFormatUtil.parse_date(query_object.begin_time), time(00, 00, 00)), + datetime.combine(TimeFormatUtil.parse_date(query_object.end_time), time(23, 59, 59)), + ) + if query_object.begin_time and query_object.end_time + else True, + ) + .distinct() + .order_by(order_by_column) + ) + operation_log_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return operation_log_list + + @classmethod + async def add_operation_log_dao(cls, db: AsyncSession, operation_log: OperLogModel): + """ + 新增操作日志数据库操作 + + :param db: orm对象 + :param operation_log: 操作日志对象 + :return: 新增校验结果 + """ + db_operation_log = SysOperLog(**operation_log.model_dump()) + db.add(db_operation_log) + await db.flush() + + return db_operation_log + + @classmethod + async def delete_operation_log_dao(cls, db: AsyncSession, operation_log: OperLogModel): + """ + 删除操作日志数据库操作 + + :param db: orm对象 + :param operation_log: 操作日志对象 + :return: + """ + await db.execute(delete(SysOperLog).where(SysOperLog.oper_id.in_([operation_log.oper_id]))) + + @classmethod + async def clear_operation_log_dao(cls, db: AsyncSession): + """ + 清除操作日志数据库操作 + + :param db: orm对象 + :return: + """ + await db.execute(delete(SysOperLog)) + + +class LoginLogDao: + """ + 登录日志管理模块数据库操作层 + """ + + @classmethod + async def get_login_log_list(cls, db: AsyncSession, query_object: LoginLogPageQueryModel, is_page: bool = False): + """ + 根据查询参数获取登录日志列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 登录日志列表信息对象 + """ + if query_object.is_asc == 'ascending': + order_by_column = asc( + getattr(SysLogininfor, SnakeCaseUtil.camel_to_snake(query_object.order_by_column), None) + ) + elif query_object.is_asc == 'descending': + order_by_column = desc( + getattr(SysLogininfor, SnakeCaseUtil.camel_to_snake(query_object.order_by_column), None) + ) + else: + order_by_column = desc(SysLogininfor.login_time) + query = ( + select(SysLogininfor) + .where( + SysLogininfor.ipaddr.like(f'%{query_object.ipaddr}%') if query_object.ipaddr else True, + SysLogininfor.user_name.like(f'%{query_object.user_name}%') if query_object.user_name else True, + SysLogininfor.status == query_object.status if query_object.status else True, + SysLogininfor.login_time.between( + datetime.combine(TimeFormatUtil.parse_date(query_object.begin_time), time(00, 00, 00)), + datetime.combine(TimeFormatUtil.parse_date(query_object.end_time), time(23, 59, 59)), + ) + if query_object.begin_time and query_object.end_time + else True, + ) + .distinct() + .order_by(order_by_column) + ) + login_log_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return login_log_list + + @classmethod + async def add_login_log_dao(cls, db: AsyncSession, login_log: LogininforModel): + """ + 新增登录日志数据库操作 + + :param db: orm对象 + :param login_log: 登录日志对象 + :return: 新增校验结果 + """ + db_login_log = SysLogininfor(**login_log.model_dump()) + db.add(db_login_log) + await db.flush() + + return db_login_log + + @classmethod + async def delete_login_log_dao(cls, db: AsyncSession, login_log: LogininforModel): + """ + 删除登录日志数据库操作 + + :param db: orm对象 + :param login_log: 登录日志对象 + :return: + """ + await db.execute(delete(SysLogininfor).where(SysLogininfor.info_id.in_([login_log.info_id]))) + + @classmethod + async def clear_login_log_dao(cls, db: AsyncSession): + """ + 清除登录日志数据库操作 + + :param db: orm对象 + :return: + """ + await db.execute(delete(SysLogininfor)) diff --git a/ruoyi-fastapi-backend/module_admin/dao/login_dao.py b/ruoyi-fastapi-backend/module_admin/dao/login_dao.py new file mode 100644 index 0000000..9764a4a --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/login_dao.py @@ -0,0 +1,28 @@ +from sqlalchemy import and_, select +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.dept_do import SysDept +from module_admin.entity.do.user_do import SysUser + + +async def login_by_account(db: AsyncSession, user_name: str): + """ + 根据用户名查询用户信息 + + :param db: orm对象 + :param user_name: 用户名 + :return: 用户对象 + """ + user = ( + await db.execute( + select(SysUser, SysDept) + .where(SysUser.user_name == user_name, SysUser.del_flag == '0') + .join( + SysDept, + and_(SysUser.dept_id == SysDept.dept_id, SysDept.status == '0', SysDept.del_flag == '0'), + isouter=True, + ) + .distinct() + ) + ).first() + + return user diff --git a/ruoyi-fastapi-backend/module_admin/dao/menu_dao.py b/ruoyi-fastapi-backend/module_admin/dao/menu_dao.py new file mode 100644 index 0000000..976fcdf --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/menu_dao.py @@ -0,0 +1,227 @@ +from sqlalchemy import and_, delete, func, select, update +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.menu_do import SysMenu +from module_admin.entity.do.role_do import SysRole, SysRoleMenu +from module_admin.entity.do.user_do import SysUser, SysUserRole +from module_admin.entity.vo.menu_vo import MenuModel, MenuQueryModel + + +class MenuDao: + """ + 菜单管理模块数据库操作层 + """ + + @classmethod + async def get_menu_detail_by_id(cls, db: AsyncSession, menu_id: int): + """ + 根据菜单id获取菜单详细信息 + + :param db: orm对象 + :param menu_id: 菜单id + :return: 菜单信息对象 + """ + menu_info = (await db.execute(select(SysMenu).where(SysMenu.menu_id == menu_id))).scalars().first() + + return menu_info + + @classmethod + async def get_menu_detail_by_info(cls, db: AsyncSession, menu: MenuModel): + """ + 根据菜单参数获取菜单信息 + + :param db: orm对象 + :param menu: 菜单参数对象 + :return: 菜单信息对象 + """ + menu_info = ( + ( + await db.execute( + select(SysMenu).where( + SysMenu.parent_id == menu.parent_id if menu.parent_id else True, + SysMenu.menu_name == menu.menu_name if menu.menu_name else True, + SysMenu.menu_type == menu.menu_type if menu.menu_type else True, + ) + ) + ) + .scalars() + .first() + ) + + return menu_info + + @classmethod + async def get_menu_list_for_tree(cls, db: AsyncSession, user_id: int, role: list): + """ + 根据角色信息获取所有在用菜单列表信息 + + :param db: orm对象 + :param user_id: 用户id + :param role: 用户角色列表信息 + :return: 菜单列表信息 + """ + role_id_list = [item.role_id for item in role] + if 1 in role_id_list: + menu_query_all = ( + (await db.execute(select(SysMenu).where(SysMenu.status == '0' + # , SysMenu.visible == '0' + ).order_by(SysMenu.order_num).distinct())) + .scalars() + .all() + ) + else: + menu_query_all = ( + ( + await db.execute( + select(SysMenu) + .select_from(SysUser) + .where(SysUser.status == '0', SysUser.del_flag == '0', SysUser.user_id == user_id) + .join(SysUserRole, SysUser.user_id == SysUserRole.user_id, isouter=True) + .join( + SysRole, + and_( + SysUserRole.role_id == SysRole.role_id, SysRole.status == '0', SysRole.del_flag == '0' + ), + isouter=True, + ) + .join(SysRoleMenu, SysRole.role_id == SysRoleMenu.role_id, isouter=True) + .join(SysMenu, and_(SysRoleMenu.menu_id == SysMenu.menu_id, SysMenu.status == '0' + # , SysMenu.visible == '0' + )) + .order_by(SysMenu.order_num) + .distinct() + ) + ) + .scalars() + .all() + ) + + return menu_query_all + + @classmethod + async def get_menu_list(cls, db: AsyncSession, page_object: MenuQueryModel, user_id: int, role: list): + """ + 根据查询参数获取菜单列表信息 + + :param db: orm对象 + :param page_object: 不分页查询参数对象 + :param user_id: 用户id + :param role: 用户角色列表 + :return: 菜单列表信息对象 + """ + role_id_list = [item.role_id for item in role] + if 1 in role_id_list: + menu_query_all = ( + ( + await db.execute( + select(SysMenu) + .where( + SysMenu.status == page_object.status if page_object.status else True, + SysMenu.menu_name.like(f'%{page_object.menu_name}%') if page_object.menu_name else True, + ) + .order_by(SysMenu.order_num) + .distinct() + ) + ) + .scalars() + .all() + ) + else: + menu_query_all = ( + ( + await db.execute( + select(SysMenu) + .select_from(SysUser) + .where(SysUser.status == '0', SysUser.del_flag == '0', SysUser.user_id == user_id) + .join(SysUserRole, SysUser.user_id == SysUserRole.user_id, isouter=True) + .join( + SysRole, + and_( + SysUserRole.role_id == SysRole.role_id, SysRole.status == '0', SysRole.del_flag == '0' + ), + isouter=True, + ) + .join(SysRoleMenu, SysRole.role_id == SysRoleMenu.role_id, isouter=True) + .join( + SysMenu, + and_( + SysRoleMenu.menu_id == SysMenu.menu_id, + SysMenu.status == page_object.status if page_object.status else True, + SysMenu.menu_name.like(f'%{page_object.menu_name}%') if page_object.menu_name else True, + ), + ) + .order_by(SysMenu.order_num) + .distinct() + ) + ) + .scalars() + .all() + ) + + return menu_query_all + + @classmethod + async def add_menu_dao(cls, db: AsyncSession, menu: MenuModel): + """ + 新增菜单数据库操作 + + :param db: orm对象 + :param menu: 菜单对象 + :return: + """ + db_menu = SysMenu(**menu.model_dump()) + db.add(db_menu) + await db.flush() + + return db_menu + + @classmethod + async def edit_menu_dao(cls, db: AsyncSession, menu: dict): + """ + 编辑菜单数据库操作 + + :param db: orm对象 + :param menu: 需要更新的菜单字典 + :return: + """ + await db.execute(update(SysMenu), [menu]) + + @classmethod + async def delete_menu_dao(cls, db: AsyncSession, menu: MenuModel): + """ + 删除菜单数据库操作 + + :param db: orm对象 + :param menu: 菜单对象 + :return: + """ + await db.execute(delete(SysMenu).where(SysMenu.menu_id.in_([menu.menu_id]))) + + @classmethod + async def has_child_by_menu_id_dao(cls, db: AsyncSession, menu_id: int): + """ + 根据菜单id查询菜单关联子菜单的数量 + + :param db: orm对象 + :param menu_id: 菜单id + :return: 菜单关联子菜单的数量 + """ + menu_count = ( + await db.execute(select(func.count('*')).select_from(SysMenu).where(SysMenu.parent_id == menu_id)) + ).scalar() + + return menu_count + + @classmethod + async def check_menu_exist_role_dao(cls, db: AsyncSession, menu_id: int): + """ + 根据菜单id查询菜单关联角色数量 + + :param db: orm对象 + :param menu_id: 菜单id + :return: 菜单关联角色数量 + """ + role_count = ( + await db.execute(select(func.count('*')).select_from(SysRoleMenu).where(SysRoleMenu.menu_id == menu_id)) + ).scalar() + + return role_count diff --git a/ruoyi-fastapi-backend/module_admin/dao/message_dao.py b/ruoyi-fastapi-backend/module_admin/dao/message_dao.py new file mode 100644 index 0000000..d2b90c0 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/message_dao.py @@ -0,0 +1,175 @@ +from sqlalchemy import delete, select, update, func +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.message_do import Message +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.entity.vo.message_vo import MessageModel, MessagePageQueryModel +from utils.page_util import PageUtil +from datetime import datetime, time + + +class MessageDao: + """ + 系统消息模块数据库操作层 + """ + + @classmethod + async def get_alarm_type_distribution(cls, db: AsyncSession): + stmt = ( + select(Message.specific_type, func.count(Message.specific_type).label("cnt")) + .where(Message.type == '0') + .group_by(Message.specific_type) + ) + return (await db.execute(stmt)).mappings().all() + + @classmethod + async def handle_all_message(cls, db: AsyncSession): + query = ( + update(Message) + .where(Message.status == '0', Message.type == '0') + .values(status='1') + ) + + + await db.execute(query) + + + + @classmethod + async def get_message_detail_by_id(cls, db: AsyncSession, message_id: int): + """ + 根据消息主键获取系统消息详细信息 + + :param db: orm对象 + :param message_id: 消息主键 + :return: 系统消息信息对象 + """ + message_info = ( + ( + await db.execute( + select(Message) + .where( + Message.message_id == message_id + ) + ) + ) + .scalars() + .first() + ) + + return message_info + + @classmethod + async def count_message(cls, db: AsyncSession): + query_unhandled_message_count = ( + select(func.count(Message.message_id)) + .where(Message.status=='0', Message.type=='0') + ) + + unhandled_message_count = ( + await db.execute(query_unhandled_message_count) + ).scalar() + + query_handled_message_count = ( + select(func.count(Message.message_id)) + .where(Message.status=='1', Message.type=='0') + ) + + handled_message_count = ( + await db.execute(query_handled_message_count) + ).scalar() + + return unhandled_message_count+ handled_message_count, unhandled_message_count, handled_message_count + + @classmethod + async def get_message_detail_by_info(cls, db: AsyncSession, message: MessageModel): + """ + 根据系统消息参数获取系统消息信息 + + :param db: orm对象 + :param message: 系统消息参数对象 + :return: 系统消息信息对象 + """ + message_info = ( + ( + await db.execute( + select(Message).where( + ) + ) + ) + .scalars() + .first() + ) + + return message_info + + @classmethod + async def get_message_list(cls, db: AsyncSession, query_object: MessagePageQueryModel, is_page: bool = False): + """ + 根据查询参数获取系统消息列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 系统消息列表信息对象 + """ + query = ( + select(Message) + .where( + Message.type == query_object.type if query_object.type else True, + Message.status == query_object.status if query_object.status else True, + # Message.detail == query_object.detail if query_object.detail else True, + Message.create_time.between( + datetime.combine(datetime.strptime(query_object.begin_create_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_create_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_create_time and query_object.end_create_time else True, + Message.update_time.between( + datetime.combine(datetime.strptime(query_object.begin_update_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_update_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_update_time and query_object.end_update_time else True + ) + .order_by(Message.message_id) + .distinct() + ) + message_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return message_list + + @classmethod + async def add_message_dao(cls, db: AsyncSession, message: MessageModel): + """ + 新增系统消息数据库操作 + + :param db: orm对象 + :param message: 系统消息对象 + :return: + """ + db_message = Message(**message.model_dump(exclude={})) + db.add(db_message) + await db.flush() + + return db_message + + @classmethod + async def edit_message_dao(cls, db: AsyncSession, message: dict): + """ + 编辑系统消息数据库操作 + + :param db: orm对象 + :param message: 需要更新的系统消息字典 + :return: + """ + await db.execute(update(Message), [message]) + + @classmethod + async def delete_message_dao(cls, db: AsyncSession, message: MessageModel): + """ + 删除系统消息数据库操作 + + :param db: orm对象 + :param message: 系统消息对象 + :return: + """ + await db.execute(delete(Message).where(Message.message_id.in_([message.message_id]))) + diff --git a/ruoyi-fastapi-backend/module_admin/dao/notice_dao.py b/ruoyi-fastapi-backend/module_admin/dao/notice_dao.py new file mode 100644 index 0000000..9f48a14 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/notice_dao.py @@ -0,0 +1,117 @@ +from datetime import datetime, time +from sqlalchemy import delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.notice_do import SysNotice +from module_admin.entity.vo.notice_vo import NoticeModel, NoticePageQueryModel +from utils.page_util import PageUtil + + +class NoticeDao: + """ + 通知公告管理模块数据库操作层 + """ + + @classmethod + async def get_notice_detail_by_id(cls, db: AsyncSession, notice_id: int): + """ + 根据通知公告id获取通知公告详细信息 + + :param db: orm对象 + :param notice_id: 通知公告id + :return: 通知公告信息对象 + """ + notice_info = (await db.execute(select(SysNotice).where(SysNotice.notice_id == notice_id))).scalars().first() + + return notice_info + + @classmethod + async def get_notice_detail_by_info(cls, db: AsyncSession, notice: NoticeModel): + """ + 根据通知公告参数获取通知公告信息 + + :param db: orm对象 + :param notice: 通知公告参数对象 + :return: 通知公告信息对象 + """ + notice_info = ( + ( + await db.execute( + select(SysNotice).where( + SysNotice.notice_title == notice.notice_title, + SysNotice.notice_type == notice.notice_type, + SysNotice.notice_content == notice.notice_content, + ) + ) + ) + .scalars() + .first() + ) + + return notice_info + + @classmethod + async def get_notice_list(cls, db: AsyncSession, query_object: NoticePageQueryModel, is_page: bool = False): + """ + 根据查询参数获取通知公告列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 通知公告列表信息对象 + """ + query = ( + select(SysNotice) + .where( + SysNotice.notice_title.like(f'%{query_object.notice_title}%') if query_object.notice_title else True, + SysNotice.create_by.like(f'%{query_object.create_by}%') if query_object.create_by else True, + SysNotice.notice_type == query_object.notice_type if query_object.notice_type else True, + SysNotice.create_time.between( + datetime.combine(datetime.strptime(query_object.begin_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_time and query_object.end_time + else True, + ) + .order_by(SysNotice.notice_id) + .distinct() + ) + notice_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return notice_list + + @classmethod + async def add_notice_dao(cls, db: AsyncSession, notice: NoticeModel): + """ + 新增通知公告数据库操作 + + :param db: orm对象 + :param notice: 通知公告对象 + :return: + """ + db_notice = SysNotice(**notice.model_dump()) + db.add(db_notice) + await db.flush() + + return db_notice + + @classmethod + async def edit_notice_dao(cls, db: AsyncSession, notice: dict): + """ + 编辑通知公告数据库操作 + + :param db: orm对象 + :param notice: 需要更新的通知公告字典 + :return: + """ + await db.execute(update(SysNotice), [notice]) + + @classmethod + async def delete_notice_dao(cls, db: AsyncSession, notice: NoticeModel): + """ + 删除通知公告数据库操作 + + :param db: orm对象 + :param notice: 通知公告对象 + :return: + """ + await db.execute(delete(SysNotice).where(SysNotice.notice_id.in_([notice.notice_id]))) diff --git a/ruoyi-fastapi-backend/module_admin/dao/post_dao.py b/ruoyi-fastapi-backend/module_admin/dao/post_dao.py new file mode 100644 index 0000000..7d90088 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/post_dao.py @@ -0,0 +1,143 @@ +from sqlalchemy import delete, func, select, update +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.post_do import SysPost +from module_admin.entity.do.user_do import SysUserPost +from module_admin.entity.vo.post_vo import PostModel, PostPageQueryModel +from utils.page_util import PageUtil + + +class PostDao: + """ + 岗位管理模块数据库操作层 + """ + + @classmethod + async def get_post_by_id(cls, db: AsyncSession, post_id: int): + """ + 根据岗位id获取在用岗位详细信息 + + :param db: orm对象 + :param post_id: 岗位id + :return: 在用岗位信息对象 + """ + post_info = ( + (await db.execute(select(SysPost).where(SysPost.post_id == post_id, SysPost.status == '0'))) + .scalars() + .first() + ) + + return post_info + + @classmethod + async def get_post_detail_by_id(cls, db: AsyncSession, post_id: int): + """ + 根据岗位id获取岗位详细信息 + + :param db: orm对象 + :param post_id: 岗位id + :return: 岗位信息对象 + """ + post_info = (await db.execute(select(SysPost).where(SysPost.post_id == post_id))).scalars().first() + + return post_info + + @classmethod + async def get_post_detail_by_info(cls, db: AsyncSession, post: PostModel): + """ + 根据岗位参数获取岗位信息 + + :param db: orm对象 + :param post: 岗位参数对象 + :return: 岗位信息对象 + """ + post_info = ( + ( + await db.execute( + select(SysPost).where( + SysPost.post_name == post.post_name if post.post_name else True, + SysPost.post_code == post.post_code if post.post_code else True, + SysPost.post_sort == post.post_sort if post.post_sort else True, + ) + ) + ) + .scalars() + .first() + ) + + return post_info + + @classmethod + async def get_post_list(cls, db: AsyncSession, query_object: PostPageQueryModel, is_page: bool = False): + """ + 根据查询参数获取岗位列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 岗位列表信息对象 + """ + query = ( + select(SysPost) + .where( + SysPost.post_code.like(f'%{query_object.post_code}%') if query_object.post_code else True, + SysPost.post_name.like(f'%{query_object.post_name}%') if query_object.post_name else True, + SysPost.status == query_object.status if query_object.status else True, + ) + .order_by(SysPost.post_sort) + .distinct() + ) + post_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return post_list + + @classmethod + async def add_post_dao(cls, db: AsyncSession, post: PostModel): + """ + 新增岗位数据库操作 + + :param db: orm对象 + :param post: 岗位对象 + :return: + """ + db_post = SysPost(**post.model_dump()) + db.add(db_post) + await db.flush() + + return db_post + + @classmethod + async def edit_post_dao(cls, db: AsyncSession, post: dict): + """ + 编辑岗位数据库操作 + + :param db: orm对象 + :param post: 需要更新的岗位字典 + :return: + """ + await db.execute(update(SysPost), [post]) + + @classmethod + async def delete_post_dao(cls, db: AsyncSession, post: PostModel): + """ + 删除岗位数据库操作 + + :param db: orm对象 + :param post: 岗位对象 + :return: + """ + await db.execute(delete(SysPost).where(SysPost.post_id.in_([post.post_id]))) + + @classmethod + async def count_user_post_dao(cls, db: AsyncSession, post_id: int): + """ + 根据岗位id查询岗位关联的用户数量 + + :param db: orm对象 + :param post_id: 岗位id + :return: 岗位关联的用户数量 + """ + user_post_count = ( + await db.execute(select(func.count('*')).select_from(SysUserPost).where(SysUserPost.post_id == post_id)) + ).scalar() + + return user_post_count diff --git a/ruoyi-fastapi-backend/module_admin/dao/robot_action_dao.py b/ruoyi-fastapi-backend/module_admin/dao/robot_action_dao.py new file mode 100644 index 0000000..4e8d230 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/robot_action_dao.py @@ -0,0 +1,119 @@ +from sqlalchemy import delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.robot_action_do import RobotAction +from module_admin.entity.vo.robot_action_vo import Robot_actionModel, Robot_actionPageQueryModel +from utils.page_util import PageUtil + + +class Robot_actionDao: + """ + 机器人动作模块数据库操作层 + """ + + @classmethod + async def get_robot_action_detail_by_id(cls, db: AsyncSession, id: int): + """ + 根据主键 自增获取机器人动作详细信息 + + :param db: orm对象 + :param id: 主键 自增 + :return: 机器人动作信息对象 + """ + robot_action_info = ( + ( + await db.execute( + select(RobotAction) + .where( + RobotAction.id == id + ) + ) + ) + .scalars() + .first() + ) + + return robot_action_info + + @classmethod + async def get_robot_action_detail_by_info(cls, db: AsyncSession, robot_action: Robot_actionModel): + """ + 根据机器人动作参数获取机器人动作信息 + + :param db: orm对象 + :param robot_action: 机器人动作参数对象 + :return: 机器人动作信息对象 + """ + robot_action_info = ( + ( + await db.execute( + select(RobotAction).where( + ) + ) + ) + .scalars() + .first() + ) + + return robot_action_info + + @classmethod + async def get_robot_action_list(cls, db: AsyncSession, query_object: Robot_actionPageQueryModel, is_page: bool = False): + """ + 根据查询参数获取机器人动作列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 机器人动作列表信息对象 + """ + query = ( + select(RobotAction) + .where( + RobotAction.id == query_object.id if query_object.id else True, + RobotAction.name.like(f'%{query_object.name}%') if query_object.name else True, + RobotAction.status == query_object.status if query_object.status else True, + ) + .order_by(RobotAction.id) + .distinct() + ) + robot_action_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return robot_action_list + + @classmethod + async def add_robot_action_dao(cls, db: AsyncSession, robot_action: Robot_actionModel): + """ + 新增机器人动作数据库操作 + + :param db: orm对象 + :param robot_action: 机器人动作对象 + :return: + """ + db_robot_action = RobotAction(**robot_action.model_dump(exclude={})) + db.add(db_robot_action) + await db.flush() + + return db_robot_action + + @classmethod + async def edit_robot_action_dao(cls, db: AsyncSession, robot_action: dict): + """ + 编辑机器人动作数据库操作 + + :param db: orm对象 + :param robot_action: 需要更新的机器人动作字典 + :return: + """ + await db.execute(update(RobotAction), [robot_action]) + + @classmethod + async def delete_robot_action_dao(cls, db: AsyncSession, robot_action: Robot_actionModel): + """ + 删除机器人动作数据库操作 + + :param db: orm对象 + :param robot_action: 机器人动作对象 + :return: + """ + await db.execute(delete(RobotAction).where(RobotAction.id.in_([robot_action.id]))) + diff --git a/ruoyi-fastapi-backend/module_admin/dao/robot_role_dao.py b/ruoyi-fastapi-backend/module_admin/dao/robot_role_dao.py new file mode 100644 index 0000000..a9de2ab --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/robot_role_dao.py @@ -0,0 +1,131 @@ +from datetime import datetime, time +from sqlalchemy import delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.role_do import RobotRole +from module_admin.entity.vo.role_vo import RoleModel, RolePageQueryModel +from utils.page_util import PageUtil + + +class RoleDao: + """ + 系统角色模块数据库操作层 + """ + + @classmethod + async def get_role_detail_by_id(cls, db: AsyncSession, robot_role_id: int): + """ + 根据机器人角色ID获取系统角色详细信息 + + :param db: orm对象 + :param robot_role_id: 机器人角色ID + :return: 系统角色信息对象 + """ + role_info = ( + ( + await db.execute( + select(RobotRole) + .where( + RobotRole.robot_role_id == robot_role_id + ) + ) + ) + .scalars() + .first() + ) + + return role_info + + @classmethod + async def get_role_detail_by_info(cls, db: AsyncSession, role: RoleModel): + """ + 根据系统角色参数获取系统角色信息 + + :param db: orm对象 + :param role: 系统角色参数对象 + :return: 系统角色信息对象 + """ + role_info = ( + ( + await db.execute( + select(RobotRole).where( + ) + ) + ) + .scalars() + .first() + ) + + return role_info + + @classmethod + async def get_role_list(cls, db: AsyncSession, query_object: RolePageQueryModel, is_page: bool = False): + """ + 根据查询参数获取系统角色列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 系统角色列表信息对象 + """ + query = ( + select(RobotRole) + .where( + RobotRole.rolename.like(f'%{query_object.rolename}%') if query_object.rolename else True, + RobotRole.detail == query_object.detail if query_object.detail else True, + RobotRole.create_time.between( + datetime.combine(datetime.strptime(query_object.begin_create_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_create_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_create_time and query_object.end_create_time + else True, + RobotRole.update_time.between( + datetime.combine(datetime.strptime(query_object.begin_update_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_update_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_update_time and query_object.end_update_time + else True, + ) + .order_by(RobotRole.robot_role_id) + .distinct() + ) + role_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return role_list + + @classmethod + async def add_role_dao(cls, db: AsyncSession, role: RoleModel): + """ + 新增系统角色数据库操作 + + :param db: orm对象 + :param role: 系统角色对象 + :return: + """ + db_role = RobotRole(**role.model_dump(exclude={})) + db.add(db_role) + await db.flush() + + return db_role + + @classmethod + async def edit_role_dao(cls, db: AsyncSession, role: dict): + """ + 编辑系统角色数据库操作 + + :param db: orm对象 + :param role: 需要更新的系统角色字典 + :return: + """ + await db.execute(update(RobotRole), [role]) + + @classmethod + async def delete_role_dao(cls, db: AsyncSession, role: RoleModel): + """ + 删除系统角色数据库操作 + + :param db: orm对象 + :param role: 系统角色对象 + :return: + """ + await db.execute(delete(RobotRole).where(RobotRole.robot_role_id.in_([role.robot_role_id]))) + diff --git a/ruoyi-fastapi-backend/module_admin/dao/robot_role_pairing_dao.py b/ruoyi-fastapi-backend/module_admin/dao/robot_role_pairing_dao.py new file mode 100644 index 0000000..a00e348 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/robot_role_pairing_dao.py @@ -0,0 +1,153 @@ +from sqlalchemy import delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession + +from module_admin.entity.do.robot_role_do import RobotRole +from module_admin.entity.do.robot_role_pairing_do import RobotRolePairing + +from module_admin.entity.vo.robot_role_pairing_vo import PairingModel, PairingPageQueryModel +from utils.page_util import PageUtil + + +class PairingDao: + """ + 角色-机器人-映射模块数据库操作层 + """ + + @classmethod + async def edit_pairing_status(cls, db: AsyncSession,robot_id: int = None, pairing_id: int = None, status: str = '0'): + + condition = list() + if robot_id: + condition.append(RobotRolePairing.robot_id == robot_id) + if pairing_id: + condition.append(RobotRolePairing.pairing_id == pairing_id) + query = ( + update(RobotRolePairing) + .values(status=status) + .where( + * condition + ) + ) + await db.execute(query) + + @classmethod + async def get_role_pairing_list(cls, db: AsyncSession, robot_id: int): + + query = ( + select(RobotRolePairing.pairing_id, RobotRole.robot_role_id ,RobotRole.rolename, RobotRole.detail, RobotRolePairing.status) + .select_from(RobotRolePairing) + .join(RobotRole, RobotRole.robot_role_id == RobotRolePairing.robot_role_id) + .where(RobotRolePairing.robot_id == robot_id) + ) + role_pairing_list = ( + await db.execute(query) + ).mappings().all() + return role_pairing_list + @classmethod + async def get_pairing_detail_by_id(cls, db: AsyncSession, pairing_id: int): + """ + 根据主键ID获取角色-机器人-映射详细信息 + + :param db: orm对象 + :param pairing_id: 主键ID + :return: 角色-机器人-映射信息对象 + """ + pairing_info = ( + ( + await db.execute( + select(RobotRolePairing) + .where( + RobotRolePairing.pairing_id == pairing_id + ) + ) + ) + .scalars() + .first() + ) + + return pairing_info + + @classmethod + async def get_pairing_detail_by_info(cls, db: AsyncSession, pairing: PairingModel): + """ + 根据角色-机器人-映射参数获取角色-机器人-映射信息 + + :param db: orm对象 + :param pairing: 角色-机器人-映射参数对象 + :return: 角色-机器人-映射信息对象 + """ + pairing_info = ( + ( + await db.execute( + select(RobotRolePairing).where( + ) + ) + ) + .scalars() + .first() + ) + + return pairing_info + + @classmethod + async def get_pairing_list(cls, db: AsyncSession, query_object: PairingPageQueryModel, is_page: bool = False): + """ + 根据查询参数获取角色-机器人-映射列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 角色-机器人-映射列表信息对象 + """ + query = ( + select(RobotRolePairing) + .where( + RobotRolePairing.pairing_id == query_object.pairing_id if query_object.pairing_id else True, + RobotRolePairing.robot_role_id == query_object.robot_role_id if query_object.robot_role_id else True, + RobotRolePairing.robot_id == query_object.robot_id if query_object.robot_id else True, + RobotRolePairing.status == query_object.status if query_object.status else True, + ) + .order_by(RobotRolePairing.pairing_id) + .distinct() + ) + pairing_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return pairing_list + + @classmethod + async def add_pairing_dao(cls, db: AsyncSession, pairing: PairingModel): + """ + 新增角色-机器人-映射数据库操作 + + :param db: orm对象 + :param pairing: 角色-机器人-映射对象 + :return: + """ + db_pairing = RobotRolePairing(**pairing.model_dump(exclude={})) + db.add(db_pairing) + await db.flush() + + return db_pairing + + @classmethod + async def edit_pairing_dao(cls, db: AsyncSession, pairing: dict): + """ + 编辑角色-机器人-映射数据库操作 + + :param db: orm对象 + :param pairing: 需要更新的角色-机器人-映射字典 + :return: + """ + await db.execute(update(RobotRolePairing), [pairing]) + + @classmethod + async def delete_pairing_dao(cls, db: AsyncSession, pairing: PairingModel): + """ + 删除角色-机器人-映射数据库操作 + + :param db: orm对象 + :param pairing: 角色-机器人-映射对象 + :return: + """ + await db.execute(delete(RobotRolePairing).where(RobotRolePairing.pairing_id.in_([pairing.pairing_id]))) + diff --git a/ruoyi-fastapi-backend/module_admin/dao/role_dao.py b/ruoyi-fastapi-backend/module_admin/dao/role_dao.py new file mode 100644 index 0000000..534c7f3 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/role_dao.py @@ -0,0 +1,338 @@ +from datetime import datetime, time +from sqlalchemy import and_, delete, desc, func, or_, select, update # noqa: F401 +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.dept_do import SysDept +from module_admin.entity.do.menu_do import SysMenu +from module_admin.entity.do.role_do import SysRole, SysRoleMenu, SysRoleDept +from module_admin.entity.do.user_do import SysUser, SysUserRole +from module_admin.entity.vo.role_vo import RoleDeptModel, RoleMenuModel, RoleModel, RolePageQueryModel +from utils.page_util import PageUtil + + +class RoleDao: + """ + 角色管理模块数据库操作层 + """ + + @classmethod + async def get_role_by_name(cls, db: AsyncSession, role_name: str): + """ + 根据角色名获取在用角色信息 + + :param db: orm对象 + :param role_name: 角色名 + :return: 当前角色名的角色信息对象 + """ + query_role_info = ( + ( + await db.execute( + select(SysRole) + .where(SysRole.status == '0', SysRole.del_flag == '0', SysRole.role_name == role_name) + .order_by(desc(SysRole.create_time)) + .distinct() + ) + ) + .scalars() + .first() + ) + + return query_role_info + + @classmethod + async def get_role_by_info(cls, db: AsyncSession, role: RoleModel): + """ + 根据角色参数获取角色信息 + + :param db: orm对象 + :param role: 角色参数 + :return: 当前角色参数的角色信息对象 + """ + query_role_info = ( + ( + await db.execute( + select(SysRole) + .where( + SysRole.del_flag == '0', + SysRole.role_name == role.role_name if role.role_name else True, + SysRole.role_key == role.role_key if role.role_key else True, + ) + .order_by(desc(SysRole.create_time)) + .distinct() + ) + ) + .scalars() + .first() + ) + + return query_role_info + + @classmethod + async def get_role_by_id(cls, db: AsyncSession, role_id: int): + """ + 根据角色id获取在用角色信息 + + :param db: orm对象 + :param role_id: 角色id + :return: 当前角色id的角色信息对象 + """ + role_info = ( + ( + await db.execute( + select(SysRole).where(SysRole.role_id == role_id, SysRole.status == '0', SysRole.del_flag == '0') + ) + ) + .scalars() + .first() + ) + + return role_info + + @classmethod + async def get_role_detail_by_id(cls, db: AsyncSession, role_id: int): + """ + 根据role_id获取角色详细信息 + + :param db: orm对象 + :param role_id: 角色id + :return: 当前role_id的角色信息对象 + """ + query_role_info = ( + (await db.execute(select(SysRole).where(SysRole.del_flag == '0', SysRole.role_id == role_id).distinct())) + .scalars() + .first() + ) + + return query_role_info + + @classmethod + async def get_role_select_option_dao(cls, db: AsyncSession): + """ + 获取编辑页面对应的在用角色列表信息 + + :param db: orm对象 + :return: 角色列表信息 + """ + role_info = ( + ( + await db.execute( + select(SysRole).where(SysRole.role_id != 1, SysRole.status == '0', SysRole.del_flag == '0') + ) + ) + .scalars() + .all() + ) + + return role_info + + @classmethod + async def get_role_list( + cls, db: AsyncSession, query_object: RolePageQueryModel, data_scope_sql: str, is_page: bool = False + ): + """ + 根据查询参数获取角色列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param data_scope_sql: 数据权限对应的查询sql语句 + :param is_page: 是否开启分页 + :return: 角色列表信息对象 + """ + query = ( + select(SysRole) + .join(SysUserRole, SysUserRole.role_id == SysRole.role_id, isouter=True) + .join(SysUser, SysUser.user_id == SysUserRole.user_id, isouter=True) + .join(SysDept, SysDept.dept_id == SysUser.dept_id, isouter=True) + .where( + SysRole.del_flag == '0', + SysRole.role_id == query_object.role_id if query_object.role_id is not None else True, + SysRole.role_name.like(f'%{query_object.role_name}%') if query_object.role_name else True, + SysRole.role_key.like(f'%{query_object.role_key}%') if query_object.role_key else True, + SysRole.status == query_object.status if query_object.status else True, + SysRole.create_time.between( + datetime.combine(datetime.strptime(query_object.begin_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_time and query_object.end_time + else True, + eval(data_scope_sql), + ) + .order_by(SysRole.role_sort) + .distinct() + ) + role_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return role_list + + @classmethod + async def add_role_dao(cls, db: AsyncSession, role: RoleModel): + """ + 新增角色数据库操作 + + :param db: orm对象 + :param role: 角色对象 + :return: + """ + db_role = SysRole(**role.model_dump(exclude={'admin'})) + db.add(db_role) + await db.flush() + + return db_role + + @classmethod + async def edit_role_dao(cls, db: AsyncSession, role: dict): + """ + 编辑角色数据库操作 + + :param db: orm对象 + :param role: 需要更新的角色字典 + :return: + """ + await db.execute(update(SysRole), [role]) + + @classmethod + async def delete_role_dao(cls, db: AsyncSession, role: RoleModel): + """ + 删除角色数据库操作 + + :param db: orm对象 + :param role: 角色对象 + :return: + """ + await db.execute( + update(SysRole) + .where(SysRole.role_id == role.role_id) + .values(del_flag='2', update_by=role.update_by, update_time=role.update_time) + ) + + @classmethod + async def get_role_menu_dao(cls, db: AsyncSession, role: RoleModel): + """ + 根据角色id获取角色菜单关联列表信息 + + :param db: orm对象 + :param role: 角色对象 + :return: 角色菜单关联列表信息 + """ + role_menu_query_all = ( + ( + await db.execute( + select(SysMenu) + .join(SysRoleMenu, SysRoleMenu.menu_id == SysMenu.menu_id) + .where( + SysRoleMenu.role_id == role.role_id, + ~SysMenu.menu_id.in_( + select(SysMenu.parent_id) + .select_from(SysMenu) + .join( + SysRoleMenu, + and_(SysRoleMenu.menu_id == SysMenu.menu_id, SysRoleMenu.role_id == role.role_id), + ) + ) + if role.menu_check_strictly + else True, + ) + .order_by(SysMenu.parent_id, SysMenu.order_num) + ) + ) + .scalars() + .all() + ) + + return role_menu_query_all + + @classmethod + async def add_role_menu_dao(cls, db: AsyncSession, role_menu: RoleMenuModel): + """ + 新增角色菜单关联信息数据库操作 + + :param db: orm对象 + :param role_menu: 用户角色菜单关联对象 + :return: + """ + db_role_menu = SysRoleMenu(**role_menu.model_dump()) + db.add(db_role_menu) + + @classmethod + async def delete_role_menu_dao(cls, db: AsyncSession, role_menu: RoleMenuModel): + """ + 删除角色菜单关联信息数据库操作 + + :param db: orm对象 + :param role_menu: 角色菜单关联对象 + :return: + """ + await db.execute(delete(SysRoleMenu).where(SysRoleMenu.role_id.in_([role_menu.role_id]))) + + @classmethod + async def get_role_dept_dao(cls, db: AsyncSession, role: RoleModel): + """ + 根据角色id获取角色部门关联列表信息 + + :param db: orm对象 + :param role: 角色对象 + :return: 角色部门关联列表信息 + """ + role_dept_query_all = ( + ( + await db.execute( + select(SysDept) + .join(SysRoleDept, SysRoleDept.dept_id == SysDept.dept_id) + .where( + SysRoleDept.role_id == role.role_id, + ~SysDept.dept_id.in_( + select(SysDept.parent_id) + .select_from(SysDept) + .join( + SysRoleDept, + and_(SysRoleDept.dept_id == SysDept.dept_id, SysRoleDept.role_id == role.role_id), + ) + ) + if role.dept_check_strictly + else True, + ) + .order_by(SysDept.parent_id, SysDept.order_num) + ) + ) + .scalars() + .all() + ) + + return role_dept_query_all + + @classmethod + async def add_role_dept_dao(cls, db: AsyncSession, role_dept: RoleDeptModel): + """ + 新增角色部门关联信息数据库操作 + + :param db: orm对象 + :param role_dept: 用户角色部门关联对象 + :return: + """ + db_role_dept = SysRoleDept(**role_dept.dict()) + db.add(db_role_dept) + + @classmethod + async def delete_role_dept_dao(cls, db: AsyncSession, role_dept: RoleDeptModel): + """ + 删除角色部门关联信息数据库操作 + + :param db: orm对象 + :param role_dept: 角色部门关联对象 + :return: + """ + await db.execute(delete(SysRoleDept).where(SysRoleDept.role_id.in_([role_dept.role_id]))) + + @classmethod + async def count_user_role_dao(cls, db: AsyncSession, role_id: int): + """ + 根据角色id查询角色关联用户数量 + + :param db: orm对象 + :param role_id: 角色id + :return: 角色关联用户数量 + """ + user_count = ( + await db.execute(select(func.count('*')).select_from(SysUserRole).where(SysUserRole.role_id == role_id)) + ).scalar() + + return user_count diff --git a/ruoyi-fastapi-backend/module_admin/dao/sys_alert_dao.py b/ruoyi-fastapi-backend/module_admin/dao/sys_alert_dao.py new file mode 100644 index 0000000..c6ecb0e --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/sys_alert_dao.py @@ -0,0 +1,132 @@ +from datetime import datetime, time +from sqlalchemy import delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.sys_alert_do import SysAlert +from module_admin.entity.vo.sys_alert_vo import Sys_alertModel, Sys_alertPageQueryModel +from utils.page_util import PageUtil + + +class Sys_alertDao: + """ + 系统告警模块数据库操作层 + """ + + @classmethod + async def get_sys_alert_detail_by_id(cls, db: AsyncSession, id: int): + """ + 根据主键 自增获取系统告警详细信息 + + :param db: orm对象 + :param id: 主键 自增 + :return: 系统告警信息对象 + """ + sys_alert_info = ( + ( + await db.execute( + select(SysAlert) + .where( + SysAlert.id == id + ) + ) + ) + .scalars() + .first() + ) + + return sys_alert_info + + @classmethod + async def get_sys_alert_detail_by_info(cls, db: AsyncSession, sys_alert: Sys_alertModel): + """ + 根据系统告警参数获取系统告警信息 + + :param db: orm对象 + :param sys_alert: 系统告警参数对象 + :return: 系统告警信息对象 + """ + sys_alert_info = ( + ( + await db.execute( + select(SysAlert).where( + ) + ) + ) + .scalars() + .first() + ) + + return sys_alert_info + + @classmethod + async def get_sys_alert_list(cls, db: AsyncSession, query_object: Sys_alertPageQueryModel, is_page: bool = False): + """ + 根据查询参数获取系统告警列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 系统告警列表信息对象 + """ + query = ( + select(SysAlert) + .where( + SysAlert.detail.like(f'%{query_object.detail}%') if query_object.detail else True, + SysAlert.type == query_object.type if query_object.type else True, + SysAlert.status == query_object.status if query_object.status else True, + SysAlert.create_time.between( + datetime.combine(datetime.strptime(query_object.begin_create_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_create_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_create_time and query_object.end_create_time + else True, + SysAlert.update_time.between( + datetime.combine(datetime.strptime(query_object.begin_update_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_update_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_update_time and query_object.end_update_time + else True, + ) + .order_by(SysAlert.id) + .distinct() + ) + sys_alert_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return sys_alert_list + + @classmethod + async def add_sys_alert_dao(cls, db: AsyncSession, sys_alert: Sys_alertModel): + """ + 新增系统告警数据库操作 + + :param db: orm对象 + :param sys_alert: 系统告警对象 + :return: + """ + db_sys_alert = SysAlert(**sys_alert.model_dump(exclude={})) + db.add(db_sys_alert) + await db.flush() + + return db_sys_alert + + @classmethod + async def edit_sys_alert_dao(cls, db: AsyncSession, sys_alert: dict): + """ + 编辑系统告警数据库操作 + + :param db: orm对象 + :param sys_alert: 需要更新的系统告警字典 + :return: + """ + await db.execute(update(SysAlert), [sys_alert]) + + @classmethod + async def delete_sys_alert_dao(cls, db: AsyncSession, sys_alert: Sys_alertModel): + """ + 删除系统告警数据库操作 + + :param db: orm对象 + :param sys_alert: 系统告警对象 + :return: + """ + await db.execute(delete(SysAlert).where(SysAlert.id.in_([sys_alert.id]))) + diff --git a/ruoyi-fastapi-backend/module_admin/dao/sys_statistics_dao.py b/ruoyi-fastapi-backend/module_admin/dao/sys_statistics_dao.py new file mode 100644 index 0000000..4f63829 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/sys_statistics_dao.py @@ -0,0 +1,266 @@ +from sqlalchemy import delete, select, update, and_, desc, insert, func +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.sys_statistics_do import SysStatistics +from module_admin.entity.vo.sys_statistics_vo import Sys_statisticsModel, Sys_statisticsPageQueryModel +from utils.page_util import PageUtil +from datetime import datetime, date + + +class Sys_statisticsDao: + """ + 系统统计数据模块数据库操作层 + """ + + @classmethod + async def get_total_statistics_data(cls, db: AsyncSession): + """ + 添加系统统计数据 + :param query_db: + :param page_object: + :return: + """ + stmt = ( + select( + func.sum(SysStatistics.llm_call).label('llm_call'), + func.sum(SysStatistics.access_control_count).label('access_control_count'), + func.sum(SysStatistics.visitor_guide).label('visitor_guide'), + func.sum(SysStatistics.exhibition_explanation).label('exhibition_explanation'), + ) + ) + + result = await db.execute(stmt) + return result.mappings().first() + + @classmethod + async def add_count(cls, db: AsyncSession, field: str, user_name:str , count: int = 1): + stmt = ( + select(SysStatistics) + .where( + SysStatistics.stat_date == datetime.now().date() + ) + ) + result = (await db.execute(stmt)).scalars().first() + if result: + update_stmt = ( + update(SysStatistics) + .where( + SysStatistics.statistic_id == result.statistic_id + ) + .values( + { + field: getattr(result, field) + count, + "update_time": datetime.now(), + "update_by": user_name, + } + ) + ) + await db.execute(update_stmt) + else: + insert_stmt = ( + insert(SysStatistics) + .values( + { + "stat_date": datetime.now().date(), + field: 1, + "create_time": datetime.now(), + "update_time": datetime.now(), + "create_by": user_name, + } + ) + ) + await db.execute(insert_stmt) + + @classmethod + async def add_model_call_count(cls, db: AsyncSession, user_name: str): + stmt = ( + select(SysStatistics) + .where( + SysStatistics.stat_date == datetime.now().date() + ) + ) + result = (await db.execute(stmt)).scalars().first() + if result: + update_stmt = ( + update(SysStatistics) + .where( + SysStatistics.statistic_id == result.statistic_id + ) + .values( + { + "llm_call": result.llm_call + 1, + "update_time": datetime.now(), + "update_by": user_name, + } + ) + ) + await db.execute(update_stmt) + else: + insert_stmt = ( + insert(SysStatistics) + .values( + { + "stat_date": datetime.now().date(), + "llm_call": 1, + "create_time": datetime.now(), + "update_time": datetime.now(), + "create_by": user_name, + } + ) + ) + await db.execute(insert_stmt) + + + + + + @classmethod + async def get_access_control_success_rate(cls, db: AsyncSession, start_time: datetime, end_time: datetime): + stmt = ( + select(SysStatistics.access_control_success_rate, SysStatistics.stat_date) + .where( + and_( + SysStatistics.stat_date >= start_time, + SysStatistics.stat_date <= end_time + ) + ) + .order_by(desc(SysStatistics.stat_date)) + ) + + return (await db.execute(stmt)).mappings().all() + + @classmethod + async def get_visitor_count(cls, db: AsyncSession, start_time: date, end_time: date): + stmt = ( + select(SysStatistics.visitor_guide, SysStatistics.stat_date) + .where( + and_( + SysStatistics.stat_date >= start_time, + SysStatistics.stat_date <= end_time, + ) + ) + .order_by(desc(SysStatistics.stat_date)) + ) + + return (await db.execute(stmt)).mappings().all() + + @classmethod + async def get_statistics_data(cls, db: AsyncSession, dd: date): + # 使用 func.date() 确保日期格式正确比较 + stmt = ( + select( + SysStatistics.llm_call, + SysStatistics.access_control_count, + SysStatistics.visitor_guide, + SysStatistics.exhibition_explanation + ) + .where(func.date(SysStatistics.stat_date) == dd) + ) + result = (await db.execute(stmt)).mappings().first() + return result + + + @classmethod + async def get_sys_statistics_detail_by_id(cls, db: AsyncSession, statistic_id: int): + """ + 根据统计项目ID获取系统统计数据详细信息 + + :param db: orm对象 + :param statistic_id: 统计项目ID + :return: 系统统计数据信息对象 + """ + sys_statistics_info = ( + ( + await db.execute( + select(SysStatistics) + .where( + SysStatistics.statistic_id == statistic_id + ) + ) + ) + .scalars() + .first() + ) + + return sys_statistics_info + + @classmethod + async def get_sys_statistics_detail_by_info(cls, db: AsyncSession, sys_statistics: Sys_statisticsModel): + """ + 根据系统统计数据参数获取系统统计数据信息 + + :param db: orm对象 + :param sys_statistics: 系统统计数据参数对象 + :return: 系统统计数据信息对象 + """ + sys_statistics_info = ( + ( + await db.execute( + select(SysStatistics).where( + ) + ) + ) + .scalars() + .first() + ) + + return sys_statistics_info + + @classmethod + async def get_sys_statistics_list(cls, db: AsyncSession, query_object: Sys_statisticsPageQueryModel, is_page: bool = False): + """ + 根据查询参数获取系统统计数据列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 系统统计数据列表信息对象 + """ + query = ( + select(SysStatistics) + .where( + SysStatistics.stat_date == query_object.stat_date if query_object.stat_date else True, + ) + .order_by(SysStatistics.statistic_id) + .distinct() + ) + sys_statistics_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return sys_statistics_list + + @classmethod + async def add_sys_statistics_dao(cls, db: AsyncSession, sys_statistics: Sys_statisticsModel): + """ + 新增系统统计数据数据库操作 + + :param db: orm对象 + :param sys_statistics: 系统统计数据对象 + :return: + """ + db_sys_statistics = SysStatistics(**sys_statistics.model_dump(exclude={})) + db.add(db_sys_statistics) + await db.flush() + + return db_sys_statistics + + @classmethod + async def edit_sys_statistics_dao(cls, db: AsyncSession, sys_statistics: dict): + """ + 编辑系统统计数据数据库操作 + + :param db: orm对象 + :param sys_statistics: 需要更新的系统统计数据字典 + :return: + """ + await db.execute(update(SysStatistics), [sys_statistics]) + + @classmethod + async def delete_sys_statistics_dao(cls, db: AsyncSession, sys_statistics: Sys_statisticsModel): + """ + 删除系统统计数据数据库操作 + + :param db: orm对象 + :param sys_statistics: 系统统计数据对象 + :return: + """ + await db.execute(delete(SysStatistics).where(SysStatistics.statistic_id.in_([sys_statistics.statistic_id]))) + diff --git a/ruoyi-fastapi-backend/module_admin/dao/test_dao.py b/ruoyi-fastapi-backend/module_admin/dao/test_dao.py new file mode 100644 index 0000000..f1651fc --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/test_dao.py @@ -0,0 +1,16 @@ +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.user_do import SysUser +from utils.page_util import PageUtil +from module_admin.entity.vo.test_vo import TestUserNamePage + + +class TestDao: + @classmethod + async def get_user_name(cls, db: AsyncSession, query_paramter: TestUserNamePage): + query = ( + select(SysUser.user_name) + ) + # user_name = await db.execute(select(SysUser.user_name)) + user_name = await PageUtil.paginate(db, query, query_paramter.page_num, query_paramter.page_size, True) + return user_name diff --git a/ruoyi-fastapi-backend/module_admin/dao/user_dao.py b/ruoyi-fastapi-backend/module_admin/dao/user_dao.py new file mode 100644 index 0000000..cc53566 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/user_dao.py @@ -0,0 +1,568 @@ +from datetime import datetime, time +from sqlalchemy import and_, delete, desc, func, or_, select, update +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.dept_do import SysDept +from module_admin.entity.do.menu_do import SysMenu +from module_admin.entity.do.post_do import SysPost +from module_admin.entity.do.role_do import SysRole, SysRoleDept, SysRoleMenu # noqa: F401 +from module_admin.entity.do.user_do import SysUser, SysUserPost, SysUserRole +from module_admin.entity.vo.user_vo import ( + UserModel, + UserPageQueryModel, + UserPostModel, + UserRoleModel, + UserRolePageQueryModel, + UserRoleQueryModel, +) +from utils.page_util import PageUtil + + +class UserDao: + """ + 用户管理模块数据库操作层 + """ + + @classmethod + async def get_user_by_name(cls, db: AsyncSession, user_name: str): + """ + 根据用户名获取用户信息 + + :param db: orm对象 + :param user_name: 用户名 + :return: 当前用户名的用户信息对象 + """ + query_user_info = ( + ( + await db.execute( + select(SysUser) + .where(SysUser.status == '0', SysUser.del_flag == '0', SysUser.user_name == user_name) + .order_by(desc(SysUser.create_time)) + .distinct() + ) + ) + .scalars() + .first() + ) + + return query_user_info + + @classmethod + async def get_user_by_info(cls, db: AsyncSession, user: UserModel): + """ + 根据用户参数获取用户信息 + + :param db: orm对象 + :param user: 用户参数 + :return: 当前用户参数的用户信息对象 + """ + query_user_info = ( + ( + await db.execute( + select(SysUser) + .where( + SysUser.del_flag == '0', + SysUser.user_name == user.user_name if user.user_name else True, + SysUser.phonenumber == user.phonenumber if user.phonenumber else True, + SysUser.email == user.email if user.email else True, + ) + .order_by(desc(SysUser.create_time)) + .distinct() + ) + ) + .scalars() + .first() + ) + + return query_user_info + + @classmethod + async def get_user_by_id(cls, db: AsyncSession, user_id: int): + """ + 根据user_id获取用户信息 + + :param db: orm对象 + :param user_id: 用户id + :return: 当前user_id的用户信息对象 + """ + query_user_basic_info = ( + ( + await db.execute( + select(SysUser) + .where(SysUser.status == '0', SysUser.del_flag == '0', SysUser.user_id == user_id) + .distinct() + ) + ) + .scalars() + .first() + ) + query_user_dept_info = ( + ( + await db.execute( + select(SysDept) + .select_from(SysUser) + .where(SysUser.status == '0', SysUser.del_flag == '0', SysUser.user_id == user_id) + .join( + SysDept, + and_(SysUser.dept_id == SysDept.dept_id, SysDept.status == '0', SysDept.del_flag == '0'), + ) + .distinct() + ) + ) + .scalars() + .first() + ) + query_user_role_info = ( + ( + await db.execute( + select(SysRole) + .select_from(SysUser) + .where(SysUser.status == '0', SysUser.del_flag == '0', SysUser.user_id == user_id) + .join(SysUserRole, SysUser.user_id == SysUserRole.user_id, isouter=True) + .join( + SysRole, + and_(SysUserRole.role_id == SysRole.role_id, SysRole.status == '0', SysRole.del_flag == '0'), + ) + .distinct() + ) + ) + .scalars() + .all() + ) + query_user_post_info = ( + ( + await db.execute( + select(SysPost) + .select_from(SysUser) + .where(SysUser.status == '0', SysUser.del_flag == '0', SysUser.user_id == user_id) + .join(SysUserPost, SysUser.user_id == SysUserPost.user_id, isouter=True) + .join(SysPost, and_(SysUserPost.post_id == SysPost.post_id, SysPost.status == '0')) + .distinct() + ) + ) + .scalars() + .all() + ) + role_id_list = [item.role_id for item in query_user_role_info] + if 1 in role_id_list: + query_user_menu_info = ( + (await db.execute(select(SysMenu).where(SysMenu.status == '0').distinct())).scalars().all() + ) + else: + query_user_menu_info = ( + ( + await db.execute( + select(SysMenu) + .select_from(SysUser) + .where(SysUser.status == '0', SysUser.del_flag == '0', SysUser.user_id == user_id) + .join(SysUserRole, SysUser.user_id == SysUserRole.user_id, isouter=True) + .join( + SysRole, + and_( + SysUserRole.role_id == SysRole.role_id, SysRole.status == '0', SysRole.del_flag == '0' + ), + isouter=True, + ) + .join(SysRoleMenu, SysRole.role_id == SysRoleMenu.role_id, isouter=True) + .join(SysMenu, and_(SysRoleMenu.menu_id == SysMenu.menu_id, SysMenu.status == '0')) + .order_by(SysMenu.order_num) + .distinct() + ) + ) + .scalars() + .all() + ) + + results = dict( + user_basic_info=query_user_basic_info, + user_dept_info=query_user_dept_info, + user_role_info=query_user_role_info, + user_post_info=query_user_post_info, + user_menu_info=query_user_menu_info, + ) + + return results + + @classmethod + async def get_user_detail_by_id(cls, db: AsyncSession, user_id: int): + """ + 根据user_id获取用户详细信息 + + :param db: orm对象 + :param user_id: 用户id + :return: 当前user_id的用户信息对象 + """ + query_user_basic_info = ( + (await db.execute(select(SysUser).where(SysUser.del_flag == '0', SysUser.user_id == user_id).distinct())) + .scalars() + .first() + ) + query_user_dept_info = ( + ( + await db.execute( + select(SysDept) + .select_from(SysUser) + .where(SysUser.del_flag == '0', SysUser.user_id == user_id) + .join( + SysDept, + and_(SysUser.dept_id == SysDept.dept_id, SysDept.status == '0', SysDept.del_flag == '0'), + ) + .distinct() + ) + ) + .scalars() + .first() + ) + query_user_role_info = ( + ( + await db.execute( + select(SysRole) + .select_from(SysUser) + .where(SysUser.del_flag == '0', SysUser.user_id == user_id) + .join(SysUserRole, SysUser.user_id == SysUserRole.user_id, isouter=True) + .join( + SysRole, + and_(SysUserRole.role_id == SysRole.role_id, SysRole.status == '0', SysRole.del_flag == '0'), + ) + .distinct() + ) + ) + .scalars() + .all() + ) + query_user_post_info = ( + ( + await db.execute( + select(SysPost) + .select_from(SysUser) + .where(SysUser.del_flag == '0', SysUser.user_id == user_id) + .join(SysUserPost, SysUser.user_id == SysUserPost.user_id, isouter=True) + .join(SysPost, and_(SysUserPost.post_id == SysPost.post_id, SysPost.status == '0')) + .distinct() + ) + ) + .scalars() + .all() + ) + query_user_menu_info = ( + ( + await db.execute( + select(SysMenu) + .select_from(SysUser) + .where(SysUser.del_flag == '0', SysUser.user_id == user_id) + .join(SysUserRole, SysUser.user_id == SysUserRole.user_id, isouter=True) + .join( + SysRole, + and_(SysUserRole.role_id == SysRole.role_id, SysRole.status == '0', SysRole.del_flag == '0'), + isouter=True, + ) + .join(SysRoleMenu, SysRole.role_id == SysRoleMenu.role_id, isouter=True) + .join(SysMenu, and_(SysRoleMenu.menu_id == SysMenu.menu_id, SysMenu.status == '0')) + .distinct() + ) + ) + .scalars() + .all() + ) + results = dict( + user_basic_info=query_user_basic_info, + user_dept_info=query_user_dept_info, + user_role_info=query_user_role_info, + user_post_info=query_user_post_info, + user_menu_info=query_user_menu_info, + ) + + return results + + @classmethod + async def get_user_list( + cls, db: AsyncSession, query_object: UserPageQueryModel, data_scope_sql: str, is_page: bool = False + ): + """ + 根据查询参数获取用户列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param data_scope_sql: 数据权限对应的查询sql语句 + :param is_page: 是否开启分页 + :return: 用户列表信息对象 + """ + query = ( + select(SysUser, SysDept) + .where( + SysUser.del_flag == '0', + or_( + SysUser.dept_id == query_object.dept_id, + SysUser.dept_id.in_( + select(SysDept.dept_id).where(func.find_in_set(query_object.dept_id, SysDept.ancestors)) + ), + ) + if query_object.dept_id + else True, + SysUser.user_id == query_object.user_id if query_object.user_id is not None else True, + SysUser.user_name.like(f'%{query_object.user_name}%') if query_object.user_name else True, + SysUser.nick_name.like(f'%{query_object.nick_name}%') if query_object.nick_name else True, + SysUser.email.like(f'%{query_object.email}%') if query_object.email else True, + SysUser.phonenumber.like(f'%{query_object.phonenumber}%') if query_object.phonenumber else True, + SysUser.status == query_object.status if query_object.status else True, + SysUser.sex == query_object.sex if query_object.sex else True, + SysUser.create_time.between( + datetime.combine(datetime.strptime(query_object.begin_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_time and query_object.end_time + else True, + eval(data_scope_sql), + ) + .join( + SysDept, + and_(SysUser.dept_id == SysDept.dept_id, SysDept.status == '0', SysDept.del_flag == '0'), + isouter=True, + ) + .order_by(SysUser.user_id) + .distinct() + ) + user_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return user_list + + @classmethod + async def add_user_dao(cls, db: AsyncSession, user: UserModel): + """ + 新增用户数据库操作 + + :param db: orm对象 + :param user: 用户对象 + :return: 新增校验结果 + """ + db_user = SysUser(**user.model_dump(exclude={'admin'})) + db.add(db_user) + await db.flush() + + return db_user + + @classmethod + async def edit_user_dao(cls, db: AsyncSession, user: dict): + """ + 编辑用户数据库操作 + + :param db: orm对象 + :param user: 需要更新的用户字典 + :return: 编辑校验结果 + """ + await db.execute(update(SysUser), [user]) + + @classmethod + async def delete_user_dao(cls, db: AsyncSession, user: UserModel): + """ + 删除用户数据库操作 + + :param db: orm对象 + :param user: 用户对象 + :return: + """ + await db.execute( + update(SysUser) + .where(SysUser.user_id == user.user_id) + .values(del_flag='2', update_by=user.update_by, update_time=user.update_time) + ) + + @classmethod + async def get_user_role_allocated_list_by_user_id(cls, db: AsyncSession, query_object: UserRoleQueryModel): + """ + 根据用户id获取用户已分配的角色列表信息数据库操作 + + :param db: orm对象 + :param query_object: 用户角色查询对象 + :return: 用户已分配的角色列表信息 + """ + allocated_role_list = ( + ( + await db.execute( + select(SysRole) + .where( + SysRole.del_flag == '0', + SysRole.role_id != 1, + SysRole.role_name == query_object.role_name if query_object.role_name else True, + SysRole.role_key == query_object.role_key if query_object.role_key else True, + SysRole.role_id.in_( + select(SysUserRole.role_id).where(SysUserRole.user_id == query_object.user_id) + ), + ) + .distinct() + ) + ) + .scalars() + .all() + ) + + return allocated_role_list + + @classmethod + async def get_user_role_allocated_list_by_role_id( + cls, db: AsyncSession, query_object: UserRolePageQueryModel, data_scope_sql: str, is_page: bool = False + ): + """ + 根据角色id获取已分配的用户列表信息 + + :param db: orm对象 + :param query_object: 用户角色查询对象 + :param data_scope_sql: 数据权限对应的查询sql语句 + :param is_page: 是否开启分页 + :return: 角色已分配的用户列表信息 + """ + query = ( + select(SysUser) + .join(SysDept, SysDept.dept_id == SysUser.dept_id, isouter=True) + .join(SysUserRole, SysUserRole.user_id == SysUser.user_id, isouter=True) + .join(SysRole, SysRole.role_id == SysUserRole.role_id, isouter=True) + .where( + SysUser.del_flag == '0', + SysUser.user_name == query_object.user_name if query_object.user_name else True, + SysUser.phonenumber == query_object.phonenumber if query_object.phonenumber else True, + SysRole.role_id == query_object.role_id, + eval(data_scope_sql), + ) + .distinct() + ) + allocated_user_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return allocated_user_list + + @classmethod + async def get_user_role_unallocated_list_by_role_id( + cls, db: AsyncSession, query_object: UserRolePageQueryModel, data_scope_sql: str, is_page: bool = False + ): + """ + 根据角色id获取未分配的用户列表信息 + + :param db: orm对象 + :param query_object: 用户角色查询对象 + :param data_scope_sql: 数据权限对应的查询sql语句 + :param is_page: 是否开启分页 + :return: 角色未分配的用户列表信息 + """ + query = ( + select(SysUser) + .join(SysDept, SysDept.dept_id == SysUser.dept_id, isouter=True) + .join(SysUserRole, SysUserRole.user_id == SysUser.user_id, isouter=True) + .join(SysRole, SysRole.role_id == SysUserRole.role_id, isouter=True) + .where( + SysUser.del_flag == '0', + SysUser.user_name == query_object.user_name if query_object.user_name else True, + SysUser.phonenumber == query_object.phonenumber if query_object.phonenumber else True, + or_(SysRole.role_id != query_object.role_id, SysRole.role_id.is_(None)), + ~SysUser.user_id.in_( + select(SysUser.user_id) + .select_from(SysUser) + .join( + SysUserRole, + and_(SysUserRole.user_id == SysUser.user_id, SysUserRole.role_id == query_object.role_id), + ) + ), + eval(data_scope_sql), + ) + .distinct() + ) + unallocated_user_list = await PageUtil.paginate( + db, query, query_object.page_num, query_object.page_size, is_page + ) + + return unallocated_user_list + + @classmethod + async def add_user_role_dao(cls, db: AsyncSession, user_role: UserRoleModel): + """ + 新增用户角色关联信息数据库操作 + + :param db: orm对象 + :param user_role: 用户角色关联对象 + :return: + """ + db_user_role = SysUserRole(**user_role.model_dump()) + db.add(db_user_role) + + @classmethod + async def delete_user_role_dao(cls, db: AsyncSession, user_role: UserRoleModel): + """ + 删除用户角色关联信息数据库操作 + + :param db: orm对象 + :param user_role: 用户角色关联对象 + :return: + """ + await db.execute(delete(SysUserRole).where(SysUserRole.user_id.in_([user_role.user_id]))) + + @classmethod + async def delete_user_role_by_user_and_role_dao(cls, db: AsyncSession, user_role: UserRoleModel): + """ + 根据用户id及角色id删除用户角色关联信息数据库操作 + + :param db: orm对象 + :param user_role: 用户角色关联对象 + :return: + """ + await db.execute( + delete(SysUserRole).where( + SysUserRole.user_id == user_role.user_id if user_role.user_id else True, + SysUserRole.role_id == user_role.role_id if user_role.role_id else True, + ) + ) + + @classmethod + async def get_user_role_detail(cls, db: AsyncSession, user_role: UserRoleModel): + """ + 根据用户角色关联获取用户角色关联详细信息 + + :param db: orm对象 + :param user_role: 用户角色关联对象 + :return: 用户角色关联信息 + """ + user_role_info = ( + ( + await db.execute( + select(SysUserRole) + .where(SysUserRole.user_id == user_role.user_id, SysUserRole.role_id == user_role.role_id) + .distinct() + ) + ) + .scalars() + .first() + ) + + return user_role_info + + @classmethod + async def add_user_post_dao(cls, db: AsyncSession, user_post: UserPostModel): + """ + 新增用户岗位关联信息数据库操作 + + :param db: orm对象 + :param user_post: 用户岗位关联对象 + :return: + """ + db_user_post = SysUserPost(**user_post.model_dump()) + db.add(db_user_post) + + @classmethod + async def delete_user_post_dao(cls, db: AsyncSession, user_post: UserPostModel): + """ + 删除用户岗位关联信息数据库操作 + + :param db: orm对象 + :param user_post: 用户岗位关联对象 + :return: + """ + await db.execute(delete(SysUserPost).where(SysUserPost.user_id.in_([user_post.user_id]))) + + @classmethod + async def get_user_dept_info(cls, db: AsyncSession, dept_id: int): + dept_basic_info = ( + ( + await db.execute( + select(SysDept).where(SysDept.dept_id == dept_id, SysDept.status == '0', SysDept.del_flag == '0') + ) + ) + .scalars() + .first() + ) + return dept_basic_info diff --git a/ruoyi-fastapi-backend/module_admin/dao/visitor_type_dao.py b/ruoyi-fastapi-backend/module_admin/dao/visitor_type_dao.py new file mode 100644 index 0000000..cdf2bd3 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/visitor_type_dao.py @@ -0,0 +1,125 @@ +from sqlalchemy import delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.visitor_type_do import VisitorType +from module_admin.entity.vo.visitor_type_vo import Visitor_typeModel, Visitor_typePageQueryModel +from utils.page_util import PageUtil + + +class Visitor_typeDao: + """ + 访客类型模块数据库操作层 + """ + + @classmethod + async def get_system_visitor_type_list(cls, db: AsyncSession): + stmt = ( + select(VisitorType.type_id, VisitorType.name, VisitorType.value) + ) + return (await db.execute(stmt)).mappings().all() + + @classmethod + async def get_visitor_type_detail_by_id(cls, db: AsyncSession, type_id: int): + """ + 根据访客类型ID获取访客类型详细信息 + + :param db: orm对象 + :param type_id: 访客类型ID + :return: 访客类型信息对象 + """ + visitor_type_info = ( + ( + await db.execute( + select(VisitorType) + .where( + VisitorType.type_id == type_id + ) + ) + ) + .scalars() + .first() + ) + + return visitor_type_info + + @classmethod + async def get_visitor_type_detail_by_info(cls, db: AsyncSession, visitor_type: Visitor_typeModel): + """ + 根据访客类型参数获取访客类型信息 + + :param db: orm对象 + :param visitor_type: 访客类型参数对象 + :return: 访客类型信息对象 + """ + visitor_type_info = ( + ( + await db.execute( + select(VisitorType).where( + ) + ) + ) + .scalars() + .first() + ) + + return visitor_type_info + + @classmethod + async def get_visitor_type_list(cls, db: AsyncSession, query_object: Visitor_typePageQueryModel, is_page: bool = False): + """ + 根据查询参数获取访客类型列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 访客类型列表信息对象 + """ + query = ( + select(VisitorType) + .where( + VisitorType.name.like(f'%{query_object.name}%') if query_object.name else True, + VisitorType.value == query_object.value if query_object.value else True, + ) + .order_by(VisitorType.type_id) + .distinct() + ) + visitor_type_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return visitor_type_list + + @classmethod + async def add_visitor_type_dao(cls, db: AsyncSession, visitor_type: Visitor_typeModel): + """ + 新增访客类型数据库操作 + + :param db: orm对象 + :param visitor_type: 访客类型对象 + :return: + """ + db_visitor_type = VisitorType(**visitor_type.model_dump(exclude={})) + db.add(db_visitor_type) + await db.flush() + + return db_visitor_type + + @classmethod + async def edit_visitor_type_dao(cls, db: AsyncSession, visitor_type: dict): + """ + 编辑访客类型数据库操作 + + :param db: orm对象 + :param visitor_type: 需要更新的访客类型字典 + :return: + """ + await db.execute(update(VisitorType), [visitor_type]) + + @classmethod + async def delete_visitor_type_dao(cls, db: AsyncSession, visitor_type: Visitor_typeModel): + """ + 删除访客类型数据库操作 + + :param db: orm对象 + :param visitor_type: 访客类型对象 + :return: + """ + await db.execute(delete(VisitorType).where(VisitorType.type_id.in_([visitor_type.type_id]))) + diff --git a/ruoyi-fastapi-backend/module_admin/dao/words_dao.py b/ruoyi-fastapi-backend/module_admin/dao/words_dao.py new file mode 100644 index 0000000..08a6de1 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/dao/words_dao.py @@ -0,0 +1,161 @@ +from datetime import datetime, time +from sqlalchemy import delete, select, update, desc +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.do.words_do import GuideWords +from module_admin.entity.vo.words_vo import WordsModel, WordsPageQueryModel +from utils.page_util import PageUtil + + +class WordsDao: + """ + 引导词模块数据库操作层 + """ + + @classmethod + async def get_words_by_type(cls, db: AsyncSession, type: str): + """ + 获取随机引导词 + + :param db: orm对象 + :param type: 引导词类型 + :return: 引导词对象 + """ + guide_word = ( + ( + await db.execute( + select(GuideWords.text) + .where( + GuideWords.type == type, + GuideWords.del_flag == '0' + ) + ) + ) + ) + return guide_word.scalars().all() + + + @classmethod + async def edit_del_words(cls, db: AsyncSession, guide_ids: str, del_flag:str): + + guide_id_list = guide_ids.strip(' ').split(',') + + stmt = ( + update(GuideWords) + .values(del_flag = del_flag) + .where(GuideWords.guide_id.in_(guide_id_list)) + ) + + return await db.execute(stmt) + + @classmethod + async def get_words_detail_by_id(cls, db: AsyncSession, guide_id: int): + """ + 根据引导词id获取引导词详细信息 + + :param db: orm对象 + :param guide_id: 引导词id + :return: 引导词信息对象 + """ + words_info = ( + ( + await db.execute( + select(GuideWords) + .where( + GuideWords.guide_id == guide_id + ) + ) + ) + .scalars() + .first() + ) + + return words_info + + @classmethod + async def get_words_detail_by_info(cls, db: AsyncSession, words: WordsModel): + """ + 根据引导词参数获取引导词信息 + + :param db: orm对象 + :param words: 引导词参数对象 + :return: 引导词信息对象 + """ + words_info = ( + ( + await db.execute( + select(GuideWords).where( + ) + ) + ) + .scalars() + .first() + ) + + return words_info + + @classmethod + async def get_words_list(cls, db: AsyncSession, query_object: WordsPageQueryModel, is_page: bool = False): + """ + 根据查询参数获取引导词列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 引导词列表信息对象 + """ + query = ( + select(GuideWords) + .where( + GuideWords.type == query_object.type if query_object.type else True, + GuideWords.text.like(f'%{query_object.text}%') if query_object.text else True, + GuideWords.create_time.between( + datetime.combine(datetime.strptime(query_object.begin_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_time and query_object.end_time + else True, + ) + .order_by(desc(GuideWords.create_time)) + .distinct() + ) + words_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return words_list + + @classmethod + async def add_words_dao(cls, db: AsyncSession, words: WordsModel): + """ + 新增引导词数据库操作 + + :param db: orm对象 + :param words: 引导词对象 + :return: + """ + db_words = GuideWords(**words.model_dump(exclude={})) + db.add(db_words) + await db.flush() + + return db_words + + @classmethod + async def edit_words_dao(cls, db: AsyncSession, words: dict): + """ + 编辑引导词数据库操作 + + :param db: orm对象 + :param words: 需要更新的引导词字典 + :return: + """ + await db.execute(update(GuideWords), [words]) + + @classmethod + async def delete_words_dao(cls, db: AsyncSession, words: WordsModel): + """ + 删除引导词数据库操作 + + :param db: orm对象 + :param words: 引导词对象 + :return: + """ + await db.execute(delete(GuideWords).where(GuideWords.guide_id.in_([words.guide_id]))) + diff --git a/ruoyi-fastapi-backend/module_admin/entity/__init__.py b/ruoyi-fastapi-backend/module_admin/entity/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/__init__.py b/ruoyi-fastapi-backend/module_admin/entity/do/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/config_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/config_do.py new file mode 100644 index 0000000..012d2be --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/config_do.py @@ -0,0 +1,22 @@ +from datetime import datetime +from sqlalchemy import Column, DateTime, Integer, String +from config.database import Base + + +class SysConfig(Base): + """ + 参数配置表 + """ + + __tablename__ = 'sys_config' + + config_id = Column(Integer, primary_key=True, autoincrement=True, comment='参数主键') + config_name = Column(String(100), nullable=True, default='', comment='参数名称') + config_key = Column(String(100), nullable=True, default='', comment='参数键名') + config_value = Column(String(500), nullable=True, default='', comment='参数键值') + config_type = Column(String(1), nullable=True, default='N', comment='系统内置(Y是 N否)') + create_by = Column(String(64), nullable=True, default='', comment='创建者') + create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间') + update_by = Column(String(64), nullable=True, default='', comment='更新者') + update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间') + remark = Column(String(500), nullable=True, default=None, comment='备注') diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/dept_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/dept_do.py new file mode 100644 index 0000000..44e2f02 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/dept_do.py @@ -0,0 +1,26 @@ +from datetime import datetime +from sqlalchemy import Column, DateTime, Integer, String +from config.database import Base + + +class SysDept(Base): + """ + 部门表 + """ + + __tablename__ = 'sys_dept' + + dept_id = Column(Integer, primary_key=True, autoincrement=True, comment='部门id') + parent_id = Column(Integer, default=0, comment='父部门id') + ancestors = Column(String(50), nullable=True, default='', comment='祖级列表') + dept_name = Column(String(30), nullable=True, default='', comment='部门名称') + order_num = Column(Integer, default=0, comment='显示顺序') + leader = Column(String(20), nullable=True, default=None, comment='负责人') + phone = Column(String(11), nullable=True, default=None, comment='联系电话') + email = Column(String(50), nullable=True, default=None, comment='邮箱') + status = Column(String(1), nullable=True, default='0', comment='部门状态(0正常 1停用)') + del_flag = Column(String(1), nullable=True, default='0', comment='删除标志(0代表存在 2代表删除)') + create_by = Column(String(64), nullable=True, default='', comment='创建者') + create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间') + update_by = Column(String(64), nullable=True, default='', comment='更新者') + update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间') diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/dict_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/dict_do.py new file mode 100644 index 0000000..7a155ea --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/dict_do.py @@ -0,0 +1,46 @@ +from datetime import datetime +from sqlalchemy import Column, DateTime, Integer, String, UniqueConstraint +from config.database import Base + + +class SysDictType(Base): + """ + 字典类型表 + """ + + __tablename__ = 'sys_dict_type' + + dict_id = Column(Integer, primary_key=True, autoincrement=True, comment='字典主键') + dict_name = Column(String(100), nullable=True, default='', comment='字典名称') + dict_type = Column(String(100), nullable=True, default='', comment='字典类型') + status = Column(String(1), nullable=True, default='0', comment='状态(0正常 1停用)') + create_by = Column(String(64), nullable=True, default='', comment='创建者') + create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间') + update_by = Column(String(64), nullable=True, default='', comment='更新者') + update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间') + remark = Column(String(500), nullable=True, default=None, comment='备注') + + __table_args__ = (UniqueConstraint('dict_type', name='uq_sys_dict_type_dict_type'),) + + +class SysDictData(Base): + """ + 字典数据表 + """ + + __tablename__ = 'sys_dict_data' + + dict_code = Column(Integer, primary_key=True, autoincrement=True, comment='字典编码') + dict_sort = Column(Integer, nullable=True, default=0, comment='字典排序') + dict_label = Column(String(100), nullable=True, default='', comment='字典标签') + dict_value = Column(String(100), nullable=True, default='', comment='字典键值') + dict_type = Column(String(100), nullable=True, default='', comment='字典类型') + css_class = Column(String(100), nullable=True, default=None, comment='样式属性(其他样式扩展)') + list_class = Column(String(100), nullable=True, default=None, comment='表格回显样式') + is_default = Column(String(1), nullable=True, default='N', comment='是否默认(Y是 N否)') + status = Column(String(1), nullable=True, default='0', comment='状态(0正常 1停用)') + create_by = Column(String(64), nullable=True, default='', comment='创建者') + create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间') + update_by = Column(String(64), nullable=True, default='', comment='更新者') + update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间') + remark = Column(String(500), nullable=True, default=None, comment='备注') diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/door_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/door_do.py new file mode 100644 index 0000000..fe4d4fd --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/door_do.py @@ -0,0 +1,24 @@ +from sqlalchemy import Column, BigInteger, String, DateTime +from config.database import Base + + +class Door(Base): + """ + 门禁设备表 + """ + + __tablename__ = 'door' + + id = Column(BigInteger, primary_key=True, autoincrement=True, nullable=False, comment='主键自增') + indexCode = Column(String(64), nullable=True, comment='设备索引编码') + name = Column(String(32), nullable=True, comment='门禁名称') + install_location = Column(String(32), nullable=True, comment='位置名称') + status = Column(String(2), nullable=True, comment='设备状态, 0 离线, 1 正常在线') + permission = Column(String(2), nullable=True, comment='权限, 0 禁用, 1 正常调用') + create_time = Column(DateTime, nullable=True, comment='创建时间') + create_by = Column(String(64), nullable=True, comment='创建者') + update_time = Column(DateTime, nullable=True, comment='更新时间') + update_by = Column(String(64), nullable=True, comment='更新者') + + + diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/explanation_content_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/explanation_content_do.py new file mode 100644 index 0000000..6eb29dc --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/explanation_content_do.py @@ -0,0 +1,25 @@ +from sqlalchemy import Column, Text, DateTime, String, BigInteger, Integer +from config.database import Base + + +class ExplanationContent(Base): + """ + 讲解内容表 + """ + + __tablename__ = 'explanation_content' + + explanation_content_id = Column(BigInteger, primary_key=True, autoincrement=True, nullable=False, comment='主键ID') + title = Column(String(64), nullable=False, comment='标题') + content = Column(Text, nullable=True, comment='讲解内容') + type = Column(String(2), nullable=True, comment='类型(0科技类)') + spend_time = Column(Integer, nullable=True, comment='预计时长(分钟)') + language = Column(String(2), nullable=True, comment='语言(0中文,1英文)') + status = Column(String(2), nullable=True, comment='状态(0未开始,1已就绪,2播放中,3已完成)') + create_time = Column(DateTime, nullable=True, comment='创建时间') + create_by = Column(String(64), nullable=True, comment='创建者') + update_time = Column(DateTime, nullable=True, comment='修改时间') + update_by = Column(String(64), nullable=True, comment='修改者') + + + diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/explanation_content_type_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/explanation_content_type_do.py new file mode 100644 index 0000000..49fe731 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/explanation_content_type_do.py @@ -0,0 +1,21 @@ +from sqlalchemy import BigInteger, DateTime, String, Column +from config.database import Base + + +class ExplanationContentType(Base): + """ + 讲解内容类型表 + """ + + __tablename__ = 'explanation_content_type' + + content_type_id = Column(BigInteger, primary_key=True, autoincrement=True, nullable=False, comment='主键ID') + name = Column(String(32), nullable=True, comment='类型名称') + type_value = Column(String(2), nullable=True, comment='类型值(与讲解内容表中的type对应)') + create_time = Column(DateTime, nullable=True, comment='创建时间') + create_by = Column(String(64), nullable=True, comment='创建者') + update_time = Column(DateTime, nullable=True, comment='修改时间') + update_by = Column(String(64), nullable=True, comment='修改者') + + + diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/explanation_style_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/explanation_style_do.py new file mode 100644 index 0000000..df7a137 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/explanation_style_do.py @@ -0,0 +1,21 @@ +from sqlalchemy import BigInteger, DateTime, String, Column +from config.database import Base + + +class ExplanationStyle(Base): + """ + 讲解风格表 + """ + + __tablename__ = 'explanation_style' + + explanation_style_id = Column(BigInteger, primary_key=True, autoincrement=True, nullable=False, comment='主键ID') + name = Column(String(32), nullable=True, comment='风格名称') + detail = Column(String(64), nullable=True, comment='风格简介') + create_time = Column(DateTime, nullable=True, comment='创建时间') + create_by = Column(String(64), nullable=True, comment='创建者') + update_time = Column(DateTime, nullable=True, comment='修改时间') + update_by = Column(String(64), nullable=True, comment='修改者') + + + diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/explanation_style_robot_pair_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/explanation_style_robot_pair_do.py new file mode 100644 index 0000000..3ee4b24 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/explanation_style_robot_pair_do.py @@ -0,0 +1,24 @@ +from sqlalchemy import BigInteger, DateTime, String, Column +from config.database import Base + + +class ExplanationStyleRobotPairing(Base): + """ + 讲解风格--机器人配对表 + """ + + __tablename__ = 'explanation_style_robot_pairing' + + pairing_id = Column(BigInteger, primary_key=True, autoincrement=True, nullable=False, comment='主键ID') + robot_id = Column(BigInteger, nullable=False, comment='机器人ID(关联robot_info表)') + robot_role_id = Column(BigInteger, nullable=False, comment='机器人角色ID(关联robot_role表)') + explanation_style_id = Column(BigInteger, nullable=False, comment='讲解风格ID(关联explanation_style表)') + prompt = Column(String(128), nullable=True, comment='提示词') + status = Column(String(2), nullable=True, comment='状态(0停用,1使用)') + create_time = Column(DateTime, nullable=True, comment='创建时间') + create_by = Column(String(64), nullable=True, comment='创建者') + update_time = Column(DateTime, nullable=True, comment='修改时间') + update_by = Column(String(64), nullable=True, comment='修改者') + + + diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/identification_record_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/identification_record_do.py new file mode 100644 index 0000000..7145067 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/identification_record_do.py @@ -0,0 +1,24 @@ +from sqlalchemy import DateTime, String, BigInteger, Column +from config.database import Base + + +class IdentificationRecord(Base): + """ + 识别记录表 + """ + + __tablename__ = 'identification_record' + + id = Column(BigInteger, primary_key=True, autoincrement=True, nullable=False, comment='主键 自增') + person_name = Column(String(20), nullable=True, comment='识别人姓名') + door_name = Column(String(20), nullable=True, comment='门禁点') + status = Column(String(2), nullable=True, comment='识别结果, 0 拒绝, 1 通过') + source = Column(String(20), nullable=True, comment='识别来源') + pic_uri = Column(String(256), nullable=True, comment='图像url') + create_time = Column(DateTime, nullable=True, comment='创建时间') + create_by = Column(String(64), nullable=True, comment='创建者') + conf = Column(BigInteger, default=0, comment='识别置信度') + person_status = Column(String(2), nullable=True, comment=' 0 陌生人, 1 员工, 2 访客') + + + diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/identification_statistics_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/identification_statistics_do.py new file mode 100644 index 0000000..d43c5a9 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/identification_statistics_do.py @@ -0,0 +1,20 @@ +from sqlalchemy import String, Column, BigInteger, DateTime +from config.database import Base + + +class IdentificationStatistics(Base): + """ + 识别统计表 + """ + + __tablename__ = 'identification_statistics' + + id = Column(BigInteger, primary_key=True, autoincrement=True, nullable=False, comment='主键 自增') + employ = Column(BigInteger, nullable=True, comment='员工识别成功数') + visitor = Column(BigInteger, nullable=True, comment='访客识别成功数') + stranger = Column(BigInteger, nullable=True, comment='位置人员识别失败数') + create_time = Column(DateTime, nullable=True, comment='创建时间') + create_by = Column(String(64), nullable=True, comment='创建者') + + + diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/info_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/info_do.py new file mode 100644 index 0000000..2eeed5f --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/info_do.py @@ -0,0 +1,33 @@ +from sqlalchemy import DateTime, String, BigInteger, CHAR, Column +from config.database import Base + + +class RobotInfo(Base): + """ + 机器人信息表 + """ + + __tablename__ = 'robot_info' + + robot_id = Column(BigInteger, primary_key=True, autoincrement=True, nullable=False, comment='机器人ID') + name = Column(String(32), nullable=False, comment='机器人名称') + model = Column(String(32), nullable=True, comment='机器人型号') + serial = Column(String(32), nullable=True, comment='序列号') + online = Column(String(2), nullable=True, comment='机器人在线状态(0在线, 1离线)') + power = Column(String(3), nullable=True, comment='电量') + run_time = Column(BigInteger, nullable=True, comment='总运行时长(单位:分钟)') + position = Column(String(32), nullable=True, comment='位置') + image = Column(String(512), nullable=True, comment='机器人外观图地址') + create_time = Column(DateTime, nullable=True, comment='创建时间') + create_by = Column(String(64), nullable=True, comment='创建者') + update_time = Column(DateTime, nullable=True, comment='修改时间') + update_by = Column(String(64), nullable=True, comment='修改者') + del_flag = Column(CHAR(1), nullable=False, comment='删除标志(0存在, 2删除)') + remark = Column(String(512), nullable=True, comment='备注') + last_charge = Column(DateTime, nullable=True, comment='上一次充电时间') + ip = Column(String(16), nullable=True, comment='机器人ip') + status = Column(String(2), nullable=False, comment='使用情况(0待机,1正在运行)') + video_uri = Column(String(512), nullable=True, comment='视频uri') + + + diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/job_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/job_do.py new file mode 100644 index 0000000..b2cbfb8 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/job_do.py @@ -0,0 +1,54 @@ +from datetime import datetime +from sqlalchemy import Column, DateTime, Integer, String +from config.database import Base + + +class SysJob(Base): + """ + 定时任务调度表 + """ + + __tablename__ = 'sys_job' + + job_id = Column(Integer, primary_key=True, autoincrement=True, comment='任务ID') + job_name = Column(String(64), nullable=True, default='', comment='任务名称') + job_group = Column(String(64), nullable=True, default='default', comment='任务组名') + job_executor = Column(String(64), nullable=True, default='default', comment='任务执行器') + invoke_target = Column(String(500), nullable=False, comment='调用目标字符串') + job_args = Column(String(255), nullable=True, default='', comment='位置参数') + job_kwargs = Column(String(255), nullable=True, default='', comment='关键字参数') + cron_expression = Column(String(255), nullable=True, default='', comment='cron执行表达式') + misfire_policy = Column( + String(20), + nullable=True, + default='3', + comment='计划执行错误策略(1立即执行 2执行一次 3放弃执行)', + ) + concurrent = Column(String(1), nullable=True, default='1', comment='是否并发执行(0允许 1禁止)') + status = Column(String(1), nullable=True, default='0', comment='状态(0正常 1暂停)') + create_by = Column(String(64), nullable=True, default='', comment='创建者') + create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间') + update_by = Column(String(64), nullable=True, default='', comment='更新者') + update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间') + remark = Column(String(500), nullable=True, default='', comment='备注信息') + + +class SysJobLog(Base): + """ + 定时任务调度日志表 + """ + + __tablename__ = 'sys_job_log' + + job_log_id = Column(Integer, primary_key=True, autoincrement=True, comment='任务日志ID') + job_name = Column(String(64), nullable=False, comment='任务名称') + job_group = Column(String(64), nullable=False, comment='任务组名') + job_executor = Column(String(64), nullable=False, comment='任务执行器') + invoke_target = Column(String(500), nullable=False, comment='调用目标字符串') + job_args = Column(String(255), nullable=True, default='', comment='位置参数') + job_kwargs = Column(String(255), nullable=True, default='', comment='关键字参数') + job_trigger = Column(String(255), nullable=True, default='', comment='任务触发器') + job_message = Column(String(500), nullable=True, default='', comment='日志信息') + status = Column(String(1), nullable=True, default='0', comment='执行状态(0正常 1失败)') + exception_info = Column(String(2000), nullable=True, default='', comment='异常信息') + create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间') diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/log_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/log_do.py new file mode 100644 index 0000000..f9e14ab --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/log_do.py @@ -0,0 +1,54 @@ +from datetime import datetime +from sqlalchemy import BigInteger, Column, DateTime, Index, Integer, String +from config.database import Base + + +class SysLogininfor(Base): + """ + 系统访问记录 + """ + + __tablename__ = 'sys_logininfor' + + info_id = Column(Integer, primary_key=True, autoincrement=True, comment='访问ID') + user_name = Column(String(50), nullable=True, default='', comment='用户账号') + ipaddr = Column(String(128), nullable=True, default='', comment='登录IP地址') + login_location = Column(String(255), nullable=True, default='', comment='登录地点') + browser = Column(String(50), nullable=True, default='', comment='浏览器类型') + os = Column(String(50), nullable=True, default='', comment='操作系统') + status = Column(String(1), nullable=True, default='0', comment='登录状态(0成功 1失败)') + msg = Column(String(255), nullable=True, default='', comment='提示消息') + login_time = Column(DateTime, nullable=True, default=datetime.now(), comment='访问时间') + + idx_sys_logininfor_s = Index('idx_sys_logininfor_s', status) + idx_sys_logininfor_lt = Index('idx_sys_logininfor_lt', login_time) + + +class SysOperLog(Base): + """ + 操作日志记录 + """ + + __tablename__ = 'sys_oper_log' + + oper_id = Column(BigInteger, primary_key=True, autoincrement=True, comment='日志主键') + title = Column(String(50), nullable=True, default='', comment='模块标题') + business_type = Column(Integer, default=0, comment='业务类型(0其它 1新增 2修改 3删除)') + method = Column(String(100), nullable=True, default='', comment='方法名称') + request_method = Column(String(10), nullable=True, default='', comment='请求方式') + operator_type = Column(Integer, default=0, comment='操作类别(0其它 1后台用户 2手机端用户)') + oper_name = Column(String(50), nullable=True, default='', comment='操作人员') + dept_name = Column(String(50), nullable=True, default='', comment='部门名称') + oper_url = Column(String(255), nullable=True, default='', comment='请求URL') + oper_ip = Column(String(128), nullable=True, default='', comment='主机地址') + oper_location = Column(String(255), nullable=True, default='', comment='操作地点') + oper_param = Column(String(2000), nullable=True, default='', comment='请求参数') + json_result = Column(String(2000), nullable=True, default='', comment='返回参数') + status = Column(Integer, default=0, comment='操作状态(0正常 1异常)') + error_msg = Column(String(2000), nullable=True, default='', comment='错误消息') + oper_time = Column(DateTime, nullable=True, default=datetime.now(), comment='操作时间') + cost_time = Column(BigInteger, default=0, comment='消耗时间') + + idx_sys_oper_log_bt = Index('idx_sys_oper_log_bt', business_type) + idx_sys_oper_log_s = Index('idx_sys_oper_log_s', status) + idx_sys_oper_log_ot = Index('idx_sys_oper_log_ot', oper_time) diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/menu_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/menu_do.py new file mode 100644 index 0000000..d3ccffa --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/menu_do.py @@ -0,0 +1,32 @@ +from datetime import datetime +from sqlalchemy import Column, DateTime, Integer, String +from config.database import Base + + +class SysMenu(Base): + """ + 菜单权限表 + """ + + __tablename__ = 'sys_menu' + + menu_id = Column(Integer, primary_key=True, autoincrement=True, comment='菜单ID') + menu_name = Column(String(50), nullable=False, default='', comment='菜单名称') + parent_id = Column(Integer, default=0, comment='父菜单ID') + order_num = Column(Integer, default=0, comment='显示顺序') + path = Column(String(200), nullable=True, default='', comment='路由地址') + component = Column(String(255), nullable=True, default=None, comment='组件路径') + query = Column(String(255), nullable=True, default=None, comment='路由参数') + route_name = Column(String(50), nullable=True, default='', comment='路由名称') + is_frame = Column(Integer, default=1, comment='是否为外链(0是 1否)') + is_cache = Column(Integer, default=0, comment='是否缓存(0缓存 1不缓存)') + menu_type = Column(String(1), nullable=True, default='', comment='菜单类型(M目录 C菜单 F按钮)') + visible = Column(String(1), nullable=True, default='0', comment='菜单状态(0显示 1隐藏)') + status = Column(String(1), nullable=True, default='0', comment='菜单状态(0正常 1停用)') + perms = Column(String(100), nullable=True, default=None, comment='权限标识') + icon = Column(String(100), nullable=True, default='#', comment='菜单图标') + create_by = Column(String(64), nullable=True, default='', comment='创建者') + create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间') + update_by = Column(String(64), nullable=True, default='', comment='更新者') + update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间') + remark = Column(String(500), nullable=True, default='', comment='备注') diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/message_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/message_do.py new file mode 100644 index 0000000..a423f60 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/message_do.py @@ -0,0 +1,24 @@ +from sqlalchemy import DateTime, String, BigInteger, Column +from config.database import Base + + +class Message(Base): + """ + 系统消息表 + """ + + __tablename__ = 'message' + + message_id = Column(BigInteger, primary_key=True, autoincrement=True, nullable=False, comment='消息主键') + type = Column(String(2), nullable=False, comment='类型(0告警消息)') + status = Column(String(2), nullable=False, comment='状态(0未处理,1已处理)') + detail = Column(String(512), nullable=True, comment='消息描述') + create_time = Column(DateTime, nullable=False, comment='创建时间') + create_by = Column(String(64), nullable=True, comment='创建者') + update_time = Column(DateTime, nullable=False, comment='更新时间') + update_by = Column(String(64), nullable=False, comment='更新者') + remark = Column(String(512), nullable=True, comment='备注') + specific_type = Column(String(2), nullable=True, comment='具体消息类型(0 设备故障, 1 系统异常, 2安全告警, 3 网络异常)') + + + diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/notice_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/notice_do.py new file mode 100644 index 0000000..9d1eb98 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/notice_do.py @@ -0,0 +1,22 @@ +from datetime import datetime +from sqlalchemy import Column, DateTime, Integer, LargeBinary, String +from config.database import Base + + +class SysNotice(Base): + """ + 通知公告表 + """ + + __tablename__ = 'sys_notice' + + notice_id = Column(Integer, primary_key=True, autoincrement=True, comment='公告ID') + notice_title = Column(String(50), nullable=False, comment='公告标题') + notice_type = Column(String(1), nullable=False, comment='公告类型(1通知 2公告)') + notice_content = Column(LargeBinary, comment='公告内容') + status = Column(String(1), default='0', comment='公告状态(0正常 1关闭)') + create_by = Column(String(64), default='', comment='创建者') + create_time = Column(DateTime, comment='创建时间', default=datetime.now()) + update_by = Column(String(64), default='', comment='更新者') + update_time = Column(DateTime, comment='更新时间', default=datetime.now()) + remark = Column(String(255), comment='备注') diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/post_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/post_do.py new file mode 100644 index 0000000..f231f72 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/post_do.py @@ -0,0 +1,22 @@ +from datetime import datetime +from sqlalchemy import Column, DateTime, Integer, String +from config.database import Base + + +class SysPost(Base): + """ + 岗位信息表 + """ + + __tablename__ = 'sys_post' + + post_id = Column(Integer, primary_key=True, autoincrement=True, comment='岗位ID') + post_code = Column(String(64), nullable=False, comment='岗位编码') + post_name = Column(String(50), nullable=False, comment='岗位名称') + post_sort = Column(Integer, nullable=False, comment='显示顺序') + status = Column(String(1), nullable=False, default='0', comment='状态(0正常 1停用)') + create_by = Column(String(64), default='', comment='创建者') + create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间') + update_by = Column(String(64), default='', comment='更新者') + update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间') + remark = Column(String(500), nullable=True, default=None, comment='备注') diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/robot_action_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/robot_action_do.py new file mode 100644 index 0000000..e4acfdd --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/robot_action_do.py @@ -0,0 +1,21 @@ +from sqlalchemy import Integer, String, BigInteger, Column, DateTime +from config.database import Base + + +class RobotAction(Base): + """ + 机器人动作表 + """ + + __tablename__ = 'robot_action' + + id = Column(BigInteger, primary_key=True, autoincrement=True, nullable=False, comment='主键 自增') + name = Column(String(50), nullable=True, comment='动作名') + description = Column(String(50), nullable=True, comment='描述') + interval_min = Column(Integer, nullable=True, comment='最小时间间隔 单位s') + interval_max = Column(Integer, nullable=True, comment='最大时间间隔 单位s') + status = Column(String(2), nullable=True, comment='动作状态 0 未启用, 1已启用') + update_time = Column(DateTime, nullable=True, comment='修改时间') + + + diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/robot_role_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/robot_role_do.py new file mode 100644 index 0000000..eac1af0 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/robot_role_do.py @@ -0,0 +1,22 @@ +from sqlalchemy import BigInteger, String, Column, DateTime +from config.database import Base + + +class RobotRole(Base): + """ + 系统角色表 + """ + + __tablename__ = 'robot_role' + + robot_role_id = Column(BigInteger, primary_key=True, autoincrement=True, nullable=False, comment='机器人角色ID') + rolename = Column(String(32), nullable=False, comment='角色名称') + detail = Column(String(512), nullable=True, comment='角色介绍') + create_time = Column(DateTime, nullable=False, comment='创建时间') + create_by = Column(String(64), nullable=False, comment='创建者') + update_time = Column(DateTime, nullable=False, comment='修改时间') + update_by = Column(String(64), nullable=False, comment='修改者') + remark = Column(String(512), nullable=True, comment='备注') + + + diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/robot_role_pairing_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/robot_role_pairing_do.py new file mode 100644 index 0000000..ee56a05 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/robot_role_pairing_do.py @@ -0,0 +1,32 @@ +from sqlalchemy import DateTime, Column, BigInteger, String +from config.database import Base + + +class RobotRolePairing(Base): + """ + 角色-机器人-映射表 + """ + + __tablename__ = 'robot_role_pairing' + + pairing_id = Column(BigInteger, primary_key=True, autoincrement=True, nullable=False, comment='主键ID') + robot_role_id = Column(BigInteger, nullable=False, comment='机器人角色ID') + robot_id = Column(BigInteger, nullable=False, comment='机器人ID') + status = Column(String(2), nullable=False, comment='使用情况(0停用,1使用)') + update_time = Column(DateTime, nullable=False, comment='更新时间') + update_by = Column(String(64), nullable=False, comment='修改者') + + +# +# class RolePairing(Base): +# """ +# 角色-机器人-映射表对应pydantic模型, 查询角色-机器人结果的返回模型 +# """ +# +# +# pairing_id = Column(BigInteger, primary_key=True, autoincrement=True, nullable=False, comment='主键ID') +# rolename = Column(String(32), nullable=False, comment='角色名称') +# detail = Column(String(512), nullable=True, comment='角色介绍') +# status = Column(String(2), nullable=False, comment='使用情况(0停用,1使用)') + + diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/role_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/role_do.py new file mode 100644 index 0000000..58d4de1 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/role_do.py @@ -0,0 +1,52 @@ +from datetime import datetime +from sqlalchemy import Column, DateTime, Integer, String +from config.database import Base + + +class SysRole(Base): + """ + 角色信息表 + """ + + __tablename__ = 'sys_role' + + role_id = Column(Integer, primary_key=True, autoincrement=True, comment='角色ID') + role_name = Column(String(30), nullable=False, comment='角色名称') + role_key = Column(String(100), nullable=False, comment='角色权限字符串') + role_sort = Column(Integer, nullable=False, comment='显示顺序') + data_scope = Column( + String(1), + default='1', + comment='数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)', + ) + menu_check_strictly = Column(Integer, default=1, comment='菜单树选择项是否关联显示') + dept_check_strictly = Column(Integer, default=1, comment='部门树选择项是否关联显示') + status = Column(String(1), nullable=False, default='0', comment='角色状态(0正常 1停用)') + del_flag = Column(String(1), default='0', comment='删除标志(0代表存在 2代表删除)') + create_by = Column(String(64), default='', comment='创建者') + create_time = Column(DateTime, default=datetime.now(), comment='创建时间') + update_by = Column(String(64), default='', comment='更新者') + update_time = Column(DateTime, default=datetime.now(), comment='更新时间') + remark = Column(String(500), default=None, comment='备注') + + +class SysRoleDept(Base): + """ + 角色和部门关联表 + """ + + __tablename__ = 'sys_role_dept' + + role_id = Column(Integer, primary_key=True, nullable=False, comment='角色ID') + dept_id = Column(Integer, primary_key=True, nullable=False, comment='部门ID') + + +class SysRoleMenu(Base): + """ + 角色和菜单关联表 + """ + + __tablename__ = 'sys_role_menu' + + role_id = Column(Integer, primary_key=True, nullable=False, comment='角色ID') + menu_id = Column(Integer, primary_key=True, nullable=False, comment='菜单ID') diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/sys_alert_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/sys_alert_do.py new file mode 100644 index 0000000..425942a --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/sys_alert_do.py @@ -0,0 +1,22 @@ +from sqlalchemy import String, Column, BigInteger, DateTime +from config.database import Base + + +class SysAlert(Base): + """ + 系统告警表 + """ + + __tablename__ = 'sys_alert' + + id = Column(BigInteger, primary_key=True, autoincrement=True, nullable=False, comment='主键 自增') + detail = Column(String(256), nullable=True, comment='告警描述') + type = Column(String(2), nullable=True, comment='告警类型') + status = Column(String(2), nullable=True, comment='告警状态, 0 未处理, 1 已处理') + create_time = Column(DateTime, nullable=True, comment='创建时间') + create_by = Column(String(64), nullable=True, comment='创建者') + update_time = Column(DateTime, nullable=True, comment='更新时间') + update_by = Column(String(64), nullable=True, comment='更新者') + + + diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/sys_statistics_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/sys_statistics_do.py new file mode 100644 index 0000000..38d0918 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/sys_statistics_do.py @@ -0,0 +1,25 @@ +from sqlalchemy import Date, DateTime, String, BigInteger, Column, Integer +from config.database import Base + + +class SysStatistics(Base): + """ + 系统统计数据表 + """ + + __tablename__ = 'sys_statistics' + + statistic_id = Column(BigInteger, primary_key=True, autoincrement=True, nullable=False, comment='统计项目ID') + stat_date = Column(Date, nullable=False, comment='统计日期') + llm_call = Column(BigInteger, nullable=True, comment='大模型调用量') + access_control_count = Column(BigInteger, nullable=True, comment='门禁通行量') + visitor_guide = Column(BigInteger, nullable=True, comment='访客引导次数') + exhibition_explanation = Column(BigInteger, nullable=True, comment='展厅讲解次数') + access_control_success_rate = Column(Integer, nullable=True, comment='门禁识别成功率(%)') + create_time = Column(DateTime, nullable=True, comment='创建时间') + create_by = Column(String(64), nullable=True, comment='创建者') + update_time = Column(DateTime, nullable=False, comment='修改时间') + update_by = Column(String(64), nullable=True, comment='修改者') + + + diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/user_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/user_do.py new file mode 100644 index 0000000..2dd0ba0 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/user_do.py @@ -0,0 +1,53 @@ +from datetime import datetime +from sqlalchemy import Column, DateTime, Integer, String +from config.database import Base + + +class SysUser(Base): + """ + 用户信息表 + """ + + __tablename__ = 'sys_user' + + user_id = Column(Integer, primary_key=True, autoincrement=True, comment='用户ID') + dept_id = Column(Integer, default=None, comment='部门ID') + user_name = Column(String(30), nullable=False, comment='用户账号') + nick_name = Column(String(30), nullable=False, comment='用户昵称') + user_type = Column(String(2), default='00', comment='用户类型(00系统用户)') + email = Column(String(50), default='', comment='用户邮箱') + phonenumber = Column(String(11), default='', comment='手机号码') + sex = Column(String(1), default='0', comment='用户性别(0男 1女 2未知)') + avatar = Column(String(100), default='', comment='头像地址') + password = Column(String(100), default='', comment='密码') + status = Column(String(1), default='0', comment='帐号状态(0正常 1停用)') + del_flag = Column(String(1), default='0', comment='删除标志(0代表存在 2代表删除)') + login_ip = Column(String(128), default='', comment='最后登录IP') + login_date = Column(DateTime, comment='最后登录时间') + create_by = Column(String(64), default='', comment='创建者') + create_time = Column(DateTime, comment='创建时间', default=datetime.now()) + update_by = Column(String(64), default='', comment='更新者') + update_time = Column(DateTime, comment='更新时间', default=datetime.now()) + remark = Column(String(500), default=None, comment='备注') + + +class SysUserRole(Base): + """ + 用户和角色关联表 + """ + + __tablename__ = 'sys_user_role' + + user_id = Column(Integer, primary_key=True, nullable=False, comment='用户ID') + role_id = Column(Integer, primary_key=True, nullable=False, comment='角色ID') + + +class SysUserPost(Base): + """ + 用户与岗位关联表 + """ + + __tablename__ = 'sys_user_post' + + user_id = Column(Integer, primary_key=True, nullable=False, comment='用户ID') + post_id = Column(Integer, primary_key=True, nullable=False, comment='岗位ID') diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/visitor_type_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/visitor_type_do.py new file mode 100644 index 0000000..8f698af --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/visitor_type_do.py @@ -0,0 +1,21 @@ +from sqlalchemy import BigInteger, DateTime, String, Column +from config.database import Base + + +class VisitorType(Base): + """ + 访客类型表 + """ + + __tablename__ = 'visitor_type' + + type_id = Column(BigInteger, primary_key=True, autoincrement=True, nullable=False, comment='访客类型ID') + name = Column(String(64), nullable=True, comment='类型名称') + value = Column(String(2), nullable=False, comment='类型值') + create_time = Column(DateTime, nullable=True, comment='创建时间') + create_by = Column(String(64), nullable=True, comment='创建者') + update_time = Column(DateTime, nullable=True, comment='修改时间') + update_by = Column(String(64), nullable=True, comment='修改者') + + + diff --git a/ruoyi-fastapi-backend/module_admin/entity/do/words_do.py b/ruoyi-fastapi-backend/module_admin/entity/do/words_do.py new file mode 100644 index 0000000..d253fee --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/do/words_do.py @@ -0,0 +1,26 @@ +from sqlalchemy import CHAR, Column, DateTime, String, BigInteger +from config.database import Base + + +class GuideWords(Base): + """ + 引导词表 + """ + + __tablename__ = 'guide_words' + + guide_id = Column(BigInteger, primary_key=True, autoincrement=True, nullable=False, comment='引导词id') + type = Column(String(2), nullable=True, comment='引导词类型') + text = Column(String(1024), nullable=True, comment='文本内容') + create_time = Column(DateTime, nullable=True, comment='创建时间') + update_time = Column(DateTime, nullable=True, comment='修改时间') + create_by = Column(String(64), nullable=True, comment='创建者') + remark = Column(String(500), nullable=True, comment='备注') + del_flag = Column(CHAR(1), nullable=False, comment='删除标志(0代表存在 2代表删除)') + update_by = Column(String(64), nullable=True, comment='修改者') + title = Column(String(64), nullable=True, comment='引导词名称') + label = Column(String(2), nullable=True, comment='引导词标签') + trigger_type = Column(String(2), nullable=True, comment='触发方式') + + + diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/__init__.py b/ruoyi-fastapi-backend/module_admin/entity/vo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/cache_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/cache_vo.py new file mode 100644 index 0000000..79b49fb --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/cache_vo.py @@ -0,0 +1,28 @@ +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from typing import Any, List, Optional + + +class CacheMonitorModel(BaseModel): + """ + 缓存监控信息对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + command_stats: Optional[List] = Field(default=[], description='命令统计') + db_size: Optional[int] = Field(default=None, description='Key数量') + info: Optional[dict] = Field(default={}, description='Redis信息') + + +class CacheInfoModel(BaseModel): + """ + 缓存监控对象对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + cache_key: Optional[str] = Field(default=None, description='缓存键名') + cache_name: Optional[str] = Field(default=None, description='缓存名称') + cache_value: Optional[Any] = Field(default=None, description='缓存内容') + remark: Optional[str] = Field(default=None, description='备注') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/common_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/common_vo.py new file mode 100644 index 0000000..258be5d --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/common_vo.py @@ -0,0 +1,26 @@ +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from typing import Any, Optional + + +class CrudResponseModel(BaseModel): + """ + 操作响应模型 + """ + + is_success: bool = Field(description='操作是否成功') + message: str = Field(description='响应信息') + result: Optional[Any] = Field(default=None, description='响应结果') + + +class UploadResponseModel(BaseModel): + """ + 上传响应模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + file_name: Optional[str] = Field(default=None, description='新文件映射路径') + new_file_name: Optional[str] = Field(default=None, description='新文件名称') + original_filename: Optional[str] = Field(default=None, description='原文件名称') + url: Optional[str] = Field(default=None, description='新文件url') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/config_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/config_vo.py new file mode 100644 index 0000000..917d7d6 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/config_vo.py @@ -0,0 +1,74 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import NotBlank, Size +from typing import Literal, Optional +from module_admin.annotation.pydantic_annotation import as_query + + +class ConfigModel(BaseModel): + """ + 参数配置表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + config_id: Optional[int] = Field(default=None, description='参数主键') + config_name: Optional[str] = Field(default=None, description='参数名称') + config_key: Optional[str] = Field(default=None, description='参数键名') + config_value: Optional[str] = Field(default=None, description='参数键值') + config_type: Optional[Literal['Y', 'N']] = Field(default=None, description='系统内置(Y是 N否)') + create_by: Optional[str] = Field(default=None, description='创建者') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + update_by: Optional[str] = Field(default=None, description='更新者') + update_time: Optional[datetime] = Field(default=None, description='更新时间') + remark: Optional[str] = Field(default=None, description='备注') + + @NotBlank(field_name='config_key', message='参数名称不能为空') + @Size(field_name='config_key', min_length=0, max_length=100, message='参数名称长度不能超过100个字符') + def get_config_key(self): + return self.config_key + + @NotBlank(field_name='config_name', message='参数键名不能为空') + @Size(field_name='config_name', min_length=0, max_length=100, message='参数键名长度不能超过100个字符') + def get_config_name(self): + return self.config_name + + @NotBlank(field_name='config_value', message='参数键值不能为空') + @Size(field_name='config_value', min_length=0, max_length=500, message='参数键值长度不能超过500个字符') + def get_config_value(self): + return self.config_value + + def validate_fields(self): + self.get_config_key() + self.get_config_name() + self.get_config_value() + + +class ConfigQueryModel(ConfigModel): + """ + 参数配置管理不分页查询模型 + """ + + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +@as_query +class ConfigPageQueryModel(ConfigQueryModel): + """ + 参数配置管理分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteConfigModel(BaseModel): + """ + 删除参数配置模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + config_ids: str = Field(description='需要删除的参数主键') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/dept_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/dept_vo.py new file mode 100644 index 0000000..dcad117 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/dept_vo.py @@ -0,0 +1,75 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import Network, NotBlank, Size +from typing import Literal, Optional +from module_admin.annotation.pydantic_annotation import as_query + + +class DeptModel(BaseModel): + """ + 部门表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + dept_id: Optional[int] = Field(default=None, description='部门id') + parent_id: Optional[int] = Field(default=None, description='父部门id') + ancestors: Optional[str] = Field(default=None, description='祖级列表') + dept_name: Optional[str] = Field(default=None, description='部门名称') + order_num: Optional[int] = Field(default=None, description='显示顺序') + leader: Optional[str] = Field(default=None, description='负责人') + phone: Optional[str] = Field(default=None, description='联系电话') + email: Optional[str] = Field(default=None, description='邮箱') + status: Optional[Literal['0', '1']] = Field(default=None, description='部门状态(0正常 1停用)') + del_flag: Optional[Literal['0', '2']] = Field(default=None, description='删除标志(0代表存在 2代表删除)') + create_by: Optional[str] = Field(default=None, description='创建者') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + update_by: Optional[str] = Field(default=None, description='更新者') + update_time: Optional[datetime] = Field(default=None, description='更新时间') + + @NotBlank(field_name='dept_name', message='部门名称不能为空') + @Size(field_name='dept_name', min_length=0, max_length=30, message='部门名称长度不能超过30个字符') + def get_dept_name(self): + return self.dept_name + + @NotBlank(field_name='order_num', message='显示顺序不能为空') + def get_order_num(self): + return self.order_num + + @Size(field_name='phone', min_length=0, max_length=11, message='联系电话长度不能超过11个字符') + def get_phone(self): + return self.phone + + @Network(field_name='email', field_type='EmailStr', message='邮箱格式不正确') + @Size(field_name='email', min_length=0, max_length=50, message='邮箱长度不能超过50个字符') + def get_email(self): + return self.email + + def validate_fields(self): + self.get_dept_name() + self.get_order_num() + self.get_phone() + self.get_email() + + +@as_query +class DeptQueryModel(DeptModel): + """ + 部门管理不分页查询模型 + """ + + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +class DeleteDeptModel(BaseModel): + """ + 删除部门模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + dept_ids: str = Field(default=None, description='需要删除的部门id') + update_by: Optional[str] = Field(default=None, description='更新者') + update_time: Optional[str] = Field(default=None, description='更新时间') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/dict_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/dict_vo.py new file mode 100644 index 0000000..3251f08 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/dict_vo.py @@ -0,0 +1,149 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import NotBlank, Pattern, Size +from typing import Literal, Optional +from module_admin.annotation.pydantic_annotation import as_query + + +class DictTypeModel(BaseModel): + """ + 字典类型表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + dict_id: Optional[int] = Field(default=None, description='字典主键') + dict_name: Optional[str] = Field(default=None, description='字典名称') + dict_type: Optional[str] = Field(default=None, description='字典类型') + status: Optional[Literal['0', '1']] = Field(default=None, description='状态(0正常 1停用)') + create_by: Optional[str] = Field(default=None, description='创建者') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + update_by: Optional[str] = Field(default=None, description='更新者') + update_time: Optional[datetime] = Field(default=None, description='更新时间') + remark: Optional[str] = Field(default=None, description='备注') + + @NotBlank(field_name='dict_name', message='字典名称不能为空') + @Size(field_name='dict_name', min_length=0, max_length=100, message='字典类型名称长度不能超过100个字符') + def get_dict_name(self): + return self.dict_name + + @NotBlank(field_name='dict_type', message='字典类型不能为空') + @Size(field_name='dict_type', min_length=0, max_length=100, message='字典类型类型长度不能超过100个字符') + @Pattern( + field_name='dict_type', + regexp='^[a-z][a-z0-9_]*$', + message='字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)', + ) + def get_dict_type(self): + return self.dict_type + + def validate_fields(self): + self.get_dict_name() + self.get_dict_type() + + +class DictDataModel(BaseModel): + """ + 字典数据表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + dict_code: Optional[int] = Field(default=None, description='字典编码') + dict_sort: Optional[int] = Field(default=None, description='字典排序') + dict_label: Optional[str] = Field(default=None, description='字典标签') + dict_value: Optional[str] = Field(default=None, description='字典键值') + dict_type: Optional[str] = Field(default=None, description='字典类型') + css_class: Optional[str] = Field(default=None, description='样式属性(其他样式扩展)') + list_class: Optional[str] = Field(default=None, description='表格回显样式') + is_default: Optional[Literal['Y', 'N']] = Field(default=None, description='是否默认(Y是 N否)') + status: Optional[Literal['0', '1']] = Field(default=None, description='状态(0正常 1停用)') + create_by: Optional[str] = Field(default=None, description='创建者') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + update_by: Optional[str] = Field(default=None, description='更新者') + update_time: Optional[datetime] = Field(default=None, description='更新时间') + remark: Optional[str] = Field(default=None, description='备注') + + @NotBlank(field_name='dict_label', message='字典标签不能为空') + @Size(field_name='dict_label', min_length=0, max_length=100, message='字典标签长度不能超过100个字符') + def get_dict_label(self): + return self.dict_label + + @NotBlank(field_name='dict_value', message='字典键值不能为空') + @Size(field_name='dict_value', min_length=0, max_length=100, message='字典键值长度不能超过100个字符') + def get_dict_value(self): + return self.dict_value + + @NotBlank(field_name='dict_type', message='字典类型不能为空') + @Size(field_name='dict_type', min_length=0, max_length=100, message='字典类型长度不能超过100个字符') + def get_dict_type(self): + return self.dict_type + + @Size(field_name='css_class', min_length=0, max_length=100, message='样式属性长度不能超过100个字符') + def get_css_class(self): + return self.css_class + + def validate_fields(self): + self.get_dict_label() + self.get_dict_value() + self.get_dict_type() + self.get_css_class() + + +class DictTypeQueryModel(DictTypeModel): + """ + 字典类型管理不分页查询模型 + """ + + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +@as_query +class DictTypePageQueryModel(DictTypeQueryModel): + """ + 字典类型管理分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteDictTypeModel(BaseModel): + """ + 删除字典类型模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + dict_ids: str = Field(description='需要删除的字典主键') + + +class DictDataQueryModel(DictDataModel): + """ + 字典数据管理不分页查询模型 + """ + + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +@as_query +class DictDataPageQueryModel(DictDataQueryModel): + """ + 字典数据管理分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteDictDataModel(BaseModel): + """ + 删除字典数据模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + dict_codes: str = Field(description='需要删除的字典编码') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/door_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/door_vo.py new file mode 100644 index 0000000..7c3bd09 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/door_vo.py @@ -0,0 +1,57 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from typing import Optional +from module_admin.annotation.pydantic_annotation import as_query + + + + +class DoorModel(BaseModel): + """ + 门禁设备表对应pydantic模型 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + id: Optional[int] = Field(default=None, description='主键自增') + indexCode: Optional[str] = Field(default=None, description='设备索引编码') + name: Optional[str] = Field(default=None, description='门禁名称') + install_location: Optional[str] = Field(default=None, description='位置名称') + status: Optional[str] = Field(default=None, description='设备状态, 0 离线, 1 正常在线') + permission: Optional[str] = Field(default=None, description='权限, 0 禁用, 1 正常调用') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + create_by: Optional[str] = Field(default=None, description='创建者') + update_time: Optional[datetime] = Field(default=None, description='更新时间') + update_by: Optional[str] = Field(default=None, description='更新者') + + + + + + +class DoorQueryModel(DoorModel): + """ + 门禁设备不分页查询模型 + """ + key_word : Optional[str] = Field(default=None, description='模糊查询字段') + + + +@as_query +class DoorPageQueryModel(DoorQueryModel): + """ + 门禁设备分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteDoorModel(BaseModel): + """ + 删除门禁设备模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + ids: str = Field(description='需要删除的主键自增') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/explanation_content_type_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/explanation_content_type_vo.py new file mode 100644 index 0000000..fbcfb6b --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/explanation_content_type_vo.py @@ -0,0 +1,53 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from typing import Optional +from module_admin.annotation.pydantic_annotation import as_query + + + + +class Explanation_content_typeModel(BaseModel): + """ + 讲解内容类型表对应pydantic模型 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + content_type_id: Optional[int] = Field(default=None, description='主键ID') + name: Optional[str] = Field(default=None, description='类型名称') + type_value: Optional[str] = Field(default=None, description='类型值(与讲解内容表中的type对应)') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + create_by: Optional[str] = Field(default=None, description='创建者') + update_time: Optional[datetime] = Field(default=None, description='修改时间') + update_by: Optional[str] = Field(default=None, description='修改者') + + + + + + +class Explanation_content_typeQueryModel(Explanation_content_typeModel): + """ + 讲解内容类型不分页查询模型 + """ + pass + + +@as_query +class Explanation_content_typePageQueryModel(Explanation_content_typeQueryModel): + """ + 讲解内容类型分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteExplanation_content_typeModel(BaseModel): + """ + 删除讲解内容类型模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + content_type_ids: str = Field(description='需要删除的主键ID') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/explanation_content_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/explanation_content_vo.py new file mode 100644 index 0000000..d552d35 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/explanation_content_vo.py @@ -0,0 +1,67 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import NotBlank +from typing import Optional +from module_admin.annotation.pydantic_annotation import as_query + + + + +class Explanation_contentModel(BaseModel): + """ + 讲解内容表对应pydantic模型 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + explanation_content_id: Optional[int] = Field(default=None, description='主键ID') + title: Optional[str] = Field(default=None, description='标题') + content: Optional[str] = Field(default=None, description='讲解内容') + type: Optional[str] = Field(default=None, description='类型(0科技类)') + spend_time: Optional[int] = Field(default=None, description='预计时长(分钟)') + language: Optional[str] = Field(default=None, description='语言(0中文,1英文)') + status: Optional[str] = Field(default=None, description='状态(0未开始,1已就绪,2播放中,3已完成)') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + create_by: Optional[str] = Field(default=None, description='创建者') + update_time: Optional[datetime] = Field(default=None, description='修改时间') + update_by: Optional[str] = Field(default=None, description='修改者') + + @NotBlank(field_name='title', message='标题不能为空') + def get_title(self): + return self.title + + + def validate_fields(self): + self.get_title() + + + + +class Explanation_contentQueryModel(Explanation_contentModel): + """ + 讲解内容不分页查询模型 + """ + begin_create_time: Optional[str] = Field(default=None, description='开始创建时间') + end_create_time: Optional[str] = Field(default=None, description='结束创建时间') + begin_update_time: Optional[str] = Field(default=None, description='开始修改时间') + end_update_time: Optional[str] = Field(default=None, description='结束修改时间') + + +@as_query +class Explanation_contentPageQueryModel(Explanation_contentQueryModel): + """ + 讲解内容分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteExplanation_contentModel(BaseModel): + """ + 删除讲解内容模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + explanation_content_ids: str = Field(description='需要删除的主键ID') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/explanation_style_robot_pair_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/explanation_style_robot_pair_vo.py new file mode 100644 index 0000000..2c74bd2 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/explanation_style_robot_pair_vo.py @@ -0,0 +1,83 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import NotBlank +from typing import Optional +from module_admin.annotation.pydantic_annotation import as_query + + +class SwitchExplanationStyleModel(BaseModel): + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + pairing_id: int = Field(default=None, description='主键ID') + robot_id: int = Field(default=None, description='机器人ID(关联robot_info表)') + robot_role_id: int = Field(default=None, description='机器人角色ID(关联robot_role表)') + + update_by: Optional[str] = Field(default=None, description='修改者') + + + + + +class Explanation_style_robot_pairModel(BaseModel): + """ + 讲解风格--机器人配对表对应pydantic模型 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + pairing_id: Optional[int] = Field(default=None, description='主键ID') + robot_id: Optional[int] = Field(default=None, description='机器人ID(关联robot_info表)') + robot_role_id : Optional[int] = Field(default=None, description='机器人角色ID(关联robot_role表)') + explanation_style_id: Optional[int] = Field(default=None, description='讲解风格ID(关联explanation_style表)') + prompt: Optional[str] = Field(default=None, description='提示词') + status: Optional[str] = Field(default=None, description='状态(0停用,1使用)') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + create_by: Optional[str] = Field(default=None, description='创建者') + update_time: Optional[datetime] = Field(default=None, description='修改时间') + update_by: Optional[str] = Field(default=None, description='修改者') + + @NotBlank(field_name='robot_id', message='机器人ID不能为空') + def get_robot_id(self): + return self.robot_id + + @NotBlank(field_name='explanation_style_id', message='讲解风格ID不能为空') + def get_explanation_style_id(self): + return self.explanation_style_id + + + def validate_fields(self): + self.get_robot_id() + self.get_explanation_style_id() + + + + +class Explanation_style_robot_pairQueryModel(Explanation_style_robot_pairModel): + """ + 讲解风格--机器人配对不分页查询模型 + """ + begin_create_time: Optional[str] = Field(default=None, description='开始创建时间') + end_create_time: Optional[str] = Field(default=None, description='结束创建时间') + begin_update_time: Optional[str] = Field(default=None, description='开始修改时间') + end_update_time: Optional[str] = Field(default=None, description='结束修改时间') + + +@as_query +class Explanation_style_robot_pairPageQueryModel(Explanation_style_robot_pairQueryModel): + """ + 讲解风格--机器人配对分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteExplanation_style_robot_pairModel(BaseModel): + """ + 删除讲解风格--机器人配对模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + pairing_ids: str = Field(description='需要删除的主键ID') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/explanation_style_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/explanation_style_vo.py new file mode 100644 index 0000000..6920c07 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/explanation_style_vo.py @@ -0,0 +1,56 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from typing import Optional +from module_admin.annotation.pydantic_annotation import as_query + + + + +class Explanation_styleModel(BaseModel): + """ + 讲解风格表对应pydantic模型 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + explanation_style_id: Optional[int] = Field(default=None, description='主键ID') + name: Optional[str] = Field(default=None, description='风格名称') + detail: Optional[str] = Field(default=None, description='风格简介') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + create_by: Optional[str] = Field(default=None, description='创建者') + update_time: Optional[datetime] = Field(default=None, description='修改时间') + update_by: Optional[str] = Field(default=None, description='修改者') + + + + + + +class Explanation_styleQueryModel(Explanation_styleModel): + """ + 讲解风格不分页查询模型 + """ + begin_create_time: Optional[str] = Field(default=None, description='开始创建时间') + end_create_time: Optional[str] = Field(default=None, description='结束创建时间') + begin_update_time: Optional[str] = Field(default=None, description='开始修改时间') + end_update_time: Optional[str] = Field(default=None, description='结束修改时间') + + +@as_query +class Explanation_stylePageQueryModel(Explanation_styleQueryModel): + """ + 讲解风格分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteExplanation_styleModel(BaseModel): + """ + 删除讲解风格模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + explanation_style_ids: str = Field(description='需要删除的主键ID') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/haikang_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/haikang_vo.py new file mode 100644 index 0000000..d8c5158 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/haikang_vo.py @@ -0,0 +1,24 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from typing import Optional +from module_admin.annotation.pydantic_annotation import as_query + + +class VisitorReservationQueryModel(BaseModel): + + + receptionistId: Optional[str] = Field(default=None, description='接待ID') + visitorName: Optional[str] = Field(default=None, description='访客姓名,支持模糊搜索') + phoneNo: Optional[str] = Field(default=None, description='访客手机号') + visitStartTimeBegin: Optional[str] = Field(default=None, description='开始时间, 满足ISO8601格式') + visitStartTimeEnd: Optional[str] = Field(default=None, description='结束时间, 满足ISO8601格式') + visitEndTimeBegin: Optional[str] = Field(default=None, description='开始时间, 满足ISO8601格式') + visitEndTimeEnd: Optional[str] = Field(default=None, description='结束时间, 满足ISO8601格式') + + pageNo: int = Field(default=1, description='页码') + pageSize: int = Field(default=20, description='每页数量') + + visitorStatus: Optional[int] = Field(default=None, description='访客状态') + orderId: Optional[str] = Field(default=None, description='订单ID') + \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/identification_record_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/identification_record_vo.py new file mode 100644 index 0000000..684bb6f --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/identification_record_vo.py @@ -0,0 +1,57 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from typing import Optional +from module_admin.annotation.pydantic_annotation import as_query + + + + +class Identification_recordModel(BaseModel): + """ + 识别记录表对应pydantic模型 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + id: Optional[int] = Field(default=None, description='主键 自增') + person_name: Optional[str] = Field(default=None, description='识别人姓名') + door_name: Optional[str] = Field(default=None, description='门禁点') + status: Optional[str] = Field(default=None, description='识别结果, 0 拒绝, 1 通过') + source: Optional[str] = Field(default=None, description='识别来源') + pic_uri: Optional[str] = Field(default=None, description='图像url') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + create_by: Optional[str] = Field(default=None, description='创建者') + conf: Optional[int] = Field(default=None, description='识别置信度') + person_status: Optional[str] = Field(default=None, description=' 0 陌生人, 1 员工, 2 访客') + + + + + + +class Identification_recordQueryModel(Identification_recordModel): + """ + 识别记录不分页查询模型 + """ + begin_create_time: Optional[str] = Field(default=None, description='开始创建时间') + end_create_time: Optional[str] = Field(default=None, description='结束创建时间') + + +@as_query +class Identification_recordPageQueryModel(Identification_recordQueryModel): + """ + 识别记录分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteIdentification_recordModel(BaseModel): + """ + 删除识别记录模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + ids: str = Field(description='需要删除的主键 自增') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/identification_statistics_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/identification_statistics_vo.py new file mode 100644 index 0000000..c9d6992 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/identification_statistics_vo.py @@ -0,0 +1,53 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from typing import Optional +from module_admin.annotation.pydantic_annotation import as_query + + + + +class Identification_statisticsModel(BaseModel): + """ + 识别统计表对应pydantic模型 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + id: Optional[int] = Field(default=None, description='主键 自增') + employ: Optional[int] = Field(default=None, description='员工识别成功数') + visitor: Optional[int] = Field(default=None, description='访客识别成功数') + stranger: Optional[int] = Field(default=None, description='位置人员识别失败数') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + create_by: Optional[str] = Field(default=None, description='创建者') + + + + + + +class Identification_statisticsQueryModel(Identification_statisticsModel): + """ + 识别统计不分页查询模型 + """ + begin_create_time: Optional[str] = Field(default=None, description='开始创建时间') + end_create_time: Optional[str] = Field(default=None, description='结束创建时间') + + +@as_query +class Identification_statisticsPageQueryModel(Identification_statisticsQueryModel): + """ + 识别统计分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteIdentification_statisticsModel(BaseModel): + """ + 删除识别统计模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + ids: str = Field(description='需要删除的主键 自增') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/info_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/info_vo.py new file mode 100644 index 0000000..a1b5406 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/info_vo.py @@ -0,0 +1,100 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import NotBlank +from typing import Optional +from module_admin.annotation.pydantic_annotation import as_query + + + + +class InfoModel(BaseModel): + """ + 机器人信息表对应pydantic模型 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + robot_id: Optional[int] = Field(default=None, description='机器人ID') + name: Optional[str] = Field(default=None, description='机器人名称') + model: Optional[str] = Field(default=None, description='机器人型号') + serial: Optional[str] = Field(default=None, description='序列号') + online: Optional[str] = Field(default=None, description='机器人在线状态(0在线, 1离线)') + power: Optional[str] = Field(default=None, description='电量') + run_time: Optional[int] = Field(default=None, description='总运行时长(单位:分钟)') + position: Optional[str] = Field(default=None, description='位置') + image: Optional[str] = Field(default=None, description='机器人外观图地址') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + create_by: Optional[str] = Field(default=None, description='创建者') + update_time: Optional[datetime] = Field(default=None, description='修改时间') + update_by: Optional[str] = Field(default=None, description='修改者') + del_flag: Optional[str] = Field(default=None, description='删除标志(0存在, 2删除)') + remark: Optional[str] = Field(default=None, description='备注') + last_charge: Optional[datetime] = Field(default=None, description='上一次充电时间') + ip: Optional[str] = Field(default=None, description='机器人ip') + status: Optional[str] = Field(default=None, description='使用情况(0待机,1正在运行)') + video_uri: Optional[str] = Field(default=None, description='视频uri') + + + @NotBlank(field_name='name', message='机器人名称不能为空') + def get_name(self): + return self.name + + # @NotBlank(field_name='del_flag', message='删除标志(0存在, 2删除)不能为空') + # def get_del_flag(self): + # return self.del_flag + + + def validate_fields(self): + self.get_name() + # self.get_del_flag() + + + + +class InfoQueryModel(InfoModel): + """ + 机器人信息不分页查询模型 + """ + begin_create_time: Optional[str] = Field(default=None, description='开始创建时间') + end_create_time: Optional[str] = Field(default=None, description='结束创建时间') + begin_update_time: Optional[str] = Field(default=None, description='开始修改时间') + end_update_time: Optional[str] = Field(default=None, description='结束修改时间') + + +@as_query +class InfoPageQueryModel(InfoQueryModel): + """ + 机器人信息分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class InfoUpdateModel(BaseModel): + """ + 更新机器人信息模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + robot_id: Optional[int] = Field(default=None, description='机器人ID') + + name: Optional[str] = Field(default=None, description='机器人名称') + model: Optional[str] = Field(default=None, description='机器人型号') + serial: Optional[str] = Field(default=None, description='序列号') + position: Optional[str] = Field(default=None, description='位置') + image: Optional[str] = Field(default=None, description='机器人外观图地址') + remark: Optional[str] = Field(default=None, description='备注') + update_time: Optional[datetime] = Field(default=None, description='修改时间') + update_by: Optional[str] = Field(default=None, description='修改者') + + +class DeleteInfoModel(BaseModel): + """ + 删除机器人信息模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + robot_ids: str = Field(description='需要删除的机器人ID') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/job_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/job_vo.py new file mode 100644 index 0000000..960a837 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/job_vo.py @@ -0,0 +1,134 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import NotBlank, Size +from typing import Literal, Optional +from module_admin.annotation.pydantic_annotation import as_query + + +class JobModel(BaseModel): + """ + 定时任务调度表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + job_id: Optional[int] = Field(default=None, description='任务ID') + job_name: Optional[str] = Field(default=None, description='任务名称') + job_group: Optional[str] = Field(default=None, description='任务组名') + job_executor: Optional[str] = Field(default=None, description='任务执行器') + invoke_target: Optional[str] = Field(default=None, description='调用目标字符串') + job_args: Optional[str] = Field(default=None, description='位置参数') + job_kwargs: Optional[str] = Field(default=None, description='关键字参数') + cron_expression: Optional[str] = Field(default=None, description='cron执行表达式') + misfire_policy: Optional[Literal['1', '2', '3']] = Field( + default=None, description='计划执行错误策略(1立即执行 2执行一次 3放弃执行)' + ) + concurrent: Optional[Literal['0', '1']] = Field(default=None, description='是否并发执行(0允许 1禁止)') + status: Optional[Literal['0', '1']] = Field(default=None, description='状态(0正常 1暂停)') + create_by: Optional[str] = Field(default=None, description='创建者') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + update_by: Optional[str] = Field(default=None, description='更新者') + update_time: Optional[datetime] = Field(default=None, description='更新时间') + remark: Optional[str] = Field(default=None, description='备注信息') + + @NotBlank(field_name='invoke_target', message='调用目标字符串不能为空') + @Size(field_name='invoke_target', min_length=0, max_length=500, message='调用目标字符串长度不能超过500个字符') + def get_invoke_target(self): + return self.invoke_target + + @NotBlank(field_name='cron_expression', message='Cron执行表达式不能为空') + @Size(field_name='cron_expression', min_length=0, max_length=255, message='Cron执行表达式不能超过255个字符') + def get_cron_expression(self): + return self.cron_expression + + def validate_fields(self): + self.get_invoke_target() + self.get_cron_expression() + + +class JobLogModel(BaseModel): + """ + 定时任务调度日志表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + job_log_id: Optional[int] = Field(default=None, description='任务日志ID') + job_name: Optional[str] = Field(default=None, description='任务名称') + job_group: Optional[str] = Field(default=None, description='任务组名') + job_executor: Optional[str] = Field(default=None, description='任务执行器') + invoke_target: Optional[str] = Field(default=None, description='调用目标字符串') + job_args: Optional[str] = Field(default=None, description='位置参数') + job_kwargs: Optional[str] = Field(default=None, description='关键字参数') + job_trigger: Optional[str] = Field(default=None, description='任务触发器') + job_message: Optional[str] = Field(default=None, description='日志信息') + status: Optional[Literal['0', '1']] = Field(default=None, description='执行状态(0正常 1失败)') + exception_info: Optional[str] = Field(default=None, description='异常信息') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + + +class JobQueryModel(JobModel): + """ + 定时任务管理不分页查询模型 + """ + + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +@as_query +class JobPageQueryModel(JobQueryModel): + """ + 定时任务管理分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class EditJobModel(JobModel): + """ + 编辑定时任务模型 + """ + + type: Optional[str] = Field(default=None, description='操作类型') + + +class DeleteJobModel(BaseModel): + """ + 删除定时任务模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + job_ids: str = Field(description='需要删除的定时任务ID') + + +class JobLogQueryModel(JobLogModel): + """ + 定时任务日志不分页查询模型 + """ + + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +@as_query +class JobLogPageQueryModel(JobLogQueryModel): + """ + 定时任务日志管理分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteJobLogModel(BaseModel): + """ + 删除定时任务日志模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + job_log_ids: str = Field(description='需要删除的定时任务日志ID') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/log_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/log_vo.py new file mode 100644 index 0000000..739ad6c --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/log_vo.py @@ -0,0 +1,131 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from typing import Literal, Optional +from module_admin.annotation.pydantic_annotation import as_query + + +class OperLogModel(BaseModel): + """ + 操作日志表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + oper_id: Optional[int] = Field(default=None, description='日志主键') + title: Optional[str] = Field(default=None, description='模块标题') + business_type: Optional[Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']] = ( + Field( + default=None, description='业务类型(0其它 1新增 2修改 3删除 4授权 5导出 6导入 7强退 8生成代码 9清空数据)' + ) + ) + method: Optional[str] = Field(default=None, description='方法名称') + request_method: Optional[str] = Field(default=None, description='请求方式') + operator_type: Optional[Literal[0, 1, 2]] = Field( + default=None, description='操作类别(0其它 1后台用户 2手机端用户)' + ) + oper_name: Optional[str] = Field(default=None, description='操作人员') + dept_name: Optional[str] = Field(default=None, description='部门名称') + oper_url: Optional[str] = Field(default=None, description='请求URL') + oper_ip: Optional[str] = Field(default=None, description='主机地址') + oper_location: Optional[str] = Field(default=None, description='操作地点') + oper_param: Optional[str] = Field(default=None, description='请求参数') + json_result: Optional[str] = Field(default=None, description='返回参数') + status: Optional[Literal[0, 1, '0', '1']] = Field(default=None, description='操作状态(0正常 1异常)') + error_msg: Optional[str] = Field(default=None, description='错误消息') + oper_time: Optional[datetime] = Field(default=None, description='操作时间') + cost_time: Optional[int] = Field(default=None, description='消耗时间') + + +class LogininforModel(BaseModel): + """ + 登录日志表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + info_id: Optional[int] = Field(default=None, description='访问ID') + user_name: Optional[str] = Field(default=None, description='用户账号') + ipaddr: Optional[str] = Field(default=None, description='登录IP地址') + login_location: Optional[str] = Field(default=None, description='登录地点') + browser: Optional[str] = Field(default=None, description='浏览器类型') + os: Optional[str] = Field(default=None, description='操作系统') + status: Optional[Literal['0', '1']] = Field(default=None, description='登录状态(0成功 1失败)') + msg: Optional[str] = Field(default=None, description='提示消息') + login_time: Optional[datetime] = Field(default=None, description='访问时间') + + +class OperLogQueryModel(OperLogModel): + """ + 操作日志管理不分页查询模型 + """ + + order_by_column: Optional[str] = Field(default=None, description='排序的字段名称') + is_asc: Optional[Literal['ascending', 'descending']] = Field( + default=None, description='排序方式(ascending升序 descending降序)' + ) + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +@as_query +class OperLogPageQueryModel(OperLogQueryModel): + """ + 操作日志管理分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteOperLogModel(BaseModel): + """ + 删除操作日志模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + oper_ids: str = Field(description='需要删除的日志主键') + + +class LoginLogQueryModel(LogininforModel): + """ + 登录日志管理不分页查询模型 + """ + + order_by_column: Optional[str] = Field(default=None, description='排序的字段名称') + is_asc: Optional[Literal['ascending', 'descending']] = Field( + default=None, description='排序方式(ascending升序 descending降序)' + ) + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +@as_query +class LoginLogPageQueryModel(LoginLogQueryModel): + """ + 登录日志管理分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteLoginLogModel(BaseModel): + """ + 删除登录日志模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + info_ids: str = Field(description='需要删除的访问ID') + + +class UnlockUser(BaseModel): + """ + 解锁用户模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + user_name: str = Field(description='用户名称') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/login_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/login_vo.py new file mode 100644 index 0000000..fbd6e07 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/login_vo.py @@ -0,0 +1,87 @@ +import re +from pydantic import BaseModel, ConfigDict, Field, model_validator +from pydantic.alias_generators import to_camel +from typing import List, Optional, Union +from exceptions.exception import ModelValidatorException +from module_admin.entity.vo.menu_vo import MenuModel + + +class UserLogin(BaseModel): + model_config = ConfigDict(alias_generator=to_camel) + + user_name: str = Field(description='用户名称') + password: str = Field(description='用户密码') + code: Optional[str] = Field(default=None, description='验证码') + uuid: Optional[str] = Field(default=None, description='会话编号') + login_info: Optional[dict] = Field(default=None, description='登录信息,前端无需传递') + captcha_enabled: Optional[bool] = Field(default=None, description='是否启用验证码,前端无需传递') + + +class UserRegister(BaseModel): + model_config = ConfigDict(alias_generator=to_camel) + + username: str = Field(description='用户名称') + password: str = Field(description='用户密码') + confirm_password: str = Field(description='用户二次确认密码') + code: Optional[str] = Field(default=None, description='验证码') + uuid: Optional[str] = Field(default=None, description='会话编号') + + @model_validator(mode='after') + def check_password(self) -> 'UserRegister': + pattern = r"""^[^<>"'|\\]+$""" + if self.password is None or re.match(pattern, self.password): + return self + else: + raise ModelValidatorException(message='密码不能包含非法字符:< > " \' \\ |') + + +class Token(BaseModel): + access_token: str = Field(description='token信息') + token_type: str = Field(description='token类型') + + +class CaptchaCode(BaseModel): + model_config = ConfigDict(alias_generator=to_camel) + + captcha_enabled: bool = Field(description='是否启用验证码') + register_enabled: bool = Field(description='是否启用注册') + img: str = Field(description='验证码图片') + uuid: str = Field(description='会话编号') + + +class SmsCode(BaseModel): + is_success: Optional[bool] = Field(default=None, description='操作是否成功') + sms_code: str = Field(description='短信验证码') + session_id: str = Field(description='会话编号') + message: Optional[str] = Field(default=None, description='响应信息') + + +class MenuTreeModel(MenuModel): + children: Optional[Union[List['MenuTreeModel'], None]] = Field(default=None, description='子菜单') + + +class MetaModel(BaseModel): + model_config = ConfigDict(alias_generator=to_camel) + + title: Optional[str] = Field(default=None, description='设置路由在侧边栏和面包屑中展示的名字') + icon: Optional[str] = Field(default=None, description='设置路由的图标') + no_cache: Optional[bool] = Field(default=None, description='设置为true,则不会被 缓存') + link: Optional[str] = Field(default=None, description='内链地址(http(s)://开头)') + + +class RouterModel(BaseModel): + model_config = ConfigDict(alias_generator=to_camel) + + name: Optional[str] = Field(default=None, description='路由名称') + path: Optional[str] = Field(default=None, description='路由地址') + hidden: Optional[bool] = Field(default=None, description='是否隐藏路由,当设置 true 的时候该路由不会再侧边栏出现') + redirect: Optional[str] = Field( + default=None, description='重定向地址,当设置 noRedirect 的时候该路由在面包屑导航中不可被点击' + ) + component: Optional[str] = Field(default=None, description='组件地址') + query: Optional[str] = Field(default=None, description='路由参数:如 {"id": 1, "name": "ry"}') + always_show: Optional[bool] = Field( + default=None, description='当你一个路由下面的children声明的路由大于1个时,自动会变成嵌套的模式--如组件页面' + ) + meta: Optional[MetaModel] = Field(default=None, description='其他元素') + children: Optional[Union[List['RouterModel'], None]] = Field(default=None, description='子路由') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/menu_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/menu_vo.py new file mode 100644 index 0000000..9dc8d75 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/menu_vo.py @@ -0,0 +1,88 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import NotBlank, Size +from typing import Literal, Optional +from module_admin.annotation.pydantic_annotation import as_query + + +class MenuModel(BaseModel): + """ + 菜单表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + menu_id: Optional[int] = Field(default=None, description='菜单ID') + menu_name: Optional[str] = Field(default=None, description='菜单名称') + parent_id: Optional[int] = Field(default=None, description='父菜单ID') + order_num: Optional[int] = Field(default=None, description='显示顺序') + path: Optional[str] = Field(default=None, description='路由地址') + component: Optional[str] = Field(default=None, description='组件路径') + query: Optional[str] = Field(default=None, description='路由参数') + route_name: Optional[str] = Field(default=None, description='路由名称') + is_frame: Optional[Literal[0, 1]] = Field(default=None, description='是否为外链(0是 1否)') + is_cache: Optional[Literal[0, 1]] = Field(default=None, description='是否缓存(0缓存 1不缓存)') + menu_type: Optional[Literal['M', 'C', 'F']] = Field(default=None, description='菜单类型(M目录 C菜单 F按钮)') + visible: Optional[Literal['0', '1']] = Field(default=None, description='菜单状态(0显示 1隐藏)') + status: Optional[Literal['0', '1']] = Field(default=None, description='菜单状态(0正常 1停用)') + perms: Optional[str] = Field(default=None, description='权限标识') + icon: Optional[str] = Field(default=None, description='菜单图标') + create_by: Optional[str] = Field(default=None, description='创建者') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + update_by: Optional[str] = Field(default=None, description='更新者') + update_time: Optional[datetime] = Field(default=None, description='更新时间') + remark: Optional[str] = Field(default=None, description='备注') + + @NotBlank(field_name='menu_name', message='菜单名称不能为空') + @Size(field_name='menu_name', min_length=0, max_length=50, message='菜单名称长度不能超过50个字符') + def get_menu_name(self): + return self.menu_name + + @NotBlank(field_name='order_num', message='显示顺序不能为空') + def get_order_num(self): + return self.order_num + + @Size(field_name='path', min_length=0, max_length=200, message='路由地址长度不能超过200个字符') + def get_path(self): + return self.path + + @Size(field_name='component', min_length=0, max_length=255, message='组件路径长度不能超过255个字符') + def get_component(self): + return self.component + + @NotBlank(field_name='menu_type', message='菜单类型不能为空') + def get_menu_type(self): + return self.menu_type + + @Size(field_name='perms', min_length=0, max_length=100, message='权限标识长度不能超过100个字符') + def get_perms(self): + return self.perms + + def validate_fields(self): + self.get_menu_name() + self.get_order_num() + self.get_path() + self.get_component() + self.get_menu_type() + self.get_perms() + + +@as_query +class MenuQueryModel(MenuModel): + """ + 菜单管理不分页查询模型 + """ + + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +class DeleteMenuModel(BaseModel): + """ + 删除菜单模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + menu_ids: str = Field(description='需要删除的菜单ID') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/message_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/message_vo.py new file mode 100644 index 0000000..0e513ae --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/message_vo.py @@ -0,0 +1,104 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import NotBlank +from typing import Optional +from module_admin.annotation.pydantic_annotation import as_query + + + + +class MessageModel(BaseModel): + """ + 系统消息表对应pydantic模型 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + message_id: Optional[int] = Field(default=None, description='消息主键') + type: Optional[str] = Field(default=None, description='类型(0告警消息)') + status: Optional[str] = Field(default=None, description='状态(0未处理,1已处理)') + detail: Optional[str] = Field(default=None, description='消息描述') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + create_by: Optional[str] = Field(default=None, description='创建者') + update_time: Optional[datetime] = Field(default=None, description='更新时间') + update_by: Optional[str] = Field(default=None, description='更新者') + remark : Optional[str] = Field(default=None, description='备注') + specific_type: Optional[str] = Field(default=None, description='具体类型') + + # @NotBlank(field_name='type', message='类型不能为空') + # def get_type(self): + # return self.type + # + # @NotBlank(field_name='status', message='状态不能为空') + # def get_status(self): + # return self.status + # + # @NotBlank(field_name='create_time', message='创建时间不能为空') + # def get_create_time(self): + # return self.create_time + # + # @NotBlank(field_name='update_time', message='更新时间不能为空') + # def get_update_time(self): + # return self.update_time + # + # @NotBlank(field_name='update_by', message='更新者不能为空') + # def get_update_by(self): + # return self.update_by + # + # def validate_fields(self): + # self.get_type() + # self.get_status() + # self.get_create_time() + # self.get_update_time() + # self.get_update_by() + + + + +class MessageQueryModel(BaseModel): + """ + 系统消息不分页查询模型 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + type: Optional[str] = Field(default=None, description='类型(0告警消息)') + status: Optional[str] = Field(default=None, description='状态(0未处理,1已处理)') + + begin_create_time: Optional[str] = Field(default=None, description='开始创建时间') + end_create_time: Optional[str] = Field(default=None, description='结束创建时间') + begin_update_time: Optional[str] = Field(default=None, description='开始更新时间') + end_update_time: Optional[str] = Field(default=None, description='结束更新时间') + +# 编辑消息类 +class EditMessageModel(BaseModel): + """ + 系统消息编辑模型 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + message_id: int = Field(default=None, description='消息主键') + status: Optional[str] = Field(default=None, description='状态(0未处理,1已处理)') + remark: Optional[str] = Field(default=None, description='备注') + update_time: Optional[datetime] = Field(default=None, description='更新时间') + update_by: Optional[str] = Field(default=None, description='更新者') + + + +@as_query +class MessagePageQueryModel(MessageQueryModel): + """ + 系统消息分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteMessageModel(BaseModel): + """ + 删除系统消息模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + message_ids: str = Field(description='需要删除的消息主键') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/notice_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/notice_vo.py new file mode 100644 index 0000000..0b5d70c --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/notice_vo.py @@ -0,0 +1,63 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import NotBlank, Size, Xss +from typing import Literal, Optional +from module_admin.annotation.pydantic_annotation import as_query + + +class NoticeModel(BaseModel): + """ + 通知公告表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + notice_id: Optional[int] = Field(default=None, description='公告ID') + notice_title: Optional[str] = Field(default=None, description='公告标题') + notice_type: Optional[Literal['1', '2']] = Field(default=None, description='公告类型(1通知 2公告)') + notice_content: Optional[bytes] = Field(default=None, description='公告内容') + status: Optional[Literal['0', '1']] = Field(default=None, description='公告状态(0正常 1关闭)') + create_by: Optional[str] = Field(default=None, description='创建者') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + update_by: Optional[str] = Field(default=None, description='更新者') + update_time: Optional[datetime] = Field(default=None, description='更新时间') + remark: Optional[str] = Field(default=None, description='备注') + + @Xss(field_name='notice_title', message='公告标题不能包含脚本字符') + @NotBlank(field_name='notice_title', message='公告标题不能为空') + @Size(field_name='notice_title', min_length=0, max_length=50, message='公告标题不能超过50个字符') + def get_notice_title(self): + return self.notice_title + + def validate_fields(self): + self.get_notice_title() + + +class NoticeQueryModel(NoticeModel): + """ + 通知公告管理不分页查询模型 + """ + + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +@as_query +class NoticePageQueryModel(NoticeQueryModel): + """ + 通知公告管理分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteNoticeModel(BaseModel): + """ + 删除通知公告模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + notice_ids: str = Field(description='需要删除的公告ID') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/online_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/online_vo.py new file mode 100644 index 0000000..11bb475 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/online_vo.py @@ -0,0 +1,42 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from typing import Optional +from module_admin.annotation.pydantic_annotation import as_query + + +class OnlineModel(BaseModel): + """ + 在线用户对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + token_id: Optional[str] = Field(default=None, description='会话编号') + user_name: Optional[str] = Field(default=None, description='登录名称') + dept_name: Optional[str] = Field(default=None, description='所属部门') + ipaddr: Optional[str] = Field(default=None, description='主机') + login_location: Optional[str] = Field(default=None, description='登录地点') + browser: Optional[str] = Field(default=None, description='浏览器类型') + os: Optional[str] = Field(default=None, description='操作系统') + login_time: Optional[datetime] = Field(default=None, description='登录时间') + + +@as_query +class OnlineQueryModel(OnlineModel): + """ + 岗位管理不分页查询模型 + """ + + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +class DeleteOnlineModel(BaseModel): + """ + 强退在线用户模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + token_ids: str = Field(description='需要强退的会话编号') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/post_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/post_vo.py new file mode 100644 index 0000000..79390c3 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/post_vo.py @@ -0,0 +1,73 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import NotBlank, Size +from typing import Literal, Optional +from module_admin.annotation.pydantic_annotation import as_query + + +class PostModel(BaseModel): + """ + 岗位信息表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + post_id: Optional[int] = Field(default=None, description='岗位ID') + post_code: Optional[str] = Field(default=None, description='岗位编码') + post_name: Optional[str] = Field(default=None, description='岗位名称') + post_sort: Optional[int] = Field(default=None, description='显示顺序') + status: Optional[Literal['0', '1']] = Field(default=None, description='状态(0正常 1停用)') + create_by: Optional[str] = Field(default=None, description='创建者') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + update_by: Optional[str] = Field(default=None, description='更新者') + update_time: Optional[datetime] = Field(default=None, description='更新时间') + remark: Optional[str] = Field(default=None, description='备注') + + @NotBlank(field_name='post_code', message='岗位编码不能为空') + @Size(field_name='post_code', min_length=0, max_length=64, message='岗位编码长度不能超过64个字符') + def get_post_code(self): + return self.post_code + + @NotBlank(field_name='post_name', message='岗位名称不能为空') + @Size(field_name='post_name', min_length=0, max_length=50, message='岗位名称长度不能超过50个字符') + def get_post_name(self): + return self.post_name + + @NotBlank(field_name='post_sort', message='显示顺序不能为空') + def get_post_sort(self): + return self.post_sort + + def validate_fields(self): + self.get_post_code() + self.get_post_name() + self.get_post_sort() + + +class PostQueryModel(PostModel): + """ + 岗位管理不分页查询模型 + """ + + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +@as_query +class PostPageQueryModel(PostQueryModel): + """ + 岗位管理分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeletePostModel(BaseModel): + """ + 删除岗位模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + post_ids: str = Field(description='需要删除的岗位ID') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/ragflow_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/ragflow_vo.py new file mode 100644 index 0000000..276138b --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/ragflow_vo.py @@ -0,0 +1,153 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from typing import Optional, List +from module_admin.annotation.pydantic_annotation import as_query + + + +class RagflowListQueryModel(BaseModel): + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + page: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页数量') + orderby: Optional[str] = Field(default='create_time', description='排序字段') + desc: Optional[str] = Field(default='true', description='排序方式') + name: Optional[str] = Field(default=None, description='名称') + # dataset_id: Optional[str] = Field(default=None, description='数据集ID') + # chat_id: Optional[str] = Field(default=None, description='聊天ID') + +@as_query +class ListDocumentsQueryModel(BaseModel): + """ + 查询文档列表参数模型 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + page: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页数量') + orderby: Optional[str] = Field(default='create_time', description='排序字段') + desc: Optional[str] = Field(default='true', description='排序方式') + keywords: Optional[str] = Field(default=None, description='关键字') + document_id: Optional[str] = Field(default=None, description='文档ID') + document_name: Optional[str] = Field(default=None, description='文档名称') + + +class UpdateFileModel(BaseModel): + """ + 更新文件模型 + """ + # name 要带文件后缀名 + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + name: Optional[str] = Field(default=None, description='文件名称') + meta_fields: Optional[dict] = Field(default=None, description='文件元数据') + + # naive, manual qa table paper book laws presentation picture one email + chunk_method: Optional[str] = Field(default=None, description='分块方法') + + # 不同的分块方法有不同的参数. + parser_config: Optional[dict] = Field(default=None, description='解析器配置') + + # status: Optional[str] = Field(default=None, description='状态') + + +class DocumentIdsModel(BaseModel): + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + documnet_ids: list[str] = Field(default=None, description='文件ID列表') + +class DeleteFileModel(BaseModel): + + + ids: List[str] = Field(description='文档ID列表') + + +class CreateDatasetModel(BaseModel): + """ + 创建数据集参数模型 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + name:str = Field(description='数据集名称') + avatar: Optional[str] = Field(default=None, description='数据集头像, base64编码') + description: Optional[str] = Field(default=None, description='数据集描述') + embedding_model: Optional[str] = Field(default=None, description='数据集的embedding模型') + permission: Optional[str] = Field(default = "me", description='数据集权限') + chunk_method: Optional[str] = Field(default = "naive", description='数据集分块方法') + parser_config: Optional[dict] = Field(default = None, description='数据集解析配置') + +class LLM(BaseModel): + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + model_name: Optional[str] = Field(default=None, description='模型名称') + temperature: Optional[float] = Field(default=0.1, description='模型温度') + top_p: Optional[float] = Field(default=0.3, description='模型top_p') + presence_penalty: Optional[float] = Field(default=0.2, description='模型presence_penalty') + frequency_penalty: Optional[float] = Field(default=0.7, description='模型frequency_penalty') + +class Prompt(BaseModel): + """ + 聊天助手提示词参数 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + similarity_threshold: Optional[float] = Field(default=0.2, description='相似度阈值') + keywords_similarity_weight: Optional[float] = Field(default=0.7, description='关键词相似度权重') + top_n: Optional[int] = Field(default=8, description='返回结果数量') + variables: Optional[List[dict]] = Field(default=[{"key": "knowledge", "optional": "true"}], description='变量列表') + # 默认余弦相似度 + rerank_model: Optional[str] = Field(default=None, description='rerank模型') + empty_response: Optional[str] = Field(default=None, description='空结果回复') + opener: Optional[str] = Field(default=None, description='开启者') + show_quote: Optional[bool] = Field(default=True, description='是否显示引用') + prompt: Optional[str] = Field(default=None, description='提示语') + + + +class UpdateChatAssistantModel(BaseModel): + """ 修改聊天助手参数 + + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + chat_id: str = Field(default = None, description='聊天助手ID') + name: Optional[str] = Field(default = None, description='聊天助手名称') + # base64编码的图像 + avatar: Optional[str] = Field(default = None, description='聊天助手头像') + # 启用了哪些数据集? + dataset_ids: Optional[List[str]] = Field(default = None, description='数据集ID列表') + + # 模型配置参数 + llm: Optional[LLM] = Field(default = None, description='LLM模型') + + prompt: Optional[Prompt] = Field(default = None, description='LLM模型') + + +class CreateSessionWithChatModel(BaseModel): + """ + 创建会话及会话内容模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + chat_id: str = Field(default = None, description='会话ID') + name: str = Field(default = None, description='会话名称') + user_id: Optional[str] = Field(default = None, description='用户ID') + +class ConverseWithChatAssistantModel(BaseModel): + """ + 会话聊天模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + chat_id: str = Field(default=None, description='会话ID') + question: str = Field(default=None, description='问题') + stream: Optional[bool] = Field(default=True, description='是否流式返回') + session_id: Optional[str] = Field(default=None, description='会话ID') + user_id: Optional[str] = Field(default=None, description='用户ID') + + + + + + + + \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/robot_action_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/robot_action_vo.py new file mode 100644 index 0000000..38ecde2 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/robot_action_vo.py @@ -0,0 +1,54 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from typing import Optional +from module_admin.annotation.pydantic_annotation import as_query + + + + +class Robot_actionModel(BaseModel): + """ + 机器人动作表对应pydantic模型 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + id: Optional[int] = Field(default=None, description='主键 自增') + name: Optional[str] = Field(default=None, description='动作名') + description: Optional[str] = Field(default=None, description='描述') + interval_min: Optional[int] = Field(default=None, description='最小时间间隔 单位s') + interval_max: Optional[int] = Field(default=None, description='最大时间间隔 单位s') + status: Optional[str] = Field(default=None, description='动作状态 0 未启用, 1已启用') + update_time: Optional[datetime] = Field(default=None, description='修改时间') + + + + + + +class Robot_actionQueryModel(Robot_actionModel): + """ + 机器人动作不分页查询模型 + """ + begin_update_time: Optional[str] = Field(default=None, description='开始修改时间') + end_update_time: Optional[str] = Field(default=None, description='结束修改时间') + + +@as_query +class Robot_actionPageQueryModel(Robot_actionQueryModel): + """ + 机器人动作分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=20, description='每页记录数') + + +class DeleteRobot_actionModel(BaseModel): + """ + 删除机器人动作模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + ids: str = Field(description='需要删除的主键 自增') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/robot_role_pairing_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/robot_role_pairing_vo.py new file mode 100644 index 0000000..d5329fd --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/robot_role_pairing_vo.py @@ -0,0 +1,94 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import NotBlank +from typing import Optional +from module_admin.annotation.pydantic_annotation import as_query + + + + +# class RolePairing(BaseModel): +# """ +# 角色-机器人-映射表对应pydantic模型, 查询角色-机器人结果的返回模型 +# """ +# model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) +# +# pairing_id: Optional[int] = Field(default=None, description='主键ID') +# rolename: Optional[str] = Field(default=None, description='角色名称') +# detail: Optional[str] = Field(default=None, description='角色介绍') +# status: Optional[str] = Field(default=None, description='使用情况(0停用,1使用)') + +class ChangeRobotRoleModel(BaseModel): + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + robot_id: Optional[int] = Field(default=None, description='机器人ID') + pairing_id: Optional[int] = Field(default=None, description='主键ID') + +class PairingModel(BaseModel): + """ + 角色-机器人-映射表对应pydantic模型 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + pairing_id: Optional[int] = Field(default=None, description='主键ID') + robot_role_id: Optional[int] = Field(default=None, description='机器人角色ID') + robot_id: Optional[int] = Field(default=None, description='机器人ID') + status: Optional[str] = Field(default=None, description='使用情况(0停用,1使用)') + update_time: Optional[datetime] = Field(default=None, description='更新时间') + update_by: Optional[str] = Field(default=None, description='修改者') + + @NotBlank(field_name='robot_role_id', message='机器人角色ID不能为空') + def get_robot_role_id(self): + return self.robot_role_id + + @NotBlank(field_name='robot_id', message='机器人ID不能为空') + def get_robot_id(self): + return self.robot_id + + @NotBlank(field_name='status', message='使用情况(0停用,1使用)不能为空') + def get_status(self): + return self.status + + @NotBlank(field_name='update_time', message='更新时间不能为空') + def get_update_time(self): + return self.update_time + + @NotBlank(field_name='update_by', message='修改者不能为空') + def get_update_by(self): + return self.update_by + + def validate_fields(self): + self.get_robot_role_id() + self.get_robot_id() + self.get_status() + self.get_update_time() + self.get_update_by() + + + + +class PairingQueryModel(PairingModel): + """ + 角色-机器人-映射不分页查询模型 + """ + pass + + +@as_query +class PairingPageQueryModel(PairingQueryModel): + """ + 角色-机器人-映射分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeletePairingModel(BaseModel): + """ + 删除角色-机器人-映射模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + pairing_ids: str = Field(description='需要删除的主键ID') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/robot_role_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/robot_role_vo.py new file mode 100644 index 0000000..30317e1 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/robot_role_vo.py @@ -0,0 +1,84 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import NotBlank +from typing import Optional +from module_admin.annotation.pydantic_annotation import as_query + + + + +class RoleModel(BaseModel): + """ + 系统角色表对应pydantic模型 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + robot_role_id: Optional[int] = Field(default=None, description='机器人角色ID') + rolename: Optional[str] = Field(default=None, description='角色名称') + detail: Optional[str] = Field(default=None, description='角色介绍') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + create_by: Optional[str] = Field(default=None, description='创建者') + update_time: Optional[datetime] = Field(default=None, description='修改时间') + update_by: Optional[str] = Field(default=None, description='修改者') + remark: Optional[str] = Field(default=None, description='备注') + + @NotBlank(field_name='rolename', message='角色名称不能为空') + def get_rolename(self): + return self.rolename + + @NotBlank(field_name='create_time', message='创建时间不能为空') + def get_create_time(self): + return self.create_time + + @NotBlank(field_name='create_by', message='创建者不能为空') + def get_create_by(self): + return self.create_by + + @NotBlank(field_name='update_time', message='修改时间不能为空') + def get_update_time(self): + return self.update_time + + @NotBlank(field_name='update_by', message='修改者不能为空') + def get_update_by(self): + return self.update_by + + + def validate_fields(self): + self.get_rolename() + self.get_create_time() + self.get_create_by() + self.get_update_time() + self.get_update_by() + + + + +class RoleQueryModel(RoleModel): + """ + 系统角色不分页查询模型 + """ + begin_create_time: Optional[str] = Field(default=None, description='开始创建时间') + end_create_time: Optional[str] = Field(default=None, description='结束创建时间') + begin_update_time: Optional[str] = Field(default=None, description='开始修改时间') + end_update_time: Optional[str] = Field(default=None, description='结束修改时间') + + +@as_query +class RolePageQueryModel(RoleQueryModel): + """ + 系统角色分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteRoleModel(BaseModel): + """ + 删除系统角色模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + robot_role_ids: str = Field(description='需要删除的机器人角色ID') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/role_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/role_vo.py new file mode 100644 index 0000000..a81f8d5 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/role_vo.py @@ -0,0 +1,158 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import NotBlank, Size +from typing import List, Literal, Optional, Union +from module_admin.annotation.pydantic_annotation import as_query + + +class RoleModel(BaseModel): + """ + 角色表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + role_id: Optional[int] = Field(default=None, description='角色ID') + role_name: Optional[str] = Field(default=None, description='角色名称') + role_key: Optional[str] = Field(default=None, description='角色权限字符串') + role_sort: Optional[int] = Field(default=None, description='显示顺序') + data_scope: Optional[Literal['1', '2', '3', '4', '5']] = Field( + default=None, + description='数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限 5:仅本人数据权限)', + ) + menu_check_strictly: Optional[Union[int, bool]] = Field(default=None, description='菜单树选择项是否关联显示') + dept_check_strictly: Optional[Union[int, bool]] = Field(default=None, description='部门树选择项是否关联显示') + status: Optional[Literal['0', '1']] = Field(default=None, description='角色状态(0正常 1停用)') + del_flag: Optional[Literal['0', '2']] = Field(default=None, description='删除标志(0代表存在 2代表删除)') + create_by: Optional[str] = Field(default=None, description='创建者') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + update_by: Optional[str] = Field(default=None, description='更新者') + update_time: Optional[datetime] = Field(default=None, description='更新时间') + remark: Optional[str] = Field(default=None, description='备注') + admin: Optional[bool] = Field(default=False, description='是否为admin') + + @field_validator('menu_check_strictly', 'dept_check_strictly') + @classmethod + def check_filed_mapping(cls, v: Union[int, bool]) -> Union[int, bool]: + if v == 1: + v = True + elif v == 0: + v = False + elif v is True: + v = 1 + elif v is False: + v = 0 + return v + + @model_validator(mode='after') + def check_admin(self) -> 'RoleModel': + if self.role_id == 1: + self.admin = True + else: + self.admin = False + return self + + @NotBlank(field_name='role_name', message='角色名称不能为空') + @Size(field_name='role_name', min_length=0, max_length=30, message='角色名称长度不能超过30个字符') + def get_role_name(self): + return self.role_name + + @NotBlank(field_name='role_key', message='权限字符不能为空') + @Size(field_name='role_key', min_length=0, max_length=100, message='权限字符长度不能超过100个字符') + def get_role_key(self): + return self.role_key + + @NotBlank(field_name='role_sort', message='显示顺序不能为空') + def get_role_sort(self): + return self.role_sort + + def validate_fields(self): + self.get_role_name() + self.get_role_key() + self.get_role_sort() + + +class RoleMenuModel(BaseModel): + """ + 角色和菜单关联表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + role_id: Optional[int] = Field(default=None, description='角色ID') + menu_id: Optional[int] = Field(default=None, description='菜单ID') + + +class RoleDeptModel(BaseModel): + """ + 角色和部门关联表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + role_id: Optional[int] = Field(default=None, description='角色ID') + dept_id: Optional[int] = Field(default=None, description='部门ID') + + +class RoleQueryModel(RoleModel): + """ + 角色管理不分页查询模型 + """ + + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +@as_query +class RolePageQueryModel(RoleQueryModel): + """ + 角色管理分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class RoleMenuQueryModel(BaseModel): + """ + 角色菜单查询模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + menus: List = Field(default=[], description='菜单信息') + checked_keys: List[int] = Field(default=[], description='已选择的菜单ID信息') + + +class RoleDeptQueryModel(BaseModel): + """ + 角色部门查询模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + depts: List = Field(default=[], description='部门信息') + checked_keys: List[int] = Field(default=[], description='已选择的部门ID信息') + + +class AddRoleModel(RoleModel): + """ + 新增角色模型 + """ + + dept_ids: List = Field(default=[], description='部门ID信息') + menu_ids: List = Field(default=[], description='菜单ID信息') + type: Optional[str] = Field(default=None, description='操作类型') + + +class DeleteRoleModel(BaseModel): + """ + 删除角色模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + role_ids: str = Field(description='需要删除的菜单ID') + update_by: Optional[str] = Field(default=None, description='更新者') + update_time: Optional[datetime] = Field(default=None, description='更新时间') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/server_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/server_vo.py new file mode 100644 index 0000000..810ecac --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/server_vo.py @@ -0,0 +1,67 @@ +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from typing import List, Optional + + +class CpuInfo(BaseModel): + model_config = ConfigDict(alias_generator=to_camel) + + cpu_num: Optional[int] = Field(default=None, description='核心数') + used: Optional[float] = Field(default=None, description='CPU用户使用率') + sys: Optional[float] = Field(default=None, description='CPU系统使用率') + free: Optional[float] = Field(default=None, description='CPU当前空闲率') + + +class MemoryInfo(BaseModel): + model_config = ConfigDict(alias_generator=to_camel) + + total: Optional[str] = Field(default=None, description='内存总量') + used: Optional[str] = Field(default=None, description='已用内存') + free: Optional[str] = Field(default=None, description='剩余内存') + usage: Optional[float] = Field(default=None, description='使用率') + + +class SysInfo(BaseModel): + model_config = ConfigDict(alias_generator=to_camel) + + computer_ip: Optional[str] = Field(default=None, description='服务器IP') + computer_name: Optional[str] = Field(default=None, description='服务器名称') + os_arch: Optional[str] = Field(default=None, description='系统架构') + os_name: Optional[str] = Field(default=None, description='操作系统') + user_dir: Optional[str] = Field(default=None, description='项目路径') + + +class PyInfo(MemoryInfo): + model_config = ConfigDict(alias_generator=to_camel) + + name: Optional[str] = Field(default=None, description='Python名称') + version: Optional[str] = Field(default=None, description='Python版本') + start_time: Optional[str] = Field(default=None, description='启动时间') + run_time: Optional[str] = Field(default=None, description='运行时长') + home: Optional[str] = Field(default=None, description='安装路径') + + +class SysFiles(BaseModel): + model_config = ConfigDict(alias_generator=to_camel) + + dir_name: Optional[str] = Field(default=None, description='盘符路径') + sys_type_name: Optional[str] = Field(default=None, description='盘符类型') + type_name: Optional[str] = Field(default=None, description='文件类型') + total: Optional[str] = Field(default=None, description='总大小') + used: Optional[str] = Field(default=None, description='已经使用量') + free: Optional[str] = Field(default=None, description='剩余大小') + usage: Optional[str] = Field(default=None, description='资源的使用率') + + +class ServerMonitorModel(BaseModel): + """ + 服务监控对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + cpu: Optional[CpuInfo] = Field(description='CPU相关信息') + py: Optional[PyInfo] = Field(description='Python相关信息') + mem: Optional[MemoryInfo] = Field(description='內存相关信息') + sys: Optional[SysInfo] = Field(description='服务器相关信息') + sys_files: Optional[List[SysFiles]] = Field(description='磁盘相关信息') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/sys_alert_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/sys_alert_vo.py new file mode 100644 index 0000000..cfb949e --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/sys_alert_vo.py @@ -0,0 +1,57 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from typing import Optional +from module_admin.annotation.pydantic_annotation import as_query + + + + +class Sys_alertModel(BaseModel): + """ + 系统告警表对应pydantic模型 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + id: Optional[int] = Field(default=None, description='主键 自增') + detail: Optional[str] = Field(default=None, description='告警描述') + type: Optional[str] = Field(default=None, description='告警类型') + status: Optional[str] = Field(default=None, description='告警状态, 0 未处理, 1 已处理') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + create_by: Optional[str] = Field(default=None, description='创建者') + update_time: Optional[datetime] = Field(default=None, description='更新时间') + update_by: Optional[str] = Field(default=None, description='更新者') + + + + + + +class Sys_alertQueryModel(Sys_alertModel): + """ + 系统告警不分页查询模型 + """ + begin_create_time: Optional[str] = Field(default=None, description='开始创建时间') + end_create_time: Optional[str] = Field(default=None, description='结束创建时间') + begin_update_time: Optional[str] = Field(default=None, description='开始更新时间') + end_update_time: Optional[str] = Field(default=None, description='结束更新时间') + + +@as_query +class Sys_alertPageQueryModel(Sys_alertQueryModel): + """ + 系统告警分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteSys_alertModel(BaseModel): + """ + 删除系统告警模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + ids: str = Field(description='需要删除的主键 自增') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/sys_statistics_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/sys_statistics_vo.py new file mode 100644 index 0000000..92a263c --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/sys_statistics_vo.py @@ -0,0 +1,69 @@ +from datetime import datetime, date +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import NotBlank +from typing import Optional +from module_admin.annotation.pydantic_annotation import as_query + + + + +class Sys_statisticsModel(BaseModel): + """ + 系统统计数据表对应pydantic模型 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + statistic_id: Optional[int] = Field(default=None, description='统计项目ID') + stat_date: Optional[date] = Field(default=None, description='统计日期') + llm_call: Optional[int] = Field(default=None, description='大模型调用量') + access_control_count: Optional[int] = Field(default=None, description='门禁通行量') + visitor_guide: Optional[int] = Field(default=None, description='访客引导次数') + exhibition_explanation: Optional[int] = Field(default=None, description='展厅讲解次数') + access_control_success_rate: Optional[int] = Field(default=None, description='门禁识别成功率(%)') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + create_by: Optional[str] = Field(default=None, description='创建者') + update_time: Optional[datetime] = Field(default=None, description='修改时间') + update_by: Optional[str] = Field(default=None, description='修改者') + + @NotBlank(field_name='stat_date', message='统计日期不能为空') + def get_stat_date(self): + return self.stat_date + + @NotBlank(field_name='update_time', message='修改时间不能为空') + def get_update_time(self): + return self.update_time + + + def validate_fields(self): + self.get_stat_date() + self.get_update_time() + + + + +class Sys_statisticsQueryModel(Sys_statisticsModel): + """ + 系统统计数据不分页查询模型 + """ + pass + + +@as_query +class Sys_statisticsPageQueryModel(Sys_statisticsQueryModel): + """ + 系统统计数据分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteSys_statisticsModel(BaseModel): + """ + 删除系统统计数据模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + statistic_ids: str = Field(description='需要删除的统计项目ID') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/test_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/test_vo.py new file mode 100644 index 0000000..e2ad1d6 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/test_vo.py @@ -0,0 +1,21 @@ +from typing import Optional +from pydantic import BaseModel, Field, ConfigDict +from module_admin.annotation.pydantic_annotation import as_query +from pydantic.alias_generators import to_camel + + + +class TestUserName(BaseModel): + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + user_name: Optional[str] = Field(default=None, description='用户名') + + +class TestPostForm(BaseModel): + user_name: Optional[str] = None + password: Optional[str] = None + +@as_query +class TestUserNamePage(TestUserName): + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/user_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/user_vo.py new file mode 100644 index 0000000..4eac953 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/user_vo.py @@ -0,0 +1,282 @@ +import re +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field, model_validator +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import Network, NotBlank, Size, Xss +from typing import List, Literal, Optional, Union +from exceptions.exception import ModelValidatorException +from module_admin.annotation.pydantic_annotation import as_query +from module_admin.entity.vo.dept_vo import DeptModel +from module_admin.entity.vo.post_vo import PostModel +from module_admin.entity.vo.role_vo import RoleModel + + +class TokenData(BaseModel): + """ + token解析结果 + """ + + user_id: Union[int, None] = Field(default=None, description='用户ID') + + +class UserModel(BaseModel): + """ + 用户表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + user_id: Optional[int] = Field(default=None, description='用户ID') + dept_id: Optional[int] = Field(default=None, description='部门ID') + user_name: Optional[str] = Field(default=None, description='用户账号') + nick_name: Optional[str] = Field(default=None, description='用户昵称') + user_type: Optional[str] = Field(default=None, description='用户类型(00系统用户)') + email: Optional[str] = Field(default=None, description='用户邮箱') + phonenumber: Optional[str] = Field(default=None, description='手机号码') + sex: Optional[Literal['0', '1', '2']] = Field(default=None, description='用户性别(0男 1女 2未知)') + avatar: Optional[str] = Field(default=None, description='头像地址') + password: Optional[str] = Field(default=None, description='密码') + status: Optional[Literal['0', '1']] = Field(default=None, description='帐号状态(0正常 1停用)') + del_flag: Optional[Literal['0', '2']] = Field(default=None, description='删除标志(0代表存在 2代表删除)') + login_ip: Optional[str] = Field(default=None, description='最后登录IP') + login_date: Optional[datetime] = Field(default=None, description='最后登录时间') + create_by: Optional[str] = Field(default=None, description='创建者') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + update_by: Optional[str] = Field(default=None, description='更新者') + update_time: Optional[datetime] = Field(default=None, description='更新时间') + remark: Optional[str] = Field(default=None, description='备注') + admin: Optional[bool] = Field(default=False, description='是否为admin') + + @model_validator(mode='after') + def check_password(self) -> 'UserModel': + pattern = r"""^[^<>"'|\\]+$""" + if self.password is None or re.match(pattern, self.password): + return self + else: + raise ModelValidatorException(message='密码不能包含非法字符:< > " \' \\ |') + + @model_validator(mode='after') + def check_admin(self) -> 'UserModel': + if self.user_id == 1: + self.admin = True + else: + self.admin = False + return self + + @Xss(field_name='user_name', message='用户账号不能包含脚本字符') + @NotBlank(field_name='user_name', message='用户账号不能为空') + @Size(field_name='user_name', min_length=0, max_length=30, message='用户账号长度不能超过30个字符') + def get_user_name(self): + return self.user_name + + @Xss(field_name='nick_name', message='用户昵称不能包含脚本字符') + @Size(field_name='nick_name', min_length=0, max_length=30, message='用户昵称长度不能超过30个字符') + def get_nick_name(self): + return self.nick_name + + @Network(field_name='email', field_type='EmailStr', message='邮箱格式不正确') + @Size(field_name='email', min_length=0, max_length=50, message='邮箱长度不能超过50个字符') + def get_email(self): + return self.email + + @Size(field_name='phonenumber', min_length=0, max_length=11, message='手机号码长度不能超过11个字符') + def get_phonenumber(self): + return self.phonenumber + + def validate_fields(self): + self.get_user_name() + self.get_nick_name() + self.get_email() + self.get_phonenumber() + + +class UserRoleModel(BaseModel): + """ + 用户和角色关联表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + user_id: Optional[int] = Field(default=None, description='用户ID') + role_id: Optional[int] = Field(default=None, description='角色ID') + + +class UserPostModel(BaseModel): + """ + 用户与岗位关联表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + user_id: Optional[int] = Field(default=None, description='用户ID') + post_id: Optional[int] = Field(default=None, description='岗位ID') + + +class UserInfoModel(UserModel): + post_ids: Optional[Union[str, None]] = Field(default=None, description='岗位ID信息') + role_ids: Optional[Union[str, None]] = Field(default=None, description='角色ID信息') + dept: Optional[Union[DeptModel, None]] = Field(default=None, description='部门信息') + role: Optional[List[Union[RoleModel, None]]] = Field(default=[], description='角色信息') + + +class CurrentUserModel(BaseModel): + model_config = ConfigDict(alias_generator=to_camel) + + permissions: List = Field(description='权限信息') + roles: List = Field(description='角色信息') + user: Union[UserInfoModel, None] = Field(description='用户信息') + + +class UserDetailModel(BaseModel): + """ + 获取用户详情信息响应模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + data: Optional[Union[UserInfoModel, None]] = Field(default=None, description='用户信息') + post_ids: Optional[List] = Field(default=None, description='岗位ID信息') + posts: List[Union[PostModel, None]] = Field(description='岗位信息') + role_ids: Optional[List] = Field(default=None, description='角色ID信息') + roles: List[Union[RoleModel, None]] = Field(description='角色信息') + + +class UserProfileModel(BaseModel): + """ + 获取个人信息响应模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + data: Union[UserInfoModel, None] = Field(description='用户信息') + post_group: Union[str, None] = Field(description='岗位信息') + role_group: Union[str, None] = Field(description='角色信息') + + +class UserQueryModel(UserModel): + """ + 用户管理不分页查询模型 + """ + + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +@as_query +class UserPageQueryModel(UserQueryModel): + """ + 用户管理分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class AddUserModel(UserModel): + """ + 新增用户模型 + """ + + role_ids: Optional[List] = Field(default=[], description='角色ID信息') + post_ids: Optional[List] = Field(default=[], description='岗位ID信息') + type: Optional[str] = Field(default=None, description='操作类型') + + +class EditUserModel(AddUserModel): + """ + 编辑用户模型 + """ + + role: Optional[List] = Field(default=[], description='角色信息') + + +class ResetPasswordModel(BaseModel): + """ + 重置密码模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + old_password: Optional[str] = Field(default=None, description='旧密码') + new_password: Optional[str] = Field(default=None, description='新密码') + + @model_validator(mode='after') + def check_new_password(self) -> 'ResetPasswordModel': + pattern = r"""^[^<>"'|\\]+$""" + if self.new_password is None or re.match(pattern, self.new_password): + return self + else: + raise ModelValidatorException(message='密码不能包含非法字符:< > " \' \\ |') + + +class ResetUserModel(UserModel): + """ + 重置用户密码模型 + """ + + old_password: Optional[str] = Field(default=None, description='旧密码') + sms_code: Optional[str] = Field(default=None, description='验证码') + session_id: Optional[str] = Field(default=None, description='会话id') + + +class DeleteUserModel(BaseModel): + """ + 删除用户模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + user_ids: str = Field(description='需要删除的用户ID') + update_by: Optional[str] = Field(default=None, description='更新者') + update_time: Optional[datetime] = Field(default=None, description='更新时间') + + +class UserRoleQueryModel(UserModel): + """ + 用户角色关联管理不分页查询模型 + """ + + role_id: Optional[int] = Field(default=None, description='角色ID') + + +@as_query +class UserRolePageQueryModel(UserRoleQueryModel): + """ + 用户角色关联管理分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class SelectedRoleModel(RoleModel): + """ + 是否选择角色模型 + """ + + flag: Optional[bool] = Field(default=False, description='选择标识') + + +class UserRoleResponseModel(BaseModel): + """ + 用户角色关联管理列表返回模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + roles: List[Union[SelectedRoleModel, None]] = Field(default=[], description='角色信息') + user: UserInfoModel = Field(description='用户信息') + + +@as_query +class CrudUserRoleModel(BaseModel): + """ + 新增、删除用户关联角色及角色关联用户模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + user_id: Optional[int] = Field(default=None, description='用户ID') + user_ids: Optional[str] = Field(default=None, description='用户ID信息') + role_id: Optional[int] = Field(default=None, description='角色ID') + role_ids: Optional[str] = Field(default=None, description='角色ID信息') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/visitor_type_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/visitor_type_vo.py new file mode 100644 index 0000000..f7f53ff --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/visitor_type_vo.py @@ -0,0 +1,60 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import NotBlank +from typing import Optional +from module_admin.annotation.pydantic_annotation import as_query + + + + +class Visitor_typeModel(BaseModel): + """ + 访客类型表对应pydantic模型 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + type_id: Optional[int] = Field(default=None, description='访客类型ID') + name: Optional[str] = Field(default=None, description='类型名称') + value: Optional[str] = Field(default=None, description='类型值') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + create_by: Optional[str] = Field(default=None, description='创建者') + update_time: Optional[datetime] = Field(default=None, description='修改时间') + update_by: Optional[str] = Field(default=None, description='修改者') + + @NotBlank(field_name='value', message='类型值不能为空') + def get_value(self): + return self.value + + + def validate_fields(self): + self.get_value() + + + + +class Visitor_typeQueryModel(Visitor_typeModel): + """ + 访客类型不分页查询模型 + """ + pass + + +@as_query +class Visitor_typePageQueryModel(Visitor_typeQueryModel): + """ + 访客类型分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteVisitor_typeModel(BaseModel): + """ + 删除访客类型模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + type_ids: str = Field(description='需要删除的访客类型ID') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/words_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/words_vo.py new file mode 100644 index 0000000..c6139ff --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/words_vo.py @@ -0,0 +1,80 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import NotBlank +from typing import Optional +from module_admin.annotation.pydantic_annotation import as_query + + + + +class WordsModel(BaseModel): + """ + 引导词表对应pydantic模型 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + guide_id: Optional[int] = Field(default=None, description='引导词id') + type: Optional[str] = Field(default=None, description='引导词类型') + text: Optional[str] = Field(default=None, description='文本内容') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + update_time: Optional[datetime] = Field(default=None, description='修改时间') + create_by: Optional[str] = Field(default=None, description='创建者') + remark: Optional[str] = Field(default=None, description='备注') + del_flag: Optional[str] = Field(default=None, description='删除标志(0代表存在 2代表删除)') + update_by : Optional[str] = Field(default=None, description='修改者') + title: Optional[str] = Field(default=None, description='引导词名称') + label: Optional[str] = Field(default=None, description='引导词标签') + trigger_type: Optional[str] = Field(default=None, description='触发方式') + + + + + # @NotBlank(field_name='del_flag', message='删除标志不能为空') + def get_del_flag(self): + return self.del_flag + + def validate_fields(self): + self.get_del_flag() + +class WordsUpdateModel(WordsModel): + """ + 引导词更新模型 + """ + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + guide_id: int = Field(description='引导词id') + text: str = Field(default=None, description='文本内容') + type: Optional[str] = Field(default=None, description='引导词类型') + remark : Optional[str] = Field(default=None, description='备注') + update_time: Optional[datetime] = Field(default=None, description='修改时间') + update_by : Optional[str] = Field(default=None, description='修改者') + + +class WordsQueryModel(WordsModel): + """ + 引导词不分页查询模型 + """ + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +@as_query +class WordsPageQueryModel(WordsQueryModel): + """ + 引导词分页查询模型 + """ + + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteWordsModel(BaseModel): + """ + 删除引导词模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + guide_ids: str = Field(description='需要删除的引导词id') diff --git a/ruoyi-fastapi-backend/module_admin/service/__init__.py b/ruoyi-fastapi-backend/module_admin/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ruoyi-fastapi-backend/module_admin/service/cache_service.py b/ruoyi-fastapi-backend/module_admin/service/cache_service.py new file mode 100644 index 0000000..72212cb --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/cache_service.py @@ -0,0 +1,124 @@ +from fastapi import Request +from config.enums import RedisInitKeyConfig +from config.get_redis import RedisUtil +from module_admin.entity.vo.cache_vo import CacheInfoModel, CacheMonitorModel +from module_admin.entity.vo.common_vo import CrudResponseModel + + +class CacheService: + """ + 缓存监控模块服务层 + """ + + @classmethod + async def get_cache_monitor_statistical_info_services(cls, request: Request): + """ + 获取缓存监控信息service + + :param request: Request对象 + :return: 缓存监控信息 + """ + info = await request.app.state.redis.info() + db_size = await request.app.state.redis.dbsize() + command_stats_dict = await request.app.state.redis.info('commandstats') + command_stats = [ + dict(name=key.split('_')[1], value=str(value.get('calls'))) for key, value in command_stats_dict.items() + ] + result = CacheMonitorModel(commandStats=command_stats, dbSize=db_size, info=info) + + return result + + @classmethod + async def get_cache_monitor_cache_name_services(cls): + """ + 获取缓存名称列表信息service + + :return: 缓存名称列表信息 + """ + name_list = [] + for key_config in RedisInitKeyConfig: + name_list.append( + CacheInfoModel( + cacheKey='', + cacheName=key_config.key, + cacheValue='', + remark=key_config.remark, + ) + ) + + return name_list + + @classmethod + async def get_cache_monitor_cache_key_services(cls, request: Request, cache_name: str): + """ + 获取缓存键名列表信息service + + :param request: Request对象 + :param cache_name: 缓存名称 + :return: 缓存键名列表信息 + """ + cache_keys = await request.app.state.redis.keys(f'{cache_name}*') + cache_key_list = [key.split(':', 1)[1] for key in cache_keys if key.startswith(f'{cache_name}:')] + + return cache_key_list + + @classmethod + async def get_cache_monitor_cache_value_services(cls, request: Request, cache_name: str, cache_key: str): + """ + 获取缓存内容信息service + + :param request: Request对象 + :param cache_name: 缓存名称 + :param cache_key: 缓存键名 + :return: 缓存内容信息 + """ + cache_value = await request.app.state.redis.get(f'{cache_name}:{cache_key}') + + return CacheInfoModel(cacheKey=cache_key, cacheName=cache_name, cacheValue=cache_value, remark='') + + @classmethod + async def clear_cache_monitor_cache_name_services(cls, request: Request, cache_name: str): + """ + 清除缓存名称对应所有键值service + + :param request: Request对象 + :param cache_name: 缓存名称 + :return: 操作缓存响应信息 + """ + cache_keys = await request.app.state.redis.keys(f'{cache_name}*') + if cache_keys: + await request.app.state.redis.delete(*cache_keys) + + return CrudResponseModel(is_success=True, message=f'{cache_name}对应键值清除成功') + + @classmethod + async def clear_cache_monitor_cache_key_services(cls, request: Request, cache_key: str): + """ + 清除缓存名称对应所有键值service + + :param request: Request对象 + :param cache_key: 缓存键名 + :return: 操作缓存响应信息 + """ + cache_keys = await request.app.state.redis.keys(f'*{cache_key}') + if cache_keys: + await request.app.state.redis.delete(*cache_keys) + + return CrudResponseModel(is_success=True, message=f'{cache_key}清除成功') + + @classmethod + async def clear_cache_monitor_all_services(cls, request: Request): + """ + 清除所有缓存service + + :param request: Request对象 + :return: 操作缓存响应信息 + """ + cache_keys = await request.app.state.redis.keys() + if cache_keys: + await request.app.state.redis.delete(*cache_keys) + + await RedisUtil.init_sys_dict(request.app.state.redis) + await RedisUtil.init_sys_config(request.app.state.redis) + + return CrudResponseModel(is_success=True, message='所有缓存清除成功') diff --git a/ruoyi-fastapi-backend/module_admin/service/captcha_service.py b/ruoyi-fastapi-backend/module_admin/service/captcha_service.py new file mode 100644 index 0000000..1be8ffb --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/captcha_service.py @@ -0,0 +1,48 @@ +import base64 +import io +import os +import random +from PIL import Image, ImageDraw, ImageFont + + +class CaptchaService: + """ + 验证码模块服务层 + """ + + @classmethod + async def create_captcha_image_service(cls): + # 创建空白图像 + image = Image.new('RGB', (160, 60), color='#EAEAEA') + + # 创建绘图对象 + draw = ImageDraw.Draw(image) + + # 设置字体 + font = ImageFont.truetype(os.path.join(os.path.abspath(os.getcwd()), 'assets', 'font', 'Arial.ttf'), size=30) + + # 生成两个0-9之间的随机整数 + num1 = random.randint(0, 9) + num2 = random.randint(0, 9) + # 从运算符列表中随机选择一个 + operational_character_list = ['+', '-', '*'] + operational_character = random.choice(operational_character_list) + # 根据选择的运算符进行计算 + if operational_character == '+': + result = num1 + num2 + elif operational_character == '-': + result = num1 - num2 + else: + result = num1 * num2 + # 绘制文本 + text = f'{num1} {operational_character} {num2} = ?' + draw.text((25, 15), text, fill='blue', font=font) + + # 将图像数据保存到内存中 + buffer = io.BytesIO() + image.save(buffer, format='PNG') + + # 将图像数据转换为base64字符串 + base64_string = base64.b64encode(buffer.getvalue()).decode() + + return [base64_string, result] diff --git a/ruoyi-fastapi-backend/module_admin/service/common_service.py b/ruoyi-fastapi-backend/module_admin/service/common_service.py new file mode 100644 index 0000000..20eb868 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/common_service.py @@ -0,0 +1,91 @@ +import os +from datetime import datetime +from fastapi import BackgroundTasks, Request, UploadFile +from config.env import UploadConfig +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel, UploadResponseModel +from utils.upload_util import UploadUtil + + +class CommonService: + """ + 通用模块服务层 + """ + + @classmethod + async def upload_service(cls, request: Request, file: UploadFile): + """ + 通用上传service + + :param request: Request对象 + :param file: 上传文件对象 + :return: 上传结果 + """ + if not UploadUtil.check_file_extension(file): + raise ServiceException(message='文件类型不合法') + else: + relative_path = f'upload/{datetime.now().strftime("%Y")}/{datetime.now().strftime("%m")}/{datetime.now().strftime("%d")}' + dir_path = os.path.join(UploadConfig.UPLOAD_PATH, relative_path) + try: + os.makedirs(dir_path) + except FileExistsError: + pass + filename = f'{file.filename.rsplit(".", 1)[0]}_{datetime.now().strftime("%Y%m%d%H%M%S")}{UploadConfig.UPLOAD_MACHINE}{UploadUtil.generate_random_number()}.{file.filename.rsplit(".")[-1]}' + filepath = os.path.join(dir_path, filename) + with open(filepath, 'wb') as f: + # 流式写出大型文件,这里的10代表10MB + for chunk in iter(lambda: file.file.read(1024 * 1024 * 10), b''): + f.write(chunk) + + return CrudResponseModel( + is_success=True, + result=UploadResponseModel( + fileName=f'{UploadConfig.UPLOAD_PREFIX}/{relative_path}/{filename}', + newFileName=filename, + originalFilename=file.filename, + url=f'{request.base_url}{UploadConfig.UPLOAD_PREFIX[1:]}/{relative_path}/{filename}', + ), + message='上传成功', + ) + + @classmethod + async def download_services(cls, background_tasks: BackgroundTasks, file_name, delete: bool): + """ + 下载下载目录文件service + + :param background_tasks: 后台任务对象 + :param file_name: 下载的文件名称 + :param delete: 是否在下载完成后删除文件 + :return: 上传结果 + """ + filepath = os.path.join(UploadConfig.DOWNLOAD_PATH, file_name) + if '..' in file_name: + raise ServiceException(message='文件名称不合法') + elif not UploadUtil.check_file_exists(filepath): + raise ServiceException(message='文件不存在') + else: + if delete: + background_tasks.add_task(UploadUtil.delete_file, filepath) + return CrudResponseModel(is_success=True, result=UploadUtil.generate_file(filepath), message='下载成功') + + @classmethod + async def download_resource_services(cls, resource: str): + """ + 下载上传目录文件service + + :param resource: 下载的文件名称 + :return: 上传结果 + """ + filepath = os.path.join(resource.replace(UploadConfig.UPLOAD_PREFIX, UploadConfig.UPLOAD_PATH)) + filename = resource.rsplit('/', 1)[-1] + if ( + '..' in filename + or not UploadUtil.check_file_timestamp(filename) + or not UploadUtil.check_file_machine(filename) + or not UploadUtil.check_file_random_code(filename) + ): + raise ServiceException(message='文件名称不合法') + elif not UploadUtil.check_file_exists(filepath): + raise ServiceException(message='文件不存在') + else: + return CrudResponseModel(is_success=True, result=UploadUtil.generate_file(filepath), message='下载成功') diff --git a/ruoyi-fastapi-backend/module_admin/service/compreface_service.py b/ruoyi-fastapi-backend/module_admin/service/compreface_service.py new file mode 100644 index 0000000..7764e98 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/compreface_service.py @@ -0,0 +1,185 @@ +from typing import Dict, List, Optional + +from utils.compreface_util import ComprefaceUtil +from config.env import ComprefaceConfig + +POSE_MAX_ANGLE = getattr(ComprefaceConfig, "COMPREFACE_POSE_MAX_ANGLE", 10) +MIN_FACE_WIDTH = getattr(ComprefaceConfig, "COMPREFACE_MIN_FACE_WIDTH", 200) +MIN_FACE_HEIGHT = getattr(ComprefaceConfig, "COMPREFACE_MIN_FACE_HEIGHT", 200) +MIN_DETECTION_PROBABILITY = getattr(ComprefaceConfig, "COMPREFACE_MIN_DETECTION_PROBABILITY", 0.9) + +class ComprefaceService(ComprefaceUtil): + + """compreface服务类""" + + @classmethod + def _is_pose_front(cls, pose: Dict | None) -> bool: + if not pose: + return False + return all(abs(float(pose.get(axis, 0))) <= POSE_MAX_ANGLE for axis in ("pitch", "roll", "yaw")) + + @classmethod + def _is_face_close(cls, box: Dict | None) -> bool: + if not box: + return False + width = float(box.get("x_max", 0)) - float(box.get("x_min", 0)) + height = float(box.get("y_max", 0)) - float(box.get("y_min", 0)) + probability = float(box.get("probability", 0)) + return ( + width >= MIN_FACE_WIDTH + and height >= MIN_FACE_HEIGHT + and probability >= MIN_DETECTION_PROBABILITY + ) + + @classmethod + def _select_valid_detection(cls, detections: List[Dict]) -> Optional[Dict]: + for detection in detections: + pose = detection.get("pose") + box = detection.get("box") + if cls._is_pose_front(pose) and cls._is_face_close(box): + return detection + return None + + @staticmethod + def _pending_response() -> Dict[str, str]: + return { + "name": "未识别", + "role": "等待正脸" + } + + # 人脸检测 + @classmethod + async def face_detection_service(cls, image, options: dict = {}): + """ 人脸检测 + + Args: + image_path (str): 图片路径 + options (dict, optional): 检测参数. Defaults to {}. + + Returns: + [type]: [description] + """ + options={ + "limit": 0, + "det_prob_threshold": 0.8, + "prediction_count": 1, + # 可选参数 age,gender,landmarks,calculator + "face_plugins": "pose", + "status": "false", + } + result = await ComprefaceUtil.face_detection(image, options) + return result.get("result", []) + + # 人脸识别 + @classmethod + async def face_recognition_service(cls, image, options: dict = {}): + """ 人脸识别 + + Args: + image_path (str): 图片路径 + options (dict, optional): 参数. Defaults to {}. + + Returns: + [type]: [description] + """ + detections = await cls.face_detection_service(image) + detection = cls._select_valid_detection(detections) + if not detection: + return cls._pending_response() + + result = await ComprefaceUtil.face_recognition(image, options) + recognition_list = result.get('result') or [] + if not recognition_list: + return cls._pending_response() + + subjects = recognition_list[0].get('subjects') or [] + if not subjects: + return cls._pending_response() + + subject = subjects[0] + name = subject.get('subject', '') + similarity = subject.get('similarity', 0) + if similarity < ComprefaceConfig.COMPREFACE_SIMILARITY_THRESHOLD: + return { + "name": "未知", + "role": "陌生人" + } + else: + t = name.split("_") + return { + "name": t[0], + "role": t[1] if len(t) > 1 else "员工" + } + + # 添加图像到人脸库 + @classmethod + async def face_addition_service(cls, image, name): + result = await ComprefaceUtil.face_addition(image, name) + return result + + # 批量添加图像到人脸库 + @classmethod + async def face_addition_batch_service(cls, t): + result_list = [] + for image, name in t: + result = await ComprefaceUtil.face_addition(image, name) + result_list.append(result) + return result_list + + # 删除过期的访客人脸 + @classmethod + async def delete_expired_visitor_faces_service(cls, active_visitor_names: list): + """删除CompreFace中已过期的访客人脸数据 + + Args: + active_visitor_names (list): 当前有效的访客名字列表 + + Returns: + dict: 包含删除成功和失败的subject列表 + """ + try: + # 获取CompreFace中所有的subjects + all_subjects_result = await ComprefaceUtil.get_all_subjects() + all_subjects = all_subjects_result.get('subjects', []) + + deleted_subjects = [] + failed_subjects = [] + + # 遍历所有subjects,删除不在active_visitor_names中的访客 + for subject in all_subjects: + # 假设访客的subject格式为 "姓名_访客" 或 "姓名" + # 只删除访客类型的人脸,员工人脸不删除 + if subject not in active_visitor_names: + # 如果subject不在活跃访客列表中,且不是员工(员工一般包含"_员工"等标识) + # 这里假设访客没有特殊后缀,或者可以根据实际情况调整判断逻辑 + # 如果subject包含"_员工"等标识,则跳过 + if "_员工" in subject or "_职员" in subject or "_工作人员" in subject: + continue + + try: + # 删除该subject + await ComprefaceUtil.delete_subject(subject) + deleted_subjects.append(subject) + except Exception as e: + failed_subjects.append({"subject": subject, "error": str(e)}) + + return { + "deleted_count": len(deleted_subjects), + "deleted_subjects": deleted_subjects, + "failed_count": len(failed_subjects), + "failed_subjects": failed_subjects + } + except Exception as e: + return { + "error": f"删除过期访客失败: {str(e)}", + "deleted_count": 0, + "deleted_subjects": [], + "failed_count": 0, + "failed_subjects": [] + } + + + + + + diff --git a/ruoyi-fastapi-backend/module_admin/service/config_service.py b/ruoyi-fastapi-backend/module_admin/service/config_service.py new file mode 100644 index 0000000..312006d --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/config_service.py @@ -0,0 +1,231 @@ +from fastapi import Request +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from config.enums import RedisInitKeyConfig +from exceptions.exception import ServiceException +from module_admin.dao.config_dao import ConfigDao +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.entity.vo.config_vo import ConfigModel, ConfigPageQueryModel, DeleteConfigModel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil + + +class ConfigService: + """ + 参数配置管理模块服务层 + """ + + @classmethod + async def get_config_list_services( + cls, query_db: AsyncSession, query_object: ConfigPageQueryModel, is_page: bool = False + ): + """ + 获取参数配置列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 参数配置列表信息对象 + """ + config_list_result = await ConfigDao.get_config_list(query_db, query_object, is_page) + + return config_list_result + + @classmethod + async def init_cache_sys_config_services(cls, query_db: AsyncSession, redis): + """ + 应用初始化:获取所有参数配置对应的键值对信息并缓存service + + :param query_db: orm对象 + :param redis: redis对象 + :return: + """ + # 获取以sys_config:开头的键列表 + keys = await redis.keys(f'{RedisInitKeyConfig.SYS_CONFIG.key}:*') + # 删除匹配的键 + if keys: + await redis.delete(*keys) + config_all = await ConfigDao.get_config_list(query_db, ConfigPageQueryModel(**dict()), is_page=False) + for config_obj in config_all: + await redis.set( + f"{RedisInitKeyConfig.SYS_CONFIG.key}:{config_obj.get('configKey')}", + config_obj.get('configValue'), + ) + + @classmethod + async def query_config_list_from_cache_services(cls, redis, config_key: str): + """ + 从缓存获取参数键名对应值service + + :param redis: redis对象 + :param config_key: 参数键名 + :return: 参数键名对应值 + """ + result = await redis.get(f'{RedisInitKeyConfig.SYS_CONFIG.key}:{config_key}') + + return result + + @classmethod + async def check_config_key_unique_services(cls, query_db: AsyncSession, page_object: ConfigModel): + """ + 校验参数键名是否唯一service + + :param query_db: orm对象 + :param page_object: 参数配置对象 + :return: 校验结果 + """ + config_id = -1 if page_object.config_id is None else page_object.config_id + config = await ConfigDao.get_config_detail_by_info(query_db, ConfigModel(configKey=page_object.config_key)) + if config and config.config_id != config_id: + return CommonConstant.NOT_UNIQUE + return CommonConstant.UNIQUE + + @classmethod + async def add_config_services(cls, request: Request, query_db: AsyncSession, page_object: ConfigModel): + """ + 新增参数配置信息service + + :param request: Request对象 + :param query_db: orm对象 + :param page_object: 新增参数配置对象 + :return: 新增参数配置校验结果 + """ + if not await cls.check_config_key_unique_services(query_db, page_object): + raise ServiceException(message=f'新增参数{page_object.config_name}失败,参数键名已存在') + else: + try: + await ConfigDao.add_config_dao(query_db, page_object) + await query_db.commit() + await request.app.state.redis.set( + f'{RedisInitKeyConfig.SYS_CONFIG.key}:{page_object.config_key}', page_object.config_value + ) + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_config_services(cls, request: Request, query_db: AsyncSession, page_object: ConfigModel): + """ + 编辑参数配置信息service + + :param request: Request对象 + :param query_db: orm对象 + :param page_object: 编辑参数配置对象 + :return: 编辑参数配置校验结果 + """ + edit_config = page_object.model_dump(exclude_unset=True) + config_info = await cls.config_detail_services(query_db, page_object.config_id) + if config_info.config_id: + if not await cls.check_config_key_unique_services(query_db, page_object): + raise ServiceException(message=f'修改参数{page_object.config_name}失败,参数键名已存在') + else: + try: + await ConfigDao.edit_config_dao(query_db, edit_config) + await query_db.commit() + if config_info.config_key != page_object.config_key: + await request.app.state.redis.delete( + f'{RedisInitKeyConfig.SYS_CONFIG.key}:{config_info.config_key}' + ) + await request.app.state.redis.set( + f'{RedisInitKeyConfig.SYS_CONFIG.key}:{page_object.config_key}', page_object.config_value + ) + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='参数配置不存在') + + @classmethod + async def delete_config_services(cls, request: Request, query_db: AsyncSession, page_object: DeleteConfigModel): + """ + 删除参数配置信息service + + :param request: Request对象 + :param query_db: orm对象 + :param page_object: 删除参数配置对象 + :return: 删除参数配置校验结果 + """ + if page_object.config_ids: + config_id_list = page_object.config_ids.split(',') + try: + delete_config_key_list = [] + for config_id in config_id_list: + config_info = await cls.config_detail_services(query_db, int(config_id)) + if config_info.config_type == CommonConstant.YES: + raise ServiceException(message=f'内置参数{config_info.config_key}不能删除') + else: + await ConfigDao.delete_config_dao(query_db, ConfigModel(configId=int(config_id))) + delete_config_key_list.append(f'{RedisInitKeyConfig.SYS_CONFIG.key}:{config_info.config_key}') + await query_db.commit() + if delete_config_key_list: + await request.app.state.redis.delete(*delete_config_key_list) + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入参数配置id为空') + + @classmethod + async def config_detail_services(cls, query_db: AsyncSession, config_id: int): + """ + 获取参数配置详细信息service + + :param query_db: orm对象 + :param config_id: 参数配置id + :return: 参数配置id对应的信息 + """ + config = await ConfigDao.get_config_detail_by_id(query_db, config_id=config_id) + if config: + result = ConfigModel(**CamelCaseUtil.transform_result(config)) + else: + result = ConfigModel(**dict()) + + return result + + @staticmethod + async def export_config_list_services(config_list: List): + """ + 导出参数配置信息service + + :param config_list: 参数配置信息列表 + :return: 参数配置信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'configId': '参数主键', + 'configName': '参数名称', + 'configKey': '参数键名', + 'configValue': '参数键值', + 'configType': '系统内置', + 'createBy': '创建者', + 'createTime': '创建时间', + 'updateBy': '更新者', + 'updateTime': '更新时间', + 'remark': '备注', + } + + for item in config_list: + if item.get('configType') == 'Y': + item['configType'] = '是' + else: + item['configType'] = '否' + binary_data = ExcelUtil.export_list2excel(config_list, mapping_dict) + + return binary_data + + @classmethod + async def refresh_sys_config_services(cls, request: Request, query_db: AsyncSession): + """ + 刷新字典缓存信息service + + :param request: Request对象 + :param query_db: orm对象 + :return: 刷新字典缓存校验结果 + """ + await cls.init_cache_sys_config_services(query_db, request.app.state.redis) + + return CrudResponseModel(is_success=True, message='刷新成功') diff --git a/ruoyi-fastapi-backend/module_admin/service/dept_service.py b/ruoyi-fastapi-backend/module_admin/service/dept_service.py new file mode 100644 index 0000000..50594c8 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/dept_service.py @@ -0,0 +1,275 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from config.constant import CommonConstant +from exceptions.exception import ServiceException, ServiceWarning +from module_admin.dao.dept_dao import DeptDao +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.entity.vo.dept_vo import DeleteDeptModel, DeptModel +from utils.common_util import CamelCaseUtil + + +class DeptService: + """ + 部门管理模块服务层 + """ + + @classmethod + async def get_dept_tree_services(cls, query_db: AsyncSession, page_object: DeptModel, data_scope_sql: str): + """ + 获取部门树信息service + + :param query_db: orm对象 + :param page_object: 查询参数对象 + :param data_scope_sql: 数据权限对应的查询sql语句 + :return: 部门树信息对象 + """ + dept_list_result = await DeptDao.get_dept_list_for_tree(query_db, page_object, data_scope_sql) + dept_tree_result = cls.list_to_tree(dept_list_result) + + return dept_tree_result + + @classmethod + async def get_dept_for_edit_option_services( + cls, query_db: AsyncSession, page_object: DeptModel, data_scope_sql: str + ): + """ + 获取部门编辑部门树信息service + + :param query_db: orm对象 + :param page_object: 查询参数对象 + :param data_scope_sql: 数据权限对应的查询sql语句 + :return: 部门树信息对象 + """ + dept_list_result = await DeptDao.get_dept_info_for_edit_option(query_db, page_object, data_scope_sql) + + return CamelCaseUtil.transform_result(dept_list_result) + + @classmethod + async def get_dept_list_services(cls, query_db: AsyncSession, page_object: DeptModel, data_scope_sql: str): + """ + 获取部门列表信息service + + :param query_db: orm对象 + :param page_object: 分页查询参数对象 + :param data_scope_sql: 数据权限对应的查询sql语句 + :return: 部门列表信息对象 + """ + dept_list_result = await DeptDao.get_dept_list(query_db, page_object, data_scope_sql) + + return CamelCaseUtil.transform_result(dept_list_result) + + @classmethod + async def check_dept_data_scope_services(cls, query_db: AsyncSession, dept_id: int, data_scope_sql: str): + """ + 校验部门是否有数据权限service + + :param query_db: orm对象 + :param dept_id: 部门id + :param data_scope_sql: 数据权限对应的查询sql语句 + :return: 校验结果 + """ + depts = await DeptDao.get_dept_list(query_db, DeptModel(deptId=dept_id), data_scope_sql) + if depts: + return CrudResponseModel(is_success=True, message='校验通过') + else: + raise ServiceException(message='没有权限访问部门数据') + + @classmethod + async def check_dept_name_unique_services(cls, query_db: AsyncSession, page_object: DeptModel): + """ + 校验部门名称是否唯一service + + :param query_db: orm对象 + :param page_object: 部门对象 + :return: 校验结果 + """ + dept_id = -1 if page_object.dept_id is None else page_object.dept_id + dept = await DeptDao.get_dept_detail_by_info( + query_db, DeptModel(deptName=page_object.dept_name, parentId=page_object.parent_id) + ) + if dept and dept.dept_id != dept_id: + return CommonConstant.NOT_UNIQUE + return CommonConstant.UNIQUE + + @classmethod + async def add_dept_services(cls, query_db: AsyncSession, page_object: DeptModel): + """ + 新增部门信息service + + :param query_db: orm对象 + :param page_object: 新增部门对象 + :return: 新增部门校验结果 + """ + if not await cls.check_dept_name_unique_services(query_db, page_object): + raise ServiceException(message=f'新增部门{page_object.dept_name}失败,部门名称已存在') + parent_info = await DeptDao.get_dept_by_id(query_db, page_object.parent_id) + if parent_info.status != CommonConstant.DEPT_NORMAL: + raise ServiceException(message=f'部门{parent_info.dept_name}停用,不允许新增') + page_object.ancestors = f'{parent_info.ancestors},{page_object.parent_id}' + try: + await DeptDao.add_dept_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_dept_services(cls, query_db: AsyncSession, page_object: DeptModel): + """ + 编辑部门信息service + + :param query_db: orm对象 + :param page_object: 编辑部门对象 + :return: 编辑部门校验结果 + """ + if not await cls.check_dept_name_unique_services(query_db, page_object): + raise ServiceException(message=f'修改部门{page_object.dept_name}失败,部门名称已存在') + elif page_object.dept_id == page_object.parent_id: + raise ServiceException(message=f'修改部门{page_object.dept_name}失败,上级部门不能是自己') + elif ( + page_object.status == CommonConstant.DEPT_DISABLE + and (await DeptDao.count_normal_children_dept_dao(query_db, page_object.dept_id)) > 0 + ): + raise ServiceException(message=f'修改部门{page_object.dept_name}失败,该部门包含未停用的子部门') + new_parent_dept = await DeptDao.get_dept_by_id(query_db, page_object.parent_id) + old_dept = await DeptDao.get_dept_by_id(query_db, page_object.dept_id) + try: + if new_parent_dept and old_dept: + new_ancestors = f'{new_parent_dept.ancestors},{new_parent_dept.dept_id}' + old_ancestors = old_dept.ancestors + page_object.ancestors = new_ancestors + await cls.update_dept_children(query_db, page_object.dept_id, new_ancestors, old_ancestors) + edit_dept = page_object.model_dump(exclude_unset=True) + await DeptDao.edit_dept_dao(query_db, edit_dept) + if ( + page_object.status == CommonConstant.DEPT_NORMAL + and page_object.ancestors + and page_object.ancestors != 0 + ): + await cls.update_parent_dept_status_normal(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def delete_dept_services(cls, query_db: AsyncSession, page_object: DeleteDeptModel): + """ + 删除部门信息service + + :param query_db: orm对象 + :param page_object: 删除部门对象 + :return: 删除部门校验结果 + """ + if page_object.dept_ids: + dept_id_list = page_object.dept_ids.split(',') + try: + for dept_id in dept_id_list: + if (await DeptDao.count_children_dept_dao(query_db, int(dept_id))) > 0: + raise ServiceWarning(message='存在下级部门,不允许删除') + elif (await DeptDao.count_dept_user_dao(query_db, int(dept_id))) > 0: + raise ServiceWarning(message='部门存在用户,不允许删除') + + await DeptDao.delete_dept_dao(query_db, DeptModel(deptId=dept_id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入部门id为空') + + @classmethod + async def dept_detail_services(cls, query_db: AsyncSession, dept_id: int): + """ + 获取部门详细信息service + + :param query_db: orm对象 + :param dept_id: 部门id + :return: 部门id对应的信息 + """ + dept = await DeptDao.get_dept_detail_by_id(query_db, dept_id=dept_id) + if dept: + result = DeptModel(**CamelCaseUtil.transform_result(dept)) + else: + result = DeptModel(**dict()) + + return result + + @classmethod + def list_to_tree(cls, permission_list: list) -> list: + """ + 工具方法:根据部门列表信息生成树形嵌套数据 + + :param permission_list: 部门列表信息 + :return: 部门树形嵌套数据 + """ + permission_list = [ + dict(id=item.dept_id, label=item.dept_name, parentId=item.parent_id) for item in permission_list + ] + # 转成id为key的字典 + mapping: dict = dict(zip([i['id'] for i in permission_list], permission_list)) + + # 树容器 + container: list = [] + + for d in permission_list: + # 如果找不到父级项,则是根节点 + parent: dict = mapping.get(d['parentId']) + if parent is None: + container.append(d) + else: + children: list = parent.get('children') + if not children: + children = [] + children.append(d) + parent.update({'children': children}) + + return container + + @classmethod + async def replace_first(cls, original_str: str, old_str: str, new_str: str): + """ + 工具方法:替换字符串 + + :param original_str: 需要替换的原始字符串 + :param old_str: 用于匹配的字符串 + :param new_str: 替换的字符串 + :return: 替换后的字符串 + """ + if original_str.startswith(old_str): + return original_str.replace(old_str, new_str, 1) + else: + return original_str + + @classmethod + async def update_parent_dept_status_normal(cls, query_db: AsyncSession, dept: DeptModel): + """ + 更新父部门状态为正常 + + :param query_db: orm对象 + :param dept: 部门对象 + :return: + """ + dept_id_list = dept.ancestors.split(',') + await DeptDao.update_dept_status_normal_dao(query_db, list(map(int, dept_id_list))) + + @classmethod + async def update_dept_children(cls, query_db: AsyncSession, dept_id: int, new_ancestors: str, old_ancestors: str): + """ + 更新子部门信息 + + :param query_db: orm对象 + :param dept_id: 部门id + :param new_ancestors: 新的祖先 + :param old_ancestors: 旧的祖先 + :return: + """ + children = await DeptDao.get_children_dept_dao(query_db, dept_id) + update_children = [] + for child in children: + child_ancestors = await cls.replace_first(child.ancestors, old_ancestors, new_ancestors) + update_children.append({'dept_id': child.dept_id, 'ancestors': child_ancestors}) + if children: + await DeptDao.update_dept_children_dao(query_db, update_children) diff --git a/ruoyi-fastapi-backend/module_admin/service/dict_service.py b/ruoyi-fastapi-backend/module_admin/service/dict_service.py new file mode 100644 index 0000000..0acfd73 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/dict_service.py @@ -0,0 +1,459 @@ +import json +from fastapi import Request +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from config.enums import RedisInitKeyConfig +from exceptions.exception import ServiceException +from module_admin.dao.dict_dao import DictDataDao, DictTypeDao +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.entity.vo.dict_vo import ( + DeleteDictDataModel, + DeleteDictTypeModel, + DictDataModel, + DictDataPageQueryModel, + DictTypeModel, + DictTypePageQueryModel, +) +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil + + +class DictTypeService: + """ + 字典类型管理模块服务层 + """ + + @classmethod + async def get_dict_type_list_services( + cls, query_db: AsyncSession, query_object: DictTypePageQueryModel, is_page: bool = False + ): + """ + 获取字典类型列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 字典类型列表信息对象 + """ + dict_type_list_result = await DictTypeDao.get_dict_type_list(query_db, query_object, is_page) + + return dict_type_list_result + + @classmethod + async def check_dict_type_unique_services(cls, query_db: AsyncSession, page_object: DictTypeModel): + """ + 校验字典类型称是否唯一service + + :param query_db: orm对象 + :param page_object: 字典类型对象 + :return: 校验结果 + """ + dict_id = -1 if page_object.dict_id is None else page_object.dict_id + dict_type = await DictTypeDao.get_dict_type_detail_by_info( + query_db, DictTypeModel(dictType=page_object.dict_type) + ) + if dict_type and dict_type.dict_id != dict_id: + return CommonConstant.NOT_UNIQUE + return CommonConstant.UNIQUE + + @classmethod + async def add_dict_type_services(cls, request: Request, query_db: AsyncSession, page_object: DictTypeModel): + """ + 新增字典类型信息service + + :param request: Request对象 + :param query_db: orm对象 + :param page_object: 新增岗位对象 + :return: 新增字典类型校验结果 + """ + if not await cls.check_dict_type_unique_services(query_db, page_object): + raise ServiceException(message=f'新增字典{page_object.dict_name}失败,字典类型已存在') + else: + try: + await DictTypeDao.add_dict_type_dao(query_db, page_object) + await query_db.commit() + await request.app.state.redis.set(f'{RedisInitKeyConfig.SYS_DICT.key}:{page_object.dict_type}', '') + result = dict(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + return CrudResponseModel(**result) + + @classmethod + async def edit_dict_type_services(cls, request: Request, query_db: AsyncSession, page_object: DictTypeModel): + """ + 编辑字典类型信息service + + :param request: Request对象 + :param query_db: orm对象 + :param page_object: 编辑字典类型对象 + :return: 编辑字典类型校验结果 + """ + edit_dict_type = page_object.model_dump(exclude_unset=True) + dict_type_info = await cls.dict_type_detail_services(query_db, page_object.dict_id) + if dict_type_info.dict_id: + if not await cls.check_dict_type_unique_services(query_db, page_object): + raise ServiceException(message=f'修改字典{page_object.dict_name}失败,字典类型已存在') + else: + try: + query_dict_data = DictDataPageQueryModel(dictType=dict_type_info.dict_type) + dict_data_list = await DictDataDao.get_dict_data_list(query_db, query_dict_data, is_page=False) + if dict_type_info.dict_type != page_object.dict_type: + for dict_data in dict_data_list: + edit_dict_data = DictDataModel( + dictCode=dict_data.get('dict_code'), + dictType=page_object.dict_type, + updateBy=page_object.update_by, + updateTime=page_object.update_time, + ).model_dump(exclude_unset=True) + await DictDataDao.edit_dict_data_dao(query_db, edit_dict_data) + await DictTypeDao.edit_dict_type_dao(query_db, edit_dict_type) + await query_db.commit() + if dict_type_info.dict_type != page_object.dict_type: + dict_data = [CamelCaseUtil.transform_result(row) for row in dict_data_list if row] + await request.app.state.redis.set( + f'{RedisInitKeyConfig.SYS_DICT.key}:{page_object.dict_type}', + json.dumps(dict_data, ensure_ascii=False, default=str), + ) + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='字典类型不存在') + + @classmethod + async def delete_dict_type_services( + cls, request: Request, query_db: AsyncSession, page_object: DeleteDictTypeModel + ): + """ + 删除字典类型信息service + + :param request: Request对象 + :param query_db: orm对象 + :param page_object: 删除字典类型对象 + :return: 删除字典类型校验结果 + """ + if page_object.dict_ids: + dict_id_list = page_object.dict_ids.split(',') + try: + delete_dict_type_list = [] + for dict_id in dict_id_list: + dict_type_into = await cls.dict_type_detail_services(query_db, int(dict_id)) + if (await DictDataDao.count_dict_data_dao(query_db, dict_type_into.dict_type)) > 0: + raise ServiceException(message=f'{dict_type_into.dict_name}已分配,不能删除') + await DictTypeDao.delete_dict_type_dao(query_db, DictTypeModel(dictId=int(dict_id))) + delete_dict_type_list.append(f'{RedisInitKeyConfig.SYS_DICT.key}:{dict_type_into.dict_type}') + await query_db.commit() + if delete_dict_type_list: + await request.app.state.redis.delete(*delete_dict_type_list) + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入字典类型id为空') + + @classmethod + async def dict_type_detail_services(cls, query_db: AsyncSession, dict_id: int): + """ + 获取字典类型详细信息service + + :param query_db: orm对象 + :param dict_id: 字典类型id + :return: 字典类型id对应的信息 + """ + dict_type = await DictTypeDao.get_dict_type_detail_by_id(query_db, dict_id=dict_id) + if dict_type: + result = DictTypeModel(**CamelCaseUtil.transform_result(dict_type)) + else: + result = DictTypeModel(**dict()) + + return result + + @staticmethod + async def export_dict_type_list_services(dict_type_list: List): + """ + 导出字典类型信息service + + :param dict_type_list: 字典信息列表 + :return: 字典信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'dictId': '字典编号', + 'dictName': '字典名称', + 'dictType': '字典类型', + 'status': '状态', + 'createBy': '创建者', + 'createTime': '创建时间', + 'updateBy': '更新者', + 'updateTime': '更新时间', + 'remark': '备注', + } + + for item in dict_type_list: + if item.get('status') == '0': + item['status'] = '正常' + else: + item['status'] = '停用' + binary_data = ExcelUtil.export_list2excel(dict_type_list, mapping_dict) + + return binary_data + + @classmethod + async def refresh_sys_dict_services(cls, request: Request, query_db: AsyncSession): + """ + 刷新字典缓存信息service + + :param request: Request对象 + :param query_db: orm对象 + :return: 刷新字典缓存校验结果 + """ + await DictDataService.init_cache_sys_dict_services(query_db, request.app.state.redis) + result = dict(is_success=True, message='刷新成功') + + return CrudResponseModel(**result) + + +class DictDataService: + """ + 字典数据管理模块服务层 + """ + + @classmethod + async def get_dict_data_list_services( + cls, query_db: AsyncSession, query_object: DictDataPageQueryModel, is_page: bool = False + ): + """ + 获取字典数据列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 字典数据列表信息对象 + """ + dict_data_list_result = await DictDataDao.get_dict_data_list(query_db, query_object, is_page) + + return dict_data_list_result + + @classmethod + async def query_dict_data_list_services(cls, query_db: AsyncSession, dict_type: str): + """ + 获取字典数据列表信息service + + :param query_db: orm对象 + :param dict_type: 字典类型 + :return: 字典数据列表信息对象 + """ + dict_data_list_result = await DictDataDao.query_dict_data_list(query_db, dict_type) + + return dict_data_list_result + + @classmethod + async def init_cache_sys_dict_services(cls, query_db: AsyncSession, redis): + """ + 应用初始化:获取所有字典类型对应的字典数据信息并缓存service + + :param query_db: orm对象 + :param redis: redis对象 + :return: + """ + # 获取以sys_dict:开头的键列表 + keys = await redis.keys(f'{RedisInitKeyConfig.SYS_DICT.key}:*') + # 删除匹配的键 + if keys: + await redis.delete(*keys) + dict_type_all = await DictTypeDao.get_all_dict_type(query_db) + for dict_type_obj in [item for item in dict_type_all if item.status == '0']: + dict_type = dict_type_obj.dict_type + dict_data_list = await DictDataDao.query_dict_data_list(query_db, dict_type) + dict_data = [CamelCaseUtil.transform_result(row) for row in dict_data_list if row] + await redis.set( + f'{RedisInitKeyConfig.SYS_DICT.key}:{dict_type}', + json.dumps(dict_data, ensure_ascii=False, default=str), + ) + + @classmethod + async def query_dict_data_list_from_cache_services(cls, redis, dict_type: str): + """ + 从缓存获取字典数据列表信息service + + :param redis: redis对象 + :param dict_type: 字典类型 + :return: 字典数据列表信息对象 + """ + result = [] + dict_data_list_result = await redis.get(f'{RedisInitKeyConfig.SYS_DICT.key}:{dict_type}') + if dict_data_list_result: + result = json.loads(dict_data_list_result) + + return CamelCaseUtil.transform_result(result) + + @classmethod + async def check_dict_data_unique_services(cls, query_db: AsyncSession, page_object: DictDataModel): + """ + 校验字典数据是否唯一service + + :param query_db: orm对象 + :param page_object: 字典数据对象 + :return: 校验结果 + """ + dict_code = -1 if page_object.dict_code is None else page_object.dict_code + dict_data = await DictDataDao.get_dict_data_detail_by_info(query_db, page_object) + if dict_data and dict_data.dict_code != dict_code: + return CommonConstant.NOT_UNIQUE + return CommonConstant.UNIQUE + + @classmethod + async def add_dict_data_services(cls, request: Request, query_db: AsyncSession, page_object: DictDataModel): + """ + 新增字典数据信息service + + :param request: Request对象 + :param query_db: orm对象 + :param page_object: 新增岗位对象 + :return: 新增字典数据校验结果 + """ + if not await cls.check_dict_data_unique_services(query_db, page_object): + raise ServiceException( + message=f'新增字典数据{page_object.dict_label}失败,{page_object.dict_type}下已存在该字典数据' + ) + else: + try: + await DictDataDao.add_dict_data_dao(query_db, page_object) + await query_db.commit() + dict_data_list = await cls.query_dict_data_list_services(query_db, page_object.dict_type) + await request.app.state.redis.set( + f'{RedisInitKeyConfig.SYS_DICT.key}:{page_object.dict_type}', + json.dumps(CamelCaseUtil.transform_result(dict_data_list), ensure_ascii=False, default=str), + ) + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_dict_data_services(cls, request: Request, query_db: AsyncSession, page_object: DictDataModel): + """ + 编辑字典数据信息service + + :param request: Request对象 + :param query_db: orm对象 + :param page_object: 编辑字典数据对象 + :return: 编辑字典数据校验结果 + """ + edit_data_type = page_object.model_dump(exclude_unset=True) + dict_data_info = await cls.dict_data_detail_services(query_db, page_object.dict_code) + if dict_data_info.dict_code: + if not await cls.check_dict_data_unique_services(query_db, page_object): + raise ServiceException( + message=f'新增字典数据{page_object.dict_label}失败,{page_object.dict_type}下已存在该字典数据' + ) + else: + try: + await DictDataDao.edit_dict_data_dao(query_db, edit_data_type) + await query_db.commit() + dict_data_list = await cls.query_dict_data_list_services(query_db, page_object.dict_type) + await request.app.state.redis.set( + f'{RedisInitKeyConfig.SYS_DICT.key}:{page_object.dict_type}', + json.dumps(CamelCaseUtil.transform_result(dict_data_list), ensure_ascii=False, default=str), + ) + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='字典数据不存在') + + @classmethod + async def delete_dict_data_services( + cls, request: Request, query_db: AsyncSession, page_object: DeleteDictDataModel + ): + """ + 删除字典数据信息service + + :param request: Request对象 + :param query_db: orm对象 + :param page_object: 删除字典数据对象 + :return: 删除字典数据校验结果 + """ + if page_object.dict_codes: + dict_code_list = page_object.dict_codes.split(',') + try: + delete_dict_type_list = [] + for dict_code in dict_code_list: + dict_data = await cls.dict_data_detail_services(query_db, int(dict_code)) + await DictDataDao.delete_dict_data_dao(query_db, DictDataModel(dictCode=dict_code)) + delete_dict_type_list.append(dict_data.dict_type) + await query_db.commit() + for dict_type in list(set(delete_dict_type_list)): + dict_data_list = await cls.query_dict_data_list_services(query_db, dict_type) + await request.app.state.redis.set( + f'{RedisInitKeyConfig.SYS_DICT.key}:{dict_type}', + json.dumps(CamelCaseUtil.transform_result(dict_data_list), ensure_ascii=False, default=str), + ) + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入字典数据id为空') + + @classmethod + async def dict_data_detail_services(cls, query_db: AsyncSession, dict_code: int): + """ + 获取字典数据详细信息service + + :param query_db: orm对象 + :param dict_code: 字典数据id + :return: 字典数据id对应的信息 + """ + dict_data = await DictDataDao.get_dict_data_detail_by_id(query_db, dict_code=dict_code) + if dict_data: + result = DictDataModel(**CamelCaseUtil.transform_result(dict_data)) + else: + result = DictDataModel(**dict()) + + return result + + @staticmethod + async def export_dict_data_list_services(dict_data_list: List): + """ + 导出字典数据信息service + + :param dict_data_list: 字典数据信息列表 + :return: 字典数据信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'dictCode': '字典编码', + 'dictSort': '字典标签', + 'dictLabel': '字典键值', + 'dictValue': '字典排序', + 'dictType': '字典类型', + 'cssClass': '样式属性', + 'listClass': '表格回显样式', + 'isDefault': '是否默认', + 'status': '状态', + 'createBy': '创建者', + 'createTime': '创建时间', + 'updateBy': '更新者', + 'updateTime': '更新时间', + 'remark': '备注', + } + + for item in dict_data_list: + if item.get('status') == '0': + item['status'] = '正常' + else: + item['status'] = '停用' + if item.get('isDefault') == 'Y': + item['isDefault'] = '是' + else: + item['isDefault'] = '否' + binary_data = ExcelUtil.export_list2excel(dict_data_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/door_service.py b/ruoyi-fastapi-backend/module_admin/service/door_service.py new file mode 100644 index 0000000..70537ba --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/door_service.py @@ -0,0 +1,178 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.dao.door_dao import DoorDao +from module_admin.dao.info_dao import InfoDao +from module_admin.entity.vo.door_vo import DeleteDoorModel, DoorModel, DoorPageQueryModel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil +from utils.haikang_util import HaikangUtil + + +class DoorService: + """ + 门禁设备模块服务层 + """ + + @classmethod + async def get_video_uri_service(cls, robot_id: int , query_db: AsyncSession): + """获取机器人视频流 + """ + video_uri = await InfoDao.get_video_uri(robot_id, query_db) + return video_uri + + + @classmethod + async def get_door_status_service(cls, query_db: AsyncSession): + """获取门禁状态 + + Args: + query_db (AsyncSession): orm对象 + Returns: + _type_: _description_ + """ + + door_indexcode_name = await DoorDao.get_door_door_status(query_db) + + t_d = dict() + index_code_list = list() + for t in door_indexcode_name: + t_d[t["indexCode"]] = t["name"] + index_code_list.append(t["indexCode"]) + + door_online_status_result = await HaikangUtil.door_online_status(index_code_list) + + if door_online_status_result[0]: + back = list() + for door_online_status in door_online_status_result[1]['list']: + back.append({ + "name": t_d.get(door_online_status['indexCode'], None), + "online": door_online_status['online'] + }) + + return True, back + return door_online_status_result + + @classmethod + async def get_door_list_services( + cls, query_db: AsyncSession, query_object: DoorPageQueryModel, is_page: bool = False + ): + """ + 获取门禁设备列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 门禁设备列表信息对象 + """ + door_list_result = await DoorDao.get_door_list(query_db, query_object, is_page) + + # 获取门禁在线状态 + + return door_list_result + + + @classmethod + async def add_door_services(cls, query_db: AsyncSession, page_object: DoorModel): + """ + 新增门禁设备信息service + + :param query_db: orm对象 + :param page_object: 新增门禁设备对象 + :return: 新增门禁设备校验结果 + """ + try: + await DoorDao.add_door_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_door_services(cls, query_db: AsyncSession, page_object: DoorModel): + """ + 编辑门禁设备信息service + + :param query_db: orm对象 + :param page_object: 编辑门禁设备对象 + :return: 编辑门禁设备校验结果 + """ + edit_door = page_object.model_dump(exclude_unset=True, exclude={'create_time', 'create_by', }) + door_info = await cls.door_detail_services(query_db, page_object.id) + if door_info.id: + try: + await DoorDao.edit_door_dao(query_db, edit_door) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='门禁设备不存在') + + @classmethod + async def delete_door_services(cls, query_db: AsyncSession, page_object: DeleteDoorModel): + """ + 删除门禁设备信息service + + :param query_db: orm对象 + :param page_object: 删除门禁设备对象 + :return: 删除门禁设备校验结果 + """ + if page_object.ids: + id_list = page_object.ids.split(',') + try: + for id in id_list: + await DoorDao.delete_door_dao(query_db, DoorModel(id=id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入主键自增为空') + + @classmethod + async def door_detail_services(cls, query_db: AsyncSession, id: int): + """ + 获取门禁设备详细信息service + + :param query_db: orm对象 + :param id: 主键自增 + :return: 主键自增对应的信息 + """ + door = await DoorDao.get_door_detail_by_id(query_db, id=id) + if door: + result = DoorModel(**CamelCaseUtil.transform_result(door)) + else: + result = DoorModel(**dict()) + + return result + + @staticmethod + async def export_door_list_services(door_list: List): + """ + 导出门禁设备信息service + + :param door_list: 门禁设备信息列表 + :return: 门禁设备信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'id': '主键自增', + 'indexCode': '设备索引编码', + 'name': '门禁名称', + 'installLocation': '位置名称', + 'status': '设备状态, 0 离线, 1 正常在线', + 'permission': '权限, 0 禁用, 1 正常调用', + 'createTime': '创建时间', + 'createBy': '创建者', + 'updateTime': '更新时间', + 'updateBy': '更新者', + } + binary_data = ExcelUtil.export_list2excel(door_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/explanation_content_service.py b/ruoyi-fastapi-backend/module_admin/service/explanation_content_service.py new file mode 100644 index 0000000..70c250d --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/explanation_content_service.py @@ -0,0 +1,136 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.dao.explanation_content_dao import Explanation_contentDao +from module_admin.entity.vo.explanation_content_vo import DeleteExplanation_contentModel, Explanation_contentModel, Explanation_contentPageQueryModel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil + + +class Explanation_contentService: + """ + 讲解内容模块服务层 + """ + + @classmethod + async def get_explanation_content_list_services( + cls, query_db: AsyncSession, query_object: Explanation_contentPageQueryModel, is_page: bool = False + ): + """ + 获取讲解内容列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 讲解内容列表信息对象 + """ + explanation_content_list_result = await Explanation_contentDao.get_explanation_content_list(query_db, query_object, is_page) + + return explanation_content_list_result + + + @classmethod + async def add_explanation_content_services(cls, query_db: AsyncSession, page_object: Explanation_contentModel): + """ + 新增讲解内容信息service + + :param query_db: orm对象 + :param page_object: 新增讲解内容对象 + :return: 新增讲解内容校验结果 + """ + try: + await Explanation_contentDao.add_explanation_content_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_explanation_content_services(cls, query_db: AsyncSession, page_object: Explanation_contentModel): + """ + 编辑讲解内容信息service + + :param query_db: orm对象 + :param page_object: 编辑讲解内容对象 + :return: 编辑讲解内容校验结果 + """ + edit_explanation_content = page_object.model_dump(exclude_unset=True, exclude={'create_time', 'create_by', }) + explanation_content_info = await cls.explanation_content_detail_services(query_db, page_object.explanation_content_id) + if explanation_content_info.explanation_content_id: + try: + await Explanation_contentDao.edit_explanation_content_dao(query_db, edit_explanation_content) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='讲解内容不存在') + + @classmethod + async def delete_explanation_content_services(cls, query_db: AsyncSession, page_object: DeleteExplanation_contentModel): + """ + 删除讲解内容信息service + + :param query_db: orm对象 + :param page_object: 删除讲解内容对象 + :return: 删除讲解内容校验结果 + """ + if page_object.explanation_content_ids: + explanation_content_id_list = page_object.explanation_content_ids.split(',') + try: + for explanation_content_id in explanation_content_id_list: + await Explanation_contentDao.delete_explanation_content_dao(query_db, Explanation_contentModel(explanationContentId=explanation_content_id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入主键ID为空') + + @classmethod + async def explanation_content_detail_services(cls, query_db: AsyncSession, explanation_content_id: int): + """ + 获取讲解内容详细信息service + + :param query_db: orm对象 + :param explanation_content_id: 主键ID + :return: 主键ID对应的信息 + """ + explanation_content = await Explanation_contentDao.get_explanation_content_detail_by_id(query_db, explanation_content_id=explanation_content_id) + if explanation_content: + result = Explanation_contentModel(**CamelCaseUtil.transform_result(explanation_content)) + else: + result = Explanation_contentModel(**dict()) + + return result + + @staticmethod + async def export_explanation_content_list_services(explanation_content_list: List): + """ + 导出讲解内容信息service + + :param explanation_content_list: 讲解内容信息列表 + :return: 讲解内容信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'explanationContentId': '主键ID', + 'title': '标题', + 'content': '讲解内容', + 'type': '类型(0科技类)', + 'spendTime': '预计时长(分钟)', + 'language': '语言(0中文,1英文)', + 'status': '状态(0未开始,1已就绪,2播放中,3已完成)', + 'createTime': '创建时间', + 'createBy': '创建者', + 'updateTime': '修改时间', + 'updateBy': '修改者', + } + binary_data = ExcelUtil.export_list2excel(explanation_content_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/explanation_content_type_service.py b/ruoyi-fastapi-backend/module_admin/service/explanation_content_type_service.py new file mode 100644 index 0000000..ff7a1e1 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/explanation_content_type_service.py @@ -0,0 +1,132 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.dao.explanation_content_type_dao import Explanation_content_typeDao +from module_admin.entity.vo.explanation_content_type_vo import DeleteExplanation_content_typeModel, Explanation_content_typeModel, Explanation_content_typePageQueryModel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil + + +class Explanation_content_typeService: + """ + 讲解内容类型模块服务层 + """ + + @classmethod + async def get_explanation_content_type_list_services( + cls, query_db: AsyncSession, query_object: Explanation_content_typePageQueryModel, is_page: bool = False + ): + """ + 获取讲解内容类型列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 讲解内容类型列表信息对象 + """ + explanation_content_type_list_result = await Explanation_content_typeDao.get_explanation_content_type_list(query_db, query_object, is_page) + + return explanation_content_type_list_result + + + @classmethod + async def add_explanation_content_type_services(cls, query_db: AsyncSession, page_object: Explanation_content_typeModel): + """ + 新增讲解内容类型信息service + + :param query_db: orm对象 + :param page_object: 新增讲解内容类型对象 + :return: 新增讲解内容类型校验结果 + """ + try: + await Explanation_content_typeDao.add_explanation_content_type_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_explanation_content_type_services(cls, query_db: AsyncSession, page_object: Explanation_content_typeModel): + """ + 编辑讲解内容类型信息service + + :param query_db: orm对象 + :param page_object: 编辑讲解内容类型对象 + :return: 编辑讲解内容类型校验结果 + """ + edit_explanation_content_type = page_object.model_dump(exclude_unset=True, exclude={'create_time', 'create_by', }) + explanation_content_type_info = await cls.explanation_content_type_detail_services(query_db, page_object.content_type_id) + if explanation_content_type_info.content_type_id: + try: + await Explanation_content_typeDao.edit_explanation_content_type_dao(query_db, edit_explanation_content_type) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='讲解内容类型不存在') + + @classmethod + async def delete_explanation_content_type_services(cls, query_db: AsyncSession, page_object: DeleteExplanation_content_typeModel): + """ + 删除讲解内容类型信息service + + :param query_db: orm对象 + :param page_object: 删除讲解内容类型对象 + :return: 删除讲解内容类型校验结果 + """ + if page_object.content_type_ids: + content_type_id_list = page_object.content_type_ids.split(',') + try: + for content_type_id in content_type_id_list: + await Explanation_content_typeDao.delete_explanation_content_type_dao(query_db, Explanation_content_typeModel(contentTypeId=content_type_id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入主键ID为空') + + @classmethod + async def explanation_content_type_detail_services(cls, query_db: AsyncSession, content_type_id: int): + """ + 获取讲解内容类型详细信息service + + :param query_db: orm对象 + :param content_type_id: 主键ID + :return: 主键ID对应的信息 + """ + explanation_content_type = await Explanation_content_typeDao.get_explanation_content_type_detail_by_id(query_db, content_type_id=content_type_id) + if explanation_content_type: + result = Explanation_content_typeModel(**CamelCaseUtil.transform_result(explanation_content_type)) + else: + result = Explanation_content_typeModel(**dict()) + + return result + + @staticmethod + async def export_explanation_content_type_list_services(explanation_content_type_list: List): + """ + 导出讲解内容类型信息service + + :param explanation_content_type_list: 讲解内容类型信息列表 + :return: 讲解内容类型信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'contentTypeId': '主键ID', + 'name': '类型名称', + 'typeValue': '类型值', + 'createTime': '创建时间', + 'createBy': '创建者', + 'updateTime': '修改时间', + 'updateBy': '修改者', + } + binary_data = ExcelUtil.export_list2excel(explanation_content_type_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/explanation_style_robot_pair_service.py b/ruoyi-fastapi-backend/module_admin/service/explanation_style_robot_pair_service.py new file mode 100644 index 0000000..aefc13e --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/explanation_style_robot_pair_service.py @@ -0,0 +1,159 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.dao.explanation_style_robot_pair_dao import Explanation_style_robot_pairDao +from module_admin.entity.vo.explanation_style_robot_pair_vo import DeleteExplanation_style_robot_pairModel, \ + Explanation_style_robot_pairModel, Explanation_style_robot_pairPageQueryModel, SwitchExplanationStyleModel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil + + +class Explanation_style_robot_pairService: + """ + 讲解风格--机器人配对模块服务层 + """ + + @classmethod + async def switch_explanation_style_services(cls, query_db: AsyncSession, switch_explanation_style: SwitchExplanationStyleModel): + try: + # 现将所有风格都置为0 + result1 = await Explanation_style_robot_pairDao.switch_explanation_style(query_db, switch_explanation_style.robot_id, None, switch_explanation_style.robot_role_id ,"0", switch_explanation_style.update_by) + # 再将选择的风格置为1 + result2 = await Explanation_style_robot_pairDao.switch_explanation_style(query_db, None, switch_explanation_style.pairing_id,None ,"1", switch_explanation_style.update_by) + count = result2.rowcount + + await query_db.commit() + if count == 0: + await query_db.rollback() + return CrudResponseModel(is_success=False, message='修改失败') + + return CrudResponseModel(is_success=True, message='修改成功') + except Exception as e: + await query_db.rollback() + raise e + + + @classmethod + async def get_style_robot_pair_list_services(cls, query_db: AsyncSession, robot_id: int): + return await Explanation_style_robot_pairDao.get_style_robot_pair_list(query_db, robot_id) + + @classmethod + async def get_explanation_style_robot_pair_list_services( + cls, query_db: AsyncSession, query_object: Explanation_style_robot_pairPageQueryModel, is_page: bool = False + ): + """ + 获取讲解风格--机器人配对列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 讲解风格--机器人配对列表信息对象 + """ + explanation_style_robot_pair_list_result = await Explanation_style_robot_pairDao.get_explanation_style_robot_pair_list(query_db, query_object, is_page) + + return explanation_style_robot_pair_list_result + + + @classmethod + async def add_explanation_style_robot_pair_services(cls, query_db: AsyncSession, page_object: Explanation_style_robot_pairModel): + """ + 新增讲解风格--机器人配对信息service + + :param query_db: orm对象 + :param page_object: 新增讲解风格--机器人配对对象 + :return: 新增讲解风格--机器人配对校验结果 + """ + try: + await Explanation_style_robot_pairDao.add_explanation_style_robot_pair_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_explanation_style_robot_pair_services(cls, query_db: AsyncSession, page_object: Explanation_style_robot_pairModel): + """ + 编辑讲解风格--机器人配对信息service + + :param query_db: orm对象 + :param page_object: 编辑讲解风格--机器人配对对象 + :return: 编辑讲解风格--机器人配对校验结果 + """ + edit_explanation_style_robot_pair = page_object.model_dump(exclude_unset=True, exclude={'create_time', 'create_by', }) + explanation_style_robot_pair_info = await cls.explanation_style_robot_pair_detail_services(query_db, page_object.pairing_id) + if explanation_style_robot_pair_info.pairing_id: + try: + await Explanation_style_robot_pairDao.edit_explanation_style_robot_pair_dao(query_db, edit_explanation_style_robot_pair) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='讲解风格--机器人配对不存在') + + @classmethod + async def delete_explanation_style_robot_pair_services(cls, query_db: AsyncSession, page_object: DeleteExplanation_style_robot_pairModel): + """ + 删除讲解风格--机器人配对信息service + + :param query_db: orm对象 + :param page_object: 删除讲解风格--机器人配对对象 + :return: 删除讲解风格--机器人配对校验结果 + """ + if page_object.pairing_ids: + pairing_id_list = page_object.pairing_ids.split(',') + try: + for pairing_id in pairing_id_list: + await Explanation_style_robot_pairDao.delete_explanation_style_robot_pair_dao(query_db, Explanation_style_robot_pairModel(pairingId=pairing_id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入主键ID为空') + + @classmethod + async def explanation_style_robot_pair_detail_services(cls, query_db: AsyncSession, pairing_id: int): + """ + 获取讲解风格--机器人配对详细信息service + + :param query_db: orm对象 + :param pairing_id: 主键ID + :return: 主键ID对应的信息 + """ + explanation_style_robot_pair = await Explanation_style_robot_pairDao.get_explanation_style_robot_pair_detail_by_id(query_db, pairing_id=pairing_id) + if explanation_style_robot_pair: + result = Explanation_style_robot_pairModel(**CamelCaseUtil.transform_result(explanation_style_robot_pair)) + else: + result = Explanation_style_robot_pairModel(**dict()) + + return result + + @staticmethod + async def export_explanation_style_robot_pair_list_services(explanation_style_robot_pair_list: List): + """ + 导出讲解风格--机器人配对信息service + + :param explanation_style_robot_pair_list: 讲解风格--机器人配对信息列表 + :return: 讲解风格--机器人配对信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'pairingId': '主键ID', + 'robotId': '机器人ID', + 'explanationStyleId': '讲解风格ID', + 'prompt': '提示词', + 'status': '状态(0停用,1使用)', + 'createTime': '创建时间', + 'createBy': '创建者', + 'updateTime': '修改时间', + 'updateBy': '修改者', + } + binary_data = ExcelUtil.export_list2excel(explanation_style_robot_pair_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/explanation_style_service.py b/ruoyi-fastapi-backend/module_admin/service/explanation_style_service.py new file mode 100644 index 0000000..631c29b --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/explanation_style_service.py @@ -0,0 +1,132 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.dao.explanation_style_dao import Explanation_styleDao +from module_admin.entity.vo.explanation_style_vo import DeleteExplanation_styleModel, Explanation_styleModel, Explanation_stylePageQueryModel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil + + +class Explanation_styleService: + """ + 讲解风格模块服务层 + """ + + @classmethod + async def get_explanation_style_list_services( + cls, query_db: AsyncSession, query_object: Explanation_stylePageQueryModel, is_page: bool = False + ): + """ + 获取讲解风格列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 讲解风格列表信息对象 + """ + explanation_style_list_result = await Explanation_styleDao.get_explanation_style_list(query_db, query_object, is_page) + + return explanation_style_list_result + + + @classmethod + async def add_explanation_style_services(cls, query_db: AsyncSession, page_object: Explanation_styleModel): + """ + 新增讲解风格信息service + + :param query_db: orm对象 + :param page_object: 新增讲解风格对象 + :return: 新增讲解风格校验结果 + """ + try: + await Explanation_styleDao.add_explanation_style_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_explanation_style_services(cls, query_db: AsyncSession, page_object: Explanation_styleModel): + """ + 编辑讲解风格信息service + + :param query_db: orm对象 + :param page_object: 编辑讲解风格对象 + :return: 编辑讲解风格校验结果 + """ + edit_explanation_style = page_object.model_dump(exclude_unset=True, exclude={'create_time', 'create_by', }) + explanation_style_info = await cls.explanation_style_detail_services(query_db, page_object.explanation_style_id) + if explanation_style_info.explanation_style_id: + try: + await Explanation_styleDao.edit_explanation_style_dao(query_db, edit_explanation_style) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='讲解风格不存在') + + @classmethod + async def delete_explanation_style_services(cls, query_db: AsyncSession, page_object: DeleteExplanation_styleModel): + """ + 删除讲解风格信息service + + :param query_db: orm对象 + :param page_object: 删除讲解风格对象 + :return: 删除讲解风格校验结果 + """ + if page_object.explanation_style_ids: + explanation_style_id_list = page_object.explanation_style_ids.split(',') + try: + for explanation_style_id in explanation_style_id_list: + await Explanation_styleDao.delete_explanation_style_dao(query_db, Explanation_styleModel(explanationStyleId=explanation_style_id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入主键ID为空') + + @classmethod + async def explanation_style_detail_services(cls, query_db: AsyncSession, explanation_style_id: int): + """ + 获取讲解风格详细信息service + + :param query_db: orm对象 + :param explanation_style_id: 主键ID + :return: 主键ID对应的信息 + """ + explanation_style = await Explanation_styleDao.get_explanation_style_detail_by_id(query_db, explanation_style_id=explanation_style_id) + if explanation_style: + result = Explanation_styleModel(**CamelCaseUtil.transform_result(explanation_style)) + else: + result = Explanation_styleModel(**dict()) + + return result + + @staticmethod + async def export_explanation_style_list_services(explanation_style_list: List): + """ + 导出讲解风格信息service + + :param explanation_style_list: 讲解风格信息列表 + :return: 讲解风格信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'explanationStyleId': '主键ID', + 'name': '风格名称', + 'detail': '风格简介', + 'createTime': '创建时间', + 'createBy': '创建者', + 'updateTime': '修改时间', + 'updateBy': '修改者', + } + binary_data = ExcelUtil.export_list2excel(explanation_style_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/haikang_service.py b/ruoyi-fastapi-backend/module_admin/service/haikang_service.py new file mode 100644 index 0000000..09cf3cb --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/haikang_service.py @@ -0,0 +1,313 @@ +from utils.haikang_util import HaikangUtil +from module_admin.entity.vo.haikang_vo import VisitorReservationQueryModel +from config.env import HaiKangConfig +import json +import base64 +import asyncio +import os + +class HaiKangService: + """ + 海康服务 + """ + # 获取门禁列表 + @classmethod + async def get_door_list_service(cls, pageNo: int = 1, pageSize: int = 10): + result = await HaikangUtil.get_door_list_v2(pageNo, pageSize) + print(result) + with open("get_door_list_service.json", "w", encoding="utf-8") as f: + f.write(json.dumps(result[1])) + + # 查询门禁状态 + @classmethod + async def get_door_status_service(cls, door_index_codes): + result = await HaikangUtil.get_door_status(door_index_codes) + print(result) + with open("get_door_status_service.json", "w", encoding="utf-8") as f: + f.write(json.dumps(result[1])) + + # 门禁控制 + @classmethod + async def door_do_control_service(cls, door_index_codes, control_type): + try: + result = await cls.door_do_control(door_index_codes, control_type) + return result + except Exception as e: + return [False,"请求失败"] + + + # print(result) + # with open("door_do_control_service.json", "w", encoding="utf-8") as f: + # f.write(json.dumps(result[1])) + + # 查询门禁点事件 + @classmethod + async def query_door_events_service(cls, door_index_code,pageNo, pageSize, startTime, endTime): + result = await HaikangUtil.query_door_events_v2(door_index_code, pageNo=pageNo, pageSize=pageSize ,startTime=startTime, endTime=endTime) + print(result) + with open("query_door_events_service.json", "w", encoding="utf-8") as f: + f.write(json.dumps(result[1])) + + # 查看门禁点在线状态 + @classmethod + async def door_online_status_service(cls, door_index_codes): + result = await HaikangUtil.door_online_status(door_index_codes) + print(result) + with open("door_online_status_service.json", "w", encoding="utf-8") as f: + f.write(json.dumps(result[1])) + + # 按条件查询人脸分组, 很重要 + @classmethod + async def get_face_group_service(cls): + result = await HaikangUtil.get_face_group() + print(result) + with open("get_face_group_service.json", "w", encoding="utf-8") as f: + f.write(json.dumps(result[1])) + + # 人脸分组1vN搜索 + @classmethod + async def face_group_1vN_search_service(cls, image_path,faceGroupIndexCodes:list, pageNo=1, pageSize=5, searchNum:int=5 , minSimilarity=50): + + with open(image_path, 'rb') as f: + image_data = f.read() + + encoded_image = base64.b64encode(image_data).decode('utf-8') + + # with open("image_base64.txt", "w", encoding="utf-8") as f: + # f.write(encoded_image) + + result = await HaikangUtil.face_group_1vN_search( + facePicBinaryData=encoded_image, + pageNo=pageNo, + pageSize=pageSize, + searchNum=searchNum, + minSimilarity=minSimilarity, + faceGroupIndexCodes=faceGroupIndexCodes + ) + print(result) + with open("face_group_1vN_search_service.json", "w", encoding="utf-8") as f: + f.write(json.dumps(result[1])) + + # 人脸评分 + @classmethod + async def face_picture_check(cls, image_path): + + with open(image_path, 'rb') as f: + image_data = f.read() + + encoded_image = base64.b64encode(image_data).decode('utf-8') + + # print(encoded_image) + result = await HaikangUtil.face_picture_check( + facePicBinaryData=encoded_image + ) + print(result) + with open("face_picture_check.json", "w", encoding="utf-8") as f: + f.write(json.dumps(result[1])) + + # 查询访客预约记录 + @classmethod + async def query_visitor_record(cls): + result = await HaikangUtil.query_visitor_record(pageNo=1, pageSize=10, visitorStatus=1) + print(result) + with open("query_visitor_record.json", "w", encoding="utf-8") as f: + f.write(json.dumps(result[1])) + + + # 查询访客预约记录图片 + @classmethod + async def query_visitor_record_pictures_service(cls, svrIndexCode, picUri): + await HaikangUtil.query_visitor_record_pictures(svrIndexCode, picUri) + + + # 获取所有访客图片 + @classmethod + async def get_all_visitor_pictures(cls, pageNo=1, pageSize=10, visitorStatus=1): + + + result = await HaikangUtil.query_visitor_record(pageNo=pageNo, pageSize=pageSize, visitorStatus=visitorStatus) + + visitor_list = list() + if result[0]: + total = result[1]['total'] + visitor_list += result[1]['list'] + while pageNo*pageSize < total: + pageNo += 1 + result = await HaikangUtil.query_visitor_record(pageNo=pageNo+1, pageSize=pageSize, visitorStatus=visitorStatus) + if result[0]: + visitor_list += result[1]['list'] + else: + break + # else: + # return [] + # + # print(visitor_list) + + os.makedirs(os.path.dirname(HaiKangConfig.HAIKANG_VISITOR_PICTURES_SAVE_PATH), exist_ok=True) + + with open(HaiKangConfig.HAIKANG_VISITOR_RECORD_TXT, "r", encoding="utf-8") as f: + visitor_ids = [line.rstrip('\n') for line in f] + # print(visitor_ids) + + + visitor_save_path_name_list = list() + visitor_ids_add = list() + for visitor in visitor_list: + if visitor["visitorId"] not in visitor_ids: + visitor_ids_add.append(visitor["visitorId"]) + # 获取图片 + try: + result = await HaikangUtil.query_visitor_record_pictures(visitor["svrIndexCode"], visitor["picUri"], f"{HaiKangConfig.HAIKANG_VISITOR_PICTURES_SAVE_PATH}{visitor['visitorName']}") + await asyncio.sleep(1) + visitor_save_path_name_list.append(result) + except Exception as e: + print(e, visitor["visitorId"]) + + with open(HaiKangConfig.HAIKANG_VISITOR_RECORD_TXT, "a", encoding="utf-8") as f: + for visitor_id in visitor_ids_add: + f.write(visitor_id + "\n") + + return visitor_save_path_name_list + + + + # 获取所有员工图片 + @classmethod + async def get_all_employee_pictures(cls, pageNo=1, pageSize=500): + + result = await HaikangUtil.get_person_list(pageNo=pageNo, pageSize=pageSize) + + employee_list = list() + if result[0]: + total = result[1]['total'] + employee_list += result[1]['list'] + while pageNo * pageSize < total: + pageNo += 1 + result = await HaikangUtil.get_person_list(pageNo=pageNo + 1, pageSize=pageSize, ) + if result[0]: + employee_list += result[1]['list'] + else: + break + + + with open("./employee/personIds.txt", "r", encoding="utf-8") as f: + person_ids = [line.rstrip('\n') for line in f] + # print(visitor_ids) + person_ids_add = list() + for employee in employee_list: + # print(employee) + if employee["personId"] not in person_ids: + person_ids_add.append(employee["personId"]) + # 获取图片 + try: + await HaikangUtil.get_person_picture(employee.get("personPhoto")[0]["serverIndexCode"], employee.get("personPhoto")[0]["picUri"], f"./employee/face_images/{employee['personName']}") + await asyncio.sleep(0.5) + except Exception as e: + print(f"error: {e}, personId: {employee['personId']}") + + + with open("./employee/personIds.txt", "a", encoding="utf-8") as f: + for person_id in person_ids_add: + f.write(person_id + "\n") + + + # 添加单个人脸分组 + @classmethod + async def face_group_addition_service(cls, name, description): + result = await HaikangUtil.face_group_addition(name, description) + print(result) + with open("face_group_addition_service.json", "a", encoding="utf-8") as f: + f.write(json.dumps(result[1])) + + # 向分组中添加单个人脸 + @classmethod + async def face_single_addition_service(cls, faceGroupIndexCode, image_path, name): + + with open(image_path, 'rb') as f: + image_data = f.read() + + encoded_image = base64.b64encode(image_data).decode('utf-8') + + faceInfo = { + "name": name, + } + facePic = { + "facePicBinaryData": encoded_image, + "facePicUrl":"" + } + + + result = await HaikangUtil.face_single_addition(faceGroupIndexCode, faceInfo, facePic) + print(result) + with open("face_single_addition_service.json", "a", encoding="utf-8") as f: + f.write(json.dumps(result[1])) + + # 批量删除人脸 + @classmethod + async def face_delete_service(cls, faceGroupIndexCode: str, indexCodes: list): + result = await HaikangUtil.face_delete(faceGroupIndexCode, indexCodes) + print(result) + with open("face_delete_service.json", "a", encoding="utf-8") as f: + f.write(json.dumps(result[1])) + + + + + # # 查询访客列表 + @classmethod + async def get_visitor_list_service(cls, visitor_query: VisitorReservationQueryModel): + """查询访客列表 + """ + + result = await HaikangUtil.query_visitor_record(**cls.parse_dict(visitor_query.model_dump())) + + return result + + + # # 查询门禁点列表v2 + # @classmethod + # async def get_door_list_service(cls, pageNo: int = 1, pageSize: int = 10): + # """查询门禁点列表v2 + # """ + + # result = await HaikangUtil.get_door_list_v2(pageNo=pageNo, pageSize=pageSize) + + # return result + + # # 查询门禁状态 + # @classmethod + # async def get_door_status_service(cls, door_index_codes): + # """查询门禁状态 + + # Args: + # door_index_codes (list): 门禁点唯一标识 + # """ + + # result = await HaikangUtil.get_door_status(door_index_codes) + + # if result[0]: + # noAuthDoorIndexCodeList = result[1] + # if door_index_codes in noAuthDoorIndexCodeList: + # return False, 500 ,'无门禁权限' + # else: + # return True, result[1] + # else: + # return result + + # @classmethod + # def parse_dict(cls, d): + # t_d = dict() + # for k, v in d.items(): + # if v: + # t_d[k] = v + # return t_d + + # 门禁点反控 + # @classmethod + # async def door_do_control_service(cls, door_index_code: list, control_type: int): + # """门禁点反控服务 + + # Args: + # door_index_code (list): _description_ + # control_type (int): _description_ + # """ \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_admin/service/identification_record_service.py b/ruoyi-fastapi-backend/module_admin/service/identification_record_service.py new file mode 100644 index 0000000..53e592a --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/identification_record_service.py @@ -0,0 +1,187 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.entity.do.identification_record_do import IdentificationRecord +from module_admin.dao.identification_record_dao import Identification_recordDao +from module_admin.dao.door_dao import DoorDao +from module_admin.dao.identification_statistics_dao import Identification_statisticsDao +from module_admin.entity.vo.identification_record_vo import DeleteIdentification_recordModel, Identification_recordModel, Identification_recordPageQueryModel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil +from utils.haikang_util import HaikangUtil + +from datetime import datetime +from dateutil import tz + + +class Identification_recordService: + """ + 识别记录模块服务层 + """ + + @classmethod + async def get_identification_record_list_services( + cls, query_db: AsyncSession, query_object: Identification_recordPageQueryModel, is_page: bool = False + ): + """ + 获取识别记录列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 识别记录列表信息对象 + """ + t = query_object.page_size + query_object.page_size = query_object.page_size//2 + identification_record_list_result = await Identification_recordDao.get_identification_record_list(query_db, query_object, is_page) + + # 这里也要获取海康的识别记录, 查询时间需要满足ISO8601标准 + start_time = cls.time_2_iso8601(query_object.begin_create_time) if query_object.begin_create_time else None + end_time = cls.time_2_iso8601(query_object.end_create_time) if query_object.end_create_time else None + + # 获取门禁点列表, 获取机器人添加的门禁设备列表,有权限的. + door_index_code_list = await DoorDao.get_door_index_code_list(query_db, permission='1') + try: + haikang_record_list = await HaikangUtil.query_door_events_v2(door_index_code_list + ,pageNo=query_object.page_num + , pageSize=max(t-query_object.page_size, t-len(identification_record_list_result.rows)) + ,startTime=start_time, endTime=end_time + ,personName=query_object.person_name) + return identification_record_list_result.rows + cls.parse_haikang_record_list(haikang_record_list[1]["list"]) + except Exception as e: + print(e) + + # print(identification_record_list_result.rows) + # print("*"*100) + # print(haikang_record_list[1]) + return identification_record_list_result.rows + # return None + + + @classmethod + async def add_identification_record_services(cls, query_db: AsyncSession, page_object: Identification_recordModel): + """ + 新增识别记录信息service + + :param query_db: orm对象 + :param page_object: 新增识别记录对象 + :return: 新增识别记录校验结果 + """ + try: + await Identification_recordDao.add_identification_record_dao(query_db, page_object) + + # 更新识别统计数据 + # 更新识别统计数据 - 根据person_status更新相应字段 + if page_object.person_status: + await Identification_statisticsDao.update_identification_statistics_dao(query_db, page_object.person_status) + + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_identification_record_services(cls, query_db: AsyncSession, page_object: Identification_recordModel): + """ + 编辑识别记录信息service + + :param query_db: orm对象 + :param page_object: 编辑识别记录对象 + :return: 编辑识别记录校验结果 + """ + edit_identification_record = page_object.model_dump(exclude_unset=True, exclude={'create_time', 'create_by'}) + identification_record_info = await cls.identification_record_detail_services(query_db, page_object.id) + if identification_record_info.id: + try: + await Identification_recordDao.edit_identification_record_dao(query_db, edit_identification_record) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='识别记录不存在') + + @classmethod + async def delete_identification_record_services(cls, query_db: AsyncSession, page_object: DeleteIdentification_recordModel): + """ + 删除识别记录信息service + + :param query_db: orm对象 + :param page_object: 删除识别记录对象 + :return: 删除识别记录校验结果 + """ + if page_object.ids: + id_list = page_object.ids.split(',') + try: + for id in id_list: + await Identification_recordDao.delete_identification_record_dao(query_db, Identification_recordModel(id=id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入主键 自增为空') + + @classmethod + async def identification_record_detail_services(cls, query_db: AsyncSession, id: int): + """ + 获取识别记录详细信息service + + :param query_db: orm对象 + :param id: 主键 自增 + :return: 主键 自增对应的信息 + """ + identification_record = await Identification_recordDao.get_identification_record_detail_by_id(query_db, id=id) + if identification_record: + result = Identification_recordModel(**CamelCaseUtil.transform_result(identification_record)) + else: + result = Identification_recordModel(**dict()) + + return result + + @staticmethod + async def export_identification_record_list_services(identification_record_list: List): + """ + 导出识别记录信息service + + :param identification_record_list: 识别记录信息列表 + :return: 识别记录信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'id': '主键 自增', + 'personName': '识别人姓名', + 'doorName': '门禁点', + 'status': '识别结果, 0 拒绝, 1 通过', + 'source': '识别来源', + 'picUri': '图像url', + 'createTime': '创建时间', + 'createBy': '创建者', + } + binary_data = ExcelUtil.export_list2excel(identification_record_list, mapping_dict) + + return binary_data + + @classmethod + def time_2_iso8601(cls, time_str): + # 解析日期并设置时区为东八区 + dt = datetime.strptime(time_str, "%Y-%m-%d").replace(tzinfo=tz.gettz("Asia/Shanghai")) + + # 转换为 ISO 8601 格式 + iso_format = dt.isoformat() + + return iso_format + + @classmethod + def parse_haikang_record_list(cls, record_list: list): + back = list() + for record in record_list: + back.append(IdentificationRecord(person_name=record['personName'], person_status="1", door_name=record['doorName'], pic_uri=record['picUri'] + , status="1", source="haikang", conf=100, create_time=record['eventTime'])) + + return back diff --git a/ruoyi-fastapi-backend/module_admin/service/identification_statistics_service.py b/ruoyi-fastapi-backend/module_admin/service/identification_statistics_service.py new file mode 100644 index 0000000..d19869e --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/identification_statistics_service.py @@ -0,0 +1,152 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.dao.identification_statistics_dao import Identification_statisticsDao +from module_admin.entity.vo.identification_statistics_vo import DeleteIdentification_statisticsModel, Identification_statisticsModel, Identification_statisticsPageQueryModel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil + + +class Identification_statisticsService: + """ + 识别统计模块服务层 + """ + + @classmethod + async def get_today_identification_statistics_services(cls, query_db: AsyncSession ): + """ + 获取识别统计列表 + :param query_object: + :param is_page: + :return: + """ + result = await Identification_statisticsDao.get_today_identification_statistics(query_db) + return result + @classmethod + async def get_total_identification_statistics_services(cls, query_db: AsyncSession): + """ + 获取总的识别统计数据 - 返回各个字段的总和 + + :param query_db: orm对象 + :return: 总和统计数据 + """ + result = await Identification_statisticsDao.get_total_identification_statistics(query_db) + return result + + @classmethod + async def get_identification_statistics_list_services( + cls, query_db: AsyncSession, query_object: Identification_statisticsPageQueryModel, is_page: bool = False + ): + """ + 获取识别统计列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 识别统计列表信息对象 + """ + identification_statistics_list_result = await Identification_statisticsDao.get_identification_statistics_list(query_db, query_object, is_page) + + return identification_statistics_list_result + + + @classmethod + async def add_identification_statistics_services(cls, query_db: AsyncSession, page_object: Identification_statisticsModel): + """ + 新增识别统计信息service + + :param query_db: orm对象 + :param page_object: 新增识别统计对象 + :return: 新增识别统计校验结果 + """ + try: + await Identification_statisticsDao.add_identification_statistics_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_identification_statistics_services(cls, query_db: AsyncSession, page_object: Identification_statisticsModel): + """ + 编辑识别统计信息service + + :param query_db: orm对象 + :param page_object: 编辑识别统计对象 + :return: 编辑识别统计校验结果 + """ + edit_identification_statistics = page_object.model_dump(exclude_unset=True, exclude={'create_time', 'create_by'}) + identification_statistics_info = await cls.identification_statistics_detail_services(query_db, page_object.id) + if identification_statistics_info.id: + try: + await Identification_statisticsDao.edit_identification_statistics_dao(query_db, edit_identification_statistics) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='识别统计不存在') + + @classmethod + async def delete_identification_statistics_services(cls, query_db: AsyncSession, page_object: DeleteIdentification_statisticsModel): + """ + 删除识别统计信息service + + :param query_db: orm对象 + :param page_object: 删除识别统计对象 + :return: 删除识别统计校验结果 + """ + if page_object.ids: + id_list = page_object.ids.split(',') + try: + for id in id_list: + await Identification_statisticsDao.delete_identification_statistics_dao(query_db, Identification_statisticsModel(id=id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入主键 自增为空') + + @classmethod + async def identification_statistics_detail_services(cls, query_db: AsyncSession, id: int): + """ + 获取识别统计详细信息service + + :param query_db: orm对象 + :param id: 主键 自增 + :return: 主键 自增对应的信息 + """ + identification_statistics = await Identification_statisticsDao.get_identification_statistics_detail_by_id(query_db, id=id) + if identification_statistics: + result = Identification_statisticsModel(**CamelCaseUtil.transform_result(identification_statistics)) + else: + result = Identification_statisticsModel(**dict()) + + return result + + @staticmethod + async def export_identification_statistics_list_services(identification_statistics_list: List): + """ + 导出识别统计信息service + + :param identification_statistics_list: 识别统计信息列表 + :return: 识别统计信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'id': '主键 自增', + 'employ': '员工识别成功数', + 'visitor': '访客识别成功数', + 'stranger': '位置人员识别失败数', + 'createTime': '创建时间', + 'createBy': '创建者', + } + binary_data = ExcelUtil.export_list2excel(identification_statistics_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/info_service.py b/ruoyi-fastapi-backend/module_admin/service/info_service.py new file mode 100644 index 0000000..7bc8a30 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/info_service.py @@ -0,0 +1,145 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.dao.info_dao import InfoDao +from module_admin.entity.vo.info_vo import DeleteInfoModel, InfoModel, InfoPageQueryModel, InfoUpdateModel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil + + +class InfoService: + """ + 机器人信息模块服务层 + """ + + + @classmethod + async def refresh_robot_services(cls, query_db: AsyncSession, robot_ids: str): + return await InfoDao.refresh_robot_by_ids(query_db, robot_ids) + + @classmethod + async def get_info_list_services( + cls, query_db: AsyncSession, query_object: InfoPageQueryModel, is_page: bool = False + ): + """ + 获取机器人信息列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 机器人信息列表信息对象 + """ + info_list_result = await InfoDao.get_info_list(query_db, query_object, is_page) + + return info_list_result + + + @classmethod + async def add_info_services(cls, query_db: AsyncSession, page_object: InfoModel): + """ + 新增机器人信息信息service + + :param query_db: orm对象 + :param page_object: 新增机器人信息对象 + :return: 新增机器人信息校验结果 + """ + try: + await InfoDao.add_info_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_info_services(cls, query_db: AsyncSession, page_object: InfoUpdateModel): + """ + 编辑机器人信息信息service + + :param query_db: orm对象 + :param page_object: 编辑机器人信息对象 + :return: 编辑机器人信息校验结果 + """ + edit_info = page_object.model_dump(exclude_unset=True, exclude={'create_time', 'create_by', 'del_flag', }) + info_info = await cls.info_detail_services(query_db, page_object.robot_id) + if info_info.robot_id: + try: + await InfoDao.edit_info_dao(query_db, edit_info) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='机器人信息不存在') + + @classmethod + async def delete_info_services(cls, query_db: AsyncSession, page_object: DeleteInfoModel): + """ + 删除机器人信息信息service + + :param query_db: orm对象 + :param page_object: 删除机器人信息对象 + :return: 删除机器人信息校验结果 + """ + if page_object.robot_ids: + robot_id_list = page_object.robot_ids.split(',') + try: + for robot_id in robot_id_list: + await InfoDao.delete_info_dao(query_db, InfoModel(robotId=robot_id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入机器人ID为空') + + @classmethod + async def info_detail_services(cls, query_db: AsyncSession, robot_id: int): + """ + 获取机器人信息详细信息service + + :param query_db: orm对象 + :param robot_id: 机器人ID + :return: 机器人ID对应的信息 + """ + info = await InfoDao.get_info_detail_by_id(query_db, robot_id=robot_id) + if info: + result = InfoModel(**CamelCaseUtil.transform_result(info)) + else: + result = InfoModel(**dict()) + + return result + + @staticmethod + async def export_info_list_services(info_list: List): + """ + 导出机器人信息信息service + + :param info_list: 机器人信息信息列表 + :return: 机器人信息信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'robotId': '机器人ID', + 'name': '机器人名称', + 'model': '机器人型号', + 'serial': '序列号', + 'online': '机器人在线状态(0在线, 1离线)', + 'power': '电量', + 'runTime': '总运行时长', + 'position': '位置', + 'image': '机器人外观图地址', + 'createTime': '创建时间', + 'createBy': '创建者', + 'updateTime': '修改时间', + 'updateBy': '修改者', + 'delFlag': '删除标志(0存在, 2删除)', + 'remark': '备注', + } + binary_data = ExcelUtil.export_list2excel(info_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/job_log_service.py b/ruoyi-fastapi-backend/module_admin/service/job_log_service.py new file mode 100644 index 0000000..596abe7 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/job_log_service.py @@ -0,0 +1,142 @@ +from fastapi import Request +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import Session +from typing import List +from module_admin.dao.job_log_dao import JobLogDao +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.entity.vo.job_vo import DeleteJobLogModel, JobLogModel, JobLogPageQueryModel +from module_admin.service.dict_service import DictDataService +from utils.excel_util import ExcelUtil + + +class JobLogService: + """ + 定时任务日志管理模块服务层 + """ + + @classmethod + async def get_job_log_list_services( + cls, query_db: AsyncSession, query_object: JobLogPageQueryModel, is_page: bool = False + ): + """ + 获取定时任务日志列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 定时任务日志列表信息对象 + """ + job_log_list_result = await JobLogDao.get_job_log_list(query_db, query_object, is_page) + + return job_log_list_result + + @classmethod + def add_job_log_services(cls, query_db: Session, page_object: JobLogModel): + """ + 新增定时任务日志信息service + + :param query_db: orm对象 + :param page_object: 新增定时任务日志对象 + :return: 新增定时任务日志校验结果 + """ + try: + JobLogDao.add_job_log_dao(query_db, page_object) + query_db.commit() + result = dict(is_success=True, message='新增成功') + except Exception as e: + query_db.rollback() + result = dict(is_success=False, message=str(e)) + + return CrudResponseModel(**result) + + @classmethod + async def delete_job_log_services(cls, query_db: AsyncSession, page_object: DeleteJobLogModel): + """ + 删除定时任务日志信息service + + :param query_db: orm对象 + :param page_object: 删除定时任务日志对象 + :return: 删除定时任务日志校验结果 + """ + if page_object.job_log_ids: + job_log_id_list = page_object.job_log_ids.split(',') + try: + for job_log_id in job_log_id_list: + await JobLogDao.delete_job_log_dao(query_db, JobLogModel(jobLogId=job_log_id)) + await query_db.commit() + result = dict(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + result = dict(is_success=False, message='传入定时任务日志id为空') + return CrudResponseModel(**result) + + @classmethod + async def clear_job_log_services(cls, query_db: AsyncSession): + """ + 清除定时任务日志信息service + + :param query_db: orm对象 + :return: 清除定时任务日志校验结果 + """ + try: + await JobLogDao.clear_job_log_dao(query_db) + await query_db.commit() + result = dict(is_success=True, message='清除成功') + except Exception as e: + await query_db.rollback() + raise e + + return CrudResponseModel(**result) + + @staticmethod + async def export_job_log_list_services(request: Request, job_log_list: List): + """ + 导出定时任务日志信息service + + :param request: Request对象 + :param job_log_list: 定时任务日志信息列表 + :return: 定时任务日志信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'jobLogId': '任务日志编码', + 'jobName': '任务名称', + 'jobGroup': '任务组名', + 'jobExecutor': '任务执行器', + 'invokeTarget': '调用目标字符串', + 'jobArgs': '位置参数', + 'jobKwargs': '关键字参数', + 'jobTrigger': '任务触发器', + 'jobMessage': '日志信息', + 'status': '执行状态', + 'exceptionInfo': '异常信息', + 'createTime': '创建时间', + } + + job_group_list = await DictDataService.query_dict_data_list_from_cache_services( + request.app.state.redis, dict_type='sys_job_group' + ) + job_group_option = [dict(label=item.get('dictLabel'), value=item.get('dictValue')) for item in job_group_list] + job_group_option_dict = {item.get('value'): item for item in job_group_option} + job_executor_list = await DictDataService.query_dict_data_list_from_cache_services( + request.app.state.redis, dict_type='sys_job_executor' + ) + job_executor_option = [ + dict(label=item.get('dictLabel'), value=item.get('dictValue')) for item in job_executor_list + ] + job_executor_option_dict = {item.get('value'): item for item in job_executor_option} + + for item in job_log_list: + if item.get('status') == '0': + item['status'] = '正常' + else: + item['status'] = '暂停' + if str(item.get('jobGroup')) in job_group_option_dict.keys(): + item['jobGroup'] = job_group_option_dict.get(str(item.get('jobGroup'))).get('label') + if str(item.get('jobExecutor')) in job_executor_option_dict.keys(): + item['jobExecutor'] = job_executor_option_dict.get(str(item.get('jobExecutor'))).get('label') + binary_data = ExcelUtil.export_list2excel(job_log_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/job_service.py b/ruoyi-fastapi-backend/module_admin/service/job_service.py new file mode 100644 index 0000000..55263c1 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/job_service.py @@ -0,0 +1,265 @@ +from fastapi import Request +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant, JobConstant +from config.get_scheduler import SchedulerUtil +from exceptions.exception import ServiceException +from module_admin.dao.job_dao import JobDao +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.entity.vo.job_vo import DeleteJobModel, EditJobModel, JobModel, JobPageQueryModel +from module_admin.service.dict_service import DictDataService +from utils.common_util import CamelCaseUtil +from utils.cron_util import CronUtil +from utils.excel_util import ExcelUtil +from utils.string_util import StringUtil + + +class JobService: + """ + 定时任务管理模块服务层 + """ + + @classmethod + async def get_job_list_services( + cls, query_db: AsyncSession, query_object: JobPageQueryModel, is_page: bool = False + ): + """ + 获取定时任务列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 定时任务列表信息对象 + """ + job_list_result = await JobDao.get_job_list(query_db, query_object, is_page) + + return job_list_result + + @classmethod + async def check_job_unique_services(cls, query_db: AsyncSession, page_object: JobModel): + """ + 校验定时任务是否存在service + + :param query_db: orm对象 + :param page_object: 定时任务对象 + :return: 校验结果 + """ + job_id = -1 if page_object.job_id is None else page_object.job_id + job = await JobDao.get_job_detail_by_info(query_db, page_object) + if job and job.job_id != job_id: + return CommonConstant.NOT_UNIQUE + return CommonConstant.UNIQUE + + @classmethod + async def add_job_services(cls, query_db: AsyncSession, page_object: JobModel): + """ + 新增定时任务信息service + + :param query_db: orm对象 + :param page_object: 新增定时任务对象 + :return: 新增定时任务校验结果 + """ + if not CronUtil.validate_cron_expression(page_object.cron_expression): + raise ServiceException(message=f'新增定时任务{page_object.job_name}失败,Cron表达式不正确') + elif StringUtil.contains_ignore_case(page_object.invoke_target, CommonConstant.LOOKUP_RMI): + raise ServiceException(message=f'新增定时任务{page_object.job_name}失败,目标字符串不允许rmi调用') + elif StringUtil.contains_any_ignore_case( + page_object.invoke_target, [CommonConstant.LOOKUP_LDAP, CommonConstant.LOOKUP_LDAPS] + ): + raise ServiceException(message=f'新增定时任务{page_object.job_name}失败,目标字符串不允许ldap(s)调用') + elif StringUtil.contains_any_ignore_case( + page_object.invoke_target, [CommonConstant.HTTP, CommonConstant.HTTPS] + ): + raise ServiceException(message=f'新增定时任务{page_object.job_name}失败,目标字符串不允许http(s)调用') + elif StringUtil.startswith_any_case(page_object.invoke_target, JobConstant.JOB_ERROR_LIST): + raise ServiceException(message=f'新增定时任务{page_object.job_name}失败,目标字符串存在违规') + elif not StringUtil.startswith_any_case(page_object.invoke_target, JobConstant.JOB_WHITE_LIST): + raise ServiceException(message=f'新增定时任务{page_object.job_name}失败,目标字符串不在白名单内') + elif not await cls.check_job_unique_services(query_db, page_object): + raise ServiceException(message=f'新增定时任务{page_object.job_name}失败,定时任务已存在') + else: + try: + add_job = await JobDao.add_job_dao(query_db, page_object) + job_info = await cls.job_detail_services(query_db, add_job.job_id) + if job_info.status == '0': + SchedulerUtil.add_scheduler_job(job_info=job_info) + await query_db.commit() + result = dict(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + return CrudResponseModel(**result) + + @classmethod + async def edit_job_services(cls, query_db: AsyncSession, page_object: EditJobModel): + """ + 编辑定时任务信息service + + :param query_db: orm对象 + :param page_object: 编辑定时任务对象 + :return: 编辑定时任务校验结果 + """ + edit_job = page_object.model_dump(exclude_unset=True) + if page_object.type == 'status': + del edit_job['type'] + job_info = await cls.job_detail_services(query_db, page_object.job_id) + if job_info: + if page_object.type != 'status': + if not CronUtil.validate_cron_expression(page_object.cron_expression): + raise ServiceException(message=f'修改定时任务{page_object.job_name}失败,Cron表达式不正确') + elif StringUtil.contains_ignore_case(page_object.invoke_target, CommonConstant.LOOKUP_RMI): + raise ServiceException(message=f'修改定时任务{page_object.job_name}失败,目标字符串不允许rmi调用') + elif StringUtil.contains_any_ignore_case( + page_object.invoke_target, [CommonConstant.LOOKUP_LDAP, CommonConstant.LOOKUP_LDAPS] + ): + raise ServiceException( + message=f'修改定时任务{page_object.job_name}失败,目标字符串不允许ldap(s)调用' + ) + elif StringUtil.contains_any_ignore_case( + page_object.invoke_target, [CommonConstant.HTTP, CommonConstant.HTTPS] + ): + raise ServiceException( + message=f'修改定时任务{page_object.job_name}失败,目标字符串不允许http(s)调用' + ) + elif StringUtil.startswith_any_case(page_object.invoke_target, JobConstant.JOB_ERROR_LIST): + raise ServiceException(message=f'修改定时任务{page_object.job_name}失败,目标字符串存在违规') + elif not StringUtil.startswith_any_case(page_object.invoke_target, JobConstant.JOB_WHITE_LIST): + raise ServiceException(message=f'修改定时任务{page_object.job_name}失败,目标字符串不在白名单内') + elif not await cls.check_job_unique_services(query_db, page_object): + raise ServiceException(message=f'修改定时任务{page_object.job_name}失败,定时任务已存在') + try: + await JobDao.edit_job_dao(query_db, edit_job) + SchedulerUtil.remove_scheduler_job(job_id=edit_job.get('job_id')) + if edit_job.get('status') == '0': + job_info = await cls.job_detail_services(query_db, edit_job.get('job_id')) + SchedulerUtil.add_scheduler_job(job_info=job_info) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='定时任务不存在') + + @classmethod + async def execute_job_once_services(cls, query_db: AsyncSession, page_object: JobModel): + """ + 执行一次定时任务service + + :param query_db: orm对象 + :param page_object: 定时任务对象 + :return: 执行一次定时任务结果 + """ + SchedulerUtil.remove_scheduler_job(job_id=page_object.job_id) + job_info = await cls.job_detail_services(query_db, page_object.job_id) + if job_info: + SchedulerUtil.execute_scheduler_job_once(job_info=job_info) + return CrudResponseModel(is_success=True, message='执行成功') + else: + raise ServiceException(message='定时任务不存在') + + @classmethod + async def delete_job_services(cls, query_db: AsyncSession, page_object: DeleteJobModel): + """ + 删除定时任务信息service + + :param query_db: orm对象 + :param page_object: 删除定时任务对象 + :return: 删除定时任务校验结果 + """ + if page_object.job_ids: + job_id_list = page_object.job_ids.split(',') + try: + for job_id in job_id_list: + await JobDao.delete_job_dao(query_db, JobModel(jobId=job_id)) + SchedulerUtil.remove_scheduler_job(job_id=job_id) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入定时任务id为空') + + @classmethod + async def job_detail_services(cls, query_db: AsyncSession, job_id: int): + """ + 获取定时任务详细信息service + + :param query_db: orm对象 + :param job_id: 定时任务id + :return: 定时任务id对应的信息 + """ + job = await JobDao.get_job_detail_by_id(query_db, job_id=job_id) + if job: + result = JobModel(**CamelCaseUtil.transform_result(job)) + else: + result = JobModel(**dict()) + + return result + + @staticmethod + async def export_job_list_services(request: Request, job_list: List): + """ + 导出定时任务信息service + + :param request: Request对象 + :param job_list: 定时任务信息列表 + :return: 定时任务信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'jobId': '任务编码', + 'jobName': '任务名称', + 'jobGroup': '任务组名', + 'jobExecutor': '任务执行器', + 'invokeTarget': '调用目标字符串', + 'jobArgs': '位置参数', + 'jobKwargs': '关键字参数', + 'cronExpression': 'cron执行表达式', + 'misfirePolicy': '计划执行错误策略', + 'concurrent': '是否并发执行', + 'status': '状态', + 'createBy': '创建者', + 'createTime': '创建时间', + 'updateBy': '更新者', + 'updateTime': '更新时间', + 'remark': '备注', + } + + job_group_list = await DictDataService.query_dict_data_list_from_cache_services( + request.app.state.redis, dict_type='sys_job_group' + ) + job_group_option = [dict(label=item.get('dictLabel'), value=item.get('dictValue')) for item in job_group_list] + job_group_option_dict = {item.get('value'): item for item in job_group_option} + job_executor_list = await DictDataService.query_dict_data_list_from_cache_services( + request.app.state.redis, dict_type='sys_job_executor' + ) + job_executor_option = [ + dict(label=item.get('dictLabel'), value=item.get('dictValue')) for item in job_executor_list + ] + job_executor_option_dict = {item.get('value'): item for item in job_executor_option} + + for item in job_list: + if item.get('status') == '0': + item['status'] = '正常' + else: + item['status'] = '暂停' + if str(item.get('jobGroup')) in job_group_option_dict.keys(): + item['jobGroup'] = job_group_option_dict.get(str(item.get('jobGroup'))).get('label') + if str(item.get('jobExecutor')) in job_executor_option_dict.keys(): + item['jobExecutor'] = job_executor_option_dict.get(str(item.get('jobExecutor'))).get('label') + if item.get('misfirePolicy') == '1': + item['misfirePolicy'] = '立即执行' + elif item.get('misfirePolicy') == '2': + item['misfirePolicy'] = '执行一次' + else: + item['misfirePolicy'] = '放弃执行' + if item.get('concurrent') == '0': + item['concurrent'] = '允许' + else: + item['concurrent'] = '禁止' + binary_data = ExcelUtil.export_list2excel(job_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/kb_es_service.py b/ruoyi-fastapi-backend/module_admin/service/kb_es_service.py new file mode 100644 index 0000000..8bc58fb --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/kb_es_service.py @@ -0,0 +1,288 @@ +import hashlib +import json +import re +import time +from dataclasses import dataclass +from typing import Any, Dict, List, Tuple + +import httpx + +from config.env import KBConfig +from utils.log_util import logger +from utils.match_service import get_match_service + + +@dataclass +class KBRetrievalMetrics: + retrieve_ms: float + top1_score: float + score_gap: float + hit_count: int + keyword_coverage: float + cache_hit: bool + + +class KBESServiceError(Exception): + pass + + +class KBESService: + """ES/OpenSearch(BM25)检索 + 置信度 + 缓存封装""" + + CACHE_PREFIX = "kb:es:retrieval" + + @classmethod + def _cache_key(cls, question: str) -> str: + q = (question or "").strip().lower() + digest = hashlib.sha256(q.encode("utf-8")).hexdigest() + return f"{cls.CACHE_PREFIX}:{digest}" + + @classmethod + async def retrieve(cls, question: str, redis=None) -> Tuple[List[Dict[str, Any]], KBRetrievalMetrics]: + if not question: + return [], KBRetrievalMetrics(0.0, 0.0, 0.0, 0, 0.0, False) + + # 仅对 KB_PROVIDER=es_bm25:禁用 KBES 检索缓存(避免 curl/API 不一致与历史命中) + enable_retrieval_cache = KBConfig.KB_PROVIDER != 'es_bm25' + + cache_hit = False + if enable_retrieval_cache and redis and KBConfig.KB_CACHE_TTL > 0: + key = cls._cache_key(question) + cached = await redis.get(key) + if cached: + try: + payload = json.loads(cached) + chunks = payload.get("chunks", []) + metrics = payload.get("metrics", {}) + return ( + chunks, + KBRetrievalMetrics( + retrieve_ms=metrics.get("retrieve_ms", 0.0), + top1_score=metrics.get("top1_score", 0.0), + score_gap=metrics.get("score_gap", 0.0), + hit_count=metrics.get("hit_count", len(chunks)), + keyword_coverage=metrics.get("keyword_coverage", 0.0), + cache_hit=True, + ), + ) + except Exception: + pass + + if not KBConfig.KB_ES_URL: + raise KBESServiceError("KB_ES_URL/KB_OS_URL 未配置") + + logger.info( + f"[KB_ES] using es_url={KBConfig.KB_ES_URL} | index={KBConfig.KB_ES_INDEX} | top_k={KBConfig.KB_TOP_K}" + ) + + t0 = time.perf_counter() + chunks, metrics_raw = await cls._es_search(question) + retrieve_ms = (time.perf_counter() - t0) * 1000 + + # KB_PROVIDER=es_bm25 场景:以 ES 返回的原始排序为准(与手工 curl 对齐),不做二次重排。 + if KBConfig.KB_PROVIDER == 'es_bm25': + best_cov = cls._rerank_chunks(question, chunks)[1] + else: + chunks, best_cov = cls._rerank_chunks(question, chunks) + + top1 = float(chunks[0].get("_score") or 0.0) if chunks else 0.0 + top2 = float(chunks[1].get("_score") or 0.0) if len(chunks) > 1 else 0.0 + gap = top1 - top2 + + metrics = KBRetrievalMetrics( + retrieve_ms=retrieve_ms, + top1_score=top1, + score_gap=gap, + hit_count=len(chunks), + keyword_coverage=best_cov, + cache_hit=cache_hit, + ) + + if enable_retrieval_cache and redis and KBConfig.KB_CACHE_TTL > 0: + try: + key = cls._cache_key(question) + await redis.set( + key, + json.dumps( + { + "chunks": chunks, + "metrics": { + "retrieve_ms": metrics.retrieve_ms, + "top1_score": metrics.top1_score, + "score_gap": metrics.score_gap, + "hit_count": metrics.hit_count, + "keyword_coverage": metrics.keyword_coverage, + }, + }, + ensure_ascii=False, + ), + ex=KBConfig.KB_CACHE_TTL, + ) + except Exception as e: + logger.debug(f"[KB_ES] 写入检索缓存失败: {e}") + + return chunks, metrics + + @classmethod + def _rerank_chunks(cls, question: str, chunks: List[Dict[str, Any]]) -> Tuple[List[Dict[str, Any]], float]: + """二次重排(轻量、无LLM),避免 ES top1 误命中导致直接输出低质答案。 + + 目标:尽量保持速度(topK 很小),只在内存里做简单特征打分。 + """ + + if not chunks: + return chunks, 0.0 + + match_service = get_match_service() + q = (question or "").strip() + q_norm = q.lower() + + # 约束词:如果用户问到公司/年份,命中片段最好也出现对应实体 + must_brand = any(x in q_norm for x in ("康达新材", "康达")) + years = set(re.findall(r"20\d{2}", q)) + + max_es = max([float(c.get("_score") or 0.0) for c in chunks] or [0.0]) + if max_es <= 0: + max_es = 1.0 + + scored: List[Tuple[float, float, float, Dict[str, Any]]] = [] + for c in chunks: + text = "\n".join( + [ + str(c.get("question") or ""), + str(c.get("answer") or ""), + str(c.get("title") or ""), + str(c.get("content") or ""), + str(c.get("source") or ""), + ] + ) + text_norm = text.lower() + + cov = match_service.calculate_keyword_coverage(q, text) + es = float(c.get("_score") or 0.0) + es_norm = es / max_es + + penalty = 0.0 + if must_brand and ("康达" not in text_norm): + penalty -= 0.35 + if years: + hit_years = any(y in text for y in years) + if not hit_years: + penalty -= 0.25 + + # 覆盖率更能反映“是否问对文档”,ES 分数反映粗排相关性 + final = 0.55 * cov + 0.45 * es_norm + penalty + + scored.append((final, cov, es, c)) + + scored.sort(key=lambda x: x[0], reverse=True) + reranked = [c for _, _, _, c in scored] + best_cov = float(scored[0][1]) if scored else 0.0 + return reranked, best_cov + + @classmethod + def is_confident(cls, metrics: KBRetrievalMetrics) -> bool: + if metrics.hit_count <= 0: + return False + if metrics.top1_score < KBConfig.KB_MIN_SCORE: + return False + if metrics.keyword_coverage < KBConfig.KB_MIN_COVERAGE: + return False + return True + + @classmethod + async def _es_search(cls, question: str) -> Tuple[List[Dict[str, Any]], Dict[str, float]]: + index = KBConfig.KB_ES_INDEX + url = f"{KBConfig.KB_ES_URL}/{index}/_search" + + # 与线上排查/运维 curl 查询保持一致,避免 DSL 差异导致“bash 命中正确、API 命中错误”。 + query = { + "size": KBConfig.KB_TOP_K, + "track_total_hits": False, + "_source": [ + "doc_id", + "chunk_id", + "qa_id", + "question", + "answer", + "title", + "content", + "source", + "tags", + "category", + "updated_at", + "version", + ], + "query": { + "multi_match": { + "query": question, + "fields": [ + "question^3", + "answer^1.5", + "title^2", + "content", + ], + "type": "best_fields", + } + }, + } + + auth = None + if KBConfig.KB_ES_USERNAME and KBConfig.KB_ES_PASSWORD: + auth = (KBConfig.KB_ES_USERNAME, KBConfig.KB_ES_PASSWORD) + + async with httpx.AsyncClient(timeout=5.0, verify=KBConfig.KB_ES_VERIFY_SSL) as client: + # 线上排障:打印实际发给 ES 的 request body,确保与手工 curl 完全一致 + logger.info(f"[KB_ES] request_body={json.dumps(query, ensure_ascii=False)}") + resp = await client.post(url, json=query, auth=auth) + if resp.status_code >= 300: + raise KBESServiceError(f"ES检索失败 HTTP {resp.status_code}: {resp.text}") + data = resp.json() + + try: + top3 = ((data.get("hits") or {}).get("hits") or [])[:3] + logger.info( + "[KB_ES] es_top3=" + + json.dumps( + [ + { + "_id": h.get("_id"), + "_score": h.get("_score"), + "question": (h.get("_source") or {}).get("question"), + } + for h in top3 + ], + ensure_ascii=False, + ) + ) + except Exception: + pass + + hits = (data.get("hits") or {}).get("hits") or [] + chunks: List[Dict[str, Any]] = [] + scores: List[float] = [] + for h in hits: + src = h.get("_source") or {} + score = float(h.get("_score") or 0.0) + scores.append(score) + chunks.append( + { + "doc_id": src.get("doc_id"), + "chunk_id": src.get("chunk_id"), + "qa_id": src.get("qa_id"), + "question": src.get("question"), + "answer": src.get("answer"), + "title": src.get("title"), + "content": src.get("content"), + "source": src.get("source"), + "tags": src.get("tags"), + "category": src.get("category"), + "updated_at": src.get("updated_at"), + "version": src.get("version"), + "_score": score, + } + ) + + top1 = scores[0] if scores else 0.0 + top2 = scores[1] if len(scores) > 1 else 0.0 + return chunks, {"top1_score": top1, "score_gap": top1 - top2} diff --git a/ruoyi-fastapi-backend/module_admin/service/log_service.py b/ruoyi-fastapi-backend/module_admin/service/log_service.py new file mode 100644 index 0000000..0983b1a --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/log_service.py @@ -0,0 +1,258 @@ +from fastapi import Request +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from exceptions.exception import ServiceException +from module_admin.dao.log_dao import LoginLogDao, OperationLogDao +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.entity.vo.log_vo import ( + DeleteLoginLogModel, + DeleteOperLogModel, + LogininforModel, + LoginLogPageQueryModel, + OperLogModel, + OperLogPageQueryModel, + UnlockUser, +) +from module_admin.service.dict_service import DictDataService +from utils.excel_util import ExcelUtil + + +class OperationLogService: + """ + 操作日志管理模块服务层 + """ + + @classmethod + async def get_operation_log_list_services( + cls, query_db: AsyncSession, query_object: OperLogPageQueryModel, is_page: bool = False + ): + """ + 获取操作日志列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 操作日志列表信息对象 + """ + operation_log_list_result = await OperationLogDao.get_operation_log_list(query_db, query_object, is_page) + + return operation_log_list_result + + @classmethod + async def add_operation_log_services(cls, query_db: AsyncSession, page_object: OperLogModel): + """ + 新增操作日志service + + :param query_db: orm对象 + :param page_object: 新增操作日志对象 + :return: 新增操作日志校验结果 + """ + try: + await OperationLogDao.add_operation_log_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def delete_operation_log_services(cls, query_db: AsyncSession, page_object: DeleteOperLogModel): + """ + 删除操作日志信息service + + :param query_db: orm对象 + :param page_object: 删除操作日志对象 + :return: 删除操作日志校验结果 + """ + if page_object.oper_ids: + oper_id_list = page_object.oper_ids.split(',') + try: + for oper_id in oper_id_list: + await OperationLogDao.delete_operation_log_dao(query_db, OperLogModel(operId=oper_id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入操作日志id为空') + + @classmethod + async def clear_operation_log_services(cls, query_db: AsyncSession): + """ + 清除操作日志信息service + + :param query_db: orm对象 + :return: 清除操作日志校验结果 + """ + try: + await OperationLogDao.clear_operation_log_dao(query_db) + await query_db.commit() + return CrudResponseModel(is_success=True, message='清除成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def export_operation_log_list_services(cls, request: Request, operation_log_list: List): + """ + 导出操作日志信息service + + :param request: Request对象 + :param operation_log_list: 操作日志信息列表 + :return: 操作日志信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'operId': '日志编号', + 'title': '系统模块', + 'businessType': '操作类型', + 'method': '方法名称', + 'requestMethod': '请求方式', + 'operName': '操作人员', + 'deptName': '部门名称', + 'operUrl': '请求URL', + 'operIp': '操作地址', + 'operLocation': '操作地点', + 'operParam': '请求参数', + 'jsonResult': '返回参数', + 'status': '操作状态', + 'error_msg': '错误消息', + 'operTime': '操作日期', + 'costTime': '消耗时间(毫秒)', + } + + operation_type_list = await DictDataService.query_dict_data_list_from_cache_services( + request.app.state.redis, dict_type='sys_oper_type' + ) + operation_type_option = [ + dict(label=item.get('dictLabel'), value=item.get('dictValue')) for item in operation_type_list + ] + operation_type_option_dict = {item.get('value'): item for item in operation_type_option} + + for item in operation_log_list: + if item.get('status') == 0: + item['status'] = '成功' + else: + item['status'] = '失败' + if str(item.get('businessType')) in operation_type_option_dict.keys(): + item['businessType'] = operation_type_option_dict.get(str(item.get('businessType'))).get('label') + binary_data = ExcelUtil.export_list2excel(operation_log_list, mapping_dict) + + return binary_data + + +class LoginLogService: + """ + 登录日志管理模块服务层 + """ + + @classmethod + async def get_login_log_list_services( + cls, query_db: AsyncSession, query_object: LoginLogPageQueryModel, is_page: bool = False + ): + """ + 获取登录日志列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 登录日志列表信息对象 + """ + operation_log_list_result = await LoginLogDao.get_login_log_list(query_db, query_object, is_page) + + return operation_log_list_result + + @classmethod + async def add_login_log_services(cls, query_db: AsyncSession, page_object: LogininforModel): + """ + 新增登录日志service + + :param query_db: orm对象 + :param page_object: 新增登录日志对象 + :return: 新增登录日志校验结果 + """ + try: + await LoginLogDao.add_login_log_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def delete_login_log_services(cls, query_db: AsyncSession, page_object: DeleteLoginLogModel): + """ + 删除操作日志信息service + + :param query_db: orm对象 + :param page_object: 删除操作日志对象 + :return: 删除操作日志校验结果 + """ + if page_object.info_ids: + info_id_list = page_object.info_ids.split(',') + try: + for info_id in info_id_list: + await LoginLogDao.delete_login_log_dao(query_db, LogininforModel(infoId=info_id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入登录日志id为空') + + @classmethod + async def clear_login_log_services(cls, query_db: AsyncSession): + """ + 清除操作日志信息service + + :param query_db: orm对象 + :return: 清除操作日志校验结果 + """ + try: + await LoginLogDao.clear_login_log_dao(query_db) + await query_db.commit() + return CrudResponseModel(is_success=True, message='清除成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def unlock_user_services(cls, request: Request, unlock_user: UnlockUser): + locked_user = await request.app.state.redis.get(f'account_lock:{unlock_user.user_name}') + if locked_user: + await request.app.state.redis.delete(f'account_lock:{unlock_user.user_name}') + return CrudResponseModel(is_success=True, message='解锁成功') + else: + raise ServiceException(message='该用户未锁定') + + @staticmethod + async def export_login_log_list_services(login_log_list: List): + """ + 导出登录日志信息service + + :param login_log_list: 登录日志信息列表 + :return: 登录日志信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'infoId': '访问编号', + 'userName': '用户名称', + 'ipaddr': '登录地址', + 'loginLocation': '登录地点', + 'browser': '浏览器', + 'os': '操作系统', + 'status': '登录状态', + 'msg': '操作信息', + 'loginTime': '登录日期', + } + + for item in login_log_list: + if item.get('status') == '0': + item['status'] = '成功' + else: + item['status'] = '失败' + binary_data = ExcelUtil.export_list2excel(login_log_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/login_service.py b/ruoyi-fastapi-backend/module_admin/service/login_service.py new file mode 100644 index 0000000..5579f0d --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/login_service.py @@ -0,0 +1,606 @@ +import jwt +import random +import uuid +from datetime import datetime, timedelta, timezone +from fastapi import Depends, Form, Request +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jwt.exceptions import InvalidTokenError +from sqlalchemy.ext.asyncio import AsyncSession +from typing import Dict, List, Optional, Union +from config.constant import CommonConstant, MenuConstant +from config.enums import RedisInitKeyConfig +from config.env import AppConfig, JwtConfig +from config.get_db import get_db +from exceptions.exception import LoginException, AuthException, ServiceException +from module_admin.dao.login_dao import login_by_account +from module_admin.dao.user_dao import UserDao +from module_admin.entity.do.menu_do import SysMenu +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.entity.vo.login_vo import MenuTreeModel, MetaModel, RouterModel, SmsCode, UserLogin, UserRegister +from module_admin.entity.vo.user_vo import AddUserModel, CurrentUserModel, ResetUserModel, TokenData, UserInfoModel +from module_admin.service.user_service import UserService +from utils.common_util import CamelCaseUtil +from utils.log_util import logger +from utils.message_util import message_service +from utils.pwd_util import PwdUtil + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl='login') + + +class CustomOAuth2PasswordRequestForm(OAuth2PasswordRequestForm): + """ + 自定义OAuth2PasswordRequestForm类,增加验证码及会话编号参数 + """ + + def __init__( + self, + grant_type: str = Form(default=None, regex='password'), + username: str = Form(), + password: str = Form(), + scope: str = Form(default=''), + client_id: Optional[str] = Form(default=None), + client_secret: Optional[str] = Form(default=None), + code: Optional[str] = Form(default=''), + uuid: Optional[str] = Form(default=''), + login_info: Optional[Dict[str, str]] = Form(default=None), + ): + super().__init__( + grant_type=grant_type, + username=username, + password=password, + scope=scope, + client_id=client_id, + client_secret=client_secret, + ) + self.code = code + self.uuid = uuid + self.login_info = login_info + + +class LoginService: + """ + 登录模块服务层 + """ + + @classmethod + async def authenticate_user(cls, request: Request, query_db: AsyncSession, login_user: UserLogin): + """ + 根据用户名密码校验用户登录 + + :param request: Request对象 + :param query_db: orm对象 + :param login_user: 登录用户对象 + :return: 校验结果 + """ + await cls.__check_login_ip(request) + account_lock = await request.app.state.redis.get( + f'{RedisInitKeyConfig.ACCOUNT_LOCK.key}:{login_user.user_name}' + ) + if login_user.user_name == account_lock: + logger.warning('账号已锁定,请稍后再试') + raise LoginException(data='', message='账号已锁定,请稍后再试') + # 判断请求是否来自于api文档,如果是返回指定格式的结果,用于修复api文档认证成功后token显示undefined的bug + request_from_swagger = ( + request.headers.get('referer').endswith('docs') if request.headers.get('referer') else False + ) + request_from_redoc = ( + request.headers.get('referer').endswith('redoc') if request.headers.get('referer') else False + ) + # 判断是否开启验证码,开启则验证,否则不验证(dev模式下来自API文档的登录请求不检验) + if not login_user.captcha_enabled or ( + (request_from_swagger or request_from_redoc) and AppConfig.app_env == 'dev' + ): + pass + else: + await cls.__check_login_captcha(request, login_user) + user = await login_by_account(query_db, login_user.user_name) + if not user: + logger.warning('用户不存在') + raise LoginException(data='', message='用户不存在') + if not PwdUtil.verify_password(login_user.password, user[0].password): + cache_password_error_count = await request.app.state.redis.get( + f'{RedisInitKeyConfig.PASSWORD_ERROR_COUNT.key}:{login_user.user_name}' + ) + password_error_counted = 0 + if cache_password_error_count: + password_error_counted = cache_password_error_count + password_error_count = int(password_error_counted) + 1 + await request.app.state.redis.set( + f'{RedisInitKeyConfig.PASSWORD_ERROR_COUNT.key}:{login_user.user_name}', + password_error_count, + ex=timedelta(minutes=10), + ) + if password_error_count > 5: + await request.app.state.redis.delete( + f'{RedisInitKeyConfig.PASSWORD_ERROR_COUNT.key}:{login_user.user_name}' + ) + await request.app.state.redis.set( + f'{RedisInitKeyConfig.ACCOUNT_LOCK.key}:{login_user.user_name}', + login_user.user_name, + ex=timedelta(minutes=10), + ) + logger.warning('10分钟内密码已输错超过5次,账号已锁定,请10分钟后再试') + raise LoginException(data='', message='10分钟内密码已输错超过5次,账号已锁定,请10分钟后再试') + logger.warning('密码错误') + raise LoginException(data='', message='密码错误') + if user[0].status == '1': + logger.warning('用户已停用') + raise LoginException(data='', message='用户已停用') + await request.app.state.redis.delete(f'{RedisInitKeyConfig.PASSWORD_ERROR_COUNT.key}:{login_user.user_name}') + return user + + @classmethod + async def __check_login_ip(cls, request: Request): + """ + 校验用户登录ip是否在黑名单内 + + :param request: Request对象 + :return: 校验结果 + """ + black_ip_value = await request.app.state.redis.get(f'{RedisInitKeyConfig.SYS_CONFIG.key}:sys.login.blackIPList') + black_ip_list = black_ip_value.split(',') if black_ip_value else [] + if request.headers.get('X-Forwarded-For') in black_ip_list: + logger.warning('当前IP禁止登录') + raise LoginException(data='', message='当前IP禁止登录') + return True + + @classmethod + async def __check_login_captcha(cls, request: Request, login_user: UserLogin): + """ + 校验用户登录验证码 + + :param request: Request对象 + :param login_user: 登录用户对象 + :return: 校验结果 + """ + captcha_value = await request.app.state.redis.get(f'{RedisInitKeyConfig.CAPTCHA_CODES.key}:{login_user.uuid}') + if not captcha_value: + logger.warning('验证码已失效') + raise LoginException(data='', message='验证码已失效') + if login_user.code != str(captcha_value): + logger.warning('验证码错误') + raise LoginException(data='', message='验证码错误') + return True + + @classmethod + async def create_access_token(cls, data: dict, expires_delta: Union[timedelta, None] = None): + """ + 根据登录信息创建当前用户token + + :param data: 登录信息 + :param expires_delta: token有效期 + :return: token + """ + to_encode = data.copy() + if expires_delta: + expire = datetime.now(timezone.utc) + expires_delta + else: + expire = datetime.now(timezone.utc) + timedelta(minutes=30) + to_encode.update({'exp': expire}) + encoded_jwt = jwt.encode(to_encode, JwtConfig.jwt_secret_key, algorithm=JwtConfig.jwt_algorithm) + return encoded_jwt + + @classmethod + async def get_current_user( + cls, request: Request = Request, token: str = Depends(oauth2_scheme), query_db: AsyncSession = Depends(get_db) + ): + """ + 根据token获取当前用户信息 + + :param request: Request对象 + :param token: 用户token + :param query_db: orm对象 + :return: 当前用户信息对象 + :raise: 令牌异常AuthException + """ + # if token[:6] != 'Bearer': + # logger.warning("用户token不合法") + # raise AuthException(data="", message="用户token不合法") + try: + if token.startswith('Bearer'): + token = token.split(' ')[1] + payload = jwt.decode(token, JwtConfig.jwt_secret_key, algorithms=[JwtConfig.jwt_algorithm]) + user_id: str = payload.get('user_id') + session_id: str = payload.get('session_id') + if not user_id: + logger.warning('用户token不合法') + raise AuthException(data='', message='用户token不合法') + token_data = TokenData(user_id=int(user_id)) + except InvalidTokenError: + logger.warning('用户token已失效,请重新登录') + raise AuthException(data='', message='用户token已失效,请重新登录') + query_user = await UserDao.get_user_by_id(query_db, user_id=token_data.user_id) + if query_user.get('user_basic_info') is None: + logger.warning('用户token不合法') + raise AuthException(data='', message='用户token不合法') + if AppConfig.app_same_time_login: + redis_token = await request.app.state.redis.get(f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}') + else: + # 此方法可实现同一账号同一时间只能登录一次 + redis_token = await request.app.state.redis.get( + f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{query_user.get('user_basic_info').user_id}" + ) + if token == redis_token: + if AppConfig.app_same_time_login: + await request.app.state.redis.set( + f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}', + redis_token, + ex=timedelta(minutes=JwtConfig.jwt_redis_expire_minutes), + ) + else: + await request.app.state.redis.set( + f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{query_user.get('user_basic_info').user_id}", + redis_token, + ex=timedelta(minutes=JwtConfig.jwt_redis_expire_minutes), + ) + + role_id_list = [item.role_id for item in query_user.get('user_role_info')] + if 1 in role_id_list: + permissions = ['*:*:*'] + else: + permissions = [row.perms for row in query_user.get('user_menu_info')] + post_ids = ','.join([str(row.post_id) for row in query_user.get('user_post_info')]) + role_ids = ','.join([str(row.role_id) for row in query_user.get('user_role_info')]) + roles = [row.role_key for row in query_user.get('user_role_info')] + + current_user = CurrentUserModel( + permissions=permissions, + roles=roles, + user=UserInfoModel( + **CamelCaseUtil.transform_result(query_user.get('user_basic_info')), + postIds=post_ids, + roleIds=role_ids, + dept=CamelCaseUtil.transform_result(query_user.get('user_dept_info')), + role=CamelCaseUtil.transform_result(query_user.get('user_role_info')), + ), + ) + return current_user + else: + logger.warning('用户token已失效,请重新登录') + raise AuthException(data='', message='用户token已失效,请重新登录') + + @classmethod + async def get_current_user_routers(cls, user_id: int, query_db: AsyncSession): + """ + 根据用户id获取当前用户路由信息 + + :param user_id: 用户id + :param query_db: orm对象 + :return: 当前用户路由信息对象 + """ + query_user = await UserDao.get_user_by_id(query_db, user_id=user_id) + user_router_menu = sorted( + [ + row + for row in query_user.get('user_menu_info') + if row.menu_type in [MenuConstant.TYPE_DIR, MenuConstant.TYPE_MENU] + ], + key=lambda x: x.order_num, + ) + menus = cls.__generate_menus(0, user_router_menu) + user_router = cls.__generate_user_router_menu(menus) + return [router.model_dump(exclude_unset=True, by_alias=True) for router in user_router] + + @classmethod + def __generate_menus(cls, pid: int, permission_list: List[SysMenu]): + """ + 工具方法:根据菜单信息生成菜单信息树形嵌套数据 + + :param pid: 菜单id + :param permission_list: 菜单列表信息 + :return: 菜单信息树形嵌套数据 + """ + menu_list: List[MenuTreeModel] = [] + for permission in permission_list: + if permission.parent_id == pid: + children = cls.__generate_menus(permission.menu_id, permission_list) + menu_list_data = MenuTreeModel(**CamelCaseUtil.transform_result(permission)) + if children: + menu_list_data.children = children + menu_list.append(menu_list_data) + + return menu_list + + @classmethod + def __generate_user_router_menu(cls, permission_list: List[MenuTreeModel]): + """ + 工具方法:根据菜单树信息生成路由信息树形嵌套数据 + + :param permission_list: 菜单树列表信息 + :return: 路由信息树形嵌套数据 + """ + router_list: List[RouterModel] = [] + for permission in permission_list: + router = RouterModel( + hidden=True if permission.visible == '1' else False, + name=RouterUtil.get_router_name(permission), + path=RouterUtil.get_router_path(permission), + component=RouterUtil.get_component(permission), + query=permission.query, + meta=MetaModel( + title=permission.menu_name, + icon=permission.icon, + noCache=True if permission.is_cache == 1 else False, + link=permission.path if RouterUtil.is_http(permission.path) else None, + ), + ) + c_menus = permission.children + if c_menus and permission.menu_type == MenuConstant.TYPE_DIR: + router.always_show = True + router.redirect = 'noRedirect' + router.children = cls.__generate_user_router_menu(c_menus) + elif RouterUtil.is_menu_frame(permission): + router.meta = None + children_list: List[RouterModel] = [] + children = RouterModel( + path=permission.path, + component=permission.component, + name=RouterUtil.get_route_name(permission.route_name, permission.path), + meta=MetaModel( + title=permission.menu_name, + icon=permission.icon, + noCache=True if permission.is_cache == 1 else False, + link=permission.path if RouterUtil.is_http(permission.path) else None, + ), + query=permission.query, + ) + children_list.append(children) + router.children = children_list + elif permission.parent_id == 0 and RouterUtil.is_inner_link(permission): + router.meta = MetaModel(title=permission.menu_name, icon=permission.icon) + router.path = '/' + children_list: List[RouterModel] = [] + router_path = RouterUtil.inner_link_replace_each(permission.path) + children = RouterModel( + path=router_path, + component=MenuConstant.INNER_LINK, + name=RouterUtil.get_route_name(permission.route_name, permission.path), + meta=MetaModel( + title=permission.menu_name, + icon=permission.icon, + link=permission.path if RouterUtil.is_http(permission.path) else None, + ), + ) + children_list.append(children) + router.children = children_list + + router_list.append(router) + + return router_list + + @classmethod + async def register_user_services(cls, request: Request, query_db: AsyncSession, user_register: UserRegister): + """ + 用户注册services + + :param request: Request对象 + :param query_db: orm对象 + :param user_register: 注册用户对象 + :return: 注册结果 + """ + register_enabled = ( + True + if await request.app.state.redis.get(f'{RedisInitKeyConfig.SYS_CONFIG.key}:sys.account.registerUser') + == 'true' + else False + ) + captcha_enabled = ( + True + if await request.app.state.redis.get(f'{RedisInitKeyConfig.SYS_CONFIG.key}:sys.account.captchaEnabled') + == 'true' + else False + ) + if user_register.password == user_register.confirm_password: + if register_enabled: + if captcha_enabled: + captcha_value = await request.app.state.redis.get( + f'{RedisInitKeyConfig.CAPTCHA_CODES.key}:{user_register.uuid}' + ) + if not captcha_value: + raise ServiceException(message='验证码已失效') + elif user_register.code != str(captcha_value): + raise ServiceException(message='验证码错误') + add_user = AddUserModel( + userName=user_register.username, + nickName=user_register.username, + password=PwdUtil.get_password_hash(user_register.password), + ) + result = await UserService.add_user_services(query_db, add_user) + return result + else: + raise ServiceException(message='注册程序已关闭,禁止注册') + else: + raise ServiceException(message='两次输入的密码不一致') + + @classmethod + async def get_sms_code_services(cls, request: Request, query_db: AsyncSession, user: ResetUserModel): + """ + 获取短信验证码service + + :param request: Request对象 + :param query_db: orm对象 + :param user: 用户对象 + :return: 短信验证码对象 + """ + redis_sms_result = await request.app.state.redis.get(f'{RedisInitKeyConfig.SMS_CODE.key}:{user.session_id}') + if redis_sms_result: + return SmsCode(**dict(is_success=False, sms_code='', session_id='', message='短信验证码仍在有效期内')) + is_user = await UserDao.get_user_by_name(query_db, user.user_name) + if is_user: + sms_code = str(random.randint(100000, 999999)) + session_id = str(uuid.uuid4()) + await request.app.state.redis.set( + f'{RedisInitKeyConfig.SMS_CODE.key}:{session_id}', sms_code, ex=timedelta(minutes=2) + ) + # 此处模拟调用短信服务 + message_service(sms_code) + + return SmsCode(**dict(is_success=True, sms_code=sms_code, session_id=session_id, message='获取成功')) + + return SmsCode(**dict(is_success=False, sms_code='', session_id='', message='用户不存在')) + + @classmethod + async def forget_user_services(cls, request: Request, query_db: AsyncSession, forget_user: ResetUserModel): + """ + 用户忘记密码services + + :param request: Request对象 + :param query_db: orm对象 + :param forget_user: 重置用户对象 + :return: 重置结果 + """ + redis_sms_result = await request.app.state.redis.get( + f'{RedisInitKeyConfig.SMS_CODE.key}:{forget_user.session_id}' + ) + if forget_user.sms_code == redis_sms_result: + forget_user.password = PwdUtil.get_password_hash(forget_user.password) + forget_user.user_id = (await UserDao.get_user_by_name(query_db, forget_user.user_name)).user_id + edit_result = await UserService.reset_user_services(query_db, forget_user) + result = edit_result.dict() + elif not redis_sms_result: + result = dict(is_success=False, message='短信验证码已过期') + else: + await request.app.state.redis.delete(f'{RedisInitKeyConfig.SMS_CODE.key}:{forget_user.session_id}') + result = dict(is_success=False, message='短信验证码不正确') + + return CrudResponseModel(**result) + + @classmethod + async def logout_services(cls, request: Request, session_id: str): + """ + 退出登录services + + :param request: Request对象 + :param session_id: 会话编号 + :return: 退出登录结果 + """ + await request.app.state.redis.delete(f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}') + # await request.app.state.redis.delete(f'{current_user.user.user_id}_access_token') + # await request.app.state.redis.delete(f'{current_user.user.user_id}_session_id') + + return True + + +class RouterUtil: + """ + 路由处理工具类 + """ + + @classmethod + def get_router_name(cls, menu: MenuTreeModel): + """ + 获取路由名称 + + :param menu: 菜单数对象 + :return: 路由名称 + """ + # 非外链并且是一级目录(类型为目录) + if cls.is_menu_frame(menu): + return '' + + return cls.get_route_name(menu.route_name, menu.path) + + @classmethod + def get_route_name(cls, name: str, path: str): + """ + 获取路由名称,如没有配置路由名称则取路由地址 + + :param name: 路由名称 + :param path: 路由地址 + :return: 路由名称(驼峰格式) + """ + router_name = name if name else path + return router_name.capitalize() + + @classmethod + def get_router_path(cls, menu: MenuTreeModel): + """ + 获取路由地址 + + :param menu: 菜单数对象 + :return: 路由地址 + """ + # 内链打开外网方式 + router_path = menu.path + if menu.parent_id != 0 and cls.is_inner_link(menu): + router_path = cls.inner_link_replace_each(router_path) + # 非外链并且是一级目录(类型为目录) + if menu.parent_id == 0 and menu.menu_type == MenuConstant.TYPE_DIR and menu.is_frame == MenuConstant.NO_FRAME: + router_path = f'/{menu.path}' + # 非外链并且是一级目录(类型为菜单) + elif cls.is_menu_frame(menu): + router_path = '/' + return router_path + + @classmethod + def get_component(cls, menu: MenuTreeModel): + """ + 获取组件信息 + + :param menu: 菜单数对象 + :return: 组件信息 + """ + component = MenuConstant.LAYOUT + if menu.component and not cls.is_menu_frame(menu): + component = menu.component + elif (menu.component is None or menu.component == '') and menu.parent_id != 0 and cls.is_inner_link(menu): + component = MenuConstant.INNER_LINK + elif (menu.component is None or menu.component == '') and cls.is_parent_view(menu): + component = MenuConstant.PARENT_VIEW + return component + + @classmethod + def is_menu_frame(cls, menu: MenuTreeModel): + """ + 判断是否为菜单内部跳转 + + :param menu: 菜单数对象 + :return: 是否为菜单内部跳转 + """ + return ( + menu.parent_id == 0 and menu.menu_type == MenuConstant.TYPE_MENU and menu.is_frame == MenuConstant.NO_FRAME + ) + + @classmethod + def is_inner_link(cls, menu: MenuTreeModel): + """ + 判断是否为内链组件 + + :param menu: 菜单数对象 + :return: 是否为内链组件 + """ + return menu.is_frame == MenuConstant.NO_FRAME and cls.is_http(menu.path) + + @classmethod + def is_parent_view(cls, menu: MenuTreeModel): + """ + 判断是否为parent_view组件 + + :param menu: 菜单数对象 + :return: 是否为parent_view组件 + """ + return menu.parent_id != 0 and menu.menu_type == MenuConstant.TYPE_DIR + + @classmethod + def is_http(cls, link: str): + """ + 判断是否为http(s)://开头 + + :param link: 链接 + :return: 是否为http(s)://开头 + """ + return link.startswith(CommonConstant.HTTP) or link.startswith(CommonConstant.HTTPS) + + @classmethod + def inner_link_replace_each(cls, path: str): + """ + 内链域名特殊字符替换 + + :param path: 内链域名 + :return: 替换后的内链域名 + """ + old_values = [CommonConstant.HTTP, CommonConstant.HTTPS, CommonConstant.WWW, '.', ':'] + new_values = ['', '', '', '/', '/'] + for old, new in zip(old_values, new_values): + path = path.replace(old, new) + return path diff --git a/ruoyi-fastapi-backend/module_admin/service/menu_service.py b/ruoyi-fastapi-backend/module_admin/service/menu_service.py new file mode 100644 index 0000000..cb6368a --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/menu_service.py @@ -0,0 +1,215 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from typing import Optional +from config.constant import CommonConstant, MenuConstant +from exceptions.exception import ServiceException, ServiceWarning +from module_admin.dao.menu_dao import MenuDao +from module_admin.dao.role_dao import RoleDao +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.entity.vo.menu_vo import DeleteMenuModel, MenuQueryModel, MenuModel +from module_admin.entity.vo.role_vo import RoleMenuQueryModel +from module_admin.entity.vo.user_vo import CurrentUserModel +from utils.common_util import CamelCaseUtil +from utils.string_util import StringUtil + + +class MenuService: + """ + 菜单管理模块服务层 + """ + + @classmethod + async def get_menu_tree_services(cls, query_db: AsyncSession, current_user: Optional[CurrentUserModel] = None): + """ + 获取菜单树信息service + + :param query_db: orm对象 + :param current_user: 当前用户对象 + :return: 菜单树信息对象 + """ + menu_list_result = await MenuDao.get_menu_list_for_tree( + query_db, current_user.user.user_id, current_user.user.role + ) + menu_tree_result = cls.list_to_tree(menu_list_result) + + return menu_tree_result + + @classmethod + async def get_role_menu_tree_services( + cls, query_db: AsyncSession, role_id: int, current_user: Optional[CurrentUserModel] = None + ): + """ + 根据角色id获取菜单树信息service + + :param query_db: orm对象 + :param role_id: 角色id + :param current_user: 当前用户对象 + :return: 当前角色id的菜单树信息对象 + """ + menu_list_result = await MenuDao.get_menu_list_for_tree( + query_db, current_user.user.user_id, current_user.user.role + ) + menu_tree_result = cls.list_to_tree(menu_list_result) + role = await RoleDao.get_role_detail_by_id(query_db, role_id) + role_menu_list = await RoleDao.get_role_menu_dao(query_db, role) + checked_keys = [row.menu_id for row in role_menu_list] + result = RoleMenuQueryModel(menus=menu_tree_result, checkedKeys=checked_keys) + + return result + + @classmethod + async def get_menu_list_services( + cls, query_db: AsyncSession, page_object: MenuQueryModel, current_user: Optional[CurrentUserModel] = None + ): + """ + 获取菜单列表信息service + + :param query_db: orm对象 + :param page_object: 分页查询参数对象 + :param current_user: 当前用户对象 + :return: 菜单列表信息对象 + """ + menu_list_result = await MenuDao.get_menu_list( + query_db, page_object, current_user.user.user_id, current_user.user.role + ) + + return CamelCaseUtil.transform_result(menu_list_result) + + @classmethod + async def check_menu_name_unique_services(cls, query_db: AsyncSession, page_object: MenuModel): + """ + 校验菜单名称是否唯一service + + :param query_db: orm对象 + :param page_object: 菜单对象 + :return: 校验结果 + """ + menu_id = -1 if page_object.menu_id is None else page_object.menu_id + menu = await MenuDao.get_menu_detail_by_info(query_db, MenuModel(menuName=page_object.menu_name)) + if menu and menu.menu_id != menu_id: + return CommonConstant.NOT_UNIQUE + return CommonConstant.UNIQUE + + @classmethod + async def add_menu_services(cls, query_db: AsyncSession, page_object: MenuModel): + """ + 新增菜单信息service + + :param query_db: orm对象 + :param page_object: 新增菜单对象 + :return: 新增菜单校验结果 + """ + if not await cls.check_menu_name_unique_services(query_db, page_object): + raise ServiceException(message=f'新增菜单{page_object.menu_name}失败,菜单名称已存在') + elif page_object.is_frame == MenuConstant.YES_FRAME and not StringUtil.is_http(page_object.path): + raise ServiceException(message=f'新增菜单{page_object.menu_name}失败,地址必须以http(s)://开头') + else: + try: + await MenuDao.add_menu_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_menu_services(cls, query_db: AsyncSession, page_object: MenuModel): + """ + 编辑菜单信息service + + :param query_db: orm对象 + :param page_object: 编辑部门对象 + :return: 编辑菜单校验结果 + """ + edit_menu = page_object.model_dump(exclude_unset=True) + menu_info = await cls.menu_detail_services(query_db, page_object.menu_id) + if menu_info.menu_id: + if not await cls.check_menu_name_unique_services(query_db, page_object): + raise ServiceException(message=f'修改菜单{page_object.menu_name}失败,菜单名称已存在') + elif page_object.is_frame == MenuConstant.YES_FRAME and not StringUtil.is_http(page_object.path): + raise ServiceException(message=f'修改菜单{page_object.menu_name}失败,地址必须以http(s)://开头') + elif page_object.menu_id == page_object.parent_id: + raise ServiceException(message=f'修改菜单{page_object.menu_name}失败,上级菜单不能选择自己') + else: + try: + await MenuDao.edit_menu_dao(query_db, edit_menu) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='菜单不存在') + + @classmethod + async def delete_menu_services(cls, query_db: AsyncSession, page_object: DeleteMenuModel): + """ + 删除菜单信息service + + :param query_db: orm对象 + :param page_object: 删除菜单对象 + :return: 删除菜单校验结果 + """ + if page_object.menu_ids: + menu_id_list = page_object.menu_ids.split(',') + try: + for menu_id in menu_id_list: + if (await MenuDao.has_child_by_menu_id_dao(query_db, int(menu_id))) > 0: + raise ServiceWarning(message='存在子菜单,不允许删除') + elif (await MenuDao.check_menu_exist_role_dao(query_db, int(menu_id))) > 0: + raise ServiceWarning(message='菜单已分配,不允许删除') + await MenuDao.delete_menu_dao(query_db, MenuModel(menuId=menu_id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入菜单id为空') + + @classmethod + async def menu_detail_services(cls, query_db: AsyncSession, menu_id: int): + """ + 获取菜单详细信息service + + :param query_db: orm对象 + :param menu_id: 菜单id + :return: 菜单id对应的信息 + """ + menu = await MenuDao.get_menu_detail_by_id(query_db, menu_id=menu_id) + if menu: + result = MenuModel(**CamelCaseUtil.transform_result(menu)) + else: + result = MenuModel(**dict()) + + return result + + @classmethod + def list_to_tree(cls, permission_list: list) -> list: + """ + 工具方法:根据菜单列表信息生成树形嵌套数据 + + :param permission_list: 菜单列表信息 + :return: 菜单树形嵌套数据 + """ + permission_list = [ + dict(id=item.menu_id, label=item.menu_name, parentId=item.parent_id) for item in permission_list + ] + # 转成id为key的字典 + mapping: dict = dict(zip([i['id'] for i in permission_list], permission_list)) + + # 树容器 + container: list = [] + + for d in permission_list: + # 如果找不到父级项,则是根节点 + parent: dict = mapping.get(d['parentId']) + if parent is None: + container.append(d) + else: + children: list = parent.get('children') + if not children: + children = [] + children.append(d) + parent.update({'children': children}) + + return container diff --git a/ruoyi-fastapi-backend/module_admin/service/message_service.py b/ruoyi-fastapi-backend/module_admin/service/message_service.py new file mode 100644 index 0000000..023d698 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/message_service.py @@ -0,0 +1,181 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.dao.message_dao import MessageDao +from module_admin.entity.vo.message_vo import DeleteMessageModel, MessageModel, MessagePageQueryModel, EditMessageModel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil + + +class MessageService: + """ + 系统消息模块服务层 + """ + # 获取告警消息列表 + @classmethod + async def get_alert_message_services(cls, query_db: AsyncSession, pageNum, pageSize): + t = MessagePageQueryModel() + t.type = '0' + t.page_num = pageNum + t.page_size = pageSize + return await MessageDao.get_message_list(query_db, t, True) + + + # 获取告警消息分布 + @classmethod + async def get_alarm_type_distribution_services(cls, query_db: AsyncSession): + result = await MessageDao.get_alarm_type_distribution(query_db) + # d = dict() + # t = 0 + # for k, v in result.items(): + # d[k] = v + # t += v + # for k, v in d.items(): + # result[k] = round(v/t, 2) + return result + + @classmethod + async def handle_all_message_services(cls,query_db: AsyncSession): + + try: + await MessageDao.handle_all_message(query_db) + await query_db.commit() + return CrudResponseModel(is_success=True, message='处理成功') + except Exception as e: + await query_db.rollback() + raise e + + + + @classmethod + async def get_message_list_services( + cls, query_db: AsyncSession, query_object: MessagePageQueryModel, is_page: bool = False + ): + """ + 获取系统消息列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 系统消息列表信息对象 + """ + message_list_result = await MessageDao.get_message_list(query_db, query_object, is_page) + + return message_list_result + + @classmethod + async def count_message(cls, query_db: AsyncSession,): + """ + 获取系统消息数量service + + :param query_db: orm对象 + :return: + total 总消息数 + unhandled 未处理消息数 + handled 已处理消息数 + """ + total, unhandled, handled = await MessageDao.count_message(query_db) + return {'total': total, 'unhandled': unhandled, 'handled': handled} + + @classmethod + async def add_message_services(cls, query_db: AsyncSession, page_object: MessageModel): + """ + 新增系统消息信息service + + :param query_db: orm对象 + :param page_object: 新增系统消息对象 + :return: 新增系统消息校验结果 + """ + try: + await MessageDao.add_message_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_message_services(cls, query_db: AsyncSession, page_object: EditMessageModel): + """ + 编辑系统消息信息service + + :param query_db: orm对象 + :param page_object: 编辑系统消息对象 + :return: 编辑系统消息校验结果 + """ + edit_message = page_object.model_dump(exclude_unset=True, exclude={'create_time', 'create_by', }) + message_info = await cls.message_detail_services(query_db, page_object.message_id) + if message_info.message_id: + try: + await MessageDao.edit_message_dao(query_db, edit_message) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='系统消息不存在') + + @classmethod + async def delete_message_services(cls, query_db: AsyncSession, page_object: DeleteMessageModel): + """ + 删除系统消息信息service + + :param query_db: orm对象 + :param page_object: 删除系统消息对象 + :return: 删除系统消息校验结果 + """ + if page_object.message_ids: + message_id_list = page_object.message_ids.split(',') + try: + for message_id in message_id_list: + await MessageDao.delete_message_dao(query_db, MessageModel(messageId=message_id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入消息主键为空') + + @classmethod + async def message_detail_services(cls, query_db: AsyncSession, message_id: int): + """ + 获取系统消息详细信息service + + :param query_db: orm对象 + :param message_id: 消息主键 + :return: 消息主键对应的信息 + """ + message = await MessageDao.get_message_detail_by_id(query_db, message_id=message_id) + if message: + result = MessageModel(**CamelCaseUtil.transform_result(message)) + else: + result = MessageModel(**dict()) + + return result + + @staticmethod + async def export_message_list_services(message_list: List): + """ + 导出系统消息信息service + + :param message_list: 系统消息信息列表 + :return: 系统消息信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'messageId': '消息主键', + 'type': '类型', + 'status': '状态', + 'detail': '消息描述', + 'createTime': '创建时间', + 'createBy': '创建者', + 'updateTime': '更新时间', + 'updateBy': '更新者', + } + binary_data = ExcelUtil.export_list2excel(message_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/notice_service.py b/ruoyi-fastapi-backend/module_admin/service/notice_service.py new file mode 100644 index 0000000..4671703 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/notice_service.py @@ -0,0 +1,128 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.dao.notice_dao import NoticeDao +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.entity.vo.notice_vo import DeleteNoticeModel, NoticeModel, NoticePageQueryModel +from utils.common_util import CamelCaseUtil + + +class NoticeService: + """ + 通知公告管理模块服务层 + """ + + @classmethod + async def get_notice_list_services( + cls, query_db: AsyncSession, query_object: NoticePageQueryModel, is_page: bool = True + ): + """ + 获取通知公告列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 通知公告列表信息对象 + """ + notice_list_result = await NoticeDao.get_notice_list(query_db, query_object, is_page) + + return notice_list_result + + @classmethod + async def check_notice_unique_services(cls, query_db: AsyncSession, page_object: NoticeModel): + """ + 校验通知公告是否存在service + + :param query_db: orm对象 + :param page_object: 通知公告对象 + :return: 校验结果 + """ + notice_id = -1 if page_object.notice_id is None else page_object.notice_id + notice = await NoticeDao.get_notice_detail_by_info(query_db, page_object) + if notice and notice.notice_id != notice_id: + return CommonConstant.NOT_UNIQUE + return CommonConstant.UNIQUE + + @classmethod + async def add_notice_services(cls, query_db: AsyncSession, page_object: NoticeModel): + """ + 新增通知公告信息service + + :param query_db: orm对象 + :param page_object: 新增通知公告对象 + :return: 新增通知公告校验结果 + """ + if not await cls.check_notice_unique_services(query_db, page_object): + raise ServiceException(message=f'新增通知公告{page_object.notice_title}失败,通知公告已存在') + else: + try: + await NoticeDao.add_notice_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_notice_services(cls, query_db: AsyncSession, page_object: NoticeModel): + """ + 编辑通知公告信息service + + :param query_db: orm对象 + :param page_object: 编辑通知公告对象 + :return: 编辑通知公告校验结果 + """ + edit_notice = page_object.model_dump(exclude_unset=True) + notice_info = await cls.notice_detail_services(query_db, page_object.notice_id) + if notice_info.notice_id: + if not await cls.check_notice_unique_services(query_db, page_object): + raise ServiceException(message=f'修改通知公告{page_object.notice_title}失败,通知公告已存在') + else: + try: + await NoticeDao.edit_notice_dao(query_db, edit_notice) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='通知公告不存在') + + @classmethod + async def delete_notice_services(cls, query_db: AsyncSession, page_object: DeleteNoticeModel): + """ + 删除通知公告信息service + + :param query_db: orm对象 + :param page_object: 删除通知公告对象 + :return: 删除通知公告校验结果 + """ + if page_object.notice_ids: + notice_id_list = page_object.notice_ids.split(',') + try: + for notice_id in notice_id_list: + await NoticeDao.delete_notice_dao(query_db, NoticeModel(noticeId=notice_id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入通知公告id为空') + + @classmethod + async def notice_detail_services(cls, query_db: AsyncSession, notice_id: int): + """ + 获取通知公告详细信息service + + :param query_db: orm对象 + :param notice_id: 通知公告id + :return: 通知公告id对应的信息 + """ + notice = await NoticeDao.get_notice_detail_by_id(query_db, notice_id=notice_id) + if notice: + result = NoticeModel(**CamelCaseUtil.transform_result(notice)) + else: + result = NoticeModel(**dict()) + + return result diff --git a/ruoyi-fastapi-backend/module_admin/service/online_service.py b/ruoyi-fastapi-backend/module_admin/service/online_service.py new file mode 100644 index 0000000..56742b0 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/online_service.py @@ -0,0 +1,76 @@ +import jwt +from fastapi import Request +from config.enums import RedisInitKeyConfig +from config.env import JwtConfig +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.entity.vo.online_vo import DeleteOnlineModel, OnlineQueryModel +from utils.common_util import CamelCaseUtil + + +class OnlineService: + """ + 在线用户管理模块服务层 + """ + + @classmethod + async def get_online_list_services(cls, request: Request, query_object: OnlineQueryModel): + """ + 获取在线用户表信息service + + :param request: Request对象 + :param query_object: 查询参数对象 + :return: 在线用户列表信息 + """ + access_token_keys = await request.app.state.redis.keys(f'{RedisInitKeyConfig.ACCESS_TOKEN.key}*') + if not access_token_keys: + access_token_keys = [] + access_token_values_list = [await request.app.state.redis.get(key) for key in access_token_keys] + online_info_list = [] + for item in access_token_values_list: + payload = jwt.decode(item, JwtConfig.jwt_secret_key, algorithms=[JwtConfig.jwt_algorithm]) + online_dict = dict( + token_id=payload.get('session_id'), + user_name=payload.get('user_name'), + dept_name=payload.get('dept_name'), + ipaddr=payload.get('login_info').get('ipaddr'), + login_location=payload.get('login_info').get('loginLocation'), + browser=payload.get('login_info').get('browser'), + os=payload.get('login_info').get('os'), + login_time=payload.get('login_info').get('loginTime'), + ) + if query_object.user_name and not query_object.ipaddr: + if query_object.user_name == payload.get('user_name'): + online_info_list = [online_dict] + break + elif not query_object.user_name and query_object.ipaddr: + if query_object.ipaddr == payload.get('login_info').get('ipaddr'): + online_info_list = [online_dict] + break + elif query_object.user_name and query_object.ipaddr: + if query_object.user_name == payload.get('user_name') and query_object.ipaddr == payload.get( + 'login_info' + ).get('ipaddr'): + online_info_list = [online_dict] + break + else: + online_info_list.append(online_dict) + + return CamelCaseUtil.transform_result(online_info_list) + + @classmethod + async def delete_online_services(cls, request: Request, page_object: DeleteOnlineModel): + """ + 强退在线用户信息service + + :param request: Request对象 + :param page_object: 强退在线用户对象 + :return: 强退在线用户校验结果 + """ + if page_object.token_ids: + token_id_list = page_object.token_ids.split(',') + for token_id in token_id_list: + await request.app.state.redis.delete(f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{token_id}') + return CrudResponseModel(is_success=True, message='强退成功') + else: + raise ServiceException(message='传入session_id为空') diff --git a/ruoyi-fastapi-backend/module_admin/service/post_service.py b/ruoyi-fastapi-backend/module_admin/service/post_service.py new file mode 100644 index 0000000..9338a9f --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/post_service.py @@ -0,0 +1,183 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.dao.post_dao import PostDao +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.entity.vo.post_vo import DeletePostModel, PostModel, PostPageQueryModel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil + + +class PostService: + """ + 岗位管理模块服务层 + """ + + @classmethod + async def get_post_list_services( + cls, query_db: AsyncSession, query_object: PostPageQueryModel, is_page: bool = False + ): + """ + 获取岗位列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 岗位列表信息对象 + """ + post_list_result = await PostDao.get_post_list(query_db, query_object, is_page) + + return post_list_result + + @classmethod + async def check_post_name_unique_services(cls, query_db: AsyncSession, page_object: PostModel): + """ + 检查岗位名称是否唯一service + + :param query_db: orm对象 + :param page_object: 岗位对象 + :return: 校验结果 + """ + post_id = -1 if page_object.post_id is None else page_object.post_id + post = await PostDao.get_post_detail_by_info(query_db, PostModel(postName=page_object.post_name)) + if post and post.post_id != post_id: + return CommonConstant.NOT_UNIQUE + return CommonConstant.UNIQUE + + @classmethod + async def check_post_code_unique_services(cls, query_db: AsyncSession, page_object: PostModel): + """ + 检查岗位编码是否唯一service + + :param query_db: orm对象 + :param page_object: 岗位对象 + :return: 校验结果 + """ + post_id = -1 if page_object.post_id is None else page_object.post_id + post = await PostDao.get_post_detail_by_info(query_db, PostModel(postCode=page_object.post_code)) + if post and post.post_id != post_id: + return CommonConstant.NOT_UNIQUE + return CommonConstant.UNIQUE + + @classmethod + async def add_post_services(cls, query_db: AsyncSession, page_object: PostModel): + """ + 新增岗位信息service + + :param query_db: orm对象 + :param page_object: 新增岗位对象 + :return: 新增岗位校验结果 + """ + if not await cls.check_post_name_unique_services(query_db, page_object): + raise ServiceException(message=f'新增岗位{page_object.post_name}失败,岗位名称已存在') + elif not await cls.check_post_code_unique_services(query_db, page_object): + raise ServiceException(message=f'新增岗位{page_object.post_name}失败,岗位编码已存在') + else: + try: + await PostDao.add_post_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_post_services(cls, query_db: AsyncSession, page_object: PostModel): + """ + 编辑岗位信息service + + :param query_db: orm对象 + :param page_object: 编辑岗位对象 + :return: 编辑岗位校验结果 + """ + edit_post = page_object.model_dump(exclude_unset=True) + post_info = await cls.post_detail_services(query_db, page_object.post_id) + if post_info.post_id: + if not await cls.check_post_name_unique_services(query_db, page_object): + raise ServiceException(message=f'修改岗位{page_object.post_name}失败,岗位名称已存在') + elif not await cls.check_post_code_unique_services(query_db, page_object): + raise ServiceException(message=f'修改岗位{page_object.post_name}失败,岗位编码已存在') + else: + try: + await PostDao.edit_post_dao(query_db, edit_post) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='岗位不存在') + + @classmethod + async def delete_post_services(cls, query_db: AsyncSession, page_object: DeletePostModel): + """ + 删除岗位信息service + + :param query_db: orm对象 + :param page_object: 删除岗位对象 + :return: 删除岗位校验结果 + """ + if page_object.post_ids: + post_id_list = page_object.post_ids.split(',') + try: + for post_id in post_id_list: + post = await cls.post_detail_services(query_db, int(post_id)) + if (await PostDao.count_user_post_dao(query_db, int(post_id))) > 0: + raise ServiceException(message=f'{post.post_name}已分配,不能删除') + await PostDao.delete_post_dao(query_db, PostModel(postId=post_id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入岗位id为空') + + @classmethod + async def post_detail_services(cls, query_db: AsyncSession, post_id: int): + """ + 获取岗位详细信息service + + :param query_db: orm对象 + :param post_id: 岗位id + :return: 岗位id对应的信息 + """ + post = await PostDao.get_post_detail_by_id(query_db, post_id=post_id) + if post: + result = PostModel(**CamelCaseUtil.transform_result(post)) + else: + result = PostModel(**dict()) + + return result + + @staticmethod + async def export_post_list_services(post_list: List): + """ + 导出岗位信息service + + :param post_list: 岗位信息列表 + :return: 岗位信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'postId': '岗位编号', + 'postCode': '岗位编码', + 'postName': '岗位名称', + 'postSort': '显示顺序', + 'status': '状态', + 'createBy': '创建者', + 'createTime': '创建时间', + 'updateBy': '更新者', + 'updateTime': '更新时间', + 'remark': '备注', + } + + for item in post_list: + if item.get('status') == '0': + item['status'] = '正常' + else: + item['status'] = '停用' + binary_data = ExcelUtil.export_list2excel(post_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/ragflow_service.py b/ruoyi-fastapi-backend/module_admin/service/ragflow_service.py new file mode 100644 index 0000000..32255ad --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/ragflow_service.py @@ -0,0 +1,154 @@ +from module_admin.entity.vo.ragflow_vo import ( + RagflowListQueryModel, + ListDocumentsQueryModel, + UpdateFileModel, + DeleteFileModel, + CreateDatasetModel, + DocumentIdsModel, + UpdateChatAssistantModel, + CreateSessionWithChatModel, + ConverseWithChatAssistantModel +) +from utils.ragflow_util import RAGFlowClient +from config.env import RAGFlowConfig +from utils.ragflow_client_manager import get_ragflow_client + + +class RAGFlowService: + """ + RAGFlow服务 - 简化版本,使用同步操作 + """ + + # 获取数据集列表 + @classmethod + def get_ragflow_dataset_list_services(cls, query_db, rage_flow_query: RagflowListQueryModel): + """ + 获取数据集列表 - 同步版本 + """ + client = RAGFlowClient(RAGFlowConfig.RAGFLOW_BASE_URL, RAGFlowConfig.RAGFLOW_API_KEY) + result = client.list_datasets(**(rage_flow_query.model_dump())) + return result + + # 创建数据集 + @classmethod + def create_dataset_services(cls, create_dataset_params: CreateDatasetModel): + """创建数据集 - 同步版本""" + client = RAGFlowClient(RAGFlowConfig.RAGFLOW_BASE_URL, RAGFlowConfig.RAGFLOW_API_KEY) + result = client.create_dataset(**(create_dataset_params.model_dump())) + return result + + # 更新数据集 + @classmethod + def update_dataset_services(cls, dataset_id: str, update_dataset_params: CreateDatasetModel): + """更新数据集信息 - 同步版本""" + client = RAGFlowClient(RAGFlowConfig.RAGFLOW_BASE_URL, RAGFlowConfig.RAGFLOW_API_KEY) + result = client.update_dataset(dataset_id=dataset_id, **(update_dataset_params.model_dump())) + return result + + # 获取数据集中文档列表 + @classmethod + def list_documents_services(cls, query_db, dataset_id: str, list_documents_query: ListDocumentsQueryModel): + """获取文档列表 - 同步版本""" + client = RAGFlowClient(RAGFlowConfig.RAGFLOW_BASE_URL, RAGFlowConfig.RAGFLOW_API_KEY) + result = client.list_documents(dataset_id=dataset_id, **(list_documents_query.model_dump())) + return result + + # 上传文档到数据集 + @classmethod + def upload_file_dataset_services(cls, dataset_id: str, files): + """上传文档 - 同步版本""" + client = RAGFlowClient(RAGFlowConfig.RAGFLOW_BASE_URL, RAGFlowConfig.RAGFLOW_API_KEY) + result = client.upload_documents_bytes(dataset_id=dataset_id, file_bytes=files) + return result + + # 开始解析文档 + @classmethod + def parse_documents_services(cls, dataset_id: str, parse_params: DocumentIdsModel): + """解析文档 - 同步版本""" + client = RAGFlowClient(RAGFlowConfig.RAGFLOW_BASE_URL, RAGFlowConfig.RAGFLOW_API_KEY) + result = client.parse_documents(dataset_id=dataset_id, document_ids=parse_params.documnet_ids) + return result + + # 停止解析文档 + @classmethod + def stop_parse_documents_services(cls, dataset_id: str, parse_params: DocumentIdsModel): + """停止解析文档 - 同步版本""" + client = RAGFlowClient(RAGFlowConfig.RAGFLOW_BASE_URL, RAGFlowConfig.RAGFLOW_API_KEY) + result = client.stop_parsing_documents(dataset_id=dataset_id, document_ids=parse_params.documnet_ids) + return result + + # 更新文档内容 + @classmethod + def update_file_dataset_services(cls, dataset_id: str, document_id: str, update_params: UpdateFileModel): + """更新文档 - 同步版本""" + client = RAGFlowClient(RAGFlowConfig.RAGFLOW_BASE_URL, RAGFlowConfig.RAGFLOW_API_KEY) + result = client.update_document(dataset_id=dataset_id, document_id=document_id, **(update_params.model_dump())) + return result + + # 删除文档 + @classmethod + def delete_file_services(cls, dataset_id: str, delete_params: DeleteFileModel): + """删除文档 - 同步版本""" + client = RAGFlowClient(RAGFlowConfig.RAGFLOW_BASE_URL, RAGFlowConfig.RAGFLOW_API_KEY) + result = client.delete_documents(dataset_id=dataset_id, **(delete_params.model_dump())) + return result + + # 删除数据集 + @classmethod + def delete_datasets_services(cls, delete_params: DeleteFileModel): + """删除数据集 - 同步版本""" + client = RAGFlowClient(RAGFlowConfig.RAGFLOW_BASE_URL, RAGFlowConfig.RAGFLOW_API_KEY) + result = client.delete_datasets(**(delete_params.model_dump())) + return result + + # 查看聊天助手列表 + @classmethod + def get_chat_assistant_list_services(cls, query_params: RagflowListQueryModel): + """获取聊天助手列表 - 同步版本""" + client = RAGFlowClient(RAGFlowConfig.RAGFLOW_BASE_URL, RAGFlowConfig.RAGFLOW_API_KEY) + result = client.list_chat_assistants(**(query_params.model_dump())) + return result + + # 修改聊天助手 + @classmethod + def update_chat_assistant_services(cls, update_params: UpdateChatAssistantModel): + """更新聊天助手 - 同步版本""" + client = RAGFlowClient(RAGFlowConfig.RAGFLOW_BASE_URL, RAGFlowConfig.RAGFLOW_API_KEY) + result = client.update_chat_assistant(**(update_params.model_dump())) + return result + + # 创建助手会话 + @classmethod + def create_session_with_chat_services(cls, create_params: CreateSessionWithChatModel): + """创建会话 - 同步版本""" + client = RAGFlowClient(RAGFlowConfig.RAGFLOW_BASE_URL, RAGFlowConfig.RAGFLOW_API_KEY) + result = client.create_session_with_chat(**(create_params.model_dump())) + return result + + # 与助手聊天 - 核心方法 + @classmethod + def converse_with_chat_assistant_services(cls, converse_params: ConverseWithChatAssistantModel): + """与聊天助手对话 - 同步版本,返回Generator""" + client = RAGFlowClient(RAGFlowConfig.RAGFLOW_BASE_URL, RAGFlowConfig.RAGFLOW_API_KEY) + # 直接返回Generator,支持流式响应 + return client.converse_with_chat_assistant( + chat_id=converse_params.chat_id, + question=converse_params.question, + stream=converse_params.stream, + session_id=converse_params.session_id + ) + + # 与助手聊天 (OpenAI Compatible) - 同步版本 + @classmethod + def converse_with_chat_assistant_services_openai(cls, converse_params: ConverseWithChatAssistantModel): + """OpenAI兼容格式 - 同步版本""" + client = RAGFlowClient(RAGFlowConfig.RAGFLOW_BASE_URL, RAGFlowConfig.RAGFLOW_API_KEY) + # Construct messages list for OpenAI format + messages = [{"role": "user", "content": converse_params.question}] + # Uses defaults for model name as per user indication "server will parse this automatically" + return client.create_chat_completion( + chat_id=converse_params.chat_id, + model="ragflow", + messages=messages, + stream=converse_params.stream + ) \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_admin/service/robot_action_service.py b/ruoyi-fastapi-backend/module_admin/service/robot_action_service.py new file mode 100644 index 0000000..254b974 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/robot_action_service.py @@ -0,0 +1,132 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.dao.robot_action_dao import Robot_actionDao +from module_admin.entity.vo.robot_action_vo import DeleteRobot_actionModel, Robot_actionModel, Robot_actionPageQueryModel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil + + +class Robot_actionService: + """ + 机器人动作模块服务层 + """ + + @classmethod + async def get_robot_action_list_services( + cls, query_db: AsyncSession, query_object: Robot_actionPageQueryModel, is_page: bool = False + ): + """ + 获取机器人动作列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 机器人动作列表信息对象 + """ + robot_action_list_result = await Robot_actionDao.get_robot_action_list(query_db, query_object, is_page) + + return robot_action_list_result + + + @classmethod + async def add_robot_action_services(cls, query_db: AsyncSession, page_object: Robot_actionModel): + """ + 新增机器人动作信息service + + :param query_db: orm对象 + :param page_object: 新增机器人动作对象 + :return: 新增机器人动作校验结果 + """ + try: + await Robot_actionDao.add_robot_action_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_robot_action_services(cls, query_db: AsyncSession, page_object: Robot_actionModel): + """ + 编辑机器人动作信息service + + :param query_db: orm对象 + :param page_object: 编辑机器人动作对象 + :return: 编辑机器人动作校验结果 + """ + edit_robot_action = page_object.model_dump(exclude_unset=True, exclude={}) + robot_action_info = await cls.robot_action_detail_services(query_db, page_object.id) + if robot_action_info.id: + try: + await Robot_actionDao.edit_robot_action_dao(query_db, edit_robot_action) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='机器人动作不存在') + + @classmethod + async def delete_robot_action_services(cls, query_db: AsyncSession, page_object: DeleteRobot_actionModel): + """ + 删除机器人动作信息service + + :param query_db: orm对象 + :param page_object: 删除机器人动作对象 + :return: 删除机器人动作校验结果 + """ + if page_object.ids: + id_list = page_object.ids.split(',') + try: + for id in id_list: + await Robot_actionDao.delete_robot_action_dao(query_db, Robot_actionModel(id=id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入主键 自增为空') + + @classmethod + async def robot_action_detail_services(cls, query_db: AsyncSession, id: int): + """ + 获取机器人动作详细信息service + + :param query_db: orm对象 + :param id: 主键 自增 + :return: 主键 自增对应的信息 + """ + robot_action = await Robot_actionDao.get_robot_action_detail_by_id(query_db, id=id) + if robot_action: + result = Robot_actionModel(**CamelCaseUtil.transform_result(robot_action)) + else: + result = Robot_actionModel(**dict()) + + return result + + @staticmethod + async def export_robot_action_list_services(robot_action_list: List): + """ + 导出机器人动作信息service + + :param robot_action_list: 机器人动作信息列表 + :return: 机器人动作信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'id': '主键 自增', + 'name': '动作名', + 'description': '描述', + 'intervalMin': '最小时间间隔 单位s', + 'intervalMax': '最大时间间隔 单位s', + 'status': '动作状态 0 未启用, 1已启用', + 'updateTime': '修改时间', + } + binary_data = ExcelUtil.export_list2excel(robot_action_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/robot_role_pairing_service.py b/ruoyi-fastapi-backend/module_admin/service/robot_role_pairing_service.py new file mode 100644 index 0000000..428f26d --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/robot_role_pairing_service.py @@ -0,0 +1,187 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.dao.robot_role_pairing_dao import PairingDao +from module_admin.dao.explanation_style_robot_pair_dao import Explanation_style_robot_pairDao +from module_admin.entity.vo.robot_role_pairing_vo import DeletePairingModel, PairingModel, PairingPageQueryModel, \ + ChangeRobotRoleModel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil + + +class PairingService: + """ + 角色-机器人-映射模块服务层 + """ + + @classmethod + async def change_robot_role_services(cls, query_db: AsyncSession,change_pairing: ChangeRobotRoleModel): + try: + # 现修改原本角色status为0 + print("xxxxxxxxxxxxxxx", change_pairing.robot_id) + await PairingDao.edit_pairing_status(query_db, change_pairing.robot_id, None, '0') + # 再修改新的角色status为1 + print("xxxxxxxxxxxxxxx", change_pairing.pairing_id) + await PairingDao.edit_pairing_status(query_db, None, change_pairing.pairing_id, '1') + + await query_db.commit() + + except Exception as e: + await query_db.rollback() + raise e + @classmethod + async def get_role_pairing_list_services(cls, query_db: AsyncSession, robot_id: int): + pairing_list = await PairingDao.get_role_pairing_list(query_db, robot_id) + + # style_list = list() + style = None + # print(pairing_list) + for pairing in pairing_list: + + if pairing["status"] == '1': + style = await Explanation_style_robot_pairDao.get_robot_role_style(query_db, robot_id, pairing["robot_role_id"]) + # style_list.append(style) + + return pairing_list, style + + @classmethod + async def get_role_pairing_list_services_v1(cls, query_db: AsyncSession, robot_id: int) -> list: + """ 获取机器人角色对齐风格列表 + + Args: + query_db (AsyncSession): db + robot_id (int): 机器人id + + Returns: + list: 机器人角色风格对齐列表 + """ + + pairing_list = await PairingDao.get_role_pairing_list(query_db, robot_id) + + style_list = list() + + for pairing in pairing_list: + + style = await Explanation_style_robot_pairDao.get_robot_role_style( + query_db, robot_id, pairing["robot_role_id"] + ) + style_list.append(style) + + return pairing_list, style_list + + @classmethod + async def get_pairing_list_services( + cls, query_db: AsyncSession, query_object: PairingPageQueryModel, is_page: bool = False + ): + """ + 获取角色-机器人-映射列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 角色-机器人-映射列表信息对象 + """ + pairing_list_result = await PairingDao.get_pairing_list(query_db, query_object, is_page) + + return pairing_list_result + + @classmethod + async def add_pairing_services(cls, query_db: AsyncSession, page_object: PairingModel): + """ + 新增角色-机器人-映射信息service + + :param query_db: orm对象 + :param page_object: 新增角色-机器人-映射对象 + :return: 新增角色-机器人-映射校验结果 + """ + try: + await PairingDao.add_pairing_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_pairing_services(cls, query_db: AsyncSession, page_object: PairingModel): + """ + 编辑角色-机器人-映射信息service + + :param query_db: orm对象 + :param page_object: 编辑角色-机器人-映射对象 + :return: 编辑角色-机器人-映射校验结果 + """ + edit_pairing = page_object.model_dump(exclude_unset=True, exclude={}) + pairing_info = await cls.pairing_detail_services(query_db, page_object.pairing_id) + if pairing_info.pairing_id: + try: + await PairingDao.edit_pairing_dao(query_db, edit_pairing) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='角色-机器人-映射不存在') + + @classmethod + async def delete_pairing_services(cls, query_db: AsyncSession, page_object: DeletePairingModel): + """ + 删除角色-机器人-映射信息service + + :param query_db: orm对象 + :param page_object: 删除角色-机器人-映射对象 + :return: 删除角色-机器人-映射校验结果 + """ + if page_object.pairing_ids: + pairing_id_list = page_object.pairing_ids.split(',') + try: + for pairing_id in pairing_id_list: + await PairingDao.delete_pairing_dao(query_db, PairingModel(pairingId=pairing_id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入主键ID为空') + + @classmethod + async def pairing_detail_services(cls, query_db: AsyncSession, pairing_id: int): + """ + 获取角色-机器人-映射详细信息service + + :param query_db: orm对象 + :param pairing_id: 主键ID + :return: 主键ID对应的信息 + """ + pairing = await PairingDao.get_pairing_detail_by_id(query_db, pairing_id=pairing_id) + if pairing: + result = PairingModel(**CamelCaseUtil.transform_result(pairing)) + else: + result = PairingModel(**dict()) + + return result + + @staticmethod + async def export_pairing_list_services(pairing_list: List): + """ + 导出角色-机器人-映射信息service + + :param pairing_list: 角色-机器人-映射信息列表 + :return: 角色-机器人-映射信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'pairingId': '主键ID', + 'robotRoleId': '机器人角色ID', + 'robotId': '机器人ID', + 'status': '使用情况(0停用,1使用)', + 'updateTime': '更新时间', + 'updateBy': '修改者', + } + binary_data = ExcelUtil.export_list2excel(pairing_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/robot_role_service.py b/ruoyi-fastapi-backend/module_admin/service/robot_role_service.py new file mode 100644 index 0000000..3f64aa6 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/robot_role_service.py @@ -0,0 +1,133 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.dao.role_dao import RoleDao +from module_admin.entity.vo.role_vo import DeleteRoleModel, RoleModel, RolePageQueryModel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil + + +class RoleService: + """ + 系统角色模块服务层 + """ + + @classmethod + async def get_role_list_services( + cls, query_db: AsyncSession, query_object: RolePageQueryModel, is_page: bool = False + ): + """ + 获取系统角色列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 系统角色列表信息对象 + """ + role_list_result = await RoleDao.get_role_list(query_db, query_object, is_page) + + return role_list_result + + + @classmethod + async def add_role_services(cls, query_db: AsyncSession, page_object: RoleModel): + """ + 新增系统角色信息service + + :param query_db: orm对象 + :param page_object: 新增系统角色对象 + :return: 新增系统角色校验结果 + """ + try: + await RoleDao.add_role_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_role_services(cls, query_db: AsyncSession, page_object: RoleModel): + """ + 编辑系统角色信息service + + :param query_db: orm对象 + :param page_object: 编辑系统角色对象 + :return: 编辑系统角色校验结果 + """ + edit_role = page_object.model_dump(exclude_unset=True, exclude={'create_time', 'create_by', }) + role_info = await cls.role_detail_services(query_db, page_object.robot_role_id) + if role_info.robot_role_id: + try: + await RoleDao.edit_role_dao(query_db, edit_role) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='系统角色不存在') + + @classmethod + async def delete_role_services(cls, query_db: AsyncSession, page_object: DeleteRoleModel): + """ + 删除系统角色信息service + + :param query_db: orm对象 + :param page_object: 删除系统角色对象 + :return: 删除系统角色校验结果 + """ + if page_object.robot_role_ids: + robot_role_id_list = page_object.robot_role_ids.split(',') + try: + for robot_role_id in robot_role_id_list: + await RoleDao.delete_role_dao(query_db, RoleModel(robotRoleId=robot_role_id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入机器人角色ID为空') + + @classmethod + async def role_detail_services(cls, query_db: AsyncSession, robot_role_id: int): + """ + 获取系统角色详细信息service + + :param query_db: orm对象 + :param robot_role_id: 机器人角色ID + :return: 机器人角色ID对应的信息 + """ + role = await RoleDao.get_role_detail_by_id(query_db, robot_role_id=robot_role_id) + if role: + result = RoleModel(**CamelCaseUtil.transform_result(role)) + else: + result = RoleModel(**dict()) + + return result + + @staticmethod + async def export_role_list_services(role_list: List): + """ + 导出系统角色信息service + + :param role_list: 系统角色信息列表 + :return: 系统角色信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'robotRoleId': '机器人角色ID', + 'rolename': '角色名称', + 'detail': '角色介绍', + 'createTime': '创建时间', + 'createBy': '创建者', + 'updateTime': '修改时间', + 'updateBy': '修改者', + 'remark': '备注', + } + binary_data = ExcelUtil.export_list2excel(role_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/role_service.py b/ruoyi-fastapi-backend/module_admin/service/role_service.py new file mode 100644 index 0000000..4b633de --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/role_service.py @@ -0,0 +1,356 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.entity.vo.role_vo import ( + AddRoleModel, + DeleteRoleModel, + RoleDeptModel, + RoleDeptQueryModel, + RoleMenuModel, + RoleModel, + RolePageQueryModel, +) +from module_admin.entity.vo.user_vo import UserInfoModel, UserRolePageQueryModel +from module_admin.dao.role_dao import RoleDao +from module_admin.dao.user_dao import UserDao +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil +from utils.page_util import PageResponseModel + + +class RoleService: + """ + 角色管理模块服务层 + """ + + @classmethod + async def get_role_select_option_services(cls, query_db: AsyncSession): + """ + 获取角色列表不分页信息service + + :param query_db: orm对象 + :return: 角色列表不分页信息对象 + """ + role_list_result = await RoleDao.get_role_select_option_dao(query_db) + + return CamelCaseUtil.transform_result(role_list_result) + + @classmethod + async def get_role_dept_tree_services(cls, query_db: AsyncSession, role_id: int): + """ + 根据角色id获取部门树信息service + + :param query_db: orm对象 + :param role_id: 角色id + :return: 当前角色id的部门树信息对象 + """ + role = await cls.role_detail_services(query_db, role_id) + role_dept_list = await RoleDao.get_role_dept_dao(query_db, role) + checked_keys = [row.dept_id for row in role_dept_list] + result = RoleDeptQueryModel(checkedKeys=checked_keys) + + return result + + @classmethod + async def get_role_list_services( + cls, query_db: AsyncSession, query_object: RolePageQueryModel, data_scope_sql: str, is_page: bool = False + ): + """ + 获取角色列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param data_scope_sql: 数据权限对应的查询sql语句 + :param is_page: 是否开启分页 + :return: 角色列表信息对象 + """ + role_list_result = await RoleDao.get_role_list(query_db, query_object, data_scope_sql, is_page) + + return role_list_result + + @classmethod + async def check_role_allowed_services(cls, check_role: RoleModel): + """ + 校验角色是否允许操作service + + :param check_role: 角色信息 + :return: 校验结果 + """ + if check_role.admin: + raise ServiceException(message='不允许操作超级管理员角色') + else: + return CrudResponseModel(is_success=True, message='校验通过') + + @classmethod + async def check_role_data_scope_services(cls, query_db: AsyncSession, role_ids: str, data_scope_sql: str): + """ + 校验角色是否有数据权限service + + :param query_db: orm对象 + :param role_ids: 角色id + :param data_scope_sql: 数据权限对应的查询sql语句 + :return: 校验结果 + """ + role_id_list = role_ids.split(',') if role_ids else [] + if role_id_list: + for role_id in role_id_list: + roles = await RoleDao.get_role_list( + query_db, RolePageQueryModel(roleId=int(role_id)), data_scope_sql, is_page=False + ) + if roles: + continue + else: + raise ServiceException(message='没有权限访问角色数据') + + @classmethod + async def check_role_name_unique_services(cls, query_db: AsyncSession, page_object: RoleModel): + """ + 校验角色名称是否唯一service + + :param query_db: orm对象 + :param page_object: 角色对象 + :return: 校验结果 + """ + role_id = -1 if page_object.role_id is None else page_object.role_id + role = await RoleDao.get_role_by_info(query_db, RoleModel(roleName=page_object.role_name)) + if role and role.role_id != role_id: + return CommonConstant.NOT_UNIQUE + return CommonConstant.UNIQUE + + @classmethod + async def check_role_key_unique_services(cls, query_db: AsyncSession, page_object: RoleModel): + """ + 校验角色权限字符是否唯一service + + :param query_db: orm对象 + :param page_object: 角色对象 + :return: 校验结果 + """ + role_id = -1 if page_object.role_id is None else page_object.role_id + role = await RoleDao.get_role_by_info(query_db, RoleModel(roleKey=page_object.role_key)) + if role and role.role_id != role_id: + return CommonConstant.NOT_UNIQUE + return CommonConstant.UNIQUE + + @classmethod + async def add_role_services(cls, query_db: AsyncSession, page_object: AddRoleModel): + """ + 新增角色信息service + + :param query_db: orm对象 + :param page_object: 新增角色对象 + :return: 新增角色校验结果 + """ + add_role = RoleModel(**page_object.model_dump(by_alias=True)) + if not await cls.check_role_name_unique_services(query_db, page_object): + raise ServiceException(message=f'新增角色{page_object.role_name}失败,角色名称已存在') + elif not await cls.check_role_key_unique_services(query_db, page_object): + raise ServiceException(message=f'新增角色{page_object.role_name}失败,角色权限已存在') + else: + try: + add_result = await RoleDao.add_role_dao(query_db, add_role) + role_id = add_result.role_id + if page_object.menu_ids: + for menu in page_object.menu_ids: + await RoleDao.add_role_menu_dao(query_db, RoleMenuModel(roleId=role_id, menuId=menu)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_role_services(cls, query_db: AsyncSession, page_object: AddRoleModel): + """ + 编辑角色信息service + + :param query_db: orm对象 + :param page_object: 编辑角色对象 + :return: 编辑角色校验结果 + """ + edit_role = page_object.model_dump(exclude_unset=True, exclude={'admin'}) + if page_object.type != 'status': + del edit_role['menu_ids'] + if page_object.type == 'status': + del edit_role['type'] + role_info = await cls.role_detail_services(query_db, edit_role.get('role_id')) + if role_info: + if page_object.type != 'status': + if not await cls.check_role_name_unique_services(query_db, page_object): + raise ServiceException(message=f'修改角色{page_object.role_name}失败,角色名称已存在') + elif not await cls.check_role_key_unique_services(query_db, page_object): + raise ServiceException(message=f'修改角色{page_object.role_name}失败,角色权限已存在') + try: + await RoleDao.edit_role_dao(query_db, edit_role) + if page_object.type != 'status': + await RoleDao.delete_role_menu_dao(query_db, RoleMenuModel(roleId=page_object.role_id)) + if page_object.menu_ids: + for menu in page_object.menu_ids: + await RoleDao.add_role_menu_dao( + query_db, RoleMenuModel(roleId=page_object.role_id, menuId=menu) + ) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='角色不存在') + + @classmethod + async def role_datascope_services(cls, query_db: AsyncSession, page_object: AddRoleModel): + """ + 分配角色数据权限service + + :param query_db: orm对象 + :param page_object: 角色数据权限对象 + :return: 分配角色数据权限结果 + """ + edit_role = page_object.model_dump(exclude_unset=True, exclude={'admin', 'dept_ids'}) + role_info = await cls.role_detail_services(query_db, page_object.role_id) + if role_info.role_id: + try: + await RoleDao.edit_role_dao(query_db, edit_role) + await RoleDao.delete_role_dept_dao(query_db, RoleDeptModel(roleId=page_object.role_id)) + if page_object.dept_ids and page_object.data_scope == '2': + for dept in page_object.dept_ids: + await RoleDao.add_role_dept_dao( + query_db, RoleDeptModel(roleId=page_object.role_id, deptId=dept) + ) + await query_db.commit() + return CrudResponseModel(is_success=True, message='分配成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='角色不存在') + + @classmethod + async def delete_role_services(cls, query_db: AsyncSession, page_object: DeleteRoleModel): + """ + 删除角色信息service + + :param query_db: orm对象 + :param page_object: 删除角色对象 + :return: 删除角色校验结果 + """ + if page_object.role_ids: + role_id_list = page_object.role_ids.split(',') + try: + for role_id in role_id_list: + role = await cls.role_detail_services(query_db, int(role_id)) + if (await RoleDao.count_user_role_dao(query_db, int(role_id))) > 0: + raise ServiceException(message=f'角色{role.role_name}已分配,不能删除') + role_id_dict = dict( + roleId=role_id, updateBy=page_object.update_by, updateTime=page_object.update_time + ) + await RoleDao.delete_role_menu_dao(query_db, RoleMenuModel(**role_id_dict)) + await RoleDao.delete_role_dept_dao(query_db, RoleDeptModel(**role_id_dict)) + await RoleDao.delete_role_dao(query_db, RoleModel(**role_id_dict)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入角色id为空') + + @classmethod + async def role_detail_services(cls, query_db: AsyncSession, role_id: int): + """ + 获取角色详细信息service + + :param query_db: orm对象 + :param role_id: 角色id + :return: 角色id对应的信息 + """ + role = await RoleDao.get_role_detail_by_id(query_db, role_id=role_id) + if role: + result = RoleModel(**CamelCaseUtil.transform_result(role)) + else: + result = RoleModel(**dict()) + + return result + + @staticmethod + async def export_role_list_services(role_list: List): + """ + 导出角色列表信息service + + :param role_list: 角色信息列表 + :return: 角色列表信息对象 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'roleId': '角色编号', + 'roleName': '角色名称', + 'roleKey': '权限字符', + 'roleSort': '显示顺序', + 'status': '状态', + 'createBy': '创建者', + 'createTime': '创建时间', + 'updateBy': '更新者', + 'updateTime': '更新时间', + 'remark': '备注', + } + + for item in role_list: + if item.get('status') == '0': + item['status'] = '正常' + else: + item['status'] = '停用' + binary_data = ExcelUtil.export_list2excel(role_list, mapping_dict) + + return binary_data + + @classmethod + async def get_role_user_allocated_list_services( + cls, query_db: AsyncSession, page_object: UserRolePageQueryModel, data_scope_sql: str, is_page: bool = False + ): + """ + 根据角色id获取已分配用户列表 + + :param query_db: orm对象 + :param page_object: 用户关联角色对象 + :param data_scope_sql: 数据权限对应的查询sql语句 + :param is_page: 是否开启分页 + :return: 已分配用户列表 + """ + query_user_list = await UserDao.get_user_role_allocated_list_by_role_id( + query_db, page_object, data_scope_sql, is_page + ) + allocated_list = PageResponseModel( + **{ + **query_user_list.model_dump(by_alias=True), + 'rows': [UserInfoModel(**row) for row in query_user_list.rows], + } + ) + + return allocated_list + + @classmethod + async def get_role_user_unallocated_list_services( + cls, query_db: AsyncSession, page_object: UserRolePageQueryModel, data_scope_sql: str, is_page: bool = False + ): + """ + 根据角色id获取未分配用户列表 + + :param query_db: orm对象 + :param page_object: 用户关联角色对象 + :param data_scope_sql: 数据权限对应的查询sql语句 + :param is_page: 是否开启分页 + :return: 未分配用户列表 + """ + query_user_list = await UserDao.get_user_role_unallocated_list_by_role_id( + query_db, page_object, data_scope_sql, is_page + ) + unallocated_list = PageResponseModel( + **{ + **query_user_list.model_dump(by_alias=True), + 'rows': [UserInfoModel(**row) for row in query_user_list.rows], + } + ) + + return unallocated_list diff --git a/ruoyi-fastapi-backend/module_admin/service/search_service.py b/ruoyi-fastapi-backend/module_admin/service/search_service.py new file mode 100644 index 0000000..ec6e2f7 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/search_service.py @@ -0,0 +1,426 @@ + +import json +import hashlib +import re +import time +import httpx +from datetime import datetime +from typing import Any, Dict, Optional +from config.env import SearchConfig +from utils.log_util import logger +from fastapi.responses import StreamingResponse +from utils.response_util import ResponseUtil +from module_admin.entity.vo.ragflow_vo import ConverseWithChatAssistantModel +from module_admin.service.ragflow_service import RAGFlowService + +def format_sse(data: dict, event: str | None = None) -> str: + payload = json.dumps(data, ensure_ascii=False) + prefix = f'event: {event}\n' if event else '' + return f'{prefix}data: {payload}\n\n' + +class SearchServiceError(Exception): + """搜索服务业务异常""" + +class SearchService: + """搜索服务 - 处理联网搜索请求""" + + # 基础搜索关键词 - 用于判断是否应该使用搜索服务 + SEARCH_KEYWORDS = ( + '搜索', '查找', '查询', '搜一下', '帮我查', '帮我搜', '检索', + '最新', '最近', '今日', '今天', '现在', '当前', '昨日', '昨天', + '天气', '温度', '气温', '降水', '下雨', '下雪', '晴', '多云', '阴天', '雾霾', + '新闻', '时事', '资讯', '动态', '报道', '消息', + '发生了什么', '怎么回事', '事件', '事故', '活动', + 'search', 'find', 'query', 'latest', 'recent', 'today', 'now', 'current', + 'weather', 'temperature', 'news', 'events', 'what happened' + ) + + # 领域特定关键词 + DOMAIN_KEYWORDS = { + '科技': ('AI', '人工智能', '区块链', '元宇宙', '量子计算', '5G', '芯片'), + '体育': ('足球', '篮球', '奥运会', '世界杯', 'NBA', '英超', '比分'), + '娱乐': ('电影', '电视剧', '明星', '演唱会', '综艺', '票房'), + '财经': ('股票', '基金', '汇率', '利率', '财经', '股市', '行情'), + '健康': ('疫情', '疫苗', '健康', '医疗', '疾病', '症状'), + '教育': ('考试', '考研', '高考', '录取', '学校', '教育'), + } + + # 否定关键词 - 公司内部相关话题不应触发搜索 + NEGATIVE_KEYWORDS = ( + # 通用否定 + '不要搜索', '不搜索', '无需搜索', '不用搜索', 'don\'t search', 'no search', + # 公司内部话题(基于 static_qa.json) + '厂区', '公司', '康达', '预约', '参观', '成立', '业务', '荣誉', '愿景', + '生产区', '生产基地', '子公司', '分支机构', '管理总部', + '高分子', '新材料', '胶粘剂', '复合材料', + '军工', '航空航天', '电子信息', '半导体', + '制造业', '制造业企业', '高新技术企业', '博士后科研工作站' + ) + + @classmethod + def should_handle(cls, question: Optional[str]) -> bool: + """判断是否应该使用搜索服务处理该问题""" + if not question: + return False + + normalized = question.strip().lower() + if any(neg_k in normalized for neg_k in cls.NEGATIVE_KEYWORDS): + return False + + if any(k in question for k in cls.SEARCH_KEYWORDS) or any(k in normalized for k in cls.SEARCH_KEYWORDS): + return True + + for domain, domain_terms in cls.DOMAIN_KEYWORDS.items(): + has_domain_term = any(term in question for term in domain_terms) or any(term.lower() in normalized for term in domain_terms) + has_time_or_action = any(k in question for k in ('最新', '最近', '今日', '今天', '搜索', '查找', '查询')) or any(k in normalized for k in ('latest', 'recent', 'today', 'search', 'find', 'query')) + if has_domain_term and has_time_or_action: + return True + + time_patterns = r'(\d{4}年|\d{1,2}月\d{1,2}日|昨天|昨日|今天|今日|最近\d+天|最近\d+周|最近\d+月)' + if re.search(time_patterns, question) and any(k in question for k in ('发生', '新闻', '事件', '动态')): + return True + + return False + + @staticmethod + def _simplify_text(text: str) -> str: + """简化文本内容""" + if not text: + return "" + text = re.sub(r'\s+', ' ', text.strip()) + text = re.sub(r'[,,、;;]+', ',', text) + text = re.sub(r'[。.!]+', '。', text) + max_length = 150 + if len(text) > max_length: + matches = list(re.finditer(r'[。.!?]', text[:max_length])) + if matches: + last_end = matches[-1].end() + text = text[:last_end] + "..." + else: + text = text[:max_length] + "..." + return text + + @classmethod + async def get_search_answer(cls, question: str, redis=None) -> Dict[str, Any]: + """获取搜索结果 (Baidu AppBuilder API)""" + # if not SearchConfig.BAIDU_APPBUILDER_API_KEY: + # raise SearchServiceError('搜索服务未配置 BAIDU_APPBUILDER_API_KEY') + + cache_key = None + if redis: + cache_key = cls._build_cache_key(question) + cached = await redis.get(cache_key) + if cached: + try: + logger.info('搜索结果命中缓存: query=%s', question) + return json.loads(cached) + except (TypeError, json.JSONDecodeError): + pass + + if not SearchConfig.BAIDU_APPBUILDER_API_KEY: + logger.error("BAIDU_APPBUILDER_API_KEY is missing in configuration") + raise SearchServiceError('搜索服务未配置 BAIDU_APPBUILDER_API_KEY') + + try: + # Baidu API Request Logic + headers = { + "X-Appbuilder-Authorization": f"Bearer {SearchConfig.BAIDU_APPBUILDER_API_KEY}", + "Content-Type": "application/json" + } + payload_data = { + "messages": [{"role": "user", "content": question}], + "search_source": "baidu_search_v1", # Can be parameterized if needed + "stream": False, + # Other optional parameters can be added here + } + + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.post( + SearchConfig.SEARCH_API_BASE, + headers=headers, + json=payload_data + ) + + if response.status_code != 200: + error_msg = f"HTTP {response.status_code}: {response.text}" + logger.error(f"Baidu API error: {error_msg}") + raise SearchServiceError(f"Baidu API error: {error_msg}") + + result_json = response.json() + # Handle potential non-200 OK business errors if any specific structure exists, though usually status code covers it or there's an error field. + + # Format Response + formatted = cls._format_baidu_response(question, result_json) + + # Cache + if redis and cache_key: + try: + await redis.set( + cache_key, + json.dumps(formatted, ensure_ascii=False), + ex=SearchConfig.SEARCH_CACHE_TTL + ) + except Exception as ex: + logger.debug('缓存搜索结果失败: %s', ex) + + return formatted + + except Exception as exc: + logger.exception('Baidu搜索失败') + raise SearchServiceError(str(exc)) from exc + + + @classmethod + async def handle_search_chat(cls, converse_params: ConverseWithChatAssistantModel, redis=None) -> Any: + """Handle chat request using Search Service + RAGFlow""" + start_time = time.perf_counter() + + try: + # 1. Check Stream + if converse_params.stream: + return StreamingResponse( + cls._stream_baidu_search(converse_params.question), + media_type='text/event-stream' + ) + + # 2. Non-stream: Get Search Answer directly (Bypass RAGFlow) + search_payload = await cls.get_search_answer(converse_params.question, redis=redis) + + # Use the "message" or "answer" from Baidu as the final response + # Format to match RAGFlow response structure if needed, or just return the helpful message + response_data = { + "answer": search_payload.get('message', ''), + "references": search_payload.get('results', []) + } + + logger.info('搜索处理耗时 %.3fs', time.perf_counter() - start_time) + return ResponseUtil.success(data=response_data) + + except SearchServiceError as exc: + logger.warning('搜索服务失败,降级到RAGFlow: %s', exc) + # Fallback to normal flow + # 修复:直接调用同步方法,不再使用await + result = RAGFlowService.converse_with_chat_assistant_services(converse_params) + + if converse_params.stream: + async def stream_response(): + try: + async for chunk in result: + payload = chunk.get('data') if isinstance(chunk, dict) else chunk + if not payload: + continue + body = payload if isinstance(payload, dict) else {'data': payload} + yield format_sse(body) + yield format_sse({'status': 'completed'}, event='end') + except Exception as exc: + logger.exception('ragflow流式对话异常: %s', exc) + yield format_sse({'message': str(exc)}, event='error') + finally: + logger.info('ragflow流式对话耗时 %.3fs', time.perf_counter() - start_time) + + return StreamingResponse(stream_response(), media_type='text/event-stream') + + code = result.get('code', 0) + if code != 0: + msg = result.get('message') or result.get('msg') or '接口异常' + return ResponseUtil.error(msg=msg, data=result.get('data', None)) + return ResponseUtil.success(data=result.get('data', None)) + + @classmethod + async def _stream_baidu_search(cls, question: str): + """Stream Search + Answer using ZhipuAI (GLM-4)""" + # Note: Method name kept for compatibility, but logic switched to ZhipuAI + + # 先回一条提示,避免联网搜索阶段用户感知“卡住” + yield format_sse({'data': '稍等片刻,达达检索调取中'}) + + if not SearchConfig.ZHIPUAI_API_KEY: + yield format_sse({'message': 'Configuration Error: Missing ZHIPUAI_API_KEY. Please set valid API key.'}, event='error') + return + + headers = { + "Authorization": f"Bearer {SearchConfig.ZHIPUAI_API_KEY}", + "Content-Type": "application/json" + } + + # ZhipuAI / GLM-4 Payload with Web Search Tool + payload = { + "model": "glm-4-flash", # Use Flash model for faster speed + "messages": [ + {"role": "user", "content": f"请回答用户问题:{question}\n要求:\n1. 务必保持简练,字数严格控制在100字以内!\n2. 直接输出答案。"} + ], + "tools": [{ + "type": "web_search", + "web_search": { + "enable": True, + "search_query": question, + "search_result": True, + "count": 3 # Limit to 3 results for speed (optional, default is 10) + } + }], + "stream": True + } + + try: + logger.info(f"ZhipuAI Search Request: {question}") + async with httpx.AsyncClient(timeout=60.0) as client: + async with client.stream('POST', SearchConfig.ZHIPUAI_API_BASE, headers=headers, json=payload) as response: + if response.status_code != 200: + error_msg = await response.read() + logger.error(f"ZhipuAI Error: {error_msg}") + yield format_sse({'message': f"Search Error: {response.status_code}"}, event='error') + return + + async for line in response.aiter_lines(): + if not line.strip(): + continue + data_str = line[6:].strip() if line.startswith('data: ') else line + if not data_str or data_str == '[DONE]': + continue + + try: + chunk = json.loads(data_str) + choices = chunk.get('choices', []) + if choices: + delta = choices[0].get('delta', {}) + content = delta.get('content', '') + + # Check for tool calls or references if needed, but GLM-4 usually streams content directly after search + if content: + yield format_sse({'data': content}) + + except json.JSONDecodeError: + pass + + yield format_sse({'status': 'completed'}, event='end') + + except Exception as exc: + logger.exception("ZhipuAI Streaming Failed") + yield format_sse({'message': str(exc)}, event='error') + + @classmethod + async def classify_intent(cls, question: str) -> str: + """ + Classify user intent: 'SEARCH' (Public Internet) vs 'RAG' (Internal KB) + Uses glm-4-flash for <300ms latency. + """ + # 0) 确定性兜底:明确公司内部话题一律走 RAG(避免 LLM 误判导致“走搜索”) + normalized = (question or '').strip().lower() + if any(neg_k in normalized for neg_k in cls.NEGATIVE_KEYWORDS) or any(k in normalized for k in ('康达新材', '康达')): + return 'RAG' + + if not SearchConfig.ZHIPUAI_API_KEY: + # Fallback to simple keyword check if no key + return 'SEARCH' if cls.should_handle(question) else 'RAG' + + headers = { + "Authorization": f"Bearer {SearchConfig.ZHIPUAI_API_KEY}", + "Content-Type": "application/json" + } + + prompt = ( + f"判断用户问题意图:\n{question}\n\n" + "背景信息:我们是'康达新材'(简称'康达')的内部助手。\n" + "分类规则:\n" + "1. RAG (内部知识): \n" + " - 涉及'康达'、'康达新材'、公司历史、架构、产品、内部规章的问题。\n" + " - 询问特定的产品手册、错误代码、内部文档。\n" + "2. SEARCH (公网搜索): \n" + " - 询问天气、今日新闻、股票大盘、通用百科知识。\n" + " - 与公司无关的外部实时资讯。\n" + "只输出分类标签 (SEARCH 或 RAG),不要其他内容。" + ) + + payload = { + "model": "glm-4-flash", + "messages": [{"role": "user", "content": prompt}], + "temperature": 0.1, # Low temp for deterministic output + "max_tokens": 10 + } + + try: + async with httpx.AsyncClient(timeout=3.0) as client: # Fast timeout + response = await client.post( + SearchConfig.ZHIPUAI_API_BASE, + headers=headers, + json=payload + ) + if response.status_code == 200: + data = response.json() + choices = data.get('choices', []) + if choices: + content = choices[0]['message']['content'].strip().upper() + # Simple heuristic cleanup + if 'SEARCH' in content: + return 'SEARCH' + return 'RAG' + except Exception: + pass # Fail safe to RAG (or logic below) + + # Fallback to keyword match if LLM fails + return 'SEARCH' if cls.should_handle(question) else 'RAG' + + @classmethod + def _format_baidu_response(cls, query: str, payload: Dict[str, Any]) -> Dict[str, Any]: + """Format Baidu AppBuilder response to match expected structure""" + # The Baidu response structure based on docs example: + # { "choices": [ { "message": { "content": "..." } } ], "references": [ { "content": "...", "title": "...", "url": "..." } ] } + + choices = payload.get('choices', []) + message_content = "" + if choices: + message_content = choices[0].get('message', {}).get('content', '') + + references = payload.get('references', []) + + results = [] + for ref in references: + results.append({ + 'title': ref.get('title', ''), + 'link': ref.get('url', ''), + 'snippet': ref.get('content', ''), # Use content as snippet + 'source': 'baidu' + }) + + # Build context for LLM (similar to original logic) + context_parts = [] + if message_content: + context_parts.append(f"智能总结:{message_content}") + + if results: + context_parts.append("\n相关信息:") + for idx, item in enumerate(results[:5], 1): # Limit to 5 + title = item['title'] + snippet = cls._simplify_text(item['snippet']) + context_parts.append(f"{idx}. {title}\n {snippet}") + + context_for_llm = "\n".join(context_parts) + + # Message for display (similar to original logic) + message_parts = [f'关于"{query}"的搜索结果:\n'] + if message_content: + message_parts.append(f"【智能回答】{message_content}\n") + + if results: + message_parts.append('\n【参考来源】') + for idx, item in enumerate(results[:3], 1): + title = item['title'] + snippet = cls._simplify_text(item['snippet']) + message_parts.append(f'\n{idx}. {title}') + else: + message_parts.append('\n未找到相关结果。') + + return { + 'type': 'search', + 'query': query, + 'results': results, # Normalized results list + 'message': '\n'.join(message_parts), # Display friendly message + 'context_for_llm': context_for_llm, # Context for RAG extraction + 'raw': payload + } + + @staticmethod + def _build_cache_key(query: str) -> str: + query_hash = hashlib.md5(query.encode('utf-8')).hexdigest()[:16] + today = datetime.utcnow().strftime('%Y%m%d') + return f'search:baidu:{query_hash}:{today}' diff --git a/ruoyi-fastapi-backend/module_admin/service/server_service.py b/ruoyi-fastapi-backend/module_admin/service/server_service.py new file mode 100644 index 0000000..2f9a53f --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/server_service.py @@ -0,0 +1,96 @@ +import os +import platform +import psutil +import socket +import time +from module_admin.entity.vo.server_vo import CpuInfo, MemoryInfo, PyInfo, ServerMonitorModel, SysFiles, SysInfo +from utils.common_util import bytes2human + + +class ServerService: + """ + 服务监控模块服务层 + """ + + @staticmethod + async def get_server_monitor_info(): + # CPU信息 + # 获取CPU总核心数 + cpu_num = psutil.cpu_count(logical=True) + cpu_usage_percent = psutil.cpu_times_percent() + cpu_used = cpu_usage_percent.user + cpu_sys = cpu_usage_percent.system + cpu_free = cpu_usage_percent.idle + cpu = CpuInfo(cpuNum=cpu_num, used=cpu_used, sys=cpu_sys, free=cpu_free) + + # 内存信息 + memory_info = psutil.virtual_memory() + memory_total = bytes2human(memory_info.total) + memory_used = bytes2human(memory_info.used) + memory_free = bytes2human(memory_info.free) + memory_usage = memory_info.percent + mem = MemoryInfo(total=memory_total, used=memory_used, free=memory_free, usage=memory_usage) + + # 主机信息 + # 获取主机名 + hostname = socket.gethostname() + # 获取IP + computer_ip = socket.gethostbyname(hostname) + os_name = platform.platform() + computer_name = platform.node() + os_arch = platform.machine() + user_dir = os.path.abspath(os.getcwd()) + sys = SysInfo( + computerIp=computer_ip, computerName=computer_name, osArch=os_arch, osName=os_name, userDir=user_dir + ) + + # python解释器信息 + current_pid = os.getpid() + current_process = psutil.Process(current_pid) + python_name = current_process.name() + python_version = platform.python_version() + python_home = current_process.exe() + start_time_stamp = current_process.create_time() + start_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(start_time_stamp)) + current_time_stamp = time.time() + difference = current_time_stamp - start_time_stamp + # 将时间差转换为天、小时和分钟数 + days = int(difference // (24 * 60 * 60)) # 每天的秒数 + hours = int((difference % (24 * 60 * 60)) // (60 * 60)) # 每小时的秒数 + minutes = int((difference % (60 * 60)) // 60) # 每分钟的秒数 + run_time = f'{days}天{hours}小时{minutes}分钟' + # 获取当前Python程序的pid + pid = os.getpid() + # 获取该进程的内存信息 + current_process_memory_info = psutil.Process(pid).memory_info() + py = PyInfo( + name=python_name, + version=python_version, + startTime=start_time, + runTime=run_time, + home=python_home, + total=bytes2human(memory_info.available), + used=bytes2human(current_process_memory_info.rss), + free=bytes2human(memory_info.available - current_process_memory_info.rss), + usage=round((current_process_memory_info.rss / memory_info.available) * 100, 2), + ) + + # 磁盘信息 + io = psutil.disk_partitions() + sys_files = [] + for i in io: + o = psutil.disk_usage(i.device) + disk_data = SysFiles( + dirName=i.device, + sysTypeName=i.fstype, + typeName='本地固定磁盘(' + i.mountpoint.replace('\\', '') + ')', + total=bytes2human(o.total), + used=bytes2human(o.used), + free=bytes2human(o.free), + usage=f'{psutil.disk_usage(i.device).percent}%', + ) + sys_files.append(disk_data) + + result = ServerMonitorModel(cpu=cpu, mem=mem, sys=sys, py=py, sysFiles=sys_files) + + return result diff --git a/ruoyi-fastapi-backend/module_admin/service/sys_alert_service.py b/ruoyi-fastapi-backend/module_admin/service/sys_alert_service.py new file mode 100644 index 0000000..aa4e6f0 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/sys_alert_service.py @@ -0,0 +1,133 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.dao.sys_alert_dao import Sys_alertDao +from module_admin.entity.vo.sys_alert_vo import DeleteSys_alertModel, Sys_alertModel, Sys_alertPageQueryModel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil + + +class Sys_alertService: + """ + 系统告警模块服务层 + """ + + @classmethod + async def get_sys_alert_list_services( + cls, query_db: AsyncSession, query_object: Sys_alertPageQueryModel, is_page: bool = False + ): + """ + 获取系统告警列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 系统告警列表信息对象 + """ + sys_alert_list_result = await Sys_alertDao.get_sys_alert_list(query_db, query_object, is_page) + + return sys_alert_list_result + + + @classmethod + async def add_sys_alert_services(cls, query_db: AsyncSession, page_object: Sys_alertModel): + """ + 新增系统告警信息service + + :param query_db: orm对象 + :param page_object: 新增系统告警对象 + :return: 新增系统告警校验结果 + """ + try: + await Sys_alertDao.add_sys_alert_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_sys_alert_services(cls, query_db: AsyncSession, page_object: Sys_alertModel): + """ + 编辑系统告警信息service + + :param query_db: orm对象 + :param page_object: 编辑系统告警对象 + :return: 编辑系统告警校验结果 + """ + edit_sys_alert = page_object.model_dump(exclude_unset=True, exclude={'create_time', 'create_by', }) + sys_alert_info = await cls.sys_alert_detail_services(query_db, page_object.id) + if sys_alert_info.id: + try: + await Sys_alertDao.edit_sys_alert_dao(query_db, edit_sys_alert) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='系统告警不存在') + + @classmethod + async def delete_sys_alert_services(cls, query_db: AsyncSession, page_object: DeleteSys_alertModel): + """ + 删除系统告警信息service + + :param query_db: orm对象 + :param page_object: 删除系统告警对象 + :return: 删除系统告警校验结果 + """ + if page_object.ids: + id_list = page_object.ids.split(',') + try: + for id in id_list: + await Sys_alertDao.delete_sys_alert_dao(query_db, Sys_alertModel(id=id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入主键 自增为空') + + @classmethod + async def sys_alert_detail_services(cls, query_db: AsyncSession, id: int): + """ + 获取系统告警详细信息service + + :param query_db: orm对象 + :param id: 主键 自增 + :return: 主键 自增对应的信息 + """ + sys_alert = await Sys_alertDao.get_sys_alert_detail_by_id(query_db, id=id) + if sys_alert: + result = Sys_alertModel(**CamelCaseUtil.transform_result(sys_alert)) + else: + result = Sys_alertModel(**dict()) + + return result + + @staticmethod + async def export_sys_alert_list_services(sys_alert_list: List): + """ + 导出系统告警信息service + + :param sys_alert_list: 系统告警信息列表 + :return: 系统告警信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'id': '主键 自增', + 'detail': '告警描述', + 'type': '告警类型', + 'status': '告警状态, 0 未处理, 1 已处理', + 'createTime': '创建时间', + 'createBy': '创建者', + 'updateTime': '更新时间', + 'updateBy': '更新者', + } + binary_data = ExcelUtil.export_list2excel(sys_alert_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/sys_statistics_service.py b/ruoyi-fastapi-backend/module_admin/service/sys_statistics_service.py new file mode 100644 index 0000000..8b6ef7f --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/sys_statistics_service.py @@ -0,0 +1,248 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.dao.sys_statistics_dao import Sys_statisticsDao +from module_admin.entity.vo.sys_statistics_vo import DeleteSys_statisticsModel, Sys_statisticsModel, Sys_statisticsPageQueryModel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil +from datetime import datetime, timedelta, time + + +class Sys_statisticsService: + """ + 系统统计数据模块服务层 + """ + # 展厅讲解次数加1 + @classmethod + async def add_explain_count_services(cls, query_db: AsyncSession, user_name: str): + """ + 新增展厅讲解次数 + :param query_db: orm对象 + :return: + """ + await Sys_statisticsDao.add_count(query_db, "exhibition_explanation", user_name, 1) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + # 访客引导量加1 + @classmethod + async def add_visitor_guide_count_services(cls, query_db: AsyncSession, user_name: str): + """ + 新增访客引导量 + :param query_db: orm对象 + :return: + """ + await Sys_statisticsDao.add_count(query_db, "visitor_guide", user_name, 1) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + # 门禁通行量加1 + @classmethod + async def add_door_pass_count_services(cls, query_db: AsyncSession, user_name: str): + """ + 新增门禁通行量 + :param query_db: orm对象 + :return: + """ + await Sys_statisticsDao.add_count(query_db, "access_control_count", user_name, 1) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + # 大模型调用量加1 + @classmethod + async def add_model_call_count_services(cls, query_db: AsyncSession, user_name: str): + """ + 新增模型调用次数 + :param query_db: orm对象 + :return: + """ + # await Sys_statisticsDao.add_model_call_count(query_db, user_name) + await Sys_statisticsDao.add_count(query_db, "llm_call", user_name, 1) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + + # 根据时间区间获取门禁识别成功率 + @classmethod + async def get_access_control_success_rate_services(cls, query_db: AsyncSession, day: int): + end_date = datetime.now().date() - timedelta(days=1) + start_date = end_date - timedelta(days=day) + result = await Sys_statisticsDao.get_access_control_success_rate(query_db, start_date, end_date) + return result + + + + # 根据时间区间获取访客数量 + @classmethod + async def get_visitor_count_services(cls, query_db: AsyncSession, day: int): + end_date = datetime.now().date() - timedelta(days=1) + start_date = end_date - timedelta(days=day) + result = await Sys_statisticsDao.get_visitor_count(query_db, start_date, end_date) + # if len(result) < day: + + return result + + + + # 获取系统统计数据 + @classmethod + async def get_statistics_data_services(cls, query_db: AsyncSession): + + # 当前日期 + today = datetime.now().date() + # yesterday = today - timedelta(days=1) + # data_1 = await Sys_statisticsDao.get_statistics_data(query_db, yesterday) + data_2 = await Sys_statisticsDao.get_statistics_data(query_db, today) + + total = await Sys_statisticsDao.get_total_statistics_data(query_db) + + if total is None: + total = { + "llm_call": 1, + "access_control_count": 1, + "visitor_guide": 1, + "exhibition_explanation": 1 + } + else: + total = dict(total) + + if data_2 is None: + data_2 = { + "llm_call": 0, + "access_control_count": 0, + "visitor_guide": 0, + "exhibition_explanation": 0 + } + else: + data_2 =dict(data_2) + # 计算今日数据占总数据的百分比 + for k, v in data_2.items(): + if total.get(k) and total[k] > 0: + # 计算百分比,保留2位小数 + data_2[k] = round((v / total[k]) * 100, 2) + else: + data_2[k] = 0.0 + + return { + 'change': data_2, + 'total': total + } + + @classmethod + async def get_sys_statistics_list_services( + cls, query_db: AsyncSession, query_object: Sys_statisticsPageQueryModel, is_page: bool = False + ): + """ + 获取系统统计数据列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 系统统计数据列表信息对象 + """ + sys_statistics_list_result = await Sys_statisticsDao.get_sys_statistics_list(query_db, query_object, is_page) + + return sys_statistics_list_result + + + @classmethod + async def add_sys_statistics_services(cls, query_db: AsyncSession, page_object: Sys_statisticsModel): + """ + 新增系统统计数据信息service + + :param query_db: orm对象 + :param page_object: 新增系统统计数据对象 + :return: 新增系统统计数据校验结果 + """ + try: + await Sys_statisticsDao.add_sys_statistics_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_sys_statistics_services(cls, query_db: AsyncSession, page_object: Sys_statisticsModel): + """ + 编辑系统统计数据信息service + + :param query_db: orm对象 + :param page_object: 编辑系统统计数据对象 + :return: 编辑系统统计数据校验结果 + """ + edit_sys_statistics = page_object.model_dump(exclude_unset=True, exclude={'create_time', 'create_by', }) + sys_statistics_info = await cls.sys_statistics_detail_services(query_db, page_object.statistic_id) + if sys_statistics_info.statistic_id: + try: + await Sys_statisticsDao.edit_sys_statistics_dao(query_db, edit_sys_statistics) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='系统统计数据不存在') + + @classmethod + async def delete_sys_statistics_services(cls, query_db: AsyncSession, page_object: DeleteSys_statisticsModel): + """ + 删除系统统计数据信息service + + :param query_db: orm对象 + :param page_object: 删除系统统计数据对象 + :return: 删除系统统计数据校验结果 + """ + if page_object.statistic_ids: + statistic_id_list = page_object.statistic_ids.split(',') + try: + for statistic_id in statistic_id_list: + await Sys_statisticsDao.delete_sys_statistics_dao(query_db, Sys_statisticsModel(statisticId=statistic_id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入统计项目ID为空') + + @classmethod + async def sys_statistics_detail_services(cls, query_db: AsyncSession, statistic_id: int): + """ + 获取系统统计数据详细信息service + + :param query_db: orm对象 + :param statistic_id: 统计项目ID + :return: 统计项目ID对应的信息 + """ + sys_statistics = await Sys_statisticsDao.get_sys_statistics_detail_by_id(query_db, statistic_id=statistic_id) + if sys_statistics: + result = Sys_statisticsModel(**CamelCaseUtil.transform_result(sys_statistics)) + else: + result = Sys_statisticsModel(**dict()) + + return result + + @staticmethod + async def export_sys_statistics_list_services(sys_statistics_list: List): + """ + 导出系统统计数据信息service + + :param sys_statistics_list: 系统统计数据信息列表 + :return: 系统统计数据信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'statisticId': '统计项目ID', + 'statDate': '统计日期', + 'llmCall': '大模型调用量', + 'accessControlCount': '门禁通行量', + 'visitorGuide': '访客引导次数', + 'exhibitionExplanation': '展厅讲解次数', + 'accessControlSuccessRate': '门禁识别成功率(%)', + 'createTime': '创建时间', + 'createBy': '创建者', + 'updateTime': '修改时间', + 'updateBy': '修改者', + } + binary_data = ExcelUtil.export_list2excel(sys_statistics_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/test_service.py b/ruoyi-fastapi-backend/module_admin/service/test_service.py new file mode 100644 index 0000000..a14fc73 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/test_service.py @@ -0,0 +1,10 @@ +from module_admin.dao.test_dao import TestDao +from sqlalchemy.ext.asyncio import AsyncSession +from module_admin.entity.vo.test_vo import TestUserNamePage + + +class TestService: + @staticmethod + async def get_user_name(db: AsyncSession, query_paramter:TestUserNamePage): + user_name = await TestDao.get_user_name(db, query_paramter) + return user_name diff --git a/ruoyi-fastapi-backend/module_admin/service/user_service.py b/ruoyi-fastapi-backend/module_admin/service/user_service.py new file mode 100644 index 0000000..f269d3f --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/user_service.py @@ -0,0 +1,639 @@ +import io +import pandas as pd +from datetime import datetime +from fastapi import Request, UploadFile +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List, Union +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.dao.user_dao import UserDao +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.entity.vo.post_vo import PostPageQueryModel +from module_admin.entity.vo.user_vo import ( + AddUserModel, + CrudUserRoleModel, + CurrentUserModel, + DeleteUserModel, + EditUserModel, + ResetUserModel, + SelectedRoleModel, + UserDetailModel, + UserInfoModel, + UserModel, + UserPageQueryModel, + UserPostModel, + UserProfileModel, + UserRoleModel, + UserRoleQueryModel, + UserRoleResponseModel, +) +from module_admin.service.config_service import ConfigService +from module_admin.service.dept_service import DeptService +from module_admin.service.post_service import PostService +from module_admin.service.role_service import RoleService +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil +from utils.page_util import PageResponseModel +from utils.pwd_util import PwdUtil + + +class UserService: + """ + 用户管理模块服务层 + """ + + @classmethod + async def get_user_list_services( + cls, query_db: AsyncSession, query_object: UserPageQueryModel, data_scope_sql: str, is_page: bool = False + ): + """ + 获取用户列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param data_scope_sql: 数据权限对应的查询sql语句 + :param is_page: 是否开启分页 + :return: 用户列表信息对象 + """ + query_result = await UserDao.get_user_list(query_db, query_object, data_scope_sql, is_page) + if is_page: + user_list_result = PageResponseModel( + **{ + **query_result.model_dump(by_alias=True), + 'rows': [{**row[0], 'dept': row[1]} for row in query_result.rows], + } + ) + else: + user_list_result = [] + if query_result: + user_list_result = [{**row[0], 'dept': row[1]} for row in query_result] + + return user_list_result + + @classmethod + async def check_user_allowed_services(cls, check_user: UserModel): + """ + 校验用户是否允许操作service + + :param check_user: 用户信息 + :return: 校验结果 + """ + if check_user.admin: + raise ServiceException(message='不允许操作超级管理员用户') + else: + return CrudResponseModel(is_success=True, message='校验通过') + + @classmethod + async def check_user_data_scope_services(cls, query_db: AsyncSession, user_id: int, data_scope_sql: str): + """ + 校验用户数据权限service + + :param query_db: orm对象 + :param user_id: 用户id + :param data_scope_sql: 数据权限对应的查询sql语句 + :return: 校验结果 + """ + users = await UserDao.get_user_list(query_db, UserPageQueryModel(userId=user_id), data_scope_sql, is_page=False) + if users: + return CrudResponseModel(is_success=True, message='校验通过') + else: + raise ServiceException(message='没有权限访问用户数据') + + @classmethod + async def check_user_name_unique_services(cls, query_db: AsyncSession, page_object: UserModel): + """ + 校验用户名是否唯一service + + :param query_db: orm对象 + :param page_object: 用户对象 + :return: 校验结果 + """ + user_id = -1 if page_object.user_id is None else page_object.user_id + user = await UserDao.get_user_by_info(query_db, UserModel(userName=page_object.user_name)) + if user and user.user_id != user_id: + return CommonConstant.NOT_UNIQUE + return CommonConstant.UNIQUE + + @classmethod + async def check_phonenumber_unique_services(cls, query_db: AsyncSession, page_object: UserModel): + """ + 校验用户手机号是否唯一service + + :param query_db: orm对象 + :param page_object: 用户对象 + :return: 校验结果 + """ + user_id = -1 if page_object.user_id is None else page_object.user_id + user = await UserDao.get_user_by_info(query_db, UserModel(phonenumber=page_object.phonenumber)) + if user and user.user_id != user_id: + return CommonConstant.NOT_UNIQUE + return CommonConstant.UNIQUE + + @classmethod + async def check_email_unique_services(cls, query_db: AsyncSession, page_object: UserModel): + """ + 校验用户邮箱是否唯一service + + :param query_db: orm对象 + :param page_object: 用户对象 + :return: 校验结果 + """ + user_id = -1 if page_object.user_id is None else page_object.user_id + user = await UserDao.get_user_by_info(query_db, UserModel(email=page_object.email)) + if user and user.user_id != user_id: + return CommonConstant.NOT_UNIQUE + return CommonConstant.UNIQUE + + @classmethod + async def add_user_services(cls, query_db: AsyncSession, page_object: AddUserModel): + """ + 新增用户信息service + + :param query_db: orm对象 + :param page_object: 新增用户对象 + :return: 新增用户校验结果 + """ + add_user = UserModel(**page_object.model_dump(by_alias=True)) + if not await cls.check_user_name_unique_services(query_db, page_object): + raise ServiceException(message=f'新增用户{page_object.user_name}失败,登录账号已存在') + elif page_object.phonenumber and not await cls.check_phonenumber_unique_services(query_db, page_object): + raise ServiceException(message=f'新增用户{page_object.user_name}失败,手机号码已存在') + elif page_object.email and not await cls.check_email_unique_services(query_db, page_object): + raise ServiceException(message=f'新增用户{page_object.user_name}失败,邮箱账号已存在') + else: + try: + add_result = await UserDao.add_user_dao(query_db, add_user) + user_id = add_result.user_id + if page_object.role_ids: + for role in page_object.role_ids: + await UserDao.add_user_role_dao(query_db, UserRoleModel(userId=user_id, roleId=role)) + if page_object.post_ids: + for post in page_object.post_ids: + await UserDao.add_user_post_dao(query_db, UserPostModel(userId=user_id, postId=post)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_user_services(cls, query_db: AsyncSession, page_object: EditUserModel): + """ + 编辑用户信息service + + :param query_db: orm对象 + :param page_object: 编辑用户对象 + :return: 编辑用户校验结果 + """ + edit_user = page_object.model_dump(exclude_unset=True, exclude={'admin'}) + if page_object.type != 'status' and page_object.type != 'avatar' and page_object.type != 'pwd': + del edit_user['role_ids'] + del edit_user['post_ids'] + del edit_user['role'] + if page_object.type == 'status' or page_object.type == 'avatar' or page_object.type == 'pwd': + del edit_user['type'] + user_info = await cls.user_detail_services(query_db, edit_user.get('user_id')) + if user_info.data and user_info.data.user_id: + if page_object.type != 'status' and page_object.type != 'avatar' and page_object.type != 'pwd': + if not await cls.check_user_name_unique_services(query_db, page_object): + raise ServiceException(message=f'修改用户{page_object.user_name}失败,登录账号已存在') + elif page_object.phonenumber and not await cls.check_phonenumber_unique_services(query_db, page_object): + raise ServiceException(message=f'修改用户{page_object.user_name}失败,手机号码已存在') + elif page_object.email and not await cls.check_email_unique_services(query_db, page_object): + raise ServiceException(message=f'修改用户{page_object.user_name}失败,邮箱账号已存在') + try: + await UserDao.edit_user_dao(query_db, edit_user) + if page_object.type != 'status' and page_object.type != 'avatar' and page_object.type != 'pwd': + await UserDao.delete_user_role_dao(query_db, UserRoleModel(userId=page_object.user_id)) + await UserDao.delete_user_post_dao(query_db, UserPostModel(userId=page_object.user_id)) + if page_object.role_ids: + for role in page_object.role_ids: + await UserDao.add_user_role_dao( + query_db, UserRoleModel(userId=page_object.user_id, roleId=role) + ) + if page_object.post_ids: + for post in page_object.post_ids: + await UserDao.add_user_post_dao( + query_db, UserPostModel(userId=page_object.user_id, postId=post) + ) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='用户不存在') + + @classmethod + async def delete_user_services(cls, query_db: AsyncSession, page_object: DeleteUserModel): + """ + 删除用户信息service + + :param query_db: orm对象 + :param page_object: 删除用户对象 + :return: 删除用户校验结果 + """ + if page_object.user_ids: + user_id_list = page_object.user_ids.split(',') + try: + for user_id in user_id_list: + user_id_dict = dict( + userId=user_id, updateBy=page_object.update_by, updateTime=page_object.update_time + ) + await UserDao.delete_user_role_dao(query_db, UserRoleModel(**user_id_dict)) + await UserDao.delete_user_post_dao(query_db, UserPostModel(**user_id_dict)) + await UserDao.delete_user_dao(query_db, UserModel(**user_id_dict)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入用户id为空') + + @classmethod + async def user_detail_services(cls, query_db: AsyncSession, user_id: Union[int, str]): + """ + 获取用户详细信息service + + :param query_db: orm对象 + :param user_id: 用户id + :return: 用户id对应的信息 + """ + posts = await PostService.get_post_list_services(query_db, PostPageQueryModel(**{}), is_page=False) + roles = await RoleService.get_role_select_option_services(query_db) + if user_id != '': + query_user = await UserDao.get_user_detail_by_id(query_db, user_id=user_id) + post_ids = ','.join([str(row.post_id) for row in query_user.get('user_post_info')]) + post_ids_list = [row.post_id for row in query_user.get('user_post_info')] + role_ids = ','.join([str(row.role_id) for row in query_user.get('user_role_info')]) + role_ids_list = [row.role_id for row in query_user.get('user_role_info')] + + return UserDetailModel( + data=UserInfoModel( + **CamelCaseUtil.transform_result(query_user.get('user_basic_info')), + postIds=post_ids, + roleIds=role_ids, + dept=CamelCaseUtil.transform_result(query_user.get('user_dept_info')), + role=CamelCaseUtil.transform_result(query_user.get('user_role_info')), + ), + postIds=post_ids_list, + posts=posts, + roleIds=role_ids_list, + roles=roles, + ) + + return UserDetailModel(posts=posts, roles=roles) + + @classmethod + async def user_profile_services(cls, query_db: AsyncSession, user_id: int): + """ + 获取用户个人详细信息service + + :param query_db: orm对象 + :param user_id: 用户id + :return: 用户id对应的信息 + """ + query_user = await UserDao.get_user_detail_by_id(query_db, user_id=user_id) + post_ids = ','.join([str(row.post_id) for row in query_user.get('user_post_info')]) + post_group = ','.join([row.post_name for row in query_user.get('user_post_info')]) + role_ids = ','.join([str(row.role_id) for row in query_user.get('user_role_info')]) + role_group = ','.join([row.role_name for row in query_user.get('user_role_info')]) + + return UserProfileModel( + data=UserInfoModel( + **CamelCaseUtil.transform_result(query_user.get('user_basic_info')), + postIds=post_ids, + roleIds=role_ids, + dept=CamelCaseUtil.transform_result(query_user.get('user_dept_info')), + role=CamelCaseUtil.transform_result(query_user.get('user_role_info')), + ), + postGroup=post_group, + roleGroup=role_group, + ) + + @classmethod + async def reset_user_services(cls, query_db: AsyncSession, page_object: ResetUserModel): + """ + 重置用户密码service + + :param query_db: orm对象 + :param page_object: 重置用户对象 + :return: 重置用户校验结果 + """ + reset_user = page_object.model_dump(exclude_unset=True, exclude={'admin'}) + if page_object.old_password: + user = (await UserDao.get_user_detail_by_id(query_db, user_id=page_object.user_id)).get('user_basic_info') + if not PwdUtil.verify_password(page_object.old_password, user.password): + raise ServiceException(message='修改密码失败,旧密码错误') + elif PwdUtil.verify_password(page_object.password, user.password): + raise ServiceException(message='新密码不能与旧密码相同') + else: + del reset_user['old_password'] + if page_object.sms_code and page_object.session_id: + del reset_user['sms_code'] + del reset_user['session_id'] + try: + reset_user['password'] = PwdUtil.get_password_hash(page_object.password) + await UserDao.edit_user_dao(query_db, reset_user) + await query_db.commit() + return CrudResponseModel(is_success=True, message='重置成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def batch_import_user_services( + cls, + request: Request, + query_db: AsyncSession, + file: UploadFile, + update_support: bool, + current_user: CurrentUserModel, + user_data_scope_sql: str, + dept_data_scope_sql: str, + ): + """ + 批量导入用户service + + :param request: Request对象 + :param query_db: orm对象 + :param file: 用户导入文件对象 + :param update_support: 用户存在时是否更新 + :param current_user: 当前用户对象 + :param user_data_scope_sql: 用户数据权限sql + :param dept_data_scope_sql: 部门数据权限sql + :return: 批量导入用户结果 + """ + header_dict = { + '部门编号': 'dept_id', + '登录名称': 'user_name', + '用户名称': 'nick_name', + '用户邮箱': 'email', + '手机号码': 'phonenumber', + '用户性别': 'sex', + '帐号状态': 'status', + } + contents = await file.read() + df = pd.read_excel(io.BytesIO(contents)) + await file.close() + df.rename(columns=header_dict, inplace=True) + add_error_result = [] + count = 0 + try: + for index, row in df.iterrows(): + count = count + 1 + if row['sex'] == '男': + row['sex'] = '0' + if row['sex'] == '女': + row['sex'] = '1' + if row['sex'] == '未知': + row['sex'] = '2' + if row['status'] == '正常': + row['status'] = '0' + if row['status'] == '停用': + row['status'] = '1' + add_user = UserModel( + deptId=row['dept_id'], + userName=row['user_name'], + password=PwdUtil.get_password_hash( + await ConfigService.query_config_list_from_cache_services( + request.app.state.redis, 'sys.user.initPassword' + ) + ), + nickName=row['nick_name'], + email=row['email'], + phonenumber=str(row['phonenumber']), + sex=row['sex'], + status=row['status'], + createBy=current_user.user.user_name, + createTime=datetime.now(), + updateBy=current_user.user.user_name, + updateTime=datetime.now(), + ) + user_info = await UserDao.get_user_by_info(query_db, UserModel(userName=row['user_name'])) + if user_info: + if update_support: + edit_user_model = UserModel( + userId=user_info.user_id, + deptId=row['dept_id'], + userName=row['user_name'], + nickName=row['nick_name'], + email=row['email'], + phonenumber=str(row['phonenumber']), + sex=row['sex'], + status=row['status'], + updateBy=current_user.user.user_name, + updateTime=datetime.now(), + ) + edit_user_model.validate_fields() + await cls.check_user_allowed_services(edit_user_model) + if not current_user.user.admin: + await cls.check_user_data_scope_services( + query_db, edit_user_model.user_id, user_data_scope_sql + ) + await DeptService.check_dept_data_scope_services( + query_db, edit_user_model.dept_id, dept_data_scope_sql + ) + edit_user = edit_user_model.model_dump(exclude_unset=True) + await UserDao.edit_user_dao(query_db, edit_user) + else: + add_error_result.append(f"{count}.用户账号{row['user_name']}已存在") + else: + add_user.validate_fields() + if not current_user.user.admin: + await DeptService.check_dept_data_scope_services( + query_db, add_user.dept_id, dept_data_scope_sql + ) + await UserDao.add_user_dao(query_db, add_user) + await query_db.commit() + return CrudResponseModel(is_success=True, message='\n'.join(add_error_result)) + except Exception as e: + await query_db.rollback() + raise e + + @staticmethod + async def get_user_import_template_services(): + """ + 获取用户导入模板service + + :return: 用户导入模板excel的二进制数据 + """ + header_list = ['部门编号', '登录名称', '用户名称', '用户邮箱', '手机号码', '用户性别', '帐号状态'] + selector_header_list = ['用户性别', '帐号状态'] + option_list = [{'用户性别': ['男', '女', '未知']}, {'帐号状态': ['正常', '停用']}] + binary_data = ExcelUtil.get_excel_template( + header_list=header_list, selector_header_list=selector_header_list, option_list=option_list + ) + + return binary_data + + @staticmethod + async def export_user_list_services(user_list: List): + """ + 导出用户信息service + + :param user_list: 用户信息列表 + :return: 用户信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'userId': '用户编号', + 'userName': '用户名称', + 'nickName': '用户昵称', + 'deptName': '部门', + 'email': '邮箱地址', + 'phonenumber': '手机号码', + 'sex': '性别', + 'status': '状态', + 'createBy': '创建者', + 'createTime': '创建时间', + 'updateBy': '更新者', + 'updateTime': '更新时间', + 'remark': '备注', + } + + for item in user_list: + # item['deptName'] = item.get('dept').get('deptName') + if item.get('status') == '0': + item['status'] = '正常' + else: + item['status'] = '停用' + if item.get('sex') == '0': + item['sex'] = '男' + elif item.get('sex') == '1': + item['sex'] = '女' + else: + item['sex'] = '未知' + binary_data = ExcelUtil.export_list2excel(user_list, mapping_dict) + + return binary_data + + @classmethod + async def get_user_role_allocated_list_services(cls, query_db: AsyncSession, page_object: UserRoleQueryModel): + """ + 根据用户id获取已分配角色列表 + + :param query_db: orm对象 + :param page_object: 用户关联角色对象 + :return: 已分配角色列表 + """ + query_user = await UserDao.get_user_detail_by_id(query_db, page_object.user_id) + post_ids = ','.join([str(row.post_id) for row in query_user.get('user_post_info')]) + role_ids = ','.join([str(row.role_id) for row in query_user.get('user_role_info')]) + user = UserInfoModel( + **CamelCaseUtil.transform_result(query_user.get('user_basic_info')), + postIds=post_ids, + roleIds=role_ids, + dept=CamelCaseUtil.transform_result(query_user.get('user_dept_info')), + role=CamelCaseUtil.transform_result(query_user.get('user_role_info')), + ) + query_role_list = [ + SelectedRoleModel(**row) for row in await RoleService.get_role_select_option_services(query_db) + ] + for model_a in query_role_list: + for model_b in user.role: + if model_a.role_id == model_b.role_id: + model_a.flag = True + result = UserRoleResponseModel(roles=query_role_list, user=user) + + return result + + @classmethod + async def add_user_role_services(cls, query_db: AsyncSession, page_object: CrudUserRoleModel): + """ + 新增用户关联角色信息service + + :param query_db: orm对象 + :param page_object: 新增用户关联角色对象 + :return: 新增用户关联角色校验结果 + """ + if page_object.user_id and page_object.role_ids: + role_id_list = page_object.role_ids.split(',') + try: + await UserDao.delete_user_role_by_user_and_role_dao(query_db, UserRoleModel(userId=page_object.user_id)) + for role_id in role_id_list: + await UserDao.add_user_role_dao(query_db, UserRoleModel(userId=page_object.user_id, roleId=role_id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='分配成功') + except Exception as e: + await query_db.rollback() + raise e + elif page_object.user_id and not page_object.role_ids: + try: + await UserDao.delete_user_role_by_user_and_role_dao(query_db, UserRoleModel(userId=page_object.user_id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='分配成功') + except Exception as e: + await query_db.rollback() + raise e + elif page_object.user_ids and page_object.role_id: + user_id_list = page_object.user_ids.split(',') + try: + for user_id in user_id_list: + user_role = await cls.detail_user_role_services( + query_db, UserRoleModel(userId=user_id, roleId=page_object.role_id) + ) + if user_role: + continue + else: + await UserDao.add_user_role_dao( + query_db, UserRoleModel(userId=user_id, roleId=page_object.role_id) + ) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='不满足新增条件') + + @classmethod + async def delete_user_role_services(cls, query_db: AsyncSession, page_object: CrudUserRoleModel): + """ + 删除用户关联角色信息service + + :param query_db: orm对象 + :param page_object: 删除用户关联角色对象 + :return: 删除用户关联角色校验结果 + """ + if (page_object.user_id and page_object.role_id) or (page_object.user_ids and page_object.role_id): + if page_object.user_id and page_object.role_id: + try: + await UserDao.delete_user_role_by_user_and_role_dao( + query_db, UserRoleModel(userId=page_object.user_id, roleId=page_object.role_id) + ) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + elif page_object.user_ids and page_object.role_id: + user_id_list = page_object.user_ids.split(',') + try: + for user_id in user_id_list: + await UserDao.delete_user_role_by_user_and_role_dao( + query_db, UserRoleModel(userId=user_id, roleId=page_object.role_id) + ) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='不满足删除条件') + else: + raise ServiceException(message='传入用户角色关联信息为空') + + @classmethod + async def detail_user_role_services(cls, query_db: AsyncSession, page_object: UserRoleModel): + """ + 获取用户关联角色详细信息service + + :param query_db: orm对象 + :param page_object: 用户关联角色对象 + :return: 用户关联角色详细信息 + """ + user_role = await UserDao.get_user_role_detail(query_db, page_object) + + return user_role diff --git a/ruoyi-fastapi-backend/module_admin/service/visitor_type_service.py b/ruoyi-fastapi-backend/module_admin/service/visitor_type_service.py new file mode 100644 index 0000000..27636c6 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/visitor_type_service.py @@ -0,0 +1,136 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.dao.visitor_type_dao import Visitor_typeDao +from module_admin.entity.vo.visitor_type_vo import DeleteVisitor_typeModel, Visitor_typeModel, Visitor_typePageQueryModel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil + + +class Visitor_typeService: + """ + 访客类型模块服务层 + """ + + @classmethod + async def get_system_visitor_type_list(cls, query_db: AsyncSession): + return await Visitor_typeDao.get_system_visitor_type_list(query_db) + + @classmethod + async def get_visitor_type_list_services( + cls, query_db: AsyncSession, query_object: Visitor_typePageQueryModel, is_page: bool = False + ): + """ + 获取访客类型列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 访客类型列表信息对象 + """ + visitor_type_list_result = await Visitor_typeDao.get_visitor_type_list(query_db, query_object, is_page) + + return visitor_type_list_result + + + @classmethod + async def add_visitor_type_services(cls, query_db: AsyncSession, page_object: Visitor_typeModel): + """ + 新增访客类型信息service + + :param query_db: orm对象 + :param page_object: 新增访客类型对象 + :return: 新增访客类型校验结果 + """ + try: + await Visitor_typeDao.add_visitor_type_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_visitor_type_services(cls, query_db: AsyncSession, page_object: Visitor_typeModel): + """ + 编辑访客类型信息service + + :param query_db: orm对象 + :param page_object: 编辑访客类型对象 + :return: 编辑访客类型校验结果 + """ + edit_visitor_type = page_object.model_dump(exclude_unset=True, exclude={'create_time', 'create_by', }) + visitor_type_info = await cls.visitor_type_detail_services(query_db, page_object.type_id) + if visitor_type_info.type_id: + try: + await Visitor_typeDao.edit_visitor_type_dao(query_db, edit_visitor_type) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='访客类型不存在') + + @classmethod + async def delete_visitor_type_services(cls, query_db: AsyncSession, page_object: DeleteVisitor_typeModel): + """ + 删除访客类型信息service + + :param query_db: orm对象 + :param page_object: 删除访客类型对象 + :return: 删除访客类型校验结果 + """ + if page_object.type_ids: + type_id_list = page_object.type_ids.split(',') + try: + for type_id in type_id_list: + await Visitor_typeDao.delete_visitor_type_dao(query_db, Visitor_typeModel(typeId=type_id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入访客类型ID为空') + + @classmethod + async def visitor_type_detail_services(cls, query_db: AsyncSession, type_id: int): + """ + 获取访客类型详细信息service + + :param query_db: orm对象 + :param type_id: 访客类型ID + :return: 访客类型ID对应的信息 + """ + visitor_type = await Visitor_typeDao.get_visitor_type_detail_by_id(query_db, type_id=type_id) + if visitor_type: + result = Visitor_typeModel(**CamelCaseUtil.transform_result(visitor_type)) + else: + result = Visitor_typeModel(**dict()) + + return result + + @staticmethod + async def export_visitor_type_list_services(visitor_type_list: List): + """ + 导出访客类型信息service + + :param visitor_type_list: 访客类型信息列表 + :return: 访客类型信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'typeId': '访客类型ID', + 'name': '类型名称', + 'value': '类型值', + 'createTime': '创建时间', + 'createBy': '创建者', + 'updateTime': '修改时间', + 'updateBy': '修改者', + } + binary_data = ExcelUtil.export_list2excel(visitor_type_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/words_service.py b/ruoyi-fastapi-backend/module_admin/service/words_service.py new file mode 100644 index 0000000..2026749 --- /dev/null +++ b/ruoyi-fastapi-backend/module_admin/service/words_service.py @@ -0,0 +1,163 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.dao.words_dao import WordsDao +from module_admin.entity.vo.words_vo import DeleteWordsModel, WordsModel, WordsPageQueryModel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil +import random + + +class WordsService: + """ + 引导词模块服务层 + """ + + # 随机获取引导词 + @classmethod + async def get_random_guide_word_services(cls, query_db: AsyncSession, type: str): + """ + 随机获取引导词 + + :param query_db: orm对象 + :param type: 引导词类型 + :return: 引导词信息 + """ + guide_word = await WordsDao.get_words_by_type(query_db, type) + if guide_word: + return guide_word[random.randint(0, len(guide_word) - 1)] + + return guide_word + + # 批量禁用/启用引导词 + @classmethod + async def edit_del_words_services(cls, query_db: AsyncSession, guide_ids: str, del_flg:str): + + try: + await WordsDao.edit_del_words(query_db, guide_ids, del_flg) + await query_db.commit() + return CrudResponseModel(is_success=True, message='批量修改') + except Exception as e: + query_db.rollback() + raise e + + + @classmethod + async def get_words_list_services( + cls, query_db: AsyncSession, query_object: WordsPageQueryModel, is_page: bool = False + ): + """ + 获取引导词列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 引导词列表信息对象 + """ + words_list_result = await WordsDao.get_words_list(query_db, query_object, is_page) + + return words_list_result + + + @classmethod + async def add_words_services(cls, query_db: AsyncSession, page_object: WordsModel): + """ + 新增引导词信息service + + :param query_db: orm对象 + :param page_object: 新增引导词对象 + :return: 新增引导词校验结果 + """ + try: + await WordsDao.add_words_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_words_services(cls, query_db: AsyncSession, page_object: WordsModel): + """ + 编辑引导词信息service + + :param query_db: orm对象 + :param page_object: 编辑引导词对象 + :return: 编辑引导词校验结果 + """ + edit_words = page_object.model_dump(exclude_unset=True, exclude={'create_time', 'create_by', }) + words_info = await cls.words_detail_services(query_db, page_object.guide_id) + if words_info.guide_id: + try: + await WordsDao.edit_words_dao(query_db, edit_words) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='引导词不存在') + + @classmethod + async def delete_words_services(cls, query_db: AsyncSession, page_object: DeleteWordsModel): + """ + 删除引导词信息service + + :param query_db: orm对象 + :param page_object: 删除引导词对象 + :return: 删除引导词校验结果 + """ + if page_object.guide_ids: + guide_id_list = page_object.guide_ids.split(',') + try: + for guide_id in guide_id_list: + await WordsDao.delete_words_dao(query_db, WordsModel(guideId=guide_id)) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入引导词id为空') + + @classmethod + async def words_detail_services(cls, query_db: AsyncSession, guide_id: int): + """ + 获取引导词详细信息service + + :param query_db: orm对象 + :param guide_id: 引导词id + :return: 引导词id对应的信息 + """ + words = await WordsDao.get_words_detail_by_id(query_db, guide_id=guide_id) + if words: + result = WordsModel(**CamelCaseUtil.transform_result(words)) + else: + result = WordsModel(**dict()) + + return result + + @staticmethod + async def export_words_list_services(words_list: List): + """ + 导出引导词信息service + + :param words_list: 引导词信息列表 + :return: 引导词信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'guideId': '引导词id', + 'type': '引导词类型', + 'text': '文本内容', + 'createTime': '创建时间', + 'updateTime': '修改时间', + 'createBy': '创建者', + 'remark': '备注', + 'delFlag': '删除标志', + } + binary_data = ExcelUtil.export_list2excel(words_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_generator/controller/gen_controller.py b/ruoyi-fastapi-backend/module_generator/controller/gen_controller.py new file mode 100644 index 0000000..4e227c1 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/controller/gen_controller.py @@ -0,0 +1,158 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Query, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.env import GenConfig +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckRoleInterfaceAuth, CheckUserInterfaceAuth +from module_admin.service.login_service import LoginService +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_generator.entity.vo.gen_vo import DeleteGenTableModel, EditGenTableModel, GenTablePageQueryModel +from module_generator.service.gen_service import GenTableColumnService, GenTableService +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +genController = APIRouter(prefix='/tool/gen', dependencies=[Depends(LoginService.get_current_user)]) + + +@genController.get( + '/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:list'))] +) +async def get_gen_table_list( + request: Request, + gen_page_query: GenTablePageQueryModel = Depends(GenTablePageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + # 获取分页数据 + gen_page_query_result = await GenTableService.get_gen_table_list_services(query_db, gen_page_query, is_page=True) + logger.info('获取成功') + + return ResponseUtil.success(model_content=gen_page_query_result) + + +@genController.get( + '/db/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:list'))] +) +async def get_gen_db_table_list( + request: Request, + gen_page_query: GenTablePageQueryModel = Depends(GenTablePageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + # 获取分页数据 + gen_page_query_result = await GenTableService.get_gen_db_table_list_services(query_db, gen_page_query, is_page=True) + logger.info('获取成功') + + return ResponseUtil.success(model_content=gen_page_query_result) + + +@genController.post('/importTable', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:import'))]) +@Log(title='代码生成', business_type=BusinessType.IMPORT) +async def import_gen_table( + request: Request, + tables: str = Query(), + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + table_names = tables.split(',') if tables else [] + add_gen_table_list = await GenTableService.get_gen_db_table_list_by_name_services(query_db, table_names) + add_gen_table_result = await GenTableService.import_gen_table_services(query_db, add_gen_table_list, current_user) + logger.info(add_gen_table_result.message) + + return ResponseUtil.success(msg=add_gen_table_result.message) + + +@genController.put('', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:edit'))]) +@ValidateFields(validate_model='edit_gen_table') +@Log(title='代码生成', business_type=BusinessType.UPDATE) +async def edit_gen_table( + request: Request, + edit_gen_table: EditGenTableModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + edit_gen_table.update_by = current_user.user.user_name + edit_gen_table.update_time = datetime.now() + await GenTableService.validate_edit(edit_gen_table) + edit_gen_result = await GenTableService.edit_gen_table_services(query_db, edit_gen_table) + logger.info(edit_gen_result.message) + + return ResponseUtil.success(msg=edit_gen_result.message) + + +@genController.delete('/{table_ids}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:remove'))]) +@Log(title='代码生成', business_type=BusinessType.DELETE) +async def delete_gen_table(request: Request, table_ids: str, query_db: AsyncSession = Depends(get_db)): + delete_gen_table = DeleteGenTableModel(tableIds=table_ids) + delete_gen_table_result = await GenTableService.delete_gen_table_services(query_db, delete_gen_table) + logger.info(delete_gen_table_result.message) + + return ResponseUtil.success(msg=delete_gen_table_result.message) + + +@genController.post('/createTable', dependencies=[Depends(CheckRoleInterfaceAuth('admin'))]) +@Log(title='创建表', business_type=BusinessType.OTHER) +async def create_table( + request: Request, + sql: str = Query(), + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + create_table_result = await GenTableService.create_table_services(query_db, sql, current_user) + logger.info(create_table_result.message) + + return ResponseUtil.success(msg=create_table_result.message) + + +@genController.get('/batchGenCode', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:code'))]) +@Log(title='代码生成', business_type=BusinessType.GENCODE) +async def batch_gen_code(request: Request, tables: str = Query(), query_db: AsyncSession = Depends(get_db)): + table_names = tables.split(',') if tables else [] + batch_gen_code_result = await GenTableService.batch_gen_code_services(query_db, table_names) + logger.info('生成代码成功') + + return ResponseUtil.streaming(data=bytes2file_response(batch_gen_code_result)) + + +@genController.get('/genCode/{table_name}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:code'))]) +@Log(title='代码生成', business_type=BusinessType.GENCODE) +async def gen_code_local(request: Request, table_name: str, query_db: AsyncSession = Depends(get_db)): + if not GenConfig.allow_overwrite: + logger.error('【系统预设】不允许生成文件覆盖到本地') + return ResponseUtil.error('【系统预设】不允许生成文件覆盖到本地') + gen_code_local_result = await GenTableService.generate_code_services(query_db, table_name) + logger.info(gen_code_local_result.message) + + return ResponseUtil.success(msg=gen_code_local_result.message) + + +@genController.get('/{table_id}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:query'))]) +async def query_detail_gen_table(request: Request, table_id: int, query_db: AsyncSession = Depends(get_db)): + gen_table = await GenTableService.get_gen_table_by_id_services(query_db, table_id) + gen_tables = await GenTableService.get_gen_table_all_services(query_db) + gen_columns = await GenTableColumnService.get_gen_table_column_list_by_table_id_services(query_db, table_id) + gen_table_detail_result = dict(info=gen_table, rows=gen_columns, tables=gen_tables) + logger.info(f'获取table_id为{table_id}的信息成功') + + return ResponseUtil.success(data=gen_table_detail_result) + + +@genController.get('/preview/{table_id}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:preview'))]) +async def preview_code(request: Request, table_id: int, query_db: AsyncSession = Depends(get_db)): + preview_code_result = await GenTableService.preview_code_services(query_db, table_id) + logger.info('获取预览代码成功') + + return ResponseUtil.success(data=preview_code_result) + + +@genController.get('/synchDb/{table_name}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:edit'))]) +@Log(title='代码生成', business_type=BusinessType.UPDATE) +async def sync_db(request: Request, table_name: str, query_db: AsyncSession = Depends(get_db)): + sync_db_result = await GenTableService.sync_db_services(query_db, table_name) + logger.info(sync_db_result.message) + + return ResponseUtil.success(data=sync_db_result.message) diff --git a/ruoyi-fastapi-backend/module_generator/dao/gen_dao.py b/ruoyi-fastapi-backend/module_generator/dao/gen_dao.py new file mode 100644 index 0000000..937668a --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/dao/gen_dao.py @@ -0,0 +1,393 @@ +from datetime import datetime, time +from sqlalchemy import delete, func, select, text, update +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload +from sqlglot.expressions import Expression +from typing import List +from config.env import DataBaseConfig +from module_generator.entity.do.gen_do import GenTable, GenTableColumn +from module_generator.entity.vo.gen_vo import ( + GenTableBaseModel, + GenTableColumnBaseModel, + GenTableColumnModel, + GenTableModel, + GenTablePageQueryModel, +) +from utils.page_util import PageUtil + + +class GenTableDao: + """ + 代码生成业务表模块数据库操作层 + """ + + @classmethod + async def get_gen_table_by_id(cls, db: AsyncSession, table_id: int): + """ + 根据业务表id获取需要生成的业务表信息 + + :param db: orm对象 + :param table_id: 业务表id + :return: 需要生成的业务表信息对象 + """ + gen_table_info = ( + ( + await db.execute( + select(GenTable).options(selectinload(GenTable.columns)).where(GenTable.table_id == table_id) + ) + ) + .scalars() + .first() + ) + + return gen_table_info + + @classmethod + async def get_gen_table_by_name(cls, db: AsyncSession, table_name: str): + """ + 根据业务表名称获取需要生成的业务表信息 + + :param db: orm对象 + :param table_name: 业务表名称 + :return: 需要生成的业务表信息对象 + """ + gen_table_info = ( + ( + await db.execute( + select(GenTable).options(selectinload(GenTable.columns)).where(GenTable.table_name == table_name) + ) + ) + .scalars() + .first() + ) + + return gen_table_info + + @classmethod + async def get_gen_table_all(cls, db: AsyncSession): + """ + 获取所有业务表信息 + + :param db: orm对象 + :return: 所有业务表信息 + """ + gen_table_all = (await db.execute(select(GenTable).options(selectinload(GenTable.columns)))).scalars().all() + + return gen_table_all + + @classmethod + async def create_table_by_sql_dao(cls, db: AsyncSession, sql_statements: List[Expression]): + """ + 根据sql语句创建表结构 + + :param db: orm对象 + :param sql_statements: sql语句的ast列表 + :return: + """ + for sql_statement in sql_statements: + sql = sql_statement.sql(dialect=DataBaseConfig.sqlglot_parse_dialect) + await db.execute(text(sql)) + + @classmethod + async def get_gen_table_list(cls, db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False): + """ + 根据查询参数获取代码生成业务表列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 代码生成业务表列表信息对象 + """ + query = ( + select(GenTable) + .options(selectinload(GenTable.columns)) + .where( + func.lower(GenTable.table_name).like(f'%{query_object.table_name.lower()}%') + if query_object.table_name + else True, + func.lower(GenTable.table_comment).like(f'%{query_object.table_comment.lower()}%') + if query_object.table_comment + else True, + GenTable.create_time.between( + datetime.combine(datetime.strptime(query_object.begin_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_time and query_object.end_time + else True, + ) + .distinct() + ) + gen_table_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return gen_table_list + + @classmethod + async def get_gen_db_table_list(cls, db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False): + """ + 根据查询参数获取数据库列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 数据库列表信息对象 + """ + if DataBaseConfig.db_type == 'postgresql': + query_sql = """ + table_name as table_name, + table_comment as table_comment, + create_time as create_time, + update_time as update_time + from + list_table + where + table_name not like 'apscheduler_%' + and table_name not like 'gen_%' + and table_name not in (select table_name from gen_table) + """ + else: + query_sql = """ + table_name as table_name, + table_comment as table_comment, + create_time as create_time, + update_time as update_time + from + information_schema.tables + where + table_schema = (select database()) + and table_name not like 'apscheduler\_%' + and table_name not like 'gen\_%' + and table_name not in (select table_name from gen_table) + """ + if query_object.table_name: + query_sql += """and lower(table_name) like lower(concat('%', :table_name, '%'))""" + if query_object.table_comment: + query_sql += """and lower(table_comment) like lower(concat('%', :table_comment, '%'))""" + if query_object.begin_time: + if DataBaseConfig.db_type == 'postgresql': + query_sql += """and create_time::date >= to_date(:begin_time, 'yyyy-MM-dd')""" + else: + query_sql += """and date_format(create_time, '%Y%m%d') >= date_format(:begin_time, '%Y%m%d')""" + if query_object.end_time: + if DataBaseConfig.db_type == 'postgresql': + query_sql += """and create_time::date <= to_date(:end_time, 'yyyy-MM-dd')""" + else: + query_sql += """and date_format(create_time, '%Y%m%d') >= date_format(:end_time, '%Y%m%d')""" + query_sql += """order by create_time desc""" + query = select( + text(query_sql).bindparams( + **{ + k: v + for k, v in query_object.model_dump(exclude_none=True, exclude={'page_num', 'page_size'}).items() + } + ) + ) + gen_db_table_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return gen_db_table_list + + @classmethod + async def get_gen_db_table_list_by_names(cls, db: AsyncSession, table_names: List[str]): + """ + 根据业务表名称组获取数据库列表信息 + + :param db: orm对象 + :param table_names: 业务表名称组 + :return: 数据库列表信息对象 + """ + if DataBaseConfig.db_type == 'postgresql': + query_sql = """ + select + table_name as table_name, + table_comment as table_comment, + create_time as create_time, + update_time as update_time + from + list_table + where + table_name not like 'qrtz_%' + and table_name not like 'gen_%' + and table_name = any(:table_names) + """ + else: + query_sql = """ + select + table_name as table_name, + table_comment as table_comment, + create_time as create_time, + update_time as update_time + from + information_schema.tables + where + table_name not like 'qrtz\_%' + and table_name not like 'gen\_%' + and table_schema = (select database()) + and table_name in :table_names + """ + query = text(query_sql).bindparams(table_names=tuple(table_names)) + gen_db_table_list = (await db.execute(query)).fetchall() + + return gen_db_table_list + + @classmethod + async def add_gen_table_dao(cls, db: AsyncSession, gen_table: GenTableModel): + """ + 新增业务表数据库操作 + + :param db: orm对象 + :param gen_table: 业务表对象 + :return: + """ + db_gen_table = GenTable(**GenTableBaseModel(**gen_table.model_dump(by_alias=True)).model_dump()) + db.add(db_gen_table) + await db.flush() + + return db_gen_table + + @classmethod + async def edit_gen_table_dao(cls, db: AsyncSession, gen_table: dict): + """ + 编辑业务表数据库操作 + + :param db: orm对象 + :param gen_table: 需要更新的业务表字典 + :return: + """ + await db.execute(update(GenTable), [GenTableBaseModel(**gen_table).model_dump()]) + + @classmethod + async def delete_gen_table_dao(cls, db: AsyncSession, gen_table: GenTableModel): + """ + 删除业务表数据库操作 + + :param db: orm对象 + :param gen_table: 业务表对象 + :return: + """ + await db.execute(delete(GenTable).where(GenTable.table_id.in_([gen_table.table_id]))) + + +class GenTableColumnDao: + """ + 代码生成业务表字段模块数据库操作层 + """ + + @classmethod + async def get_gen_table_column_list_by_table_id(cls, db: AsyncSession, table_id: int): + """ + 根据业务表id获取需要生成的业务表字段列表信息 + + :param db: orm对象 + :param table_id: 业务表id + :return: 需要生成的业务表字段列表信息对象 + """ + gen_table_column_list = ( + ( + await db.execute( + select(GenTableColumn).where(GenTableColumn.table_id == table_id).order_by(GenTableColumn.sort) + ) + ) + .scalars() + .all() + ) + + return gen_table_column_list + + @classmethod + async def get_gen_db_table_columns_by_name(cls, db: AsyncSession, table_name: str): + """ + 根据业务表名称获取业务表字段列表信息 + + :param db: orm对象 + :param table_name: 业务表名称 + :return: 业务表字段列表信息对象 + """ + if DataBaseConfig.db_type == 'postgresql': + query_sql = """ + select + column_name, is_required, is_pk, sort, column_comment, is_increment, column_type + from + list_column + where + table_name = :table_name + """ + else: + query_sql = """ + select + column_name as column_name, + case + when is_nullable = 'no' and column_key != 'PRI' then '1' + else '0' + end as is_required, + case + when column_key = 'PRI' then '1' + else '0' + end as is_pk, + ordinal_position as sort, + column_comment as column_comment, + case + when extra = 'auto_increment' then '1' + else '0' + end as is_increment, + column_type as column_type + from + information_schema.columns + where + table_schema = (select database()) + and table_name = :table_name + order by + ordinal_position + """ + query = text(query_sql).bindparams(table_name=table_name) + gen_db_table_columns = (await db.execute(query)).fetchall() + + return gen_db_table_columns + + @classmethod + async def add_gen_table_column_dao(cls, db: AsyncSession, gen_table_column: GenTableColumnModel): + """ + 新增业务表字段数据库操作 + + :param db: orm对象 + :param gen_table_column: 岗位对象 + :return: + """ + db_gen_table_column = GenTableColumn( + **GenTableColumnBaseModel(**gen_table_column.model_dump(by_alias=True)).model_dump() + ) + db.add(db_gen_table_column) + await db.flush() + + return db_gen_table_column + + @classmethod + async def edit_gen_table_column_dao(cls, db: AsyncSession, gen_table_column: dict): + """ + 编辑业务表字段数据库操作 + + :param db: orm对象 + :param gen_table_column: 需要更新的业务表字段字典 + :return: + """ + await db.execute(update(GenTableColumn), [GenTableColumnBaseModel(**gen_table_column).model_dump()]) + + @classmethod + async def delete_gen_table_column_by_table_id_dao(cls, db: AsyncSession, gen_table_column: GenTableColumnModel): + """ + 通过业务表id删除业务表字段数据库操作 + + :param db: orm对象 + :param gen_table_column: 业务表字段对象 + :return: + """ + await db.execute(delete(GenTableColumn).where(GenTableColumn.table_id.in_([gen_table_column.table_id]))) + + @classmethod + async def delete_gen_table_column_by_column_id_dao(cls, db: AsyncSession, gen_table_column: GenTableColumnModel): + """ + 通过业务字段id删除业务表字段数据库操作 + + :param db: orm对象 + :param post: 业务表字段对象 + :return: + """ + await db.execute(delete(GenTableColumn).where(GenTableColumn.column_id.in_([gen_table_column.column_id]))) diff --git a/ruoyi-fastapi-backend/module_generator/entity/do/gen_do.py b/ruoyi-fastapi-backend/module_generator/entity/do/gen_do.py new file mode 100644 index 0000000..e64d0bf --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/entity/do/gen_do.py @@ -0,0 +1,74 @@ +from datetime import datetime +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String +from sqlalchemy.orm import relationship +from config.database import Base + + +class GenTable(Base): + """ + 代码生成业务表 + """ + + __tablename__ = 'gen_table' + + table_id = Column(Integer, primary_key=True, autoincrement=True, comment='编号') + table_name = Column(String(200), nullable=True, default='', comment='表名称') + table_comment = Column(String(500), nullable=True, default='', comment='表描述') + sub_table_name = Column(String(64), nullable=True, comment='关联子表的表名') + sub_table_fk_name = Column(String(64), nullable=True, comment='子表关联的外键名') + class_name = Column(String(100), nullable=True, default='', comment='实体类名称') + tpl_category = Column(String(200), nullable=True, default='crud', comment='使用的模板(crud单表操作 tree树表操作)') + tpl_web_type = Column( + String(30), nullable=True, default='', comment='前端模板类型(element-ui模版 element-plus模版)' + ) + package_name = Column(String(100), nullable=True, comment='生成包路径') + module_name = Column(String(30), nullable=True, comment='生成模块名') + business_name = Column(String(30), nullable=True, comment='生成业务名') + function_name = Column(String(100), nullable=True, comment='生成功能名') + function_author = Column(String(100), nullable=True, comment='生成功能作者') + gen_type = Column(String(1), nullable=True, default='0', comment='生成代码方式(0zip压缩包 1自定义路径)') + gen_path = Column(String(200), nullable=True, default='/', comment='生成路径(不填默认项目路径)') + options = Column(String(1000), nullable=True, comment='其它生成选项') + create_by = Column(String(64), default='', comment='创建者') + create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间') + update_by = Column(String(64), default='', comment='更新者') + update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间') + remark = Column(String(500), nullable=True, default=None, comment='备注') + + columns = relationship('GenTableColumn', order_by='GenTableColumn.sort', back_populates='tables') + + +class GenTableColumn(Base): + """ + 代码生成业务表字段 + """ + + __tablename__ = 'gen_table_column' + + column_id = Column(Integer, primary_key=True, autoincrement=True, comment='编号') + table_id = Column(Integer, ForeignKey('gen_table.table_id'), nullable=True, comment='归属表编号') + column_name = Column(String(200), nullable=True, comment='列名称') + column_comment = Column(String(500), nullable=True, comment='列描述') + column_type = Column(String(100), nullable=True, comment='列类型') + python_type = Column(String(500), nullable=True, comment='PYTHON类型') + python_field = Column(String(200), nullable=True, comment='PYTHON字段名') + is_pk = Column(String(1), nullable=True, comment='是否主键(1是)') + is_increment = Column(String(1), nullable=True, comment='是否自增(1是)') + is_required = Column(String(1), nullable=True, comment='是否必填(1是)') + is_unique = Column(String(1), nullable=True, comment='是否唯一(1是)') + is_insert = Column(String(1), nullable=True, comment='是否为插入字段(1是)') + is_edit = Column(String(1), nullable=True, comment='是否编辑字段(1是)') + is_list = Column(String(1), nullable=True, comment='是否列表字段(1是)') + is_query = Column(String(1), nullable=True, comment='是否查询字段(1是)') + query_type = Column(String(200), nullable=True, default='EQ', comment='查询方式(等于、不等于、大于、小于、范围)') + html_type = Column( + String(200), nullable=True, comment='显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)' + ) + dict_type = Column(String(200), nullable=True, default='', comment='字典类型') + sort = Column(Integer, nullable=True, comment='排序') + create_by = Column(String(64), default='', comment='创建者') + create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间') + update_by = Column(String(64), default='', comment='更新者') + update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间') + + tables = relationship('GenTable', back_populates='columns') diff --git a/ruoyi-fastapi-backend/module_generator/entity/vo/gen_vo.py b/ruoyi-fastapi-backend/module_generator/entity/vo/gen_vo.py new file mode 100644 index 0000000..e5d7917 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/entity/vo/gen_vo.py @@ -0,0 +1,264 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field, model_validator +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import NotBlank +from typing import List, Literal, Optional +from config.constant import GenConstant +from module_admin.annotation.pydantic_annotation import as_query +from utils.string_util import StringUtil + + +class GenTableBaseModel(BaseModel): + """ + 代码生成业务表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + table_id: Optional[int] = Field(default=None, description='编号') + table_name: Optional[str] = Field(default=None, description='表名称') + table_comment: Optional[str] = Field(default=None, description='表描述') + sub_table_name: Optional[str] = Field(default=None, description='关联子表的表名') + sub_table_fk_name: Optional[str] = Field(default=None, description='子表关联的外键名') + class_name: Optional[str] = Field(default=None, description='实体类名称') + tpl_category: Optional[str] = Field(default=None, description='使用的模板(crud单表操作 tree树表操作)') + tpl_web_type: Optional[str] = Field(default=None, description='前端模板类型(element-ui模版 element-plus模版)') + package_name: Optional[str] = Field(default=None, description='生成包路径') + module_name: Optional[str] = Field(default=None, description='生成模块名') + business_name: Optional[str] = Field(default=None, description='生成业务名') + function_name: Optional[str] = Field(default=None, description='生成功能名') + function_author: Optional[str] = Field(default=None, description='生成功能作者') + gen_type: Optional[Literal['0', '1']] = Field(default=None, description='生成代码方式(0zip压缩包 1自定义路径)') + gen_path: Optional[str] = Field(default=None, description='生成路径(不填默认项目路径)') + options: Optional[str] = Field(default=None, description='其它生成选项') + create_by: Optional[str] = Field(default=None, description='创建者') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + update_by: Optional[str] = Field(default=None, description='更新者') + update_time: Optional[datetime] = Field(default=None, description='更新时间') + remark: Optional[str] = Field(default=None, description='备注') + + @NotBlank(field_name='table_name', message='表名称不能为空') + def get_table_name(self): + return self.table_name + + @NotBlank(field_name='table_comment', message='表描述不能为空') + def get_table_comment(self): + return self.table_comment + + @NotBlank(field_name='class_name', message='实体类名称不能为空') + def get_class_name(self): + return self.class_name + + @NotBlank(field_name='package_name', message='生成包路径不能为空') + def get_package_name(self): + return self.package_name + + @NotBlank(field_name='module_name', message='生成模块名不能为空') + def get_module_name(self): + return self.module_name + + @NotBlank(field_name='business_name', message='生成业务名不能为空') + def get_business_name(self): + return self.business_name + + @NotBlank(field_name='function_name', message='生成功能名不能为空') + def get_function_name(self): + return self.function_name + + @NotBlank(field_name='function_author', message='生成功能作者不能为空') + def get_function_author(self): + return self.function_author + + def validate_fields(self): + self.get_table_name() + self.get_table_comment() + self.get_class_name() + self.get_package_name() + self.get_module_name() + self.get_business_name() + self.get_function_name() + self.get_function_author() + + +class GenTableModel(GenTableBaseModel): + """ + 代码生成业务表模型 + """ + + pk_column: Optional['GenTableColumnModel'] = Field(default=None, description='主键信息') + sub_table: Optional['GenTableModel'] = Field(default=None, description='子表信息') + columns: Optional[List['GenTableColumnModel']] = Field(default=None, description='表列信息') + tree_code: Optional[str] = Field(default=None, description='树编码字段') + tree_parent_code: Optional[str] = Field(default=None, description='树父编码字段') + tree_name: Optional[str] = Field(default=None, description='树名称字段') + parent_menu_id: Optional[int] = Field(default=None, description='上级菜单ID字段') + parent_menu_name: Optional[str] = Field(default=None, description='上级菜单名称字段') + sub: Optional[bool] = Field(default=None, description='是否为子表') + tree: Optional[bool] = Field(default=None, description='是否为树表') + crud: Optional[bool] = Field(default=None, description='是否为单表') + + @model_validator(mode='after') + def check_some_is(self) -> 'GenTableModel': + self.sub = True if self.tpl_category and self.tpl_category == GenConstant.TPL_SUB else False + self.tree = True if self.tpl_category and self.tpl_category == GenConstant.TPL_TREE else False + self.crud = True if self.tpl_category and self.tpl_category == GenConstant.TPL_CRUD else False + return self + + +class EditGenTableModel(GenTableModel): + """ + 修改代码生成业务表模型 + """ + + params: Optional['GenTableParamsModel'] = Field(default=None, description='业务表参数') + + +class GenTableParamsModel(BaseModel): + """ + 代码生成业务表参数模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + tree_code: Optional[str] = Field(default=None, description='树编码字段') + tree_parent_code: Optional[str] = Field(default=None, description='树父编码字段') + tree_name: Optional[str] = Field(default=None, description='树名称字段') + parent_menu_id: Optional[int] = Field(default=None, description='上级菜单ID字段') + + +class GenTableQueryModel(GenTableBaseModel): + """ + 代码生成业务表不分页查询模型 + """ + + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +@as_query +class GenTablePageQueryModel(GenTableQueryModel): + """ + 代码生成业务表分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteGenTableModel(BaseModel): + """ + 删除代码生成业务表模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + table_ids: str = Field(description='需要删除的代码生成业务表ID') + + +class GenTableColumnBaseModel(BaseModel): + """ + 代码生成业务表字段对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + column_id: Optional[int] = Field(default=None, description='编号') + table_id: Optional[int] = Field(default=None, description='归属表编号') + column_name: Optional[str] = Field(default=None, description='列名称') + column_comment: Optional[str] = Field(default=None, description='列描述') + column_type: Optional[str] = Field(default=None, description='列类型') + python_type: Optional[str] = Field(default=None, description='PYTHON类型') + python_field: Optional[str] = Field(default=None, description='PYTHON字段名') + is_pk: Optional[str] = Field(default=None, description='是否主键(1是)') + is_increment: Optional[str] = Field(default=None, description='是否自增(1是)') + is_required: Optional[str] = Field(default=None, description='是否必填(1是)') + is_unique: Optional[str] = Field(default=None, description='是否唯一(1是)') + is_insert: Optional[str] = Field(default=None, description='是否为插入字段(1是)') + is_edit: Optional[str] = Field(default=None, description='是否编辑字段(1是)') + is_list: Optional[str] = Field(default=None, description='是否列表字段(1是)') + is_query: Optional[str] = Field(default=None, description='是否查询字段(1是)') + query_type: Optional[str] = Field(default=None, description='查询方式(等于、不等于、大于、小于、范围)') + html_type: Optional[str] = Field( + default=None, description='显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)' + ) + dict_type: Optional[str] = Field(default=None, description='字典类型') + sort: Optional[int] = Field(default=None, description='排序') + create_by: Optional[str] = Field(default=None, description='创建者') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + update_by: Optional[str] = Field(default=None, description='更新者') + update_time: Optional[datetime] = Field(default=None, description='更新时间') + + @NotBlank(field_name='python_field', message='Python属性不能为空') + def get_python_field(self): + return self.python_field + + def validate_fields(self): + self.get_python_field() + + +class GenTableColumnModel(GenTableColumnBaseModel): + """ + 代码生成业务表字段模型 + """ + + cap_python_field: Optional[str] = Field(default=None, description='字段大写形式') + pk: Optional[bool] = Field(default=None, description='是否主键') + increment: Optional[bool] = Field(default=None, description='是否自增') + required: Optional[bool] = Field(default=None, description='是否必填') + unique: Optional[bool] = Field(default=None, description='是否唯一') + insert: Optional[bool] = Field(default=None, description='是否为插入字段') + edit: Optional[bool] = Field(default=None, description='是否编辑字段') + list: Optional[bool] = Field(default=None, description='是否列表字段') + query: Optional[bool] = Field(default=None, description='是否查询字段') + super_column: Optional[bool] = Field(default=None, description='是否为基类字段') + usable_column: Optional[bool] = Field(default=None, description='是否为基类字段白名单') + + @model_validator(mode='after') + def check_some_is(self) -> 'GenTableModel': + self.cap_python_field = self.python_field[0].upper() + self.python_field[1:] if self.python_field else None + self.pk = True if self.is_pk and self.is_pk == '1' else False + self.increment = True if self.is_increment and self.is_increment == '1' else False + self.required = True if self.is_required and self.is_required == '1' else False + self.unique = True if self.is_unique and self.is_unique == '1' else False + self.insert = True if self.is_insert and self.is_insert == '1' else False + self.edit = True if self.is_edit and self.is_edit == '1' else False + self.list = True if self.is_list and self.is_list == '1' else False + self.query = True if self.is_query and self.is_query == '1' else False + self.super_column = ( + True + if StringUtil.equals_any_ignore_case(self.python_field, GenConstant.TREE_ENTITY + GenConstant.BASE_ENTITY) + else False + ) + self.usable_column = ( + True if StringUtil.equals_any_ignore_case(self.python_field, ['parentId', 'orderNum', 'remark']) else False + ) + return self + + +class GenTableColumnQueryModel(GenTableColumnBaseModel): + """ + 代码生成业务表字段不分页查询模型 + """ + + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +@as_query +class GenTableColumnPageQueryModel(GenTableColumnQueryModel): + """ + 代码生成业务表字段分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteGenTableColumnModel(BaseModel): + """ + 删除代码生成业务表字段模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + column_ids: str = Field(description='需要删除的代码生成业务表字段ID') diff --git a/ruoyi-fastapi-backend/module_generator/service/gen_service.py b/ruoyi-fastapi-backend/module_generator/service/gen_service.py new file mode 100644 index 0000000..aec0bbf --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/service/gen_service.py @@ -0,0 +1,499 @@ +import io +import json +import os +import zipfile +from datetime import datetime +from sqlalchemy.ext.asyncio import AsyncSession +from sqlglot import parse as sqlglot_parse +from sqlglot.expressions import Add, Alter, Create, Delete, Drop, Expression, Insert, Table, TruncateTable, Update +from typing import List +from config.constant import GenConstant +from config.env import DataBaseConfig, GenConfig +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_generator.entity.vo.gen_vo import ( + DeleteGenTableModel, + EditGenTableModel, + GenTableColumnModel, + GenTableModel, + GenTablePageQueryModel, +) +from module_generator.dao.gen_dao import GenTableColumnDao, GenTableDao +from utils.common_util import CamelCaseUtil +from utils.gen_util import GenUtils +from utils.template_util import TemplateInitializer, TemplateUtils + + +class GenTableService: + """ + 代码生成业务表服务层 + """ + + @classmethod + async def get_gen_table_list_services( + cls, query_db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False + ): + """ + 获取代码生成业务表列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 代码生成业务列表信息对象 + """ + gen_table_list_result = await GenTableDao.get_gen_table_list(query_db, query_object, is_page) + + return gen_table_list_result + + @classmethod + async def get_gen_db_table_list_services( + cls, query_db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False + ): + """ + 获取数据库列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 数据库列表信息对象 + """ + gen_db_table_list_result = await GenTableDao.get_gen_db_table_list(query_db, query_object, is_page) + + return gen_db_table_list_result + + @classmethod + async def get_gen_db_table_list_by_name_services(cls, query_db: AsyncSession, table_names: List[str]): + """ + 根据表名称组获取数据库列表信息service + + :param query_db: orm对象 + :param table_names: 表名称组 + :return: 数据库列表信息对象 + """ + gen_db_table_list_result = await GenTableDao.get_gen_db_table_list_by_names(query_db, table_names) + + return [GenTableModel(**gen_table) for gen_table in CamelCaseUtil.transform_result(gen_db_table_list_result)] + + @classmethod + async def import_gen_table_services( + cls, query_db: AsyncSession, gen_table_list: List[GenTableModel], current_user: CurrentUserModel + ): + """ + 导入表结构service + + :param query_db: orm对象 + :param gen_table_list: 导入表列表 + :param current_user: 当前用户信息对象 + :return: 导入结果 + """ + try: + for table in gen_table_list: + table_name = table.table_name + GenUtils.init_table(table, current_user.user.user_name) + add_gen_table = await GenTableDao.add_gen_table_dao(query_db, table) + if add_gen_table: + table.table_id = add_gen_table.table_id + gen_table_columns = await GenTableColumnDao.get_gen_db_table_columns_by_name(query_db, table_name) + for column in [ + GenTableColumnModel(**gen_table_column) + for gen_table_column in CamelCaseUtil.transform_result(gen_table_columns) + ]: + GenUtils.init_column_field(column, table) + await GenTableColumnDao.add_gen_table_column_dao(query_db, column) + await query_db.commit() + return CrudResponseModel(is_success=True, message='导入成功') + except Exception as e: + await query_db.rollback() + raise ServiceException(message=f'导入失败, {str(e)}') + + @classmethod + async def edit_gen_table_services(cls, query_db: AsyncSession, page_object: EditGenTableModel): + """ + 编辑业务表信息service + + :param query_db: orm对象 + :param page_object: 编辑业务表对象 + :return: 编辑业务表校验结果 + """ + edit_gen_table = page_object.model_dump(exclude_unset=True, by_alias=True) + gen_table_info = await cls.get_gen_table_by_id_services(query_db, page_object.table_id) + if gen_table_info.table_id: + try: + edit_gen_table['options'] = json.dumps(edit_gen_table.get('params')) + await GenTableDao.edit_gen_table_dao(query_db, edit_gen_table) + for gen_table_column in page_object.columns: + gen_table_column.update_by = page_object.update_by + gen_table_column.update_time = datetime.now() + await GenTableColumnDao.edit_gen_table_column_dao( + query_db, gen_table_column.model_dump(by_alias=True) + ) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='业务表不存在') + + @classmethod + async def delete_gen_table_services(cls, query_db: AsyncSession, page_object: DeleteGenTableModel): + """ + 删除业务表信息service + + :param query_db: orm对象 + :param page_object: 删除业务表对象 + :return: 删除业务表校验结果 + """ + if page_object.table_ids: + table_id_list = page_object.table_ids.split(',') + try: + for table_id in table_id_list: + await GenTableDao.delete_gen_table_dao(query_db, GenTableModel(tableId=table_id)) + await GenTableColumnDao.delete_gen_table_column_by_table_id_dao( + query_db, GenTableColumnModel(tableId=table_id) + ) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入业务表id为空') + + @classmethod + async def get_gen_table_by_id_services(cls, query_db: AsyncSession, table_id: int): + """ + 获取需要生成的业务表详细信息service + + :param query_db: orm对象 + :param table_id: 需要生成的业务表id + :return: 需要生成的业务表id对应的信息 + """ + gen_table = await GenTableDao.get_gen_table_by_id(query_db, table_id) + result = await cls.set_table_from_options(GenTableModel(**CamelCaseUtil.transform_result(gen_table))) + + return result + + @classmethod + async def get_gen_table_all_services(cls, query_db: AsyncSession): + """ + 获取所有业务表信息service + + :param query_db: orm对象 + :return: 所有业务表信息 + """ + gen_table_all = await GenTableDao.get_gen_table_all(query_db) + result = [GenTableModel(**gen_table) for gen_table in CamelCaseUtil.transform_result(gen_table_all)] + + return result + + @classmethod + async def create_table_services(cls, query_db: AsyncSession, sql: str, current_user: CurrentUserModel): + """ + 创建表结构service + + :param query_db: orm对象 + :param sql: 建表语句 + :param current_user: 当前用户信息对象 + :return: 创建表结构结果 + """ + sql_statements = sqlglot_parse(sql, dialect=DataBaseConfig.sqlglot_parse_dialect) + if cls.__is_valid_create_table(sql_statements): + try: + table_names = cls.__get_table_names(sql_statements) + await GenTableDao.create_table_by_sql_dao(query_db, sql_statements) + gen_table_list = await cls.get_gen_db_table_list_by_name_services(query_db, table_names) + await cls.import_gen_table_services(query_db, gen_table_list, current_user) + + return CrudResponseModel(is_success=True, message='创建表结构成功') + except Exception as e: + raise ServiceException(message=f'创建表结构异常,详细错误信息:{str(e)}') + else: + raise ServiceException(message='建表语句不合法') + + @classmethod + def __is_valid_create_table(cls, sql_statements: List[Expression]): + """ + 校验sql语句是否为合法的建表语句 + + :param sql_statements: sql语句的ast列表 + :return: 校验结果 + """ + validate_create = [isinstance(sql_statement, Create) for sql_statement in sql_statements] + validate_forbidden_keywords = [ + isinstance( + sql_statement, + (Add, Alter, Delete, Drop, Insert, TruncateTable, Update), + ) + for sql_statement in sql_statements + ] + if not any(validate_create) or any(validate_forbidden_keywords): + return False + return True + + @classmethod + def __get_table_names(cls, sql_statements: List[Expression]): + """ + 获取sql语句中所有的建表表名 + + :param sql_statements: sql语句的ast列表 + :return: 建表表名列表 + """ + table_names = [] + for sql_statement in sql_statements: + if isinstance(sql_statement, Create): + table_names.append(sql_statement.find(Table).name) + return table_names + + @classmethod + async def preview_code_services(cls, query_db: AsyncSession, table_id: int): + """ + 预览代码service + + :param query_db: orm对象 + :param table_id: 业务表id + :return: 预览数据列表 + """ + gen_table = GenTableModel( + **CamelCaseUtil.transform_result(await GenTableDao.get_gen_table_by_id(query_db, table_id)) + ) + await cls.set_sub_table(query_db, gen_table) + await cls.set_pk_column(gen_table) + env = TemplateInitializer.init_jinja2() + context = TemplateUtils.prepare_context(gen_table) + template_list = TemplateUtils.get_template_list(gen_table.tpl_category, gen_table.tpl_web_type) + preview_code_result = {} + for template in template_list: + render_content = env.get_template(template).render(**context) + preview_code_result[template] = render_content + return preview_code_result + + @classmethod + async def generate_code_services(cls, query_db: AsyncSession, table_name: str): + """ + 生成代码至指定路径service + + :param query_db: orm对象 + :param table_name: 业务表名称 + :return: 生成代码结果 + """ + env = TemplateInitializer.init_jinja2() + render_info = await cls.__get_gen_render_info(query_db, table_name) + for template in render_info[0]: + try: + render_content = env.get_template(template).render(**render_info[2]) + gen_path = cls.__get_gen_path(render_info[3], template) + os.makedirs(os.path.dirname(gen_path), exist_ok=True) + with open(gen_path, 'w', encoding='utf-8') as f: + f.write(render_content) + except Exception as e: + raise ServiceException( + message=f'渲染模板失败,表名:{render_info[3].table_name},详细错误信息:{str(e)}' + ) + + return CrudResponseModel(is_success=True, message='生成代码成功') + + @classmethod + async def batch_gen_code_services(cls, query_db: AsyncSession, table_names: List[str]): + """ + 批量生成代码service + + :param query_db: orm对象 + :param table_names: 业务表名称组 + :return: 下载代码结果 + """ + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file: + for table_name in table_names: + env = TemplateInitializer.init_jinja2() + render_info = await cls.__get_gen_render_info(query_db, table_name) + for template_file, output_file in zip(render_info[0], render_info[1]): + render_content = env.get_template(template_file).render(**render_info[2]) + zip_file.writestr(output_file, render_content) + + zip_data = zip_buffer.getvalue() + zip_buffer.close() + return zip_data + + @classmethod + async def __get_gen_render_info(cls, query_db: AsyncSession, table_name: str): + """ + 获取生成代码渲染模板相关信息 + + :param query_db: orm对象 + :param table_name: 业务表名称 + :return: 生成代码渲染模板相关信息 + """ + gen_table = GenTableModel( + **CamelCaseUtil.transform_result(await GenTableDao.get_gen_table_by_name(query_db, table_name)) + ) + await cls.set_sub_table(query_db, gen_table) + await cls.set_pk_column(gen_table) + context = TemplateUtils.prepare_context(gen_table) + template_list = TemplateUtils.get_template_list(gen_table.tpl_category, gen_table.tpl_web_type) + output_files = [TemplateUtils.get_file_name(template, gen_table) for template in template_list] + + return [template_list, output_files, context, gen_table] + + @classmethod + def __get_gen_path(cls, gen_table: GenTableModel, template: str): + """ + 根据GenTableModel对象和模板名称生成路径 + + :param gen_table: GenTableModel对象 + :param template: 模板名称 + :return: 生成的路径 + """ + gen_path = gen_table.gen_path + if gen_path == '/': + return os.path.join(os.getcwd(), GenConfig.GEN_PATH, TemplateUtils.get_file_name(template, gen_table)) + else: + return os.path.join(gen_path, TemplateUtils.get_file_name(template, gen_table)) + + @classmethod + async def sync_db_services(cls, query_db: AsyncSession, table_name: str): + """ + 同步数据库service + + :param query_db: orm对象 + :param table_name: 业务表名称 + :return: 同步数据库结果 + """ + gen_table = await GenTableDao.get_gen_table_by_name(query_db, table_name) + table = GenTableModel(**CamelCaseUtil.transform_result(gen_table)) + table_columns = table.columns + table_column_map = {column.column_name: column for column in table_columns} + query_db_table_columns = await GenTableColumnDao.get_gen_db_table_columns_by_name(query_db, table_name) + db_table_columns = [ + GenTableColumnModel(**column) for column in CamelCaseUtil.transform_result(query_db_table_columns) + ] + if not db_table_columns: + raise ServiceException('同步数据失败,原表结构不存在') + db_table_column_names = [column.column_name for column in db_table_columns] + try: + for column in db_table_columns: + GenUtils.init_column_field(column, table) + if column.column_name in table_column_map: + prev_column = table_column_map[column.column_name] + column.column_id = prev_column.column_id + if column.list: + column.dict_type = prev_column.dict_type + column.query_type = prev_column.query_type + if ( + prev_column.is_required != '' + and not column.pk + and (column.insert or column.edit) + and (column.usable_column or column.super_column) + ): + column.is_required = prev_column.is_required + column.html_type = prev_column.html_type + await GenTableColumnDao.edit_gen_table_column_dao(query_db, column.model_dump(by_alias=True)) + else: + await GenTableColumnDao.add_gen_table_column_dao(query_db, column) + del_columns = [column for column in table_columns if column.column_name not in db_table_column_names] + if del_columns: + for column in del_columns: + await GenTableColumnDao.delete_gen_table_column_by_column_id_dao(query_db, column) + await query_db.commit() + return CrudResponseModel(is_success=True, message='同步成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def set_sub_table(cls, query_db: AsyncSession, gen_table: GenTableModel): + """ + 设置主子表信息 + + :param query_db: orm对象 + :param gen_table: 业务表信息 + :return: + """ + if gen_table.sub_table_name: + sub_table = await GenTableDao.get_gen_table_by_name(query_db, gen_table.sub_table_name) + gen_table.sub_table = GenTableModel(**CamelCaseUtil.transform_result(sub_table)) + + @classmethod + async def set_pk_column(cls, gen_table: GenTableModel): + """ + 设置主键列信息 + + :param gen_table: 业务表信息 + :return: + """ + for column in gen_table.columns: + if column.pk: + gen_table.pk_column = column + break + if gen_table.pk_column is None: + gen_table.pk_column = gen_table.columns[0] + if gen_table.tpl_category == GenConstant.TPL_SUB: + for column in gen_table.sub_table.columns: + if column.pk: + gen_table.sub_table.pk_column = column + break + if gen_table.sub_table.columns is None: + gen_table.sub_table.pk_column = gen_table.sub_table.columns[0] + + @classmethod + async def set_table_from_options(cls, gen_table: GenTableModel): + """ + 设置代码生成其他选项值 + + :param gen_table: 生成对象 + :return: 设置后的生成对象 + """ + params_obj = json.loads(gen_table.options) if gen_table.options else None + if params_obj: + gen_table.tree_code = params_obj.get(GenConstant.TREE_CODE) + gen_table.tree_parent_code = params_obj.get(GenConstant.TREE_PARENT_CODE) + gen_table.tree_name = params_obj.get(GenConstant.TREE_NAME) + gen_table.parent_menu_id = params_obj.get(GenConstant.PARENT_MENU_ID) + gen_table.parent_menu_name = params_obj.get(GenConstant.PARENT_MENU_NAME) + + return gen_table + + @classmethod + async def validate_edit(cls, edit_gen_table: EditGenTableModel): + """ + 编辑保存参数校验 + + :param edit_gen_table: 编辑业务表对象 + """ + if edit_gen_table.tpl_category == GenConstant.TPL_TREE: + params_obj = edit_gen_table.params.model_dump(by_alias=True) + + if GenConstant.TREE_CODE not in params_obj: + raise ServiceException(message='树编码字段不能为空') + elif GenConstant.TREE_PARENT_CODE not in params_obj: + raise ServiceException(message='树父编码字段不能为空') + elif GenConstant.TREE_NAME not in params_obj: + raise ServiceException(message='树名称字段不能为空') + elif edit_gen_table.tpl_category == GenConstant.TPL_SUB: + if not edit_gen_table.sub_table_name: + raise ServiceException(message='关联子表的表名不能为空') + elif not edit_gen_table.sub_table_fk_name: + raise ServiceException(message='子表关联的外键名不能为空') + + +class GenTableColumnService: + """ + 代码生成业务表字段服务层 + """ + + @classmethod + async def get_gen_table_column_list_by_table_id_services(cls, query_db: AsyncSession, table_id: int): + """ + 获取业务表字段列表信息service + + :param query_db: orm对象 + :param table_id: 业务表格id + :return: 业务表字段列表信息对象 + """ + gen_table_column_list_result = await GenTableColumnDao.get_gen_table_column_list_by_table_id(query_db, table_id) + + return [ + GenTableColumnModel(**gen_table_column) + for gen_table_column in CamelCaseUtil.transform_result(gen_table_column_list_result) + ] diff --git a/ruoyi-fastapi-backend/module_generator/templates/js/api.js.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/js/api.js.jinja2 new file mode 100644 index 0000000..3a2a5a9 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/js/api.js.jinja2 @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询{{ functionName }}列表 +export function list{{ BusinessName }}(query) { + return request({ + url: '/{{ moduleName }}/{{ businessName }}/list', + method: 'get', + params: query + }) +} + +// 查询{{ functionName }}详细 +export function get{{ BusinessName }}({{ pkColumn.python_field }}) { + return request({ + url: '/{{ moduleName }}/{{ businessName }}/' + {{ pkColumn.python_field }}, + method: 'get' + }) +} + +// 新增{{ functionName }} +export function add{{ BusinessName }}(data) { + return request({ + url: '/{{ moduleName }}/{{ businessName }}', + method: 'post', + data: data + }) +} + +// 修改{{ functionName }} +export function update{{ BusinessName }}(data) { + return request({ + url: '/{{ moduleName }}/{{ businessName }}', + method: 'put', + data: data + }) +} + +// 删除{{ functionName }} +export function del{{ BusinessName }}({{ pkColumn.python_field }}) { + return request({ + url: '/{{ moduleName }}/{{ businessName }}/' + {{ pkColumn.python_field }}, + method: 'delete' + }) +} diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/controller.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/controller.py.jinja2 new file mode 100644 index 0000000..4d5a12e --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/python/controller.py.jinja2 @@ -0,0 +1,125 @@ +{% set pkField = pkColumn.python_field %} +{% set pk_field = pkColumn.python_field | camel_to_snake %} +{% for column in columns %} +{% if column.python_field == "createTime" %} +from datetime import datetime +{% endif %} +{% endfor %} +from fastapi import APIRouter, Depends, Form, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.login_service import LoginService +from {{ packageName }}.service.{{ businessName }}_service import {{ BusinessName }}Service +from {{ packageName }}.entity.vo.{{ businessName }}_vo import Delete{{ BusinessName }}Model, {{ BusinessName }}Model, {{ BusinessName }}PageQueryModel +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +{{ businessName }}Controller = APIRouter(prefix='/{{ moduleName }}/{{ businessName }}', dependencies=[Depends(LoginService.get_current_user)]) + + +@{{ businessName }}Controller.get( + '/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('{{ permissionPrefix }}:list'))] +) +async def get_{{ moduleName }}_{{ businessName }}_list( + request: Request, + {% if table.crud or table.sub %}{{ businessName }}_page_query{% elif table.tree %}{{ businessName }}_query{% endif %}: {{ BusinessName }}PageQueryModel = Depends({{ BusinessName }}PageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + {% if table.crud or table.sub %} + # 获取分页数据 + {{ businessName }}_page_query_result = await {{ BusinessName }}Service.get_{{ businessName }}_list_services(query_db, {{ businessName }}_page_query, is_page=True) + logger.info('获取成功') + + return ResponseUtil.success(model_content={{ businessName }}_page_query_result) + {% elif table.tree %} + {{ businessName }}_query_result = await {{ BusinessName }}Service.get_{{ businessName }}_list_services(query_db, {{ businessName }}_query) + logger.info('获取成功') + + return ResponseUtil.success(data={{ businessName }}_query_result) + {% endif %} + + +@{{ businessName }}Controller.post('', dependencies=[Depends(CheckUserInterfaceAuth('{{ permissionPrefix }}:add'))]) +@ValidateFields(validate_model='add_{{ businessName }}') +@Log(title='{{ functionName }}', business_type=BusinessType.INSERT) +async def add_{{ moduleName }}_{{ businessName }}( + request: Request, + add_{{ businessName }}: {{ BusinessName }}Model, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + {% for column in columns %} + {% if column.python_field == "createBy" %} + add_{{ businessName }}.create_by = current_user.user.user_name + {% elif column.python_field == "createTime" %} + add_{{ businessName }}.create_time = datetime.now() + {% elif column.python_field == "updateBy" %} + add_{{ businessName }}.update_by = current_user.user.user_name + {% elif column.python_field == "updateTime" %} + add_{{ businessName }}.update_time = datetime.now() + {% endif %} + {% endfor %} + add_{{ businessName }}_result = await {{ BusinessName }}Service.add_{{ businessName }}_services(query_db, add_{{ businessName }}) + logger.info(add_{{ businessName }}_result.message) + + return ResponseUtil.success(msg=add_{{ businessName }}_result.message) + + +@{{ businessName }}Controller.put('', dependencies=[Depends(CheckUserInterfaceAuth('{{ permissionPrefix }}:edit'))]) +@ValidateFields(validate_model='edit_{{ businessName }}') +@Log(title='{{ functionName }}', business_type=BusinessType.UPDATE) +async def edit_{{ moduleName }}_{{ businessName }}( + request: Request, + edit_{{ businessName }}: {{ BusinessName }}Model, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + edit_{{ businessName }}.update_by = current_user.user.user_name + edit_{{ businessName }}.update_time = datetime.now() + edit_{{ businessName }}_result = await {{ BusinessName }}Service.edit_{{ businessName }}_services(query_db, edit_{{ businessName }}) + logger.info(edit_{{ businessName }}_result.message) + + return ResponseUtil.success(msg=edit_{{ businessName }}_result.message) + + +@{{ businessName }}Controller.delete('/{% raw %}{{% endraw %}{{ pk_field }}s{% raw %}}{% endraw %}', dependencies=[Depends(CheckUserInterfaceAuth('{{ permissionPrefix }}:remove'))]) +@Log(title='{{ functionName }}', business_type=BusinessType.DELETE) +async def delete_{{ moduleName }}_{{ businessName }}(request: Request, {{ pk_field }}s: str, query_db: AsyncSession = Depends(get_db)): + delete_{{ businessName }} = Delete{{ BusinessName }}Model({{ pkField }}s={{ pk_field }}s) + delete_{{ businessName }}_result = await {{ BusinessName }}Service.delete_{{ businessName }}_services(query_db, delete_{{ businessName }}) + logger.info(delete_{{ businessName }}_result.message) + + return ResponseUtil.success(msg=delete_{{ businessName }}_result.message) + + +@{{ businessName }}Controller.get( + '/{% raw %}{{% endraw %}{{ pk_field }}{% raw %}}{% endraw %}', response_model={{ BusinessName }}Model, dependencies=[Depends(CheckUserInterfaceAuth('{{ permissionPrefix }}:query'))] +) +async def query_detail_{{ moduleName }}_{{ businessName }}(request: Request, {{ pk_field }}: int, query_db: AsyncSession = Depends(get_db)): + {{ businessName }}_detail_result = await {{ BusinessName }}Service.{{ businessName }}_detail_services(query_db, {{ pk_field }}) + logger.info(f'获取{{ pk_field }}为{% raw %}{{% endraw %}{{ pk_field }}{% raw %}}{% endraw %}的信息成功') + + return ResponseUtil.success(data={{ businessName }}_detail_result) + + +@{{ businessName }}Controller.post('/export', dependencies=[Depends(CheckUserInterfaceAuth('{{ permissionPrefix }}:export'))]) +@Log(title='{{ functionName }}', business_type=BusinessType.EXPORT) +async def export_{{ moduleName }}_{{ businessName }}_list( + request: Request, + {{ businessName }}_page_query: {{ BusinessName }}PageQueryModel = Form(), + query_db: AsyncSession = Depends(get_db), +): + # 获取全量数据 + {{ businessName }}_query_result = await {{ BusinessName }}Service.get_{{ businessName }}_list_services(query_db, {{ businessName }}_page_query, is_page=False) + {{ businessName }}_export_result = await {{ BusinessName }}Service.export_{{ businessName }}_list_services({% if dicts %}request, {% endif %}{{ businessName }}_query_result) + logger.info('导出成功') + + return ResponseUtil.streaming(data=bytes2file_response({{ businessName }}_export_result)) diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 new file mode 100644 index 0000000..3b186c5 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 @@ -0,0 +1,213 @@ +{% set pkField = pkColumn.python_field %} +{% set pk_field = pkColumn.python_field | camel_to_snake %} +{% set pkParentheseIndex = pkColumn.column_comment.find("(") %} +{% set pk_field_comment = pkColumn.column_comment[:pkParentheseIndex] if pkParentheseIndex != -1 else pkColumn.column_comment %} +{% for column in columns %} +{% if column.query and column.query_type == 'BETWEEN' and column.python_field == "createTime" %} +from datetime import datetime, time +{% endif %} +{% endfor %} +from sqlalchemy import delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession +{% if table.sub %} +from sqlalchemy.orm import selectinload +{% endif %} +{% if table.sub %} +from {{ packageName }}.entity.do.{{ businessName }}_do import {{ ClassName }}, {{ subClassName }} +from {{ packageName }}.entity.vo.{{ businessName }}_vo import {{ BusinessName }}Model, {{ BusinessName }}PageQueryModel, {{ subTable.business_name | capitalize }}Model +{% else %} +from {{ packageName }}.entity.do.{{ businessName }}_do import {{ ClassName }} +from {{ packageName }}.entity.vo.{{ businessName }}_vo import {{ BusinessName }}Model, {{ BusinessName }}PageQueryModel +{% endif %} +from utils.page_util import PageUtil + + +class {{ BusinessName }}Dao: + """ + {{ functionName }}模块数据库操作层 + """ + + @classmethod + async def get_{{ businessName }}_detail_by_id(cls, db: AsyncSession, {{ pk_field }}: int): + """ + 根据{{ pk_field_comment }}获取{{ functionName }}详细信息 + + :param db: orm对象 + :param {{ pk_field }}: {{ pk_field_comment }} + :return: {{ functionName }}信息对象 + """ + {{ businessName }}_info = ( + ( + await db.execute( + {% if table.sub %} + select({{ ClassName }}) + .options(selectinload({{ ClassName }}.{{ subclassName }}_list)) + {% else %} + select({{ ClassName }}) + {% endif %} + .where( + {{ ClassName }}.{{ pk_field }} == {{ pk_field }} + ) + ) + ) + .scalars() + .first() + ) + + return {{ businessName }}_info + + @classmethod + async def get_{{ businessName }}_detail_by_info(cls, db: AsyncSession, {{ businessName }}: {{ BusinessName }}Model): + """ + 根据{{ functionName }}参数获取{{ functionName }}信息 + + :param db: orm对象 + :param {{ businessName }}: {{ functionName }}参数对象 + :return: {{ functionName }}信息对象 + """ + {{ businessName }}_info = ( + ( + await db.execute( + select({{ ClassName }}).where( + {% for column in columns %} + {% if column.unique %} + {{ ClassName }}.{{ column.python_field | camel_to_snake }} == {{ businessName }}.{{ column.python_field | camel_to_snake }} if {{ businessName }}.{{ column.python_field | camel_to_snake }} else True, + {% endif %} + {% endfor %} + ) + ) + ) + .scalars() + .first() + ) + + return {{ businessName }}_info + + @classmethod + async def get_{{ businessName }}_list(cls, db: AsyncSession, query_object: {{ BusinessName }}PageQueryModel, is_page: bool = False): + """ + 根据查询参数获取{{ functionName }}列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: {{ functionName }}列表信息对象 + """ + query = ( + {% if table.sub %} + select({{ ClassName }}) + .options(selectinload({{ ClassName }}.{{ subclassName }}_list)) + {% else %} + select({{ ClassName }}) + {% endif %} + .where( + {% for column in columns %} + {% set field = column.python_field | camel_to_snake %} + {% if column.query %} + {% if column.query_type == "EQ" %} + {{ ClassName }}.{{ field }} == query_object.{{ field }} if query_object.{{ field }} else True, + {% elif column.query_type == "NE" %} + {{ ClassName }}.{{ field }} != query_object.{{ field }} if query_object.{{ field }} else True, + {% elif column.query_type == "GT" %} + {{ ClassName }}.{{ field }} > query_object.{{ field }} if query_object.{{ field }} else True, + {% elif column.query_type == "GTE" %} + {{ ClassName }}.{{ field }} >= query_object.{{ field }} if query_object.{{ field }} else True, + {% elif column.query_type == "LT" %} + {{ ClassName }}.{{ field }} < query_object.{{ field }} if query_object.{{ field }} else True, + {% elif column.query_type == "LTE" %} + {{ ClassName }}.{{ field }} <= query_object.{{ field }} if query_object.{{ field }} else True, + {% elif column.query_type == "LIKE" %} + {{ ClassName }}.{{ field }}.like(f'%{% raw %}{{% endraw %}query_object.{{ field }}{% raw %}}{% endraw %}%') if query_object.{{ field }} else True, + {% elif column.html_type == "datetime" and column.query_type == "BETWEEN" %} + {{ ClassName }}.{{ field }}.between( + datetime.combine(datetime.strptime(query_object.begin_{{ column.column_name }}, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_{{ column.column_name }}, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_{{ column.column_name }} and query_object.end_{{ column.column_name }} + else True, + {% endif %} + {% endif %} + {% endfor %} + ) + .order_by({{ ClassName }}.{{ pk_field }}) + .distinct() + ) + {{ businessName }}_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return {{ businessName }}_list + + @classmethod + async def add_{{ businessName }}_dao(cls, db: AsyncSession, {{ businessName }}: {{ BusinessName }}Model): + """ + 新增{{ functionName }}数据库操作 + + :param db: orm对象 + :param {{ businessName }}: {{ functionName }}对象 + :return: + """ + db_{{ businessName }} = {{ ClassName }}(**{{ businessName }}.model_dump(exclude={% raw %}{{% endraw %}{% if table.sub %}'{{ subclassName }}_list', {% endif %}{% for column in columns %}{% if not column.insert and column.column_name not in column_not_add_show + column_not_edit_show %}'{{ column.python_field | camel_to_snake }}'{% if not loop.last %}, {% endif %}{% endif %}{% endfor %}{% raw %}}{% endraw %})) + db.add(db_{{ businessName }}) + await db.flush() + + return db_{{ businessName }} + + @classmethod + async def edit_{{ businessName }}_dao(cls, db: AsyncSession, {{ businessName }}: dict): + """ + 编辑{{ functionName }}数据库操作 + + :param db: orm对象 + :param {{ businessName }}: 需要更新的{{ functionName }}字典 + :return: + """ + await db.execute(update({{ ClassName }}), [{{ businessName }}]) + + @classmethod + async def delete_{{ businessName }}_dao(cls, db: AsyncSession, {{ businessName }}: {{ BusinessName }}Model): + """ + 删除{{ functionName }}数据库操作 + + :param db: orm对象 + :param {{ businessName }}: {{ functionName }}对象 + :return: + """ + await db.execute(delete({{ ClassName }}).where({{ ClassName }}.{{ pk_field }}.in_([{{ businessName }}.{{ pk_field }}]))) + + {% if table.sub %} + @classmethod + async def add_{{ subTable.business_name }}_dao(cls, db: AsyncSession, {{ subTable.business_name }}: {{ subTable.business_name | capitalize }}Model): + """ + 新增{{ subTable.function_name }}数据库操作 + + :param db: orm对象 + :param {{ subTable.business_name }}: {{ subTable.function_name }}对象 + :return: + """ + db_{{ subTable.business_name }} = {{ subClassName }}(**{{ subTable.business_name }}.model_dump()) + db.add(db_{{ subTable.business_name }}) + await db.flush() + + return db_{{ subTable.business_name }} + + @classmethod + async def edit_{{ subTable.business_name }}_dao(cls, db: AsyncSession, {{ subTable.business_name }}: dict): + """ + 编辑{{ subTable.function_name }}数据库操作 + + :param db: orm对象 + :param {{ subTable.business_name }}: 需要更新的{{ subTable.function_name }}字典 + :return: + """ + await db.execute(update({{ subClassName }}), [{{ subTable.business_name }}]) + + @classmethod + async def delete_{{ subTable.business_name }}_dao(cls, db: AsyncSession, {{ subTable.business_name }}: {{ subTable.business_name | capitalize }}Model): + """ + 删除{{ subTable.function_name }}数据库操作 + + :param db: orm对象 + :param {{ subTable.business_name }}: {{ subTable.function_name }}对象 + :return: + """ + await db.execute(delete({{ subClassName }}).where({{ subClassName }}.{{ subTable.pk_column.python_field | camel_to_snake }}.in_([{{ subTable.business_name }}.{{ subTable.pk_column.python_field | camel_to_snake }}]))) + {% endif %} diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 new file mode 100644 index 0000000..22c23c4 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 @@ -0,0 +1,41 @@ +{% for do_import in doImportList %} +{{ do_import }} +{% endfor %} +{% if table.sub %} +from sqlalchemy.orm import relationship +{% endif %} +from config.database import Base + + +class {{ ClassName }}(Base): + """ + {{ functionName }}表 + """ + + __tablename__ = '{{ tableName }}' + + {% for column in columns %} + {{ column.column_name }} = Column({{ column.column_type | get_sqlalchemy_type }}, {% if column.pk %}primary_key=True, {% endif %}{% if column.increment %}autoincrement=True, {% endif %}{% if column.required or column.pk %}nullable=False{% else %}nullable=True{% endif %}, comment='{{ column.column_comment }}') + {% endfor %} + + {% if table.sub %} + {{ subclassName }}_list = relationship('{{ subClassName }}', back_populates='{{ businessName }}') + {% endif %} + + +{% if table.sub %} +class {{ subClassName }}(Base): + """ + {{ subTable.function_name }}表 + """ + + __tablename__ = '{{ subTableName }}' + + {% for column in subTable.columns %} + {{ column.column_name }} = Column({{ column.column_type | get_sqlalchemy_type }}, {% if column.column_name == subTableFkName %}ForeignKey('{{ tableName }}.{{ subTableFkName }}'), {% endif %}{% if column.pk %}primary_key=True, {% endif %}{% if column.increment %}autoincrement=True, {% endif %}{% if column.required %}nullable=True{% else %}nullable=False{% endif %}, comment='{{ column.column_comment }}') + {% endfor %} + + {% if table.sub %} + {{ businessName }} = relationship('{{ ClassName }}', back_populates='{{ subclassName }}_list') + {% endif %} +{% endif %} diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 new file mode 100644 index 0000000..5726c5d --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 @@ -0,0 +1,211 @@ +{% set pkField = pkColumn.python_field %} +{% set pk_field = pkColumn.python_field | camel_to_snake %} +{% set pkParentheseIndex = pkColumn.column_comment.find("(") %} +{% set pk_field_comment = pkColumn.column_comment[:pkParentheseIndex] if pkParentheseIndex != -1 else pkColumn.column_comment %} +{% if dicts %} +from fastapi import Request +{% endif %} +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +{% if dicts %} +from module_admin.service.dict_service import DictDataService +{% endif %} +from {{ packageName }}.dao.{{ businessName }}_dao import {{ BusinessName }}Dao +from {{ packageName }}.entity.vo.{{ businessName }}_vo import Delete{{ BusinessName }}Model, {{ BusinessName }}Model, {{ BusinessName }}PageQueryModel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil + + +class {{ BusinessName }}Service: + """ + {{ functionName }}模块服务层 + """ + + @classmethod + async def get_{{ businessName }}_list_services( + cls, query_db: AsyncSession, query_object: {{ BusinessName }}PageQueryModel, is_page: bool = False + ): + """ + 获取{{ functionName }}列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: {{ functionName }}列表信息对象 + """ + {{ businessName }}_list_result = await {{ BusinessName }}Dao.get_{{ businessName }}_list(query_db, query_object, is_page) + + return {{ businessName }}_list_result + + {% for column in columns %} + {% set parentheseIndex = column.column_comment.find("(") %} + {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} + {% if column.unique %} + @classmethod + async def check_{{ column.python_field | camel_to_snake }}_unique_services(cls, query_db: AsyncSession, page_object: {{ BusinessName }}Model): + """ + 检查{{ comment }}是否唯一service + + :param query_db: orm对象 + :param page_object: {{ functionName }}对象 + :return: 校验结果 + """ + {{ pk_field }} = -1 if page_object.{{ pk_field }} is None else page_object.{{ pk_field }} + {{ businessName }} = await {{ BusinessName }}Dao.get_{{ businessName }}_detail_by_info(query_db, {{ BusinessName }}Model({{ column.python_field }}=page_object.{{ column.python_field | camel_to_snake }})) + if {{ businessName }} and {{ businessName }}.{{ pk_field }} != {{ pk_field }}: + return CommonConstant.NOT_UNIQUE + return CommonConstant.UNIQUE + {% if not loop.last %}{{ "\n" }}{% endif %} + {% endif %} + {% endfor %} + + @classmethod + async def add_{{ businessName }}_services(cls, query_db: AsyncSession, page_object: {{ BusinessName }}Model): + """ + 新增{{ functionName }}信息service + + :param query_db: orm对象 + :param page_object: 新增{{ functionName }}对象 + :return: 新增{{ functionName }}校验结果 + """ + {% for column in columns %} + {% set parentheseIndex = column.column_comment.find("(") %} + {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} + {% if column.unique %} + if not await cls.check_{{ column.python_field | camel_to_snake }}_unique_services(query_db, page_object): + raise ServiceException(message=f'新增{{ functionName }}{page_object.{{ column.python_field | camel_to_snake }}}失败,{{ comment }}已存在') + {% endif %} + {% endfor %} + try: + {% if table.sub %} + add_{{ businessName }} = await {{ BusinessName }}Dao.add_{{ businessName }}_dao(query_db, page_object) + if add_{{ businessName }}: + for sub_table in page_object.{{ subclassName }}_list: + await {{ BusinessName }}Dao.add_{{ subTable.business_name }}_dao(query_db, sub_table) + {% else %} + await {{ BusinessName }}Dao.add_{{ businessName }}_dao(query_db, page_object) + {% endif %} + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_{{ businessName }}_services(cls, query_db: AsyncSession, page_object: {{ BusinessName }}Model): + """ + 编辑{{ functionName }}信息service + + :param query_db: orm对象 + :param page_object: 编辑{{ functionName }}对象 + :return: 编辑{{ functionName }}校验结果 + """ + edit_{{ businessName }} = page_object.model_dump(exclude_unset=True, exclude={% raw %}{{% endraw %}{% if table.sub %}'{{ subclassName }}_list', {% endif %}{% for column in columns %}{% if not column.edit and not column.pk and column.column_name not in column_not_edit_show %}'{{ column.python_field | camel_to_snake }}'{% if not loop.last %}, {% endif %}{% endif %}{% endfor %}{% raw %}}{% endraw %}) + {{ businessName }}_info = await cls.{{ businessName }}_detail_services(query_db, page_object.{{ pk_field }}) + if {{ businessName }}_info.{{ pk_field }}: + {% for column in columns %} + {% set parentheseIndex = column.column_comment.find("(") %} + {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} + {% if column.unique %} + if not await cls.check_{{ column.python_field | camel_to_snake }}_unique_services(query_db, page_object): + raise ServiceException(message=f'修改{{ functionName }}{page_object.{{ column.python_field | camel_to_snake }}}失败,{{ comment }}已存在') + {% endif %} + {% endfor %} + try: + await {{ BusinessName }}Dao.edit_{{ businessName }}_dao(query_db, edit_{{ businessName }}) + {% if table.sub %} + for sub_table in {{ businessName }}_info.{{ subclassName }}_list: + await {{ BusinessName }}Dao.delete_{{ subTable.business_name }}_dao(query_db, sub_table) + for sub_table in page_object.{{ subclassName }}_list: + await {{ BusinessName }}Dao.add_{{ subTable.business_name }}_dao(query_db, sub_table) + {% endif %} + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='{{ functionName }}不存在') + + @classmethod + async def delete_{{ businessName }}_services(cls, query_db: AsyncSession, page_object: Delete{{ BusinessName }}Model): + """ + 删除{{ functionName }}信息service + + :param query_db: orm对象 + :param page_object: 删除{{ functionName }}对象 + :return: 删除{{ functionName }}校验结果 + """ + if page_object.{{ pk_field }}s: + {{ pk_field }}_list = page_object.{{ pk_field }}s.split(',') + try: + for {{ pk_field }} in {{ pk_field }}_list: + {% if table.sub %} + {{ businessName }} = await cls.{{ businessName }}_detail_services(query_db, int({{ pk_field }})) + for sub_table in {{ businessName }}.{{ subclassName }}_list: + await {{ BusinessName }}Dao.delete_{{ subTable.business_name }}_dao(query_db, sub_table) + {% endif %} + await {{ BusinessName }}Dao.delete_{{ businessName }}_dao(query_db, {{ BusinessName }}Model({{ pkField }}={{ pk_field }})) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入{{ pk_field_comment }}为空') + + @classmethod + async def {{ businessName }}_detail_services(cls, query_db: AsyncSession, {{ pk_field }}: int): + """ + 获取{{ functionName }}详细信息service + + :param query_db: orm对象 + :param {{ pk_field }}: {{ pk_field_comment }} + :return: {{ pk_field_comment }}对应的信息 + """ + {{ businessName }} = await {{ BusinessName }}Dao.get_{{ businessName }}_detail_by_id(query_db, {{ pk_field }}={{ pk_field }}) + if {{ businessName }}: + result = {{ BusinessName }}Model(**CamelCaseUtil.transform_result({{ businessName }})) + else: + result = {{ BusinessName }}Model(**dict()) + + return result + + @staticmethod + async def export_{{ businessName }}_list_services({% if dicts %}request: Request, {% endif %}{{ businessName }}_list: List): + """ + 导出{{ functionName }}信息service + + :param {{ businessName }}_list: {{ functionName }}信息列表 + :return: {{ functionName }}信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + {% for column in columns %} + {% set parentheseIndex = column.column_comment.find("(") %} + {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} + '{{ column.python_field }}': '{{ comment }}', + {% endfor %} + } + {% if dicts %} + {% for dict_type in dicts.split(", ") %} + {{ dict_type[1:-1] }}_list = await DictDataService.query_dict_data_list_from_cache_services( + request.app.state.redis, dict_type={{ dict_type }} + ) + {{ dict_type[1:-1] }}_option = [dict(label=item.get('dictLabel'), value=item.get('dictValue')) for item in {{ dict_type[1:-1] }}_list] + {{ dict_type[1:-1] }}_option_dict = {item.get('value'): item for item in {{ dict_type[1:-1] }}_option} + {% endfor %} + for item in {{ businessName }}_list: + {% for column in columns %} + {% if column.dict_type %} + if str(item.get('{{ column.python_field }}')) in {{ column.dict_type }}_option_dict.keys(): + item['{{ column.python_field }}'] = {{ column.dict_type }}_option_dict.get(str(item.get('{{ column.python_field }}'))).get('label') + {% endif %} + {% endfor %} + {% endif %} + binary_data = ExcelUtil.export_list2excel({{ businessName }}_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/vo.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/vo.py.jinja2 new file mode 100644 index 0000000..47abbf5 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/python/vo.py.jinja2 @@ -0,0 +1,178 @@ +{% set pkField = pkColumn.python_field %} +{% set pk_field = pkColumn.python_field | camel_to_snake %} +{% set pkParentheseIndex = pkColumn.column_comment.find("(") %} +{% set pk_field_comment = pkColumn.column_comment[:pkParentheseIndex] if pkParentheseIndex != -1 else pkColumn.column_comment %} +{% set vo_field_required = namespace(has_required=False) %} +{% set vo_field_daterange = namespace(has_daterange=False) %} +{% for column in columns %} +{% if column.required %} + {% set vo_field_required.has_required = True %} +{% endif %} +{% if column.html_type == "datetime" and column.query_type == "BETWEEN" %} + {% set vo_field_daterange.has_daterange = True %} +{% endif %} +{% endfor %} +{% set sub_vo_field_required = namespace(has_required=False) %} +{% if table.sub %} +{% for sub_column in subTable.columns %} +{% if sub_column.required %} + {% set sub_vo_field_required.has_required = True %} +{% endif %} +{% endfor %} +{% endif %} +{% for vo_import in voImportList %} +{{ vo_import }} +{% endfor %} +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +{% if vo_field_required.has_required or sub_vo_field_required.has_required %} +from pydantic_validation_decorator import NotBlank +{% endif %} +{% if table.sub %} +from typing import List, Optional +{% else %} +from typing import Optional +{% endif %} +from module_admin.annotation.pydantic_annotation import as_query + + +{% if table.sub %} +class {{ BusinessName }}BaseModel(BaseModel): + """ + {{ functionName }}表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + {% for column in columns %} + {{ column.column_name }}: Optional[{{ column.python_type }}] = Field(default=None, description='{{ column.column_comment }}') + {% endfor %} + + {% for column in columns %} + {% if column.required %} + {% set parentheseIndex = column.column_comment.find("(") %} + {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} + @NotBlank(field_name='{{ column.column_name }}', message='{{ comment }}不能为空') + def get_{{ column.column_name }}(self): + return self.{{ column.column_name }} + {% if not loop.last %}{{ "\n" }}{% endif %} + {% endif %} + {% endfor %} + + {% if vo_field_required.has_required %} + def validate_fields(self): + {% for column in columns %} + {% if column.required %} + self.get_{{ column.column_name }}() + {% endif %} + {% endfor %} + {% endif %} +{% endif %} + + +class {{ BusinessName }}Model({% if table.sub %}{{ BusinessName }}BaseModel{% else %}BaseModel{% endif %}): + """ + {{ functionName }}表对应pydantic模型 + """ + {% if not table.sub %} + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + {% for column in columns %} + {{ column.column_name }}: Optional[{{ column.python_type }}] = Field(default=None, description='{{ column.column_comment }}') + {% endfor %} + {% endif %} + {% if table.sub %} + {{ subclassName }}_list: Optional[List['{{ subTable.business_name | capitalize }}Model']] = Field(default=None, description='子表列信息') + {% endif %} + + {% if not table.sub %} + {% for column in columns %} + {% if column.required %} + {% set parentheseIndex = column.column_comment.find("(") %} + {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} + @NotBlank(field_name='{{ column.column_name }}', message='{{ comment }}不能为空') + def get_{{ column.column_name }}(self): + return self.{{ column.column_name }} + {% if not loop.last %}{{ "\n" }}{% endif %} + {% endif %} + {% endfor %} + + {% if vo_field_required.has_required %} + def validate_fields(self): + {% for column in columns %} + {% if column.required %} + self.get_{{ column.column_name }}() + {% endif %} + {% endfor %} + {% endif %} + {% endif %} + + +{% if table.sub %} +class {{ subTable.business_name | capitalize }}Model(BaseModel): + """ + {{ subTable.function_name }}表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + {% for sub_column in subTable.columns %} + {{ sub_column.column_name }}: Optional[{{ sub_column.python_type }}] = Field(default=None, description='{{ sub_column.column_comment}}') + {% endfor %} + + {% for sub_column in subTable.columns %} + {% if sub_column.required %} + {% set parentheseIndex = sub_column.column_comment.find("(") %} + {% set comment = sub_column.column_comment[:parentheseIndex] if parentheseIndex != -1 else sub_column.column_comment %} + @NotBlank(field_name='{{ sub_column.column_name }}', message='{{ comment }}不能为空') + def get_{{ sub_column.column_name }}(self): + return self.{{ sub_column.column_name }} + {% if not loop.last %}{{ "\n" }}{% endif %} + {% endif %} + {% endfor %} + + {% if sub_vo_field_required.has_required %} + def validate_fields(self): + {% for sub_column in subTable.columns %} + {% if sub_column.required %} + self.get_{{ sub_column.column_name }}() + {% endif %} + {% endfor %} + {% endif %} +{% endif %} + + +class {{ BusinessName }}QueryModel({% if table.sub %}{{ BusinessName }}BaseModel{% else %}{{ BusinessName }}Model{% endif %}): + """ + {{ functionName }}不分页查询模型 + """ + {% if vo_field_daterange.has_daterange %} + {% for column in columns %} + {% if column.html_type == "datetime" and column.query_type == "BETWEEN" %} + begin_{{ column.column_name }}: Optional[str] = Field(default=None, description='开始{{ column.column_comment }}') + end_{{ column.column_name }}: Optional[str] = Field(default=None, description='结束{{ column.column_comment }}') + {% endif %} + {% endfor %} + {% else %} + pass + {% endif %} + + +@as_query +class {{ BusinessName }}PageQueryModel({{ BusinessName }}QueryModel): + """ + {{ functionName }}分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class Delete{{ BusinessName }}Model(BaseModel): + """ + 删除{{ functionName }}模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + {{ pk_field }}s: str = Field(description='需要删除的{{ pk_field_comment }}') diff --git a/ruoyi-fastapi-backend/module_generator/templates/sql/sql.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/sql/sql.jinja2 new file mode 100644 index 0000000..dd4099f --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/sql/sql.jinja2 @@ -0,0 +1,47 @@ +{% if dbType == 'postgresql' %} +-- 菜单 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}', '{{ parentMenuId }}', '1', '{{ businessName }}', '{{ moduleName }}/{{ businessName }}/index', 1, 0, 'C', '0', '0', '{{ permissionPrefix }}:list', '#', 'admin', current_timestamp, '', null, '{{ functionName }}菜单'); + +-- 按钮父菜单ID +select max(menu_id) from sys_menu; + +-- 按钮 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}查询', max(menu_id), '1', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:query', '#', 'admin', current_timestamp, '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}新增', max(menu_id), '2', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:add', '#', 'admin', current_timestamp, '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}修改', max(menu_id), '3', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:edit', '#', 'admin', current_timestamp, '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}删除', max(menu_id), '4', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:remove', '#', 'admin', current_timestamp, '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}导出', max(menu_id), '5', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:export', '#', 'admin', current_timestamp, '', null, ''); +{% else %} +-- 菜单 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}', '{{ parentMenuId }}', '1', '{{ businessName }}', '{{ moduleName }}/{{ businessName }}/index', 1, 0, 'C', '0', '0', '{{ permissionPrefix }}:list', '#', 'admin', sysdate(), '', null, '{{ functionName }}菜单'); + +-- 按钮父菜单ID +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}查询', @parentId, '1', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:query', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}新增', @parentId, '2', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:add', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}修改', @parentId, '3', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:edit', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}删除', @parentId, '4', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:remove', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}导出', @parentId, '5', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:export', '#', 'admin', sysdate(), '', null, ''); +{% endif %} \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_generator/templates/vue/index-tree.vue.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/vue/index-tree.vue.jinja2 new file mode 100644 index 0000000..eb8ceaf --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/vue/index-tree.vue.jinja2 @@ -0,0 +1,496 @@ + + + \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_generator/templates/vue/index.vue.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/vue/index.vue.jinja2 new file mode 100644 index 0000000..39f861d --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/vue/index.vue.jinja2 @@ -0,0 +1,591 @@ + + + \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index-tree.vue.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index-tree.vue.jinja2 new file mode 100644 index 0000000..52eeda9 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index-tree.vue.jinja2 @@ -0,0 +1,463 @@ + + + \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index.vue.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index.vue.jinja2 new file mode 100644 index 0000000..73dae7d --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index.vue.jinja2 @@ -0,0 +1,580 @@ + + + \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_task/__init__.py b/ruoyi-fastapi-backend/module_task/__init__.py new file mode 100644 index 0000000..1f4b412 --- /dev/null +++ b/ruoyi-fastapi-backend/module_task/__init__.py @@ -0,0 +1 @@ +from . import scheduler_test # noqa: F401 diff --git a/ruoyi-fastapi-backend/module_task/scheduler_test.py b/ruoyi-fastapi-backend/module_task/scheduler_test.py new file mode 100644 index 0000000..b282732 --- /dev/null +++ b/ruoyi-fastapi-backend/module_task/scheduler_test.py @@ -0,0 +1,19 @@ +from datetime import datetime + + +def job(*args, **kwargs): + """ + 定时任务执行同步函数示例 + """ + print(args) + print(kwargs) + print(f'{datetime.now()}同步函数执行了') + + +async def async_job(*args, **kwargs): + """ + 定时任务执行异步函数示例 + """ + print(args) + print(kwargs) + print(f'{datetime.now()}异步函数执行了') diff --git a/ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql b/ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql new file mode 100644 index 0000000..b3731aa --- /dev/null +++ b/ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql @@ -0,0 +1,1047 @@ +-- ---------------------------- +-- 1、部门表 +-- ---------------------------- +drop table if exists sys_dept; +create table sys_dept ( + dept_id bigserial, + parent_id bigint default 0, + ancestors varchar(50) default '', + dept_name varchar(30) default '', + order_num int4 default 0, + leader varchar(20) default null, + phone varchar(11) default null, + email varchar(50) default null, + status char(1) default '0', + del_flag char(1) default '0', + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + primary key (dept_id) +); +alter sequence sys_dept_dept_id_seq restart 200; +comment on column sys_dept.dept_id is '部门id'; +comment on column sys_dept.parent_id is '父部门id'; +comment on column sys_dept.ancestors is '祖级列表'; +comment on column sys_dept.dept_name is '部门名称'; +comment on column sys_dept.order_num is '显示顺序'; +comment on column sys_dept.leader is '负责人'; +comment on column sys_dept.phone is '联系电话'; +comment on column sys_dept.email is '邮箱'; +comment on column sys_dept.status is '部门状态(0正常 1停用)'; +comment on column sys_dept.del_flag is '删除标志(0代表存在 2代表删除)'; +comment on column sys_dept.create_by is '创建者'; +comment on column sys_dept.create_time is '创建时间'; +comment on column sys_dept.update_by is '更新者'; +comment on column sys_dept.update_time is '更新时间'; +comment on table sys_dept is '部门表'; + +-- ---------------------------- +-- 初始化-部门表数据 +-- ---------------------------- +insert into sys_dept values(100, 0, '0', '集团总公司', 0, '年糕', '15888888888', 'niangao@qq.com', '0', '0', 'admin', current_timestamp, '', null); +insert into sys_dept values(101, 100, '0,100', '深圳分公司', 1, '年糕', '15888888888', 'niangao@qq.com', '0', '0', 'admin', current_timestamp, '', null); +insert into sys_dept values(102, 100, '0,100', '长沙分公司', 2, '年糕', '15888888888', 'niangao@qq.com', '0', '0', 'admin', current_timestamp, '', null); +insert into sys_dept values(103, 101, '0,100,101', '研发部门', 1, '年糕', '15888888888', 'niangao@qq.com', '0', '0', 'admin', current_timestamp, '', null); +insert into sys_dept values(104, 101, '0,100,101', '市场部门', 2, '年糕', '15888888888', 'niangao@qq.com', '0', '0', 'admin', current_timestamp, '', null); +insert into sys_dept values(105, 101, '0,100,101', '测试部门', 3, '年糕', '15888888888', 'niangao@qq.com', '0', '0', 'admin', current_timestamp, '', null); +insert into sys_dept values(106, 101, '0,100,101', '财务部门', 4, '年糕', '15888888888', 'niangao@qq.com', '0', '0', 'admin', current_timestamp, '', null); +insert into sys_dept values(107, 101, '0,100,101', '运维部门', 5, '年糕', '15888888888', 'niangao@qq.com', '0', '0', 'admin', current_timestamp, '', null); +insert into sys_dept values(108, 102, '0,100,102', '市场部门', 1, '年糕', '15888888888', 'niangao@qq.com', '0', '0', 'admin', current_timestamp, '', null); +insert into sys_dept values(109, 102, '0,100,102', '财务部门', 2, '年糕', '15888888888', 'niangao@qq.com', '0', '0', 'admin', current_timestamp, '', null); + +-- ---------------------------- +-- 2、用户信息表 +-- ---------------------------- +drop table if exists sys_user; +create table sys_user ( + user_id bigserial not null, + dept_id bigint default null, + user_name varchar(30) not null, + nick_name varchar(30) not null, + user_type varchar(2) default '00', + email varchar(50) default '', + phonenumber varchar(11) default '', + sex char(1) default '0', + avatar varchar(100) default '', + password varchar(100) default '', + status char(1) default '0', + del_flag char(1) default '0', + login_ip varchar(128) default '', + login_date timestamp(0), + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + remark varchar(500) default null, + primary key (user_id) +); +alter sequence sys_user_user_id_seq restart 100; +comment on column sys_user.user_id is '用户ID'; +comment on column sys_user.dept_id is '部门ID'; +comment on column sys_user.user_name is '用户账号'; +comment on column sys_user.nick_name is '用户昵称'; +comment on column sys_user.user_type is '用户类型(00系统用户)'; +comment on column sys_user.email is '用户邮箱'; +comment on column sys_user.phonenumber is '手机号码'; +comment on column sys_user.sex is '用户性别(0男 1女 2未知)'; +comment on column sys_user.avatar is '头像地址'; +comment on column sys_user.password is '密码'; +comment on column sys_user.status is '帐号状态(0正常 1停用)'; +comment on column sys_user.del_flag is '删除标志(0代表存在 2代表删除)'; +comment on column sys_user.login_ip is '最后登录IP'; +comment on column sys_user.login_date is '最后登录时间'; +comment on column sys_user.create_by is '创建者'; +comment on column sys_user.create_time is '创建时间'; +comment on column sys_user.update_by is '更新者'; +comment on column sys_user.update_time is '更新时间'; +comment on column sys_user.remark is '备注'; +comment on table sys_user is '用户信息表'; + +-- ---------------------------- +-- 3、岗位信息表 +-- ---------------------------- +drop table if exists sys_post; +create table sys_post ( + post_id bigserial not null, + post_code varchar(64) not null, + post_name varchar(50) not null, + post_sort int4 not null, + status char(1) not null, + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + remark varchar(500) default null, + primary key (post_id) +); +alter sequence sys_post_post_id_seq restart 5; +comment on column sys_post.post_id is '岗位ID'; +comment on column sys_post.post_code is '岗位编码'; +comment on column sys_post.post_name is '岗位名称'; +comment on column sys_post.post_sort is '显示顺序'; +comment on column sys_post.status is '状态(0正常 1停用)'; +comment on column sys_post.create_by is '创建者'; +comment on column sys_post.create_time is '创建时间'; +comment on column sys_post.update_by is '更新者'; +comment on column sys_post.update_time is '更新时间'; +comment on column sys_post.remark is '备注'; +comment on table sys_post is '岗位信息表'; + +-- ---------------------------- +-- 初始化-岗位信息表数据 +-- ---------------------------- +insert into sys_post values(1, 'ceo', '董事长', 1, '0', 'admin', current_timestamp, '', null, ''); +insert into sys_post values(2, 'se', '项目经理', 2, '0', 'admin', current_timestamp, '', null, ''); +insert into sys_post values(3, 'hr', '人力资源', 3, '0', 'admin', current_timestamp, '', null, ''); +insert into sys_post values(4, 'user', '普通员工', 4, '0', 'admin', current_timestamp, '', null, ''); + +-- ---------------------------- +-- 初始化-用户信息表数据 +-- ---------------------------- +insert into sys_user values(1, 103, 'admin', '超级管理员', '00', 'niangao@163.com', '15888888888', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', current_timestamp, 'admin', current_timestamp, '', null, '管理员'); +insert into sys_user values(2, 105, 'niangao', '年糕', '00', 'niangao@qq.com', '15666666666', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', current_timestamp, 'admin', current_timestamp, '', null, '测试员'); + +-- ---------------------------- +-- 4、角色信息表 +-- ---------------------------- +drop table if exists sys_role; +create table sys_role ( + role_id bigserial not null, + role_name varchar(30) not null, + role_key varchar(100) not null, + role_sort int4 not null, + data_scope char(1) default '1', + menu_check_strictly smallint default 1, + dept_check_strictly smallint default 1, + status char(1) not null, + del_flag char(1) default '0', + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + remark varchar(500) default null, + primary key (role_id) +); +alter sequence sys_role_role_id_seq restart 3; +comment on column sys_role.role_id is '角色ID'; +comment on column sys_role.role_name is '角色名称'; +comment on column sys_role.role_key is '角色权限字符串'; +comment on column sys_role.role_sort is '显示顺序'; +comment on column sys_role.data_scope is '数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)'; +comment on column sys_role.menu_check_strictly is '菜单树选择项是否关联显示'; +comment on column sys_role.dept_check_strictly is '部门树选择项是否关联显示'; +comment on column sys_role.status is '角色状态(0正常 1停用)'; +comment on column sys_role.del_flag is '删除标志(0代表存在 2代表删除)'; +comment on column sys_role.create_by is '创建者'; +comment on column sys_role.create_time is '创建时间'; +comment on column sys_role.update_by is '更新者'; +comment on column sys_role.update_time is '更新时间'; +comment on column sys_role.remark is '备注'; +comment on table sys_role is '角色信息表'; + +-- ---------------------------- +-- 初始化-角色信息表数据 +-- ---------------------------- +insert into sys_role values(1, '超级管理员', 'admin', 1, 1, 1, 1, '0', '0', 'admin', current_timestamp, '', null, '超级管理员'); +insert into sys_role values(2, '普通角色', 'common', 2, 2, 1, 1, '0', '0', 'admin', current_timestamp, '', null, '普通角色'); + +-- ---------------------------- +-- 5、菜单权限表 +-- ---------------------------- +drop table if exists sys_menu; +create table sys_menu ( + menu_id bigserial not null, + menu_name varchar(50) not null, + parent_id bigint default 0, + order_num int4 default 0, + path varchar(200) default '', + component varchar(255) default null, + query varchar(255) default null, + route_name varchar(50) default '', + is_frame int4 default 1, + is_cache int4 default 0, + menu_type char(1) default '', + visible char(1) default '0', + status char(1) default '0', + perms varchar(100) default null, + icon varchar(100) default '#', + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + remark varchar(500) default '', + primary key (menu_id) +); +alter sequence sys_menu_menu_id_seq restart 2000; +comment on column sys_menu.menu_id is '菜单ID'; +comment on column sys_menu.menu_name is '菜单名称'; +comment on column sys_menu.parent_id is '父菜单ID'; +comment on column sys_menu.order_num is '显示顺序'; +comment on column sys_menu.path is '路由地址'; +comment on column sys_menu.component is '组件路径'; +comment on column sys_menu.query is '路由参数'; +comment on column sys_menu.route_name is '路由名称'; +comment on column sys_menu.is_frame is '是否为外链(0是 1否)'; +comment on column sys_menu.is_cache is '是否缓存(0缓存 1不缓存)'; +comment on column sys_menu.menu_type is '菜单类型(M目录 C菜单 F按钮)'; +comment on column sys_menu.visible is '菜单状态(0显示 1隐藏)'; +comment on column sys_menu.status is '菜单状态(0正常 1停用)'; +comment on column sys_menu.perms is '权限标识'; +comment on column sys_menu.icon is '菜单图标'; +comment on column sys_menu.create_by is '创建者'; +comment on column sys_menu.create_time is '创建时间'; +comment on column sys_menu.update_by is '更新者'; +comment on column sys_menu.update_time is '更新时间'; +comment on column sys_menu.remark is '备注'; +comment on table sys_menu is '菜单权限表'; + +-- ---------------------------- +-- 初始化-菜单信息表数据 +-- ---------------------------- +-- 一级菜单 +insert into sys_menu values(1, '系统管理', 0, '1', 'system', null, '', '', 1, 0, 'M', '0', '0', '', 'system', 'admin', current_timestamp, '', null, '系统管理目录'); +insert into sys_menu values(2, '系统监控', 0, '2', 'monitor', null, '', '', 1, 0, 'M', '0', '0', '', 'monitor', 'admin', current_timestamp, '', null, '系统监控目录'); +insert into sys_menu values(3, '系统工具', 0, '3', 'tool', null, '', '', 1, 0, 'M', '0', '0', '', 'tool', 'admin', current_timestamp, '', null, '系统工具目录'); +insert into sys_menu values(4, '若依官网', 0, '4', 'http://ruoyi.vip', null, '', '', 0, 0, 'M', '0', '0', '', 'guide', 'admin', current_timestamp, '', null, '若依官网地址'); +-- 二级菜单 +insert into sys_menu values(100, '用户管理', 1, '1', 'user', 'system/user/index', '', '', 1, 0, 'C', '0', '0', 'system:user:list', 'user', 'admin', current_timestamp, '', null, '用户管理菜单'); +insert into sys_menu values(101, '角色管理', 1, '2', 'role', 'system/role/index', '', '', 1, 0, 'C', '0', '0', 'system:role:list', 'peoples', 'admin', current_timestamp, '', null, '角色管理菜单'); +insert into sys_menu values(102, '菜单管理', 1, '3', 'menu', 'system/menu/index', '', '', 1, 0, 'C', '0', '0', 'system:menu:list', 'tree-table', 'admin', current_timestamp, '', null, '菜单管理菜单'); +insert into sys_menu values(103, '部门管理', 1, '4', 'dept', 'system/dept/index', '', '', 1, 0, 'C', '0', '0', 'system:dept:list', 'tree', 'admin', current_timestamp, '', null, '部门管理菜单'); +insert into sys_menu values(104, '岗位管理', 1, '5', 'post', 'system/post/index', '', '', 1, 0, 'C', '0', '0', 'system:post:list', 'post', 'admin', current_timestamp, '', null, '岗位管理菜单'); +insert into sys_menu values(105, '字典管理', 1, '6', 'dict', 'system/dict/index', '', '', 1, 0, 'C', '0', '0', 'system:dict:list', 'dict', 'admin', current_timestamp, '', null, '字典管理菜单'); +insert into sys_menu values(106, '参数设置', 1, '7', 'config', 'system/config/index', '', '', 1, 0, 'C', '0', '0', 'system:config:list', 'edit', 'admin', current_timestamp, '', null, '参数设置菜单'); +insert into sys_menu values(107, '通知公告', 1, '8', 'notice', 'system/notice/index', '', '', 1, 0, 'C', '0', '0', 'system:notice:list', 'message', 'admin', current_timestamp, '', null, '通知公告菜单'); +insert into sys_menu values(108, '日志管理', 1, '9', 'log', '', '', '', 1, 0, 'M', '0', '0', '', 'log', 'admin', current_timestamp, '', null, '日志管理菜单'); +insert into sys_menu values(109, '在线用户', 2, '1', 'online', 'monitor/online/index', '', '', 1, 0, 'C', '0', '0', 'monitor:online:list', 'online', 'admin', current_timestamp, '', null, '在线用户菜单'); +insert into sys_menu values(110, '定时任务', 2, '2', 'job', 'monitor/job/index', '', '', 1, 0, 'C', '0', '0', 'monitor:job:list', 'job', 'admin', current_timestamp, '', null, '定时任务菜单'); +insert into sys_menu values(111, '数据监控', 2, '3', 'druid', 'monitor/druid/index', '', '', 1, 0, 'C', '0', '0', 'monitor:druid:list', 'druid', 'admin', current_timestamp, '', null, '数据监控菜单'); +insert into sys_menu values(112, '服务监控', 2, '4', 'server', 'monitor/server/index', '', '', 1, 0, 'C', '0', '0', 'monitor:server:list', 'server', 'admin', current_timestamp, '', null, '服务监控菜单'); +insert into sys_menu values(113, '缓存监控', 2, '5', 'cache', 'monitor/cache/index', '', '', 1, 0, 'C', '0', '0', 'monitor:cache:list', 'redis', 'admin', current_timestamp, '', null, '缓存监控菜单'); +insert into sys_menu values(114, '缓存列表', 2, '6', 'cacheList', 'monitor/cache/list', '', '', 1, 0, 'C', '0', '0', 'monitor:cache:list', 'redis-list', 'admin', current_timestamp, '', null, '缓存列表菜单'); +insert into sys_menu values(115, '表单构建', 3, '1', 'build', 'tool/build/index', '', '', 1, 0, 'C', '0', '0', 'tool:build:list', 'build', 'admin', current_timestamp, '', null, '表单构建菜单'); +insert into sys_menu values(116, '代码生成', 3, '2', 'gen', 'tool/gen/index', '', '', 1, 0, 'C', '0', '0', 'tool:gen:list', 'code', 'admin', current_timestamp, '', null, '代码生成菜单'); +insert into sys_menu values(117, '系统接口', 3, '3', 'swagger', 'tool/swagger/index', '', '', 1, 0, 'C', '0', '0', 'tool:swagger:list', 'swagger', 'admin', current_timestamp, '', null, '系统接口菜单'); +-- 三级菜单 +insert into sys_menu values(500, '操作日志', 108, '1', 'operlog', 'monitor/operlog/index', '', '', 1, 0, 'C', '0', '0', 'monitor:operlog:list', 'form', 'admin', current_timestamp, '', null, '操作日志菜单'); +insert into sys_menu values(501, '登录日志', 108, '2', 'logininfor', 'monitor/logininfor/index', '', '', 1, 0, 'C', '0', '0', 'monitor:logininfor:list', 'logininfor', 'admin', current_timestamp, '', null, '登录日志菜单'); +-- 用户管理按钮 +insert into sys_menu values(1000, '用户查询', 100, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:query', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1001, '用户新增', 100, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:add', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1002, '用户修改', 100, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:edit', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1003, '用户删除', 100, '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:remove', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1004, '用户导出', 100, '5', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:export', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1005, '用户导入', 100, '6', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:import', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1006, '重置密码', 100, '7', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:resetPwd', '#', 'admin', current_timestamp, '', null, ''); +-- 角色管理按钮 +insert into sys_menu values(1007, '角色查询', 101, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:query', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1008, '角色新增', 101, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:add', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1009, '角色修改', 101, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:edit', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1010, '角色删除', 101, '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:remove', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1011, '角色导出', 101, '5', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:export', '#', 'admin', current_timestamp, '', null, ''); +-- 菜单管理按钮 +insert into sys_menu values(1012, '菜单查询', 102, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:query', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1013, '菜单新增', 102, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:add', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1014, '菜单修改', 102, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:edit', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1015, '菜单删除', 102, '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:remove', '#', 'admin', current_timestamp, '', null, ''); +-- 部门管理按钮 +insert into sys_menu values(1016, '部门查询', 103, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:query', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1017, '部门新增', 103, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:add', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1018, '部门修改', 103, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:edit', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1019, '部门删除', 103, '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:remove', '#', 'admin', current_timestamp, '', null, ''); +-- 岗位管理按钮 +insert into sys_menu values(1020, '岗位查询', 104, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:query', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1021, '岗位新增', 104, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:add', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1022, '岗位修改', 104, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:edit', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1023, '岗位删除', 104, '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:remove', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1024, '岗位导出', 104, '5', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:export', '#', 'admin', current_timestamp, '', null, ''); +-- 字典管理按钮 +insert into sys_menu values(1025, '字典查询', 105, '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:query', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1026, '字典新增', 105, '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:add', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1027, '字典修改', 105, '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:edit', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1028, '字典删除', 105, '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:remove', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1029, '字典导出', 105, '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:export', '#', 'admin', current_timestamp, '', null, ''); +-- 参数设置按钮 +insert into sys_menu values(1030, '参数查询', 106, '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:query', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1031, '参数新增', 106, '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:add', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1032, '参数修改', 106, '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:edit', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1033, '参数删除', 106, '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:remove', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1034, '参数导出', 106, '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:export', '#', 'admin', current_timestamp, '', null, ''); +-- 通知公告按钮 +insert into sys_menu values(1035, '公告查询', 107, '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:query', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1036, '公告新增', 107, '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:add', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1037, '公告修改', 107, '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:edit', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1038, '公告删除', 107, '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:remove', '#', 'admin', current_timestamp, '', null, ''); +-- 操作日志按钮 +insert into sys_menu values(1039, '操作查询', 500, '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:operlog:query', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1040, '操作删除', 500, '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:operlog:remove', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1041, '日志导出', 500, '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:operlog:export', '#', 'admin', current_timestamp, '', null, ''); +-- 登录日志按钮 +insert into sys_menu values(1042, '登录查询', 501, '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:query', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1043, '登录删除', 501, '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:remove', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1044, '日志导出', 501, '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:export', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1045, '账户解锁', 501, '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:unlock', '#', 'admin', current_timestamp, '', null, ''); +-- 在线用户按钮 +insert into sys_menu values(1046, '在线查询', 109, '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:online:query', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1047, '批量强退', 109, '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:online:batchLogout', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1048, '单条强退', 109, '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:online:forceLogout', '#', 'admin', current_timestamp, '', null, ''); +-- 定时任务按钮 +insert into sys_menu values(1049, '任务查询', 110, '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:query', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1050, '任务新增', 110, '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:add', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1051, '任务修改', 110, '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:edit', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1052, '任务删除', 110, '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:remove', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1053, '状态修改', 110, '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:changeStatus', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1054, '任务导出', 110, '6', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:export', '#', 'admin', current_timestamp, '', null, ''); +-- 代码生成按钮 +insert into sys_menu values(1055, '生成查询', 116, '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:query', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1056, '生成修改', 116, '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:edit', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1057, '生成删除', 116, '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:remove', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1058, '导入代码', 116, '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:import', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1059, '预览代码', 116, '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:preview', '#', 'admin', current_timestamp, '', null, ''); +insert into sys_menu values(1060, '生成代码', 116, '6', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:code', '#', 'admin', current_timestamp, '', null, ''); + +-- ---------------------------- +-- 6、用户和角色关联表 用户N-1角色 +-- ---------------------------- +drop table if exists sys_user_role; +create table sys_user_role ( + user_id bigint not null, + role_id bigint not null, + primary key (user_id, role_id) +); +comment on column sys_user_role.user_id is '用户ID'; +comment on column sys_user_role.role_id is '角色ID'; +comment on table sys_user_role is '用户和角色关联表'; + +-- ---------------------------- +-- 初始化-用户和角色关联表数据 +-- ---------------------------- +insert into sys_user_role values (1, 1); +insert into sys_user_role values (2, 2); + +-- ---------------------------- +-- 7、角色和菜单关联表 角色1-N菜单 +-- ---------------------------- +drop table if exists sys_role_menu; +create table sys_role_menu ( + role_id bigint not null, + menu_id bigint not null, + primary key (role_id, menu_id) +); +comment on column sys_role_menu.role_id is '角色ID'; +comment on column sys_role_menu.menu_id is '菜单ID'; +comment on table sys_role_menu is '角色和菜单关联表'; + +-- ---------------------------- +-- 初始化-角色和菜单关联表数据 +-- ---------------------------- +insert into sys_role_menu values (2, 1); +insert into sys_role_menu values (2, 2); +insert into sys_role_menu values (2, 3); +insert into sys_role_menu values (2, 4); +insert into sys_role_menu values (2, 100); +insert into sys_role_menu values (2, 101); +insert into sys_role_menu values (2, 102); +insert into sys_role_menu values (2, 103); +insert into sys_role_menu values (2, 104); +insert into sys_role_menu values (2, 105); +insert into sys_role_menu values (2, 106); +insert into sys_role_menu values (2, 107); +insert into sys_role_menu values (2, 108); +insert into sys_role_menu values (2, 109); +insert into sys_role_menu values (2, 110); +insert into sys_role_menu values (2, 111); +insert into sys_role_menu values (2, 112); +insert into sys_role_menu values (2, 113); +insert into sys_role_menu values (2, 114); +insert into sys_role_menu values (2, 115); +insert into sys_role_menu values (2, 116); +insert into sys_role_menu values (2, 117); +insert into sys_role_menu values (2, 500); +insert into sys_role_menu values (2, 501); +insert into sys_role_menu values (2, 1000); +insert into sys_role_menu values (2, 1001); +insert into sys_role_menu values (2, 1002); +insert into sys_role_menu values (2, 1003); +insert into sys_role_menu values (2, 1004); +insert into sys_role_menu values (2, 1005); +insert into sys_role_menu values (2, 1006); +insert into sys_role_menu values (2, 1007); +insert into sys_role_menu values (2, 1008); +insert into sys_role_menu values (2, 1009); +insert into sys_role_menu values (2, 1010); +insert into sys_role_menu values (2, 1011); +insert into sys_role_menu values (2, 1012); +insert into sys_role_menu values (2, 1013); +insert into sys_role_menu values (2, 1014); +insert into sys_role_menu values (2, 1015); +insert into sys_role_menu values (2, 1016); +insert into sys_role_menu values (2, 1017); +insert into sys_role_menu values (2, 1018); +insert into sys_role_menu values (2, 1019); +insert into sys_role_menu values (2, 1020); +insert into sys_role_menu values (2, 1021); +insert into sys_role_menu values (2, 1022); +insert into sys_role_menu values (2, 1023); +insert into sys_role_menu values (2, 1024); +insert into sys_role_menu values (2, 1025); +insert into sys_role_menu values (2, 1026); +insert into sys_role_menu values (2, 1027); +insert into sys_role_menu values (2, 1028); +insert into sys_role_menu values (2, 1029); +insert into sys_role_menu values (2, 1030); +insert into sys_role_menu values (2, 1031); +insert into sys_role_menu values (2, 1032); +insert into sys_role_menu values (2, 1033); +insert into sys_role_menu values (2, 1034); +insert into sys_role_menu values (2, 1035); +insert into sys_role_menu values (2, 1036); +insert into sys_role_menu values (2, 1037); +insert into sys_role_menu values (2, 1038); +insert into sys_role_menu values (2, 1039); +insert into sys_role_menu values (2, 1040); +insert into sys_role_menu values (2, 1041); +insert into sys_role_menu values (2, 1042); +insert into sys_role_menu values (2, 1043); +insert into sys_role_menu values (2, 1044); +insert into sys_role_menu values (2, 1045); +insert into sys_role_menu values (2, 1046); +insert into sys_role_menu values (2, 1047); +insert into sys_role_menu values (2, 1048); +insert into sys_role_menu values (2, 1049); +insert into sys_role_menu values (2, 1050); +insert into sys_role_menu values (2, 1051); +insert into sys_role_menu values (2, 1052); +insert into sys_role_menu values (2, 1053); +insert into sys_role_menu values (2, 1054); +insert into sys_role_menu values (2, 1055); +insert into sys_role_menu values (2, 1056); +insert into sys_role_menu values (2, 1057); +insert into sys_role_menu values (2, 1058); +insert into sys_role_menu values (2, 1059); +insert into sys_role_menu values (2, 1060); + +-- ---------------------------- +-- 8、角色和部门关联表 角色1-N部门 +-- ---------------------------- +drop table if exists sys_role_dept; +create table sys_role_dept ( + role_id bigint not null, + dept_id bigint not null, + primary key (role_id, dept_id) +); +comment on column sys_role_dept.role_id is '角色ID'; +comment on column sys_role_dept.dept_id is '部门ID'; +comment on table sys_role_dept is '角色和部门关联表'; + +-- ---------------------------- +-- 初始化-角色和部门关联表数据 +-- ---------------------------- +insert into sys_role_dept values (2, 100); +insert into sys_role_dept values (2, 101); +insert into sys_role_dept values (2, 105); + +-- ---------------------------- +-- 9、用户与岗位关联表 用户1-N岗位 +-- ---------------------------- +drop table if exists sys_user_post; +create table sys_user_post ( + user_id bigint not null, + post_id bigint not null, + primary key (user_id, post_id) +); +comment on column sys_user_post.user_id is '用户ID'; +comment on column sys_user_post.post_id is '岗位ID'; +comment on table sys_user_post is '用户与岗位关联表'; + +-- ---------------------------- +-- 初始化-用户与岗位关联表数据 +-- ---------------------------- +insert into sys_user_post values (1, 1); +insert into sys_user_post values (2, 2); + +-- ---------------------------- +-- 10、操作日志记录 +-- ---------------------------- +drop table if exists sys_oper_log; +create table sys_oper_log ( + oper_id bigserial not null, + title varchar(50) default '', + business_type int4 default 0, + method varchar(100) default '', + request_method varchar(10) default '', + operator_type int4 default 0, + oper_name varchar(50) default '', + dept_name varchar(50) default '', + oper_url varchar(255) default '', + oper_ip varchar(128) default '', + oper_location varchar(255) default '', + oper_param varchar(2000) default '', + json_result varchar(2000) default '', + status int4 default 0, + error_msg varchar(2000) default '', + oper_time timestamp(0), + cost_time int8 default 0, + primary key (oper_id) +); +alter sequence sys_oper_log_oper_id_seq restart 100; +create index idx_sys_oper_log_bt on sys_oper_log(business_type); +create index idx_sys_oper_log_s on sys_oper_log(status); +create index idx_sys_oper_log_ot on sys_oper_log(oper_time); +comment on column sys_oper_log.oper_id is '日志主键'; +comment on column sys_oper_log.title is '模块标题'; +comment on column sys_oper_log.business_type is '业务类型(0其它 1新增 2修改 3删除)'; +comment on column sys_oper_log.method is '方法名称'; +comment on column sys_oper_log.request_method is '请求方式'; +comment on column sys_oper_log.operator_type is '操作类别(0其它 1后台用户 2手机端用户)'; +comment on column sys_oper_log.oper_name is '操作人员'; +comment on column sys_oper_log.dept_name is '部门名称'; +comment on column sys_oper_log.oper_url is '请求URL'; +comment on column sys_oper_log.oper_ip is '主机地址'; +comment on column sys_oper_log.oper_location is '操作地点'; +comment on column sys_oper_log.oper_param is '请求参数'; +comment on column sys_oper_log.json_result is '返回参数'; +comment on column sys_oper_log.status is '操作状态(0正常 1异常)'; +comment on column sys_oper_log.error_msg is '错误消息'; +comment on column sys_oper_log.oper_time is '操作时间'; +comment on table sys_oper_log is '操作日志记录'; + +-- ---------------------------- +-- 11、字典类型表 +-- ---------------------------- +drop table if exists sys_dict_type; +create table sys_dict_type ( + dict_id bigserial not null, + dict_name varchar(100) default '', + dict_type varchar(100) unique default '', + status char(1) default '0', + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + remark varchar(500) default null, + primary key (dict_id) +); +alter sequence sys_dict_type_dict_id_seq restart 100; +comment on column sys_dict_type.dict_id is '字典主键'; +comment on column sys_dict_type.dict_name is '字典名称'; +comment on column sys_dict_type.dict_type is '字典类型'; +comment on column sys_dict_type.status is '状态(0正常 1停用)'; +comment on column sys_dict_type.create_by is '创建者'; +comment on column sys_dict_type.create_time is '创建时间'; +comment on column sys_dict_type.update_by is '更新者'; +comment on column sys_dict_type.update_time is '更新时间'; +comment on column sys_dict_type.remark is '备注'; +comment on table sys_dict_type is '字典类型表'; + +-- ---------------------------- +-- 初始化-字典类型表数据 +-- ---------------------------- +insert into sys_dict_type values(1, '用户性别', 'sys_user_sex', '0', 'admin', current_timestamp, '', null, '用户性别列表'); +insert into sys_dict_type values(2, '菜单状态', 'sys_show_hide', '0', 'admin', current_timestamp, '', null, '菜单状态列表'); +insert into sys_dict_type values(3, '系统开关', 'sys_normal_disable', '0', 'admin', current_timestamp, '', null, '系统开关列表'); +insert into sys_dict_type values(4, '任务状态', 'sys_job_status', '0', 'admin', current_timestamp, '', null, '任务状态列表'); +insert into sys_dict_type values(5, '任务分组', 'sys_job_group', '0', 'admin', current_timestamp, '', null, '任务分组列表'); +insert into sys_dict_type values(6, '任务执行器', 'sys_job_executor', '0', 'admin', current_timestamp, '', null, '任务执行器列表'); +insert into sys_dict_type values(7, '系统是否', 'sys_yes_no', '0', 'admin', current_timestamp, '', null, '系统是否列表'); +insert into sys_dict_type values(8, '通知类型', 'sys_notice_type', '0', 'admin', current_timestamp, '', null, '通知类型列表'); +insert into sys_dict_type values(9, '通知状态', 'sys_notice_status', '0', 'admin', current_timestamp, '', null, '通知状态列表'); +insert into sys_dict_type values(10, '操作类型', 'sys_oper_type', '0', 'admin', current_timestamp, '', null, '操作类型列表'); +insert into sys_dict_type values(11, '系统状态', 'sys_common_status', '0', 'admin', current_timestamp, '', null, '登录状态列表'); + +-- ---------------------------- +-- 12、字典数据表 +-- ---------------------------- +drop table if exists sys_dict_data; +create table sys_dict_data ( + dict_code bigserial not null, + dict_sort int4 default 0, + dict_label varchar(100) default '', + dict_value varchar(100) default '', + dict_type varchar(100) default '', + css_class varchar(100) default null, + list_class varchar(100) default null, + is_default char(1) default 'N', + status char(1) default '0', + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + remark varchar(500) default null, + primary key (dict_code) +); +alter sequence sys_dict_data_dict_code_seq restart 100; +comment on column sys_dict_data.dict_code is '字典编码'; +comment on column sys_dict_data.dict_sort is '字典排序'; +comment on column sys_dict_data.dict_label is '字典标签'; +comment on column sys_dict_data.dict_value is '字典键值'; +comment on column sys_dict_data.dict_type is '字典类型'; +comment on column sys_dict_data.css_class is '样式属性(其他样式扩展)'; +comment on column sys_dict_data.list_class is '表格回显样式'; +comment on column sys_dict_data.is_default is '是否默认(Y是 N否)'; +comment on column sys_dict_data.status is '状态(0正常 1停用)'; +comment on column sys_dict_data.create_by is '创建者'; +comment on column sys_dict_data.create_time is '创建时间'; +comment on column sys_dict_data.update_by is '更新者'; +comment on column sys_dict_data.update_time is '更新时间'; +comment on column sys_dict_data.remark is '备注'; +comment on table sys_dict_data is '字典数据表'; + +-- ---------------------------- +-- 初始化-字典数据表数据 +-- ---------------------------- +insert into sys_dict_data values(1, 1, '男', '0', 'sys_user_sex', '', '', 'Y', '0', 'admin', current_timestamp, '', null, '性别男'); +insert into sys_dict_data values(2, 2, '女', '1', 'sys_user_sex', '', '', 'N', '0', 'admin', current_timestamp, '', null, '性别女'); +insert into sys_dict_data values(3, 3, '未知', '2', 'sys_user_sex', '', '', 'N', '0', 'admin', current_timestamp, '', null, '性别未知'); +insert into sys_dict_data values(4, 1, '显示', '0', 'sys_show_hide', '', 'primary', 'Y', '0', 'admin', current_timestamp, '', null, '显示菜单'); +insert into sys_dict_data values(5, 2, '隐藏', '1', 'sys_show_hide', '', 'danger', 'N', '0', 'admin', current_timestamp, '', null, '隐藏菜单'); +insert into sys_dict_data values(6, 1, '正常', '0', 'sys_normal_disable', '', 'primary', 'Y', '0', 'admin', current_timestamp, '', null, '正常状态'); +insert into sys_dict_data values(7, 2, '停用', '1', 'sys_normal_disable', '', 'danger', 'N', '0', 'admin', current_timestamp, '', null, '停用状态'); +insert into sys_dict_data values(8, 1, '正常', '0', 'sys_job_status', '', 'primary', 'Y', '0', 'admin', current_timestamp, '', null, '正常状态'); +insert into sys_dict_data values(9, 2, '暂停', '1', 'sys_job_status', '', 'danger', 'N', '0', 'admin', current_timestamp, '', null, '停用状态'); +insert into sys_dict_data values(10, 1, '默认', 'default', 'sys_job_group', '', '', 'Y', '0', 'admin', current_timestamp, '', null, '默认分组'); +insert into sys_dict_data values(11, 2, '数据库', 'sqlalchemy', 'sys_job_group', '', '', 'N', '0', 'admin', current_timestamp, '', null, '数据库分组'); +insert into sys_dict_data values(12, 3, 'redis', 'redis', 'sys_job_group', '', '', 'N', '0', 'admin', current_timestamp, '', null, 'reids分组'); +insert into sys_dict_data values(13, 1, '默认', 'default', 'sys_job_executor', '', '', 'N', '0', 'admin', current_timestamp, '', null, '线程池'); +insert into sys_dict_data values(14, 2, '进程池', 'processpool', 'sys_job_executor', '', '', 'N', '0', 'admin', current_timestamp, '', null, '进程池'); +insert into sys_dict_data values(15, 1, '是', 'Y', 'sys_yes_no', '', 'primary', 'Y', '0', 'admin', current_timestamp, '', null, '系统默认是'); +insert into sys_dict_data values(16, 2, '否', 'N', 'sys_yes_no', '', 'danger', 'N', '0', 'admin', current_timestamp, '', null, '系统默认否'); +insert into sys_dict_data values(17, 1, '通知', '1', 'sys_notice_type', '', 'warning', 'Y', '0', 'admin', current_timestamp, '', null, '通知'); +insert into sys_dict_data values(18, 2, '公告', '2', 'sys_notice_type', '', 'success', 'N', '0', 'admin', current_timestamp, '', null, '公告'); +insert into sys_dict_data values(19, 1, '正常', '0', 'sys_notice_status', '', 'primary', 'Y', '0', 'admin', current_timestamp, '', null, '正常状态'); +insert into sys_dict_data values(20, 2, '关闭', '1', 'sys_notice_status', '', 'danger', 'N', '0', 'admin', current_timestamp, '', null, '关闭状态'); +insert into sys_dict_data values(21, 99, '其他', '0', 'sys_oper_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, '其他操作'); +insert into sys_dict_data values(22, 1, '新增', '1', 'sys_oper_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, '新增操作'); +insert into sys_dict_data values(23, 2, '修改', '2', 'sys_oper_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, '修改操作'); +insert into sys_dict_data values(24, 3, '删除', '3', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', current_timestamp, '', null, '删除操作'); +insert into sys_dict_data values(25, 4, '授权', '4', 'sys_oper_type', '', 'primary', 'N', '0', 'admin', current_timestamp, '', null, '授权操作'); +insert into sys_dict_data values(26, 5, '导出', '5', 'sys_oper_type', '', 'warning', 'N', '0', 'admin', current_timestamp, '', null, '导出操作'); +insert into sys_dict_data values(27, 6, '导入', '6', 'sys_oper_type', '', 'warning', 'N', '0', 'admin', current_timestamp, '', null, '导入操作'); +insert into sys_dict_data values(28, 7, '强退', '7', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', current_timestamp, '', null, '强退操作'); +insert into sys_dict_data values(29, 8, '生成代码', '8', 'sys_oper_type', '', 'warning', 'N', '0', 'admin', current_timestamp, '', null, '生成操作'); +insert into sys_dict_data values(30, 9, '清空数据', '9', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', current_timestamp, '', null, '清空操作'); +insert into sys_dict_data values(31, 1, '成功', '0', 'sys_common_status', '', 'primary', 'N', '0', 'admin', current_timestamp, '', null, '正常状态'); +insert into sys_dict_data values(32, 2, '失败', '1', 'sys_common_status', '', 'danger', 'N', '0', 'admin', current_timestamp, '', null, '停用状态'); + +-- ---------------------------- +-- 13、参数配置表 +-- ---------------------------- +drop table if exists sys_config; +create table sys_config ( + config_id serial not null, + config_name varchar(100) default '', + config_key varchar(100) default '', + config_value varchar(500) default '', + config_type char(1) default 'N', + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + remark varchar(500) default null, + primary key (config_id) +); +alter sequence sys_config_config_id_seq restart 100; +comment on column sys_config.config_id is '参数主键'; +comment on column sys_config.config_name is '参数名称'; +comment on column sys_config.config_key is '参数键名'; +comment on column sys_config.config_value is '参数键值'; +comment on column sys_config.config_type is '系统内置(Y是 N否)'; +comment on column sys_config.create_by is '创建者'; +comment on column sys_config.create_time is '创建时间'; +comment on column sys_config.update_by is '更新者'; +comment on column sys_config.update_time is '更新时间'; +comment on column sys_config.remark is '备注'; +comment on table sys_config is '参数配置表'; + +-- ---------------------------- +-- 初始化-参数配置表数据 +-- ---------------------------- +insert into sys_config values(1, '主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', 'admin', current_timestamp, '', null, '蓝色 skin-blue、绿色 skin-green、紫色 skin-purple、红色 skin-red、黄色 skin-yellow' ); +insert into sys_config values(2, '用户管理-账号初始密码', 'sys.user.initPassword', '123456', 'Y', 'admin', current_timestamp, '', null, '初始化密码 123456' ); +insert into sys_config values(3, '主框架页-侧边栏主题', 'sys.index.sideTheme', 'theme-dark', 'Y', 'admin', current_timestamp, '', null, '深色主题theme-dark,浅色主题theme-light' ); +insert into sys_config values(4, '账号自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'admin', current_timestamp, '', null, '是否开启验证码功能(true开启,false关闭)'); +insert into sys_config values(5, '账号自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 'admin', current_timestamp, '', null, '是否开启注册用户功能(true开启,false关闭)'); +insert into sys_config values(6, '用户登录-黑名单列表', 'sys.login.blackIPList', '', 'Y', 'admin', current_timestamp, '', null, '设置登录IP黑名单限制,多个匹配项以;分隔,支持匹配(*通配、网段)'); + +-- ---------------------------- +-- 14、系统访问记录 +-- ---------------------------- +drop table if exists sys_logininfor; +create table sys_logininfor ( + info_id bigserial not null, + user_name varchar(50) default '', + ipaddr varchar(128) default '', + login_location varchar(255) default '', + browser varchar(50) default '', + os varchar(50) default '', + status char(1) default '0', + msg varchar(255) default '', + login_time timestamp(0), + primary key (info_id) +); +alter sequence sys_logininfor_info_id_seq restart 100; +create index idx_sys_logininfor_s on sys_logininfor(status); +create index idx_sys_logininfor_lt on sys_logininfor(login_time); +comment on column sys_logininfor.info_id is '访问ID'; +comment on column sys_logininfor.user_name is '用户账号'; +comment on column sys_logininfor.ipaddr is '登录IP地址'; +comment on column sys_logininfor.login_location is '登录地点'; +comment on column sys_logininfor.browser is '浏览器类型'; +comment on column sys_logininfor.os is '操作系统'; +comment on column sys_logininfor.status is '登录状态(0成功 1失败)'; +comment on column sys_logininfor.msg is '提示消息'; +comment on column sys_logininfor.login_time is '访问时间'; +comment on table sys_logininfor is '系统访问记录'; + +-- ---------------------------- +-- 15、定时任务调度表 +-- ---------------------------- +drop table if exists sys_job; +create table sys_job ( + job_id bigserial not null, + job_name varchar(64) default '', + job_group varchar(64) default 'default', + job_executor varchar(64) default 'default', + invoke_target varchar(500) not null, + job_args varchar(255) default '', + job_kwargs varchar(255) default '', + cron_expression varchar(255) default '', + misfire_policy varchar(20) default '3', + concurrent char(1) default '1', + status char(1) default '0', + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + remark varchar(500) default '', + primary key (job_id, job_name, job_group) +); +alter sequence sys_job_job_id_seq restart 100; +comment on column sys_job.job_id is '任务ID'; +comment on column sys_job.job_name is '任务名称'; +comment on column sys_job.job_group is '任务组名'; +comment on column sys_job.job_executor is '任务执行器'; +comment on column sys_job.invoke_target is '调用目标字符串'; +comment on column sys_job.job_args is '位置参数'; +comment on column sys_job.job_kwargs is '关键字参数'; +comment on column sys_job.cron_expression is 'cron执行表达式'; +comment on column sys_job.misfire_policy is '计划执行错误策略(1立即执行 2执行一次 3放弃执行)'; +comment on column sys_job.concurrent is '是否并发执行(0允许 1禁止)'; +comment on column sys_job.status is '状态(0正常 1暂停)'; +comment on column sys_job.create_by is '创建者'; +comment on column sys_job.create_time is '创建时间'; +comment on column sys_job.update_by is '更新者'; +comment on column sys_job.update_time is '更新时间'; +comment on column sys_job.remark is '备注信息'; +comment on table sys_job is '定时任务调度表'; + +-- ---------------------------- +-- 初始化-定时任务调度表数据 +-- ---------------------------- +insert into sys_job values(1, '系统默认(无参)', 'default', 'default', 'module_task.scheduler_test.job', null, null, '0/10 * * * * ?', '3', '1', '1', 'admin', current_timestamp, '', null, ''); +insert into sys_job values(2, '系统默认(有参)', 'default', 'default', 'module_task.scheduler_test.job', 'test', null, '0/15 * * * * ?', '3', '1', '1', 'admin', current_timestamp, '', null, ''); +insert into sys_job values(3, '系统默认(多参)', 'default', 'default', 'module_task.scheduler_test.job', 'new', '{test: 111}', '0/20 * * * * ?', '3', '1', '1', 'admin', current_timestamp, '', null, ''); + +-- ---------------------------- +-- 16、定时任务调度日志表 +-- ---------------------------- +drop table if exists sys_job_log; +create table sys_job_log ( + job_log_id bigserial not null, + job_name varchar(64) not null, + job_group varchar(64) not null, + job_executor varchar(64) not null, + invoke_target varchar(500) not null, + job_args varchar(255) default '', + job_kwargs varchar(255) default '', + job_trigger varchar(255) default '', + job_message varchar(500), + status char(1) default '0', + exception_info varchar(2000) default '', + create_time timestamp(0), + primary key (job_log_id) +); +comment on column sys_job_log.job_log_id is '任务日志ID'; +comment on column sys_job_log.job_name is '任务名称'; +comment on column sys_job_log.job_group is '任务组名'; +comment on column sys_job_log.job_executor is '任务执行器'; +comment on column sys_job_log.invoke_target is '调用目标字符串'; +comment on column sys_job_log.job_args is '位置参数'; +comment on column sys_job_log.job_kwargs is '关键字参数'; +comment on column sys_job_log.job_trigger is '任务触发器'; +comment on column sys_job_log.job_message is '日志信息'; +comment on column sys_job_log.status is '执行状态(0正常 1失败)'; +comment on column sys_job_log.exception_info is '异常信息'; +comment on column sys_job_log.create_time is '创建时间'; +comment on table sys_job_log is '定时任务调度日志表'; + +-- ---------------------------- +-- 17、通知公告表 +-- ---------------------------- +drop table if exists sys_notice; +create table sys_notice ( + notice_id serial not null, + notice_title varchar(50) not null, + notice_type char(1) not null, + notice_content bytea default null, + status char(1) default '0', + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + remark varchar(255) default null, + primary key (notice_id) +); +alter sequence sys_notice_notice_id_seq restart 10; +comment on column sys_notice.notice_id is '公告ID'; +comment on column sys_notice.notice_title is '公告标题'; +comment on column sys_notice.notice_type is '公告类型(1通知 2公告)'; +comment on column sys_notice.notice_content is '公告内容'; +comment on column sys_notice.status is '公告状态(0正常 1关闭)'; +comment on column sys_notice.create_by is '创建者'; +comment on column sys_notice.create_time is '创建时间'; +comment on column sys_notice.update_by is '更新者'; +comment on column sys_notice.update_time is '更新时间'; +comment on column sys_notice.remark is '备注'; +comment on table sys_notice is '通知公告表'; + +-- ---------------------------- +-- 初始化-公告信息表数据 +-- ---------------------------- +insert into sys_notice values(1, '温馨提醒:2018-07-01 vfadmin新版本发布啦', '2', '新版本内容', '0', 'admin', current_timestamp, '', null, '管理员'); +insert into sys_notice values(2, '维护通知:2018-07-01 vfadmin系统凌晨维护', '1', '维护内容', '0', 'admin', current_timestamp, '', null, '管理员'); + +-- ---------------------------- +-- 18、代码生成业务表 +-- ---------------------------- +drop table if exists gen_table; +create table gen_table ( + table_id bigserial not null, + table_name varchar(200) default '', + table_comment varchar(500) default '', + sub_table_name varchar(64) default null, + sub_table_fk_name varchar(64) default null, + class_name varchar(100) default '', + tpl_category varchar(200) default 'crud', + tpl_web_type varchar(30) default '', + package_name varchar(100), + module_name varchar(30), + business_name varchar(30), + function_name varchar(50), + function_author varchar(50), + gen_type char(1) default '0', + gen_path varchar(200) default '/', + options varchar(1000), + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + remark varchar(500) default null, + primary key (table_id) +); +comment on column gen_table.table_id is '编号'; +comment on column gen_table.table_name is '表名称'; +comment on column gen_table.table_comment is '表描述'; +comment on column gen_table.sub_table_name is '关联子表的表名'; +comment on column gen_table.sub_table_fk_name is '子表关联的外键名'; +comment on column gen_table.class_name is '实体类名称'; +comment on column gen_table.tpl_category is '使用的模板(crud单表操作 tree树表操作)'; +comment on column gen_table.tpl_web_type is '前端模板类型(element-ui模版 element-plus模版)'; +comment on column gen_table.package_name is '生成包路径'; +comment on column gen_table.module_name is '生成模块名'; +comment on column gen_table.business_name is '生成业务名'; +comment on column gen_table.function_name is '生成功能名'; +comment on column gen_table.function_author is '生成功能作者'; +comment on column gen_table.gen_type is '生成代码方式(0zip压缩包 1自定义路径)'; +comment on column gen_table.gen_path is '生成路径(不填默认项目路径)'; +comment on column gen_table.options is '其它生成选项'; +comment on column gen_table.create_by is '创建者'; +comment on column gen_table.create_time is '创建时间'; +comment on column gen_table.update_by is '更新者'; +comment on column gen_table.update_time is '更新时间'; +comment on column gen_table.remark is '备注'; +comment on table gen_table is '代码生成业务表'; + +-- ---------------------------- +-- 19、代码生成业务表字段 +-- ---------------------------- +drop table if exists gen_table_column; +create table gen_table_column ( + column_id bigserial not null, + table_id bigint, + column_name varchar(200), + column_comment varchar(500), + column_type varchar(100), + python_type varchar(500), + python_field varchar(200), + is_pk char(1), + is_increment char(1), + is_required char(1), + is_unique char(1), + is_insert char(1), + is_edit char(1), + is_list char(1), + is_query char(1), + query_type varchar(200) default 'EQ', + html_type varchar(200), + dict_type varchar(200) default '', + sort int4, + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + primary key (column_id) +); +comment on column gen_table_column.column_id is '编号'; +comment on column gen_table_column.table_id is '归属表编号'; +comment on column gen_table_column.column_name is '列名称'; +comment on column gen_table_column.column_comment is '列描述'; +comment on column gen_table_column.column_type is '列类型'; +comment on column gen_table_column.python_type is 'PYTHON类型'; +comment on column gen_table_column.python_field is 'PYTHON字段名'; +comment on column gen_table_column.is_pk is '是否主键(1是)'; +comment on column gen_table_column.is_increment is '是否自增(1是)'; +comment on column gen_table_column.is_required is '是否必填(1是)'; +comment on column gen_table_column.is_unique is '是否唯一(1是)'; +comment on column gen_table_column.is_insert is '是否为插入字段(1是)'; +comment on column gen_table_column.is_edit is '是否编辑字段(1是)'; +comment on column gen_table_column.is_list is '是否列表字段(1是)'; +comment on column gen_table_column.is_query is '是否查询字段(1是)'; +comment on column gen_table_column.query_type is '查询方式(等于、不等于、大于、小于、范围)'; +comment on column gen_table_column.html_type is '显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)'; +comment on column gen_table_column.dict_type is '字典类型'; +comment on column gen_table_column.sort is '排序'; +comment on column gen_table_column.create_by is '创建者'; +comment on column gen_table_column.create_time is '创建时间'; +comment on column gen_table_column.update_by is '更新者'; +comment on column gen_table_column.update_time is '更新时间'; +comment on table gen_table_column is '代码生成业务表字段'; + +CREATE OR REPLACE FUNCTION "find_in_set"(int8, varchar) + RETURNS "pg_catalog"."bool" AS $BODY$ +DECLARE + STR ALIAS FOR $1; + STRS ALIAS FOR $2; + POS INTEGER; + STATUS BOOLEAN; +BEGIN + SELECT POSITION( ','||STR||',' IN ','||STRS||',') INTO POS; + IF POS > 0 THEN + STATUS = TRUE; + ELSE + STATUS = FALSE; + END IF; + RETURN STATUS; +END; +$BODY$ + LANGUAGE plpgsql VOLATILE + COST 100; + +create or replace view list_column as +SELECT c.relname AS table_name, + a.attname AS column_name, + d.description AS column_comment, + CASE + WHEN a.attnotnull AND con.conname IS NULL THEN '1' + ELSE '0' + END AS is_required, + CASE + WHEN con.conname IS NOT NULL THEN '1' + ELSE '0' + END AS is_pk, + a.attnum AS sort, + CASE + WHEN "position"(pg_get_expr(ad.adbin, ad.adrelid), ((c.relname::text || '_'::text) || a.attname + ::text) || '_seq'::text) > 0 THEN '1' + ELSE '0' + END AS is_increment, + btrim( + CASE + WHEN t.typelem <> 0::oid AND t.typlen = '-1'::integer THEN 'ARRAY'::text + ELSE + CASE + WHEN t.typtype = 'd'::"char" THEN format_type(t.typbasetype, NULL::integer) + ELSE format_type(a.atttypid, NULL::integer) + END + END, '"'::text) AS column_type +FROM pg_attribute a + JOIN (pg_class c + JOIN pg_namespace n ON c.relnamespace = n.oid) ON a.attrelid = c.oid + LEFT JOIN pg_description d ON d.objoid = c.oid AND a.attnum = d.objsubid + LEFT JOIN pg_constraint con ON con.conrelid = c.oid AND (a.attnum = ANY (con.conkey)) + LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum + LEFT JOIN pg_type t ON a.atttypid = t.oid +WHERE (c.relkind = ANY (ARRAY['r'::"char", 'p'::"char"])) + AND a.attnum > 0 + AND n.nspname = 'public'::name + AND not a.attisdropped + ORDER BY c.relname, a.attnum; + +create or replace view list_table as +SELECT c.relname AS table_name, + obj_description(c.oid) AS table_comment, + CURRENT_TIMESTAMP AS create_time, + CURRENT_TIMESTAMP AS update_time +FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace +WHERE (c.relkind = ANY (ARRAY['r'::"char", 'p'::"char"])) + AND c.relname !~~ 'spatial_%'::text AND n.nspname = 'public'::name AND n.nspname <> ''::name; + +CREATE OR REPLACE FUNCTION substring_index(varchar, varchar, integer) +RETURNS varchar AS $$ +DECLARE +tokens varchar[]; +length integer ; +indexnum integer; +BEGIN +tokens := pg_catalog.string_to_array($1, $2); +length := pg_catalog.array_upper(tokens, 1); +indexnum := length - ($3 * -1) + 1; +IF $3 >= 0 THEN +RETURN pg_catalog.array_to_string(tokens[1:$3], $2); +ELSE +RETURN pg_catalog.array_to_string(tokens[indexnum:length], $2); +END IF; +END; +$$ IMMUTABLE STRICT LANGUAGE PLPGSQL; diff --git a/ruoyi-fastapi-backend/sql/ruoyi-fastapi.sql b/ruoyi-fastapi-backend/sql/ruoyi-fastapi.sql new file mode 100644 index 0000000..d0ce11a --- /dev/null +++ b/ruoyi-fastapi-backend/sql/ruoyi-fastapi.sql @@ -0,0 +1,713 @@ +-- ---------------------------- +-- 1、部门表 +-- ---------------------------- +drop table if exists sys_dept; +create table sys_dept ( + dept_id bigint(20) not null auto_increment comment '部门id', + parent_id bigint(20) default 0 comment '父部门id', + ancestors varchar(50) default '' comment '祖级列表', + dept_name varchar(30) default '' comment '部门名称', + order_num int(4) default 0 comment '显示顺序', + leader varchar(20) default null comment '负责人', + phone varchar(11) default null comment '联系电话', + email varchar(50) default null comment '邮箱', + status char(1) default '0' comment '部门状态(0正常 1停用)', + del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + primary key (dept_id) +) engine=innodb auto_increment=200 comment = '部门表'; + +-- ---------------------------- +-- 初始化-部门表数据 +-- ---------------------------- +insert into sys_dept values(100, 0, '0', '集团总公司', 0, '年糕', '15888888888', 'niangao@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(101, 100, '0,100', '深圳分公司', 1, '年糕', '15888888888', 'niangao@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(102, 100, '0,100', '长沙分公司', 2, '年糕', '15888888888', 'niangao@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(103, 101, '0,100,101', '研发部门', 1, '年糕', '15888888888', 'niangao@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(104, 101, '0,100,101', '市场部门', 2, '年糕', '15888888888', 'niangao@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(105, 101, '0,100,101', '测试部门', 3, '年糕', '15888888888', 'niangao@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(106, 101, '0,100,101', '财务部门', 4, '年糕', '15888888888', 'niangao@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(107, 101, '0,100,101', '运维部门', 5, '年糕', '15888888888', 'niangao@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(108, 102, '0,100,102', '市场部门', 1, '年糕', '15888888888', 'niangao@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(109, 102, '0,100,102', '财务部门', 2, '年糕', '15888888888', 'niangao@qq.com', '0', '0', 'admin', sysdate(), '', null); + + +-- ---------------------------- +-- 2、用户信息表 +-- ---------------------------- +drop table if exists sys_user; +create table sys_user ( + user_id bigint(20) not null auto_increment comment '用户ID', + dept_id bigint(20) default null comment '部门ID', + user_name varchar(30) not null comment '用户账号', + nick_name varchar(30) not null comment '用户昵称', + user_type varchar(2) default '00' comment '用户类型(00系统用户)', + email varchar(50) default '' comment '用户邮箱', + phonenumber varchar(11) default '' comment '手机号码', + sex char(1) default '0' comment '用户性别(0男 1女 2未知)', + avatar varchar(100) default '' comment '头像地址', + password varchar(100) default '' comment '密码', + status char(1) default '0' comment '帐号状态(0正常 1停用)', + del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)', + login_ip varchar(128) default '' comment '最后登录IP', + login_date datetime comment '最后登录时间', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (user_id) +) engine=innodb auto_increment=100 comment = '用户信息表'; + +-- ---------------------------- +-- 初始化-用户信息表数据 +-- ---------------------------- +insert into sys_user values(1, 103, 'admin', '超级管理员', '00', 'niangao@163.com', '15888888888', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', sysdate(), 'admin', sysdate(), '', null, '管理员'); +insert into sys_user values(2, 105, 'niangao', '年糕', '00', 'niangao@qq.com', '15666666666', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', sysdate(), 'admin', sysdate(), '', null, '测试员'); + + +-- ---------------------------- +-- 3、岗位信息表 +-- ---------------------------- +drop table if exists sys_post; +create table sys_post +( + post_id bigint(20) not null auto_increment comment '岗位ID', + post_code varchar(64) not null comment '岗位编码', + post_name varchar(50) not null comment '岗位名称', + post_sort int(4) not null comment '显示顺序', + status char(1) not null comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (post_id) +) engine=innodb comment = '岗位信息表'; + +-- ---------------------------- +-- 初始化-岗位信息表数据 +-- ---------------------------- +insert into sys_post values(1, 'ceo', '董事长', 1, '0', 'admin', sysdate(), '', null, ''); +insert into sys_post values(2, 'se', '项目经理', 2, '0', 'admin', sysdate(), '', null, ''); +insert into sys_post values(3, 'hr', '人力资源', 3, '0', 'admin', sysdate(), '', null, ''); +insert into sys_post values(4, 'user', '普通员工', 4, '0', 'admin', sysdate(), '', null, ''); + + +-- ---------------------------- +-- 4、角色信息表 +-- ---------------------------- +drop table if exists sys_role; +create table sys_role ( + role_id bigint(20) not null auto_increment comment '角色ID', + role_name varchar(30) not null comment '角色名称', + role_key varchar(100) not null comment '角色权限字符串', + role_sort int(4) not null comment '显示顺序', + data_scope char(1) default '1' comment '数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)', + menu_check_strictly tinyint(1) default 1 comment '菜单树选择项是否关联显示', + dept_check_strictly tinyint(1) default 1 comment '部门树选择项是否关联显示', + status char(1) not null comment '角色状态(0正常 1停用)', + del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (role_id) +) engine=innodb auto_increment=100 comment = '角色信息表'; + +-- ---------------------------- +-- 初始化-角色信息表数据 +-- ---------------------------- +insert into sys_role values('1', '超级管理员', 'admin', 1, 1, 1, 1, '0', '0', 'admin', sysdate(), '', null, '超级管理员'); +insert into sys_role values('2', '普通角色', 'common', 2, 2, 1, 1, '0', '0', 'admin', sysdate(), '', null, '普通角色'); + + +-- ---------------------------- +-- 5、菜单权限表 +-- ---------------------------- +drop table if exists sys_menu; +create table sys_menu ( + menu_id bigint(20) not null auto_increment comment '菜单ID', + menu_name varchar(50) not null comment '菜单名称', + parent_id bigint(20) default 0 comment '父菜单ID', + order_num int(4) default 0 comment '显示顺序', + path varchar(200) default '' comment '路由地址', + component varchar(255) default null comment '组件路径', + query varchar(255) default null comment '路由参数', + route_name varchar(50) default '' comment '路由名称', + is_frame int(1) default 1 comment '是否为外链(0是 1否)', + is_cache int(1) default 0 comment '是否缓存(0缓存 1不缓存)', + menu_type char(1) default '' comment '菜单类型(M目录 C菜单 F按钮)', + visible char(1) default 0 comment '菜单状态(0显示 1隐藏)', + status char(1) default 0 comment '菜单状态(0正常 1停用)', + perms varchar(100) default null comment '权限标识', + icon varchar(100) default '#' comment '菜单图标', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default '' comment '备注', + primary key (menu_id) +) engine=innodb auto_increment=2000 comment = '菜单权限表'; + +-- ---------------------------- +-- 初始化-菜单信息表数据 +-- ---------------------------- +-- 一级菜单 +insert into sys_menu values('1', '系统管理', '0', '1', 'system', null, '', '', 1, 0, 'M', '0', '0', '', 'system', 'admin', sysdate(), '', null, '系统管理目录'); +insert into sys_menu values('2', '系统监控', '0', '2', 'monitor', null, '', '', 1, 0, 'M', '0', '0', '', 'monitor', 'admin', sysdate(), '', null, '系统监控目录'); +insert into sys_menu values('3', '系统工具', '0', '3', 'tool', null, '', '', 1, 0, 'M', '0', '0', '', 'tool', 'admin', sysdate(), '', null, '系统工具目录'); +insert into sys_menu values('4', '若依官网', '0', '4', 'http://ruoyi.vip', null, '', '', 0, 0, 'M', '0', '0', '', 'guide', 'admin', sysdate(), '', null, '若依官网地址'); +-- 二级菜单 +insert into sys_menu values('100', '用户管理', '1', '1', 'user', 'system/user/index', '', '', 1, 0, 'C', '0', '0', 'system:user:list', 'user', 'admin', sysdate(), '', null, '用户管理菜单'); +insert into sys_menu values('101', '角色管理', '1', '2', 'role', 'system/role/index', '', '', 1, 0, 'C', '0', '0', 'system:role:list', 'peoples', 'admin', sysdate(), '', null, '角色管理菜单'); +insert into sys_menu values('102', '菜单管理', '1', '3', 'menu', 'system/menu/index', '', '', 1, 0, 'C', '0', '0', 'system:menu:list', 'tree-table', 'admin', sysdate(), '', null, '菜单管理菜单'); +insert into sys_menu values('103', '部门管理', '1', '4', 'dept', 'system/dept/index', '', '', 1, 0, 'C', '0', '0', 'system:dept:list', 'tree', 'admin', sysdate(), '', null, '部门管理菜单'); +insert into sys_menu values('104', '岗位管理', '1', '5', 'post', 'system/post/index', '', '', 1, 0, 'C', '0', '0', 'system:post:list', 'post', 'admin', sysdate(), '', null, '岗位管理菜单'); +insert into sys_menu values('105', '字典管理', '1', '6', 'dict', 'system/dict/index', '', '', 1, 0, 'C', '0', '0', 'system:dict:list', 'dict', 'admin', sysdate(), '', null, '字典管理菜单'); +insert into sys_menu values('106', '参数设置', '1', '7', 'config', 'system/config/index', '', '', 1, 0, 'C', '0', '0', 'system:config:list', 'edit', 'admin', sysdate(), '', null, '参数设置菜单'); +insert into sys_menu values('107', '通知公告', '1', '8', 'notice', 'system/notice/index', '', '', 1, 0, 'C', '0', '0', 'system:notice:list', 'message', 'admin', sysdate(), '', null, '通知公告菜单'); +insert into sys_menu values('108', '日志管理', '1', '9', 'log', '', '', '', 1, 0, 'M', '0', '0', '', 'log', 'admin', sysdate(), '', null, '日志管理菜单'); +insert into sys_menu values('109', '在线用户', '2', '1', 'online', 'monitor/online/index', '', '', 1, 0, 'C', '0', '0', 'monitor:online:list', 'online', 'admin', sysdate(), '', null, '在线用户菜单'); +insert into sys_menu values('110', '定时任务', '2', '2', 'job', 'monitor/job/index', '', '', 1, 0, 'C', '0', '0', 'monitor:job:list', 'job', 'admin', sysdate(), '', null, '定时任务菜单'); +insert into sys_menu values('111', '数据监控', '2', '3', 'druid', 'monitor/druid/index', '', '', 1, 0, 'C', '0', '0', 'monitor:druid:list', 'druid', 'admin', sysdate(), '', null, '数据监控菜单'); +insert into sys_menu values('112', '服务监控', '2', '4', 'server', 'monitor/server/index', '', '', 1, 0, 'C', '0', '0', 'monitor:server:list', 'server', 'admin', sysdate(), '', null, '服务监控菜单'); +insert into sys_menu values('113', '缓存监控', '2', '5', 'cache', 'monitor/cache/index', '', '', 1, 0, 'C', '0', '0', 'monitor:cache:list', 'redis', 'admin', sysdate(), '', null, '缓存监控菜单'); +insert into sys_menu values('114', '缓存列表', '2', '6', 'cacheList', 'monitor/cache/list', '', '', 1, 0, 'C', '0', '0', 'monitor:cache:list', 'redis-list', 'admin', sysdate(), '', null, '缓存列表菜单'); +insert into sys_menu values('115', '表单构建', '3', '1', 'build', 'tool/build/index', '', '', 1, 0, 'C', '0', '0', 'tool:build:list', 'build', 'admin', sysdate(), '', null, '表单构建菜单'); +insert into sys_menu values('116', '代码生成', '3', '2', 'gen', 'tool/gen/index', '', '', 1, 0, 'C', '0', '0', 'tool:gen:list', 'code', 'admin', sysdate(), '', null, '代码生成菜单'); +insert into sys_menu values('117', '系统接口', '3', '3', 'swagger', 'tool/swagger/index', '', '', 1, 0, 'C', '0', '0', 'tool:swagger:list', 'swagger', 'admin', sysdate(), '', null, '系统接口菜单'); +-- 三级菜单 +insert into sys_menu values('500', '操作日志', '108', '1', 'operlog', 'monitor/operlog/index', '', '', 1, 0, 'C', '0', '0', 'monitor:operlog:list', 'form', 'admin', sysdate(), '', null, '操作日志菜单'); +insert into sys_menu values('501', '登录日志', '108', '2', 'logininfor', 'monitor/logininfor/index', '', '', 1, 0, 'C', '0', '0', 'monitor:logininfor:list', 'logininfor', 'admin', sysdate(), '', null, '登录日志菜单'); +-- 用户管理按钮 +insert into sys_menu values('1000', '用户查询', '100', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1001', '用户新增', '100', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1002', '用户修改', '100', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1003', '用户删除', '100', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1004', '用户导出', '100', '5', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:export', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1005', '用户导入', '100', '6', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:import', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1006', '重置密码', '100', '7', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:resetPwd', '#', 'admin', sysdate(), '', null, ''); +-- 角色管理按钮 +insert into sys_menu values('1007', '角色查询', '101', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1008', '角色新增', '101', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1009', '角色修改', '101', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1010', '角色删除', '101', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1011', '角色导出', '101', '5', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:export', '#', 'admin', sysdate(), '', null, ''); +-- 菜单管理按钮 +insert into sys_menu values('1012', '菜单查询', '102', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1013', '菜单新增', '102', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1014', '菜单修改', '102', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1015', '菜单删除', '102', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:remove', '#', 'admin', sysdate(), '', null, ''); +-- 部门管理按钮 +insert into sys_menu values('1016', '部门查询', '103', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1017', '部门新增', '103', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1018', '部门修改', '103', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1019', '部门删除', '103', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:remove', '#', 'admin', sysdate(), '', null, ''); +-- 岗位管理按钮 +insert into sys_menu values('1020', '岗位查询', '104', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1021', '岗位新增', '104', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1022', '岗位修改', '104', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1023', '岗位删除', '104', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1024', '岗位导出', '104', '5', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:export', '#', 'admin', sysdate(), '', null, ''); +-- 字典管理按钮 +insert into sys_menu values('1025', '字典查询', '105', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1026', '字典新增', '105', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1027', '字典修改', '105', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1028', '字典删除', '105', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1029', '字典导出', '105', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:export', '#', 'admin', sysdate(), '', null, ''); +-- 参数设置按钮 +insert into sys_menu values('1030', '参数查询', '106', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1031', '参数新增', '106', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1032', '参数修改', '106', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1033', '参数删除', '106', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1034', '参数导出', '106', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:export', '#', 'admin', sysdate(), '', null, ''); +-- 通知公告按钮 +insert into sys_menu values('1035', '公告查询', '107', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1036', '公告新增', '107', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1037', '公告修改', '107', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1038', '公告删除', '107', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:remove', '#', 'admin', sysdate(), '', null, ''); +-- 操作日志按钮 +insert into sys_menu values('1039', '操作查询', '500', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:operlog:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1040', '操作删除', '500', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:operlog:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1041', '日志导出', '500', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:operlog:export', '#', 'admin', sysdate(), '', null, ''); +-- 登录日志按钮 +insert into sys_menu values('1042', '登录查询', '501', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1043', '登录删除', '501', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1044', '日志导出', '501', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:export', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1045', '账户解锁', '501', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:unlock', '#', 'admin', sysdate(), '', null, ''); +-- 在线用户按钮 +insert into sys_menu values('1046', '在线查询', '109', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:online:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1047', '批量强退', '109', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:online:batchLogout', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1048', '单条强退', '109', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:online:forceLogout', '#', 'admin', sysdate(), '', null, ''); +-- 定时任务按钮 +insert into sys_menu values('1049', '任务查询', '110', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1050', '任务新增', '110', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1051', '任务修改', '110', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1052', '任务删除', '110', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1053', '状态修改', '110', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:changeStatus', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1054', '任务导出', '110', '6', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:export', '#', 'admin', sysdate(), '', null, ''); +-- 代码生成按钮 +insert into sys_menu values('1055', '生成查询', '116', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1056', '生成修改', '116', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1057', '生成删除', '116', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1058', '导入代码', '116', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:import', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1059', '预览代码', '116', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:preview', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1060', '生成代码', '116', '6', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:code', '#', 'admin', sysdate(), '', null, ''); + + +-- ---------------------------- +-- 6、用户和角色关联表 用户N-1角色 +-- ---------------------------- +drop table if exists sys_user_role; +create table sys_user_role ( + user_id bigint(20) not null comment '用户ID', + role_id bigint(20) not null comment '角色ID', + primary key(user_id, role_id) +) engine=innodb comment = '用户和角色关联表'; + +-- ---------------------------- +-- 初始化-用户和角色关联表数据 +-- ---------------------------- +insert into sys_user_role values ('1', '1'); +insert into sys_user_role values ('2', '2'); + + +-- ---------------------------- +-- 7、角色和菜单关联表 角色1-N菜单 +-- ---------------------------- +drop table if exists sys_role_menu; +create table sys_role_menu ( + role_id bigint(20) not null comment '角色ID', + menu_id bigint(20) not null comment '菜单ID', + primary key(role_id, menu_id) +) engine=innodb comment = '角色和菜单关联表'; + +-- ---------------------------- +-- 初始化-角色和菜单关联表数据 +-- ---------------------------- +insert into sys_role_menu values ('2', '1'); +insert into sys_role_menu values ('2', '2'); +insert into sys_role_menu values ('2', '3'); +insert into sys_role_menu values ('2', '4'); +insert into sys_role_menu values ('2', '100'); +insert into sys_role_menu values ('2', '101'); +insert into sys_role_menu values ('2', '102'); +insert into sys_role_menu values ('2', '103'); +insert into sys_role_menu values ('2', '104'); +insert into sys_role_menu values ('2', '105'); +insert into sys_role_menu values ('2', '106'); +insert into sys_role_menu values ('2', '107'); +insert into sys_role_menu values ('2', '108'); +insert into sys_role_menu values ('2', '109'); +insert into sys_role_menu values ('2', '110'); +insert into sys_role_menu values ('2', '111'); +insert into sys_role_menu values ('2', '112'); +insert into sys_role_menu values ('2', '113'); +insert into sys_role_menu values ('2', '114'); +insert into sys_role_menu values ('2', '115'); +insert into sys_role_menu values ('2', '116'); +insert into sys_role_menu values ('2', '117'); +insert into sys_role_menu values ('2', '500'); +insert into sys_role_menu values ('2', '501'); +insert into sys_role_menu values ('2', '1000'); +insert into sys_role_menu values ('2', '1001'); +insert into sys_role_menu values ('2', '1002'); +insert into sys_role_menu values ('2', '1003'); +insert into sys_role_menu values ('2', '1004'); +insert into sys_role_menu values ('2', '1005'); +insert into sys_role_menu values ('2', '1006'); +insert into sys_role_menu values ('2', '1007'); +insert into sys_role_menu values ('2', '1008'); +insert into sys_role_menu values ('2', '1009'); +insert into sys_role_menu values ('2', '1010'); +insert into sys_role_menu values ('2', '1011'); +insert into sys_role_menu values ('2', '1012'); +insert into sys_role_menu values ('2', '1013'); +insert into sys_role_menu values ('2', '1014'); +insert into sys_role_menu values ('2', '1015'); +insert into sys_role_menu values ('2', '1016'); +insert into sys_role_menu values ('2', '1017'); +insert into sys_role_menu values ('2', '1018'); +insert into sys_role_menu values ('2', '1019'); +insert into sys_role_menu values ('2', '1020'); +insert into sys_role_menu values ('2', '1021'); +insert into sys_role_menu values ('2', '1022'); +insert into sys_role_menu values ('2', '1023'); +insert into sys_role_menu values ('2', '1024'); +insert into sys_role_menu values ('2', '1025'); +insert into sys_role_menu values ('2', '1026'); +insert into sys_role_menu values ('2', '1027'); +insert into sys_role_menu values ('2', '1028'); +insert into sys_role_menu values ('2', '1029'); +insert into sys_role_menu values ('2', '1030'); +insert into sys_role_menu values ('2', '1031'); +insert into sys_role_menu values ('2', '1032'); +insert into sys_role_menu values ('2', '1033'); +insert into sys_role_menu values ('2', '1034'); +insert into sys_role_menu values ('2', '1035'); +insert into sys_role_menu values ('2', '1036'); +insert into sys_role_menu values ('2', '1037'); +insert into sys_role_menu values ('2', '1038'); +insert into sys_role_menu values ('2', '1039'); +insert into sys_role_menu values ('2', '1040'); +insert into sys_role_menu values ('2', '1041'); +insert into sys_role_menu values ('2', '1042'); +insert into sys_role_menu values ('2', '1043'); +insert into sys_role_menu values ('2', '1044'); +insert into sys_role_menu values ('2', '1045'); +insert into sys_role_menu values ('2', '1046'); +insert into sys_role_menu values ('2', '1047'); +insert into sys_role_menu values ('2', '1048'); +insert into sys_role_menu values ('2', '1049'); +insert into sys_role_menu values ('2', '1050'); +insert into sys_role_menu values ('2', '1051'); +insert into sys_role_menu values ('2', '1052'); +insert into sys_role_menu values ('2', '1053'); +insert into sys_role_menu values ('2', '1054'); +insert into sys_role_menu values ('2', '1055'); +insert into sys_role_menu values ('2', '1056'); +insert into sys_role_menu values ('2', '1057'); +insert into sys_role_menu values ('2', '1058'); +insert into sys_role_menu values ('2', '1059'); +insert into sys_role_menu values ('2', '1060'); + +-- ---------------------------- +-- 8、角色和部门关联表 角色1-N部门 +-- ---------------------------- +drop table if exists sys_role_dept; +create table sys_role_dept ( + role_id bigint(20) not null comment '角色ID', + dept_id bigint(20) not null comment '部门ID', + primary key(role_id, dept_id) +) engine=innodb comment = '角色和部门关联表'; + +-- ---------------------------- +-- 初始化-角色和部门关联表数据 +-- ---------------------------- +insert into sys_role_dept values ('2', '100'); +insert into sys_role_dept values ('2', '101'); +insert into sys_role_dept values ('2', '105'); + + +-- ---------------------------- +-- 9、用户与岗位关联表 用户1-N岗位 +-- ---------------------------- +drop table if exists sys_user_post; +create table sys_user_post +( + user_id bigint(20) not null comment '用户ID', + post_id bigint(20) not null comment '岗位ID', + primary key (user_id, post_id) +) engine=innodb comment = '用户与岗位关联表'; + +-- ---------------------------- +-- 初始化-用户与岗位关联表数据 +-- ---------------------------- +insert into sys_user_post values ('1', '1'); +insert into sys_user_post values ('2', '2'); + + +-- ---------------------------- +-- 10、操作日志记录 +-- ---------------------------- +drop table if exists sys_oper_log; +create table sys_oper_log ( + oper_id bigint(20) not null auto_increment comment '日志主键', + title varchar(50) default '' comment '模块标题', + business_type int(2) default 0 comment '业务类型(0其它 1新增 2修改 3删除)', + method varchar(100) default '' comment '方法名称', + request_method varchar(10) default '' comment '请求方式', + operator_type int(1) default 0 comment '操作类别(0其它 1后台用户 2手机端用户)', + oper_name varchar(50) default '' comment '操作人员', + dept_name varchar(50) default '' comment '部门名称', + oper_url varchar(255) default '' comment '请求URL', + oper_ip varchar(128) default '' comment '主机地址', + oper_location varchar(255) default '' comment '操作地点', + oper_param varchar(2000) default '' comment '请求参数', + json_result varchar(2000) default '' comment '返回参数', + status int(1) default 0 comment '操作状态(0正常 1异常)', + error_msg varchar(2000) default '' comment '错误消息', + oper_time datetime comment '操作时间', + cost_time bigint(20) default 0 comment '消耗时间', + primary key (oper_id), + key idx_sys_oper_log_bt (business_type), + key idx_sys_oper_log_s (status), + key idx_sys_oper_log_ot (oper_time) +) engine=innodb auto_increment=100 comment = '操作日志记录'; + + +-- ---------------------------- +-- 11、字典类型表 +-- ---------------------------- +drop table if exists sys_dict_type; +create table sys_dict_type +( + dict_id bigint(20) not null auto_increment comment '字典主键', + dict_name varchar(100) default '' comment '字典名称', + dict_type varchar(100) default '' comment '字典类型', + status char(1) default '0' comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (dict_id), + unique (dict_type) +) engine=innodb auto_increment=100 comment = '字典类型表'; + +insert into sys_dict_type values(1, '用户性别', 'sys_user_sex', '0', 'admin', sysdate(), '', null, '用户性别列表'); +insert into sys_dict_type values(2, '菜单状态', 'sys_show_hide', '0', 'admin', sysdate(), '', null, '菜单状态列表'); +insert into sys_dict_type values(3, '系统开关', 'sys_normal_disable', '0', 'admin', sysdate(), '', null, '系统开关列表'); +insert into sys_dict_type values(4, '任务状态', 'sys_job_status', '0', 'admin', sysdate(), '', null, '任务状态列表'); +insert into sys_dict_type values(5, '任务分组', 'sys_job_group', '0', 'admin', sysdate(), '', null, '任务分组列表'); +insert into sys_dict_type values(6, '任务执行器', 'sys_job_executor', '0', 'admin', sysdate(), '', null, '任务执行器列表'); +insert into sys_dict_type values(7, '系统是否', 'sys_yes_no', '0', 'admin', sysdate(), '', null, '系统是否列表'); +insert into sys_dict_type values(8, '通知类型', 'sys_notice_type', '0', 'admin', sysdate(), '', null, '通知类型列表'); +insert into sys_dict_type values(9, '通知状态', 'sys_notice_status', '0', 'admin', sysdate(), '', null, '通知状态列表'); +insert into sys_dict_type values(10, '操作类型', 'sys_oper_type', '0', 'admin', sysdate(), '', null, '操作类型列表'); +insert into sys_dict_type values(11, '系统状态', 'sys_common_status', '0', 'admin', sysdate(), '', null, '登录状态列表'); + + +-- ---------------------------- +-- 12、字典数据表 +-- ---------------------------- +drop table if exists sys_dict_data; +create table sys_dict_data +( + dict_code bigint(20) not null auto_increment comment '字典编码', + dict_sort int(4) default 0 comment '字典排序', + dict_label varchar(100) default '' comment '字典标签', + dict_value varchar(100) default '' comment '字典键值', + dict_type varchar(100) default '' comment '字典类型', + css_class varchar(100) default null comment '样式属性(其他样式扩展)', + list_class varchar(100) default null comment '表格回显样式', + is_default char(1) default 'N' comment '是否默认(Y是 N否)', + status char(1) default '0' comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (dict_code) +) engine=innodb auto_increment=100 comment = '字典数据表'; + +insert into sys_dict_data values(1, 1, '男', '0', 'sys_user_sex', '', '', 'Y', '0', 'admin', sysdate(), '', null, '性别男'); +insert into sys_dict_data values(2, 2, '女', '1', 'sys_user_sex', '', '', 'N', '0', 'admin', sysdate(), '', null, '性别女'); +insert into sys_dict_data values(3, 3, '未知', '2', 'sys_user_sex', '', '', 'N', '0', 'admin', sysdate(), '', null, '性别未知'); +insert into sys_dict_data values(4, 1, '显示', '0', 'sys_show_hide', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '显示菜单'); +insert into sys_dict_data values(5, 2, '隐藏', '1', 'sys_show_hide', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '隐藏菜单'); +insert into sys_dict_data values(6, 1, '正常', '0', 'sys_normal_disable', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '正常状态'); +insert into sys_dict_data values(7, 2, '停用', '1', 'sys_normal_disable', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '停用状态'); +insert into sys_dict_data values(8, 1, '正常', '0', 'sys_job_status', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '正常状态'); +insert into sys_dict_data values(9, 2, '暂停', '1', 'sys_job_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '停用状态'); +insert into sys_dict_data values(10, 1, '默认', 'default', 'sys_job_group', '', '', 'Y', '0', 'admin', sysdate(), '', null, '默认分组'); +insert into sys_dict_data values(11, 2, '数据库', 'sqlalchemy', 'sys_job_group', '', '', 'N', '0', 'admin', sysdate(), '', null, '数据库分组'); +insert into sys_dict_data values(12, 3, 'redis', 'redis', 'sys_job_group', '', '', 'N', '0', 'admin', sysdate(), '', null, 'reids分组'); +insert into sys_dict_data values(13, 1, '默认', 'default', 'sys_job_executor', '', '', 'N', '0', 'admin', sysdate(), '', null, '线程池'); +insert into sys_dict_data values(14, 2, '进程池', 'processpool', 'sys_job_executor', '', '', 'N', '0', 'admin', sysdate(), '', null, '进程池'); +insert into sys_dict_data values(15, 1, '是', 'Y', 'sys_yes_no', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '系统默认是'); +insert into sys_dict_data values(16, 2, '否', 'N', 'sys_yes_no', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '系统默认否'); +insert into sys_dict_data values(17, 1, '通知', '1', 'sys_notice_type', '', 'warning', 'Y', '0', 'admin', sysdate(), '', null, '通知'); +insert into sys_dict_data values(18, 2, '公告', '2', 'sys_notice_type', '', 'success', 'N', '0', 'admin', sysdate(), '', null, '公告'); +insert into sys_dict_data values(19, 1, '正常', '0', 'sys_notice_status', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '正常状态'); +insert into sys_dict_data values(20, 2, '关闭', '1', 'sys_notice_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '关闭状态'); +insert into sys_dict_data values(21, 99, '其他', '0', 'sys_oper_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '其他操作'); +insert into sys_dict_data values(22, 1, '新增', '1', 'sys_oper_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '新增操作'); +insert into sys_dict_data values(23, 2, '修改', '2', 'sys_oper_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '修改操作'); +insert into sys_dict_data values(24, 3, '删除', '3', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '删除操作'); +insert into sys_dict_data values(25, 4, '授权', '4', 'sys_oper_type', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '授权操作'); +insert into sys_dict_data values(26, 5, '导出', '5', 'sys_oper_type', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '导出操作'); +insert into sys_dict_data values(27, 6, '导入', '6', 'sys_oper_type', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '导入操作'); +insert into sys_dict_data values(28, 7, '强退', '7', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '强退操作'); +insert into sys_dict_data values(29, 8, '生成代码', '8', 'sys_oper_type', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '生成操作'); +insert into sys_dict_data values(30, 9, '清空数据', '9', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '清空操作'); +insert into sys_dict_data values(31, 1, '成功', '0', 'sys_common_status', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '正常状态'); +insert into sys_dict_data values(32, 2, '失败', '1', 'sys_common_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '停用状态'); + + +-- ---------------------------- +-- 13、参数配置表 +-- ---------------------------- +drop table if exists sys_config; +create table sys_config ( + config_id int(5) not null auto_increment comment '参数主键', + config_name varchar(100) default '' comment '参数名称', + config_key varchar(100) default '' comment '参数键名', + config_value varchar(500) default '' comment '参数键值', + config_type char(1) default 'N' comment '系统内置(Y是 N否)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (config_id) +) engine=innodb auto_increment=100 comment = '参数配置表'; + +insert into sys_config values(1, '主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', 'admin', sysdate(), '', null, '蓝色 skin-blue、绿色 skin-green、紫色 skin-purple、红色 skin-red、黄色 skin-yellow' ); +insert into sys_config values(2, '用户管理-账号初始密码', 'sys.user.initPassword', '123456', 'Y', 'admin', sysdate(), '', null, '初始化密码 123456' ); +insert into sys_config values(3, '主框架页-侧边栏主题', 'sys.index.sideTheme', 'theme-dark', 'Y', 'admin', sysdate(), '', null, '深色主题theme-dark,浅色主题theme-light' ); +insert into sys_config values(4, '账号自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'admin', sysdate(), '', null, '是否开启验证码功能(true开启,false关闭)'); +insert into sys_config values(5, '账号自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 'admin', sysdate(), '', null, '是否开启注册用户功能(true开启,false关闭)'); +insert into sys_config values(6, '用户登录-黑名单列表', 'sys.login.blackIPList', '', 'Y', 'admin', sysdate(), '', null, '设置登录IP黑名单限制,多个匹配项以;分隔,支持匹配(*通配、网段)'); + + +-- ---------------------------- +-- 14、系统访问记录 +-- ---------------------------- +drop table if exists sys_logininfor; +create table sys_logininfor ( + info_id bigint(20) not null auto_increment comment '访问ID', + user_name varchar(50) default '' comment '用户账号', + ipaddr varchar(128) default '' comment '登录IP地址', + login_location varchar(255) default '' comment '登录地点', + browser varchar(50) default '' comment '浏览器类型', + os varchar(50) default '' comment '操作系统', + status char(1) default '0' comment '登录状态(0成功 1失败)', + msg varchar(255) default '' comment '提示消息', + login_time datetime comment '访问时间', + primary key (info_id), + key idx_sys_logininfor_s (status), + key idx_sys_logininfor_lt (login_time) +) engine=innodb auto_increment=100 comment = '系统访问记录'; + + +-- ---------------------------- +-- 15、定时任务调度表 +-- ---------------------------- +drop table if exists sys_job; +create table sys_job ( + job_id bigint(20) not null auto_increment comment '任务ID', + job_name varchar(64) default '' comment '任务名称', + job_group varchar(64) default 'default' comment '任务组名', + job_executor varchar(64) default 'default' comment '任务执行器', + invoke_target varchar(500) not null comment '调用目标字符串', + job_args varchar(255) default '' comment '位置参数', + job_kwargs varchar(255) default '' comment '关键字参数', + cron_expression varchar(255) default '' comment 'cron执行表达式', + misfire_policy varchar(20) default '3' comment '计划执行错误策略(1立即执行 2执行一次 3放弃执行)', + concurrent char(1) default '1' comment '是否并发执行(0允许 1禁止)', + status char(1) default '0' comment '状态(0正常 1暂停)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default '' comment '备注信息', + primary key (job_id, job_name, job_group) +) engine=innodb auto_increment=100 comment = '定时任务调度表'; + +insert into sys_job values(1, '系统默认(无参)', 'default', 'default', 'module_task.scheduler_test.job', NULL, NULL, '0/10 * * * * ?', '3', '1', '1', 'admin', sysdate(), '', null, ''); +insert into sys_job values(2, '系统默认(有参)', 'default', 'default', 'module_task.scheduler_test.job', 'test', NULL, '0/15 * * * * ?', '3', '1', '1', 'admin', sysdate(), '', null, ''); +insert into sys_job values(3, '系统默认(多参)', 'default', 'default', 'module_task.scheduler_test.job', 'new', '{\"test\": 111}', '0/20 * * * * ?', '3', '1', '1', 'admin', sysdate(), '', null, ''); + + +-- ---------------------------- +-- 16、定时任务调度日志表 +-- ---------------------------- +drop table if exists sys_job_log; +create table sys_job_log ( + job_log_id bigint(20) not null auto_increment comment '任务日志ID', + job_name varchar(64) not null comment '任务名称', + job_group varchar(64) not null comment '任务组名', + job_executor varchar(64) not null comment '任务执行器', + invoke_target varchar(500) not null comment '调用目标字符串', + job_args varchar(255) default '' comment '位置参数', + job_kwargs varchar(255) default '' comment '关键字参数', + job_trigger varchar(255) default '' comment '任务触发器', + job_message varchar(500) comment '日志信息', + status char(1) default '0' comment '执行状态(0正常 1失败)', + exception_info varchar(2000) default '' comment '异常信息', + create_time datetime comment '创建时间', + primary key (job_log_id) +) engine=innodb comment = '定时任务调度日志表'; + + +-- ---------------------------- +-- 17、通知公告表 +-- ---------------------------- +drop table if exists sys_notice; +create table sys_notice ( + notice_id int(4) not null auto_increment comment '公告ID', + notice_title varchar(50) not null comment '公告标题', + notice_type char(1) not null comment '公告类型(1通知 2公告)', + notice_content longblob default null comment '公告内容', + status char(1) default '0' comment '公告状态(0正常 1关闭)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(255) default null comment '备注', + primary key (notice_id) +) engine=innodb auto_increment=10 comment = '通知公告表'; + +-- ---------------------------- +-- 初始化-公告信息表数据 +-- ---------------------------- +insert into sys_notice values('1', '温馨提醒:2018-07-01 vfadmin新版本发布啦', '2', '新版本内容', '0', 'admin', sysdate(), '', null, '管理员'); +insert into sys_notice values('2', '维护通知:2018-07-01 vfadmin系统凌晨维护', '1', '维护内容', '0', 'admin', sysdate(), '', null, '管理员'); + + +-- ---------------------------- +-- 18、代码生成业务表 +-- ---------------------------- +drop table if exists gen_table; +create table gen_table ( + table_id bigint(20) not null auto_increment comment '编号', + table_name varchar(200) default '' comment '表名称', + table_comment varchar(500) default '' comment '表描述', + sub_table_name varchar(64) default null comment '关联子表的表名', + sub_table_fk_name varchar(64) default null comment '子表关联的外键名', + class_name varchar(100) default '' comment '实体类名称', + tpl_category varchar(200) default 'crud' comment '使用的模板(crud单表操作 tree树表操作)', + tpl_web_type varchar(30) default '' comment '前端模板类型(element-ui模版 element-plus模版)', + package_name varchar(100) comment '生成包路径', + module_name varchar(30) comment '生成模块名', + business_name varchar(30) comment '生成业务名', + function_name varchar(50) comment '生成功能名', + function_author varchar(50) comment '生成功能作者', + gen_type char(1) default '0' comment '生成代码方式(0zip压缩包 1自定义路径)', + gen_path varchar(200) default '/' comment '生成路径(不填默认项目路径)', + options varchar(1000) comment '其它生成选项', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (table_id) +) engine=innodb auto_increment=1 comment = '代码生成业务表'; + + +-- ---------------------------- +-- 19、代码生成业务表字段 +-- ---------------------------- +drop table if exists gen_table_column; +create table gen_table_column ( + column_id bigint(20) not null auto_increment comment '编号', + table_id bigint(20) comment '归属表编号', + column_name varchar(200) comment '列名称', + column_comment varchar(500) comment '列描述', + column_type varchar(100) comment '列类型', + python_type varchar(500) comment 'PYTHON类型', + python_field varchar(200) comment 'PYTHON字段名', + is_pk char(1) comment '是否主键(1是)', + is_increment char(1) comment '是否自增(1是)', + is_required char(1) comment '是否必填(1是)', + is_unique char(1) comment '是否唯一(1是)', + is_insert char(1) comment '是否为插入字段(1是)', + is_edit char(1) comment '是否编辑字段(1是)', + is_list char(1) comment '是否列表字段(1是)', + is_query char(1) comment '是否查询字段(1是)', + query_type varchar(200) default 'EQ' comment '查询方式(等于、不等于、大于、小于、范围)', + html_type varchar(200) comment '显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)', + dict_type varchar(200) default '' comment '字典类型', + sort int comment '排序', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + primary key (column_id) +) engine=innodb auto_increment=1 comment = '代码生成业务表字段'; \ No newline at end of file diff --git a/ruoyi-fastapi-backend/sub_applications/handle.py b/ruoyi-fastapi-backend/sub_applications/handle.py new file mode 100644 index 0000000..df2a5f4 --- /dev/null +++ b/ruoyi-fastapi-backend/sub_applications/handle.py @@ -0,0 +1,10 @@ +from fastapi import FastAPI +from sub_applications.staticfiles import mount_staticfiles + + +def handle_sub_applications(app: FastAPI): + """ + 全局处理子应用挂载 + """ + # 挂载静态文件 + mount_staticfiles(app) diff --git a/ruoyi-fastapi-backend/sub_applications/kb_es/create_index.py b/ruoyi-fastapi-backend/sub_applications/kb_es/create_index.py new file mode 100644 index 0000000..2db001e --- /dev/null +++ b/ruoyi-fastapi-backend/sub_applications/kb_es/create_index.py @@ -0,0 +1,83 @@ +import os +from typing import Any, Dict + +import httpx + + +def _bool_env(name: str, default: bool) -> bool: + val = os.getenv(name) + if val is None: + return default + return val.strip().lower() in {"1", "true", "yes", "y", "on"} + + +def _get_settings() -> Dict[str, Any]: + es_url = os.getenv("KB_ES_URL") or os.getenv("KB_OS_URL") + if not es_url: + raise SystemExit("Missing KB_ES_URL (or KB_OS_URL)") + + return { + "es_url": es_url.rstrip("/"), + "index": os.getenv("KB_ES_INDEX", "kb_chunks_v1"), + "username": os.getenv("KB_ES_USERNAME", ""), + "password": os.getenv("KB_ES_PASSWORD", ""), + "verify_ssl": _bool_env("KB_ES_VERIFY_SSL", True), + } + + +def _mapping() -> Dict[str, Any]: + return { + "settings": { + "analysis": { + "analyzer": { + "kb_default": { + "type": "standard", + } + } + } + }, + "mappings": { + "dynamic": "true", + "properties": { + "doc_id": {"type": "keyword"}, + "chunk_id": {"type": "keyword"}, + "qa_id": {"type": "keyword"}, + "question": {"type": "text", "analyzer": "kb_default"}, + "answer": {"type": "text", "analyzer": "kb_default"}, + "title": {"type": "text", "analyzer": "kb_default"}, + "content": {"type": "text", "analyzer": "kb_default"}, + "source": {"type": "keyword"}, + "tags": {"type": "keyword"}, + "category": {"type": "keyword"}, + "updated_at": {"type": "date", "ignore_malformed": True}, + "version": {"type": "keyword"}, + }, + }, + } + + +async def main() -> None: + cfg = _get_settings() + url = f"{cfg['es_url']}/{cfg['index']}" + + auth = None + if cfg["username"] and cfg["password"]: + auth = (cfg["username"], cfg["password"]) + + async with httpx.AsyncClient(timeout=10.0, verify=cfg["verify_ssl"]) as client: + head = await client.head(url, auth=auth) + if head.status_code == 200: + print(f"Index already exists: {cfg['index']}") + return + + resp = await client.put(url, json=_mapping(), auth=auth) + if resp.status_code >= 300: + raise SystemExit(f"Create index failed HTTP {resp.status_code}: {resp.text}") + + print(f"Created index: {cfg['index']}") + + +if __name__ == "__main__": + import asyncio + + asyncio.run(main()) diff --git a/ruoyi-fastapi-backend/sub_applications/kb_es/ingest_folder.py b/ruoyi-fastapi-backend/sub_applications/kb_es/ingest_folder.py new file mode 100644 index 0000000..13757f7 --- /dev/null +++ b/ruoyi-fastapi-backend/sub_applications/kb_es/ingest_folder.py @@ -0,0 +1,260 @@ +import argparse +import hashlib +import json +import os +import re +from pathlib import Path +from typing import Any, Dict, Iterable, List, Tuple + +import httpx + + +def _bool_env(name: str, default: bool) -> bool: + val = os.getenv(name) + if val is None: + return default + return val.strip().lower() in {"1", "true", "yes", "y", "on"} + + +def _cfg() -> Dict[str, Any]: + es_url = os.getenv("KB_ES_URL") or os.getenv("KB_OS_URL") + if not es_url: + raise SystemExit("Missing KB_ES_URL (or KB_OS_URL)") + + return { + "es_url": es_url.rstrip("/"), + "index": os.getenv("KB_ES_INDEX", "kb_chunks_v1"), + "username": os.getenv("KB_ES_USERNAME", ""), + "password": os.getenv("KB_ES_PASSWORD", ""), + "verify_ssl": _bool_env("KB_ES_VERIFY_SSL", True), + "version": os.getenv("KB_ES_VERSION", "v1"), + } + + +def read_text_file(path: Path) -> str: + data = path.read_text(encoding="utf-8", errors="ignore") + return data.replace("\ufeff", "").strip() + + +def iter_text_files(folder: Path, exts: Tuple[str, ...]) -> Iterable[Path]: + for p in folder.rglob("*"): + if p.is_file() and p.suffix.lower() in exts: + yield p + + +def _slug_id(s: str) -> str: + return hashlib.sha1(s.encode("utf-8")).hexdigest()[:16] + + +def chunk_text(text: str, chunk_size: int, overlap: int) -> List[str]: + text = re.sub(r"\s+", " ", text).strip() + if not text: + return [] + if chunk_size <= 0: + return [text] + overlap = max(0, min(overlap, chunk_size - 1)) + out = [] + i = 0 + while i < len(text): + out.append(text[i : i + chunk_size]) + if i + chunk_size >= len(text): + break + i = i + chunk_size - overlap + return out + + +_QA_Q_RE = re.compile(r"^\s*(\d+)\s*[\..、)]\s*(.+?)\s*$") +_QA_A_RE = re.compile(r"^\s*[•\-*]\s*答案\s*[::]\s*(.*)$") + +# 兼容:markdown Q&A 版(常见:**Q5:...** **A:** ...) +_MD_Q_RE = re.compile(r"^\s*(?:\*\*)?Q\s*(\d+)?\s*[::]\s*(.+?)\s*(?:\*\*)?\s*$", re.IGNORECASE) +_MD_A_RE = re.compile(r"^\s*(?:\*\*)?A\s*[::]\s*(?:\*\*)?\s*(.*)\s*$", re.IGNORECASE) +_MD_INLINE_QA_RE = re.compile( + r"\*\*\s*Q\s*\d*\s*[::]\s*(.+?)\s*\*\*\s*(?:\*\*)?\s*A\s*[::]\s*(?:\*\*)?\s*(.+)\s*$", + re.IGNORECASE, +) + + +def parse_markdown_qa(text: str) -> List[Dict[str, str]]: + """解析形如: + 1.xxx?\n•答案:yyy + 的 Q&A 列表。 + """ + + lines = [ln.rstrip() for ln in (text or "").splitlines()] + items: List[Dict[str, str]] = [] + + cur_q: str | None = None + cur_a_lines: List[str] = [] + in_answer = False + + def flush(): + nonlocal cur_q, cur_a_lines, in_answer + if cur_q: + ans = "\n".join([a for a in cur_a_lines if a.strip()]).strip() + if ans: + items.append({"question": cur_q.strip(), "answer": ans}) + cur_q = None + cur_a_lines = [] + in_answer = False + + for ln in lines: + if not ln.strip(): + continue + + m_inline = _MD_INLINE_QA_RE.match(ln) + if m_inline: + flush() + q = m_inline.group(1).strip() + a = m_inline.group(2).strip() + if q and a: + items.append({"question": q, "answer": a}) + continue + + m_q = _QA_Q_RE.match(ln) + if m_q: + flush() + cur_q = m_q.group(2) + continue + + m_md_q = _MD_Q_RE.match(ln) + if m_md_q: + flush() + cur_q = m_md_q.group(2) + continue + + m_a = _QA_A_RE.match(ln) + if m_a and cur_q: + first = m_a.group(1).strip() + if first: + cur_a_lines.append(first) + in_answer = True + continue + + m_md_a = _MD_A_RE.match(ln) + if m_md_a and cur_q: + first = (m_md_a.group(1) or "").strip() + if first: + cur_a_lines.append(first) + in_answer = True + continue + if cur_q: + # 支持答案多行(没有“A:/•答案:”前缀的续行) + if in_answer: + s = ln.strip() + # 跳过与答案无关的章节标题/分隔线 + if s.startswith("#") or s in {"---", "***"} or re.fullmatch(r"[-*_]{3,}", s): + continue + cur_a_lines.append(s) + + flush() + return items + + +def build_docs(file_path: Path, root: Path, *, chunk_size: int, overlap: int) -> List[Dict[str, Any]]: + rel = str(file_path.relative_to(root)).replace("\\", "/") + content = read_text_file(file_path) + title = file_path.stem + doc_id = _slug_id(rel) + + # 优先按 Q&A 结构化入库(更准、更快);解析不到再退回 chunk + qa_items = parse_markdown_qa(content) if file_path.suffix.lower() == ".md" else [] + if qa_items: + docs: List[Dict[str, Any]] = [] + for i, qa in enumerate(qa_items, 1): + qa_id = f"{doc_id}_qa_{i}" + q = qa["question"].strip() + a = qa["answer"].strip() + docs.append( + { + "doc_id": doc_id, + "chunk_id": qa_id, + "qa_id": qa_id, + "title": title, + "question": q, + "answer": a, + "content": f"{q}\n{a}", + "source": rel, + } + ) + return docs + + parts = chunk_text(content, chunk_size=chunk_size, overlap=overlap) + docs = [] + for idx, part in enumerate(parts): + docs.append( + { + "doc_id": doc_id, + "chunk_id": f"{doc_id}_{idx}", + "title": title, + "content": part, + "source": rel, + } + ) + return docs + + +def to_bulk_ndjson(index: str, docs: List[Dict[str, Any]], *, version: str) -> str: + lines: List[str] = [] + for d in docs: + d = dict(d) + d["version"] = version + _id = d.get("chunk_id") + lines.append(json.dumps({"index": {"_index": index, "_id": _id}}, ensure_ascii=False)) + lines.append(json.dumps(d, ensure_ascii=False)) + return "\n".join(lines) + "\n" + + +async def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--folder", required=True, help="资料目录(建议放 txt/md)") + parser.add_argument("--ext", default=".txt,.md", help="导入的扩展名,逗号分隔") + parser.add_argument("--chunk-size", type=int, default=800) + parser.add_argument("--overlap", type=int, default=120) + parser.add_argument("--batch", type=int, default=500) + args = parser.parse_args() + + cfg = _cfg() + folder = Path(args.folder).resolve() + exts = tuple([e.strip().lower() for e in args.ext.split(",") if e.strip()]) + files = list(iter_text_files(folder, exts=exts)) + if not files: + raise SystemExit(f"No files found in {folder} for exts={exts}") + + auth = None + if cfg["username"] and cfg["password"]: + auth = (cfg["username"], cfg["password"]) + + bulk_url = f"{cfg['es_url']}/_bulk" + headers = {"Content-Type": "application/x-ndjson"} + + all_docs: List[Dict[str, Any]] = [] + for f in files: + all_docs.extend(build_docs(f, folder, chunk_size=args.chunk_size, overlap=args.overlap)) + + total = len(all_docs) + print(f"Files={len(files)} Chunks={total} Index={cfg['index']}") + + async with httpx.AsyncClient(timeout=30.0, verify=cfg["verify_ssl"]) as client: + for start in range(0, total, args.batch): + batch_docs = all_docs[start : start + args.batch] + body = to_bulk_ndjson(cfg["index"], batch_docs, version=cfg["version"]) + resp = await client.post(bulk_url, content=body.encode("utf-8"), headers=headers, auth=auth) + if resp.status_code >= 300: + raise SystemExit(f"Bulk failed HTTP {resp.status_code}: {resp.text}") + payload = resp.json() + if payload.get("errors"): + first_err = None + for item in payload.get("items", []): + v = item.get("index") or item.get("create") or item.get("update") + if v and v.get("error"): + first_err = v.get("error") + break + raise SystemExit(f"Bulk had errors: {first_err}") + print(f"Ingested {min(start + len(batch_docs), total)}/{total}") + + +if __name__ == "__main__": + import asyncio + + asyncio.run(main()) diff --git a/ruoyi-fastapi-backend/sub_applications/kb_es/ingest_md_file.py b/ruoyi-fastapi-backend/sub_applications/kb_es/ingest_md_file.py new file mode 100644 index 0000000..36007df --- /dev/null +++ b/ruoyi-fastapi-backend/sub_applications/kb_es/ingest_md_file.py @@ -0,0 +1,231 @@ +import argparse +import hashlib +import json +import os +import re +import uuid +from pathlib import Path +from typing import Any, Dict, List + +import httpx + + +def _bool_env(name: str, default: bool) -> bool: + val = os.getenv(name) + if val is None: + return default + return val.strip().lower() in {"1", "true", "yes", "y", "on"} + + +def _cfg() -> Dict[str, Any]: + es_url = os.getenv("KB_ES_URL") or os.getenv("KB_OS_URL") + if not es_url: + raise SystemExit("Missing KB_ES_URL (or KB_OS_URL)") + + return { + "es_url": es_url.rstrip("/"), + "index": os.getenv("KB_ES_INDEX", "kb_chunks_v1"), + "username": os.getenv("KB_ES_USERNAME", ""), + "password": os.getenv("KB_ES_PASSWORD", ""), + "verify_ssl": _bool_env("KB_ES_VERIFY_SSL", True), + "version": os.getenv("KB_ES_VERSION", "v1"), + } + + +def read_text_file(path: Path) -> str: + data = path.read_text(encoding="utf-8", errors="ignore") + return data.replace("\ufeff", "").strip() + + +def _slug_id(s: str) -> str: + return hashlib.sha1(s.encode("utf-8")).hexdigest()[:16] + + +_QA_Q_RE = re.compile(r"^\s*(\d+)\s*[\..、)]\s*(.+?)\s*$") +_QA_A_RE = re.compile(r"^\s*[•\-*]\s*答案\s*[::]\s*(.*)$") + +# 兼容:markdown Q&A 版(常见:**Q5:...** **A:** ...) +_MD_Q_RE = re.compile(r"^\s*(?:\*\*)?Q\s*(\d+)?\s*[::]\s*(.+?)\s*(?:\*\*)?\s*$", re.IGNORECASE) +_MD_A_RE = re.compile(r"^\s*(?:\*\*)?A\s*[::]\s*(?:\*\*)?\s*(.*)\s*$", re.IGNORECASE) +_MD_INLINE_QA_RE = re.compile( + r"\*\*\s*Q\s*\d*\s*[::]\s*(.+?)\s*\*\*\s*(?:\*\*)?\s*A\s*[::]\s*(?:\*\*)?\s*(.+)\s*$", + re.IGNORECASE, +) + + +def parse_markdown_qa(text: str) -> List[Dict[str, str]]: + lines = [ln.rstrip() for ln in (text or "").splitlines()] + items: List[Dict[str, str]] = [] + + cur_q: str | None = None + cur_a_lines: List[str] = [] + in_answer = False + + def flush() -> None: + nonlocal cur_q, cur_a_lines, in_answer + if cur_q: + ans = "\n".join([a for a in cur_a_lines if a.strip()]).strip() + if ans: + items.append({"question": cur_q.strip(), "answer": ans}) + cur_q = None + cur_a_lines = [] + in_answer = False + + for ln in lines: + if not ln.strip(): + continue + + m_inline = _MD_INLINE_QA_RE.match(ln) + if m_inline: + flush() + q = m_inline.group(1).strip() + a = m_inline.group(2).strip() + if q and a: + items.append({"question": q, "answer": a}) + continue + + m_q = _QA_Q_RE.match(ln) + if m_q: + flush() + cur_q = m_q.group(2) + continue + + m_md_q = _MD_Q_RE.match(ln) + if m_md_q: + flush() + cur_q = m_md_q.group(2) + continue + + m_a = _QA_A_RE.match(ln) + if m_a and cur_q: + first = m_a.group(1).strip() + if first: + cur_a_lines.append(first) + in_answer = True + continue + + m_md_a = _MD_A_RE.match(ln) + if m_md_a and cur_q: + first = (m_md_a.group(1) or "").strip() + if first: + cur_a_lines.append(first) + in_answer = True + continue + + if cur_q and in_answer: + s = ln.strip() + if s.startswith("#") or s in {"---", "***"} or re.fullmatch(r"[-*_]{3,}", s): + continue + cur_a_lines.append(s) + + flush() + return items + + +def build_docs(file_path: Path, *, doc_id: str) -> List[Dict[str, Any]]: + content = read_text_file(file_path) + title = file_path.stem + source = str(file_path).replace("\\", "/") + + qa_items = parse_markdown_qa(content) + if qa_items: + docs: List[Dict[str, Any]] = [] + for i, qa in enumerate(qa_items, 1): + qa_id = f"{doc_id}_qa_{i}" + q = qa["question"].strip() + a = qa["answer"].strip() + docs.append( + { + "doc_id": doc_id, + "chunk_id": qa_id, + "qa_id": qa_id, + "title": title, + "question": q, + "answer": a, + "content": f"{q}\n{a}", + "source": source, + } + ) + return docs + + # 非 Q&A 格式:整体作为一个 chunk 写入 + if not content: + return [] + return [ + { + "doc_id": doc_id, + "chunk_id": f"{doc_id}_0", + "title": title, + "content": content, + "source": source, + } + ] + + +def to_bulk_ndjson(index: str, docs: List[Dict[str, Any]], *, version: str) -> str: + lines: List[str] = [] + for d in docs: + d = dict(d) + d["version"] = version + _id = d.get("chunk_id") + lines.append(json.dumps({"index": {"_index": index, "_id": _id}}, ensure_ascii=False)) + lines.append(json.dumps(d, ensure_ascii=False)) + return "\n".join(lines) + "\n" + + +async def main() -> None: + parser = argparse.ArgumentParser(description="将单个 Markdown 文件增量写入现有 ES/OpenSearch 索引(不创建/不清空)") + parser.add_argument("--file", required=True, help="md 文件路径") + parser.add_argument( + "--doc-id", + default="", + help="可选:指定 doc_id(不传则自动生成一个新的,保证每次都是新增)", + ) + args = parser.parse_args() + + cfg = _cfg() + file_path = Path(args.file).resolve() + if not file_path.exists() or not file_path.is_file(): + raise SystemExit(f"File not found: {file_path}") + if file_path.suffix.lower() != ".md": + raise SystemExit("Only .md is supported by this script") + + if args.doc_id: + doc_id = _slug_id(args.doc_id.strip()) + else: + # 默认每次生成新 doc_id,确保不会覆盖旧数据 + doc_id = uuid.uuid4().hex[:16] + + docs = build_docs(file_path, doc_id=doc_id) + if not docs: + raise SystemExit("No content to ingest") + + auth = None + if cfg["username"] and cfg["password"]: + auth = (cfg["username"], cfg["password"]) + + bulk_url = f"{cfg['es_url']}/_bulk" + headers = {"Content-Type": "application/x-ndjson"} + body = to_bulk_ndjson(cfg["index"], docs, version=cfg["version"]) + + async with httpx.AsyncClient(timeout=30.0, verify=cfg["verify_ssl"]) as client: + resp = await client.post(bulk_url, content=body.encode("utf-8"), headers=headers, auth=auth) + if resp.status_code >= 300: + raise SystemExit(f"Bulk failed HTTP {resp.status_code}: {resp.text}") + payload = resp.json() + if payload.get("errors"): + first_err = None + for item in payload.get("items", []): + v = item.get("index") or item.get("create") or item.get("update") + if v and v.get("error"): + first_err = v.get("error") + break + raise SystemExit(f"Bulk had errors: {first_err}") + + print(f"OK index={cfg['index']} doc_id={doc_id} chunks={len(docs)} file={file_path}") + + +if __name__ == "__main__": + import asyncio + + asyncio.run(main()) diff --git a/ruoyi-fastapi-backend/sub_applications/opensearch/docker-compose.yml b/ruoyi-fastapi-backend/sub_applications/opensearch/docker-compose.yml new file mode 100644 index 0000000..9b15197 --- /dev/null +++ b/ruoyi-fastapi-backend/sub_applications/opensearch/docker-compose.yml @@ -0,0 +1,28 @@ +services: + opensearch: + image: opensearchproject/opensearch:2 + container_name: kb-opensearch + environment: + - discovery.type=single-node + - bootstrap.memory_lock=true + - OPENSEARCH_JAVA_OPTS=-Xms1g -Xmx1g + - plugins.security.disabled=true + ulimits: + memlock: + soft: -1 + hard: -1 + nofile: + soft: 65536 + hard: 65536 + volumes: + - opensearch-data:/usr/share/opensearch/data + ports: + - "9200:9200" + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:9200 >/dev/null || exit 1"] + interval: 10s + timeout: 5s + retries: 20 + +volumes: + opensearch-data: diff --git a/ruoyi-fastapi-backend/sub_applications/staticfiles.py b/ruoyi-fastapi-backend/sub_applications/staticfiles.py new file mode 100644 index 0000000..c481d72 --- /dev/null +++ b/ruoyi-fastapi-backend/sub_applications/staticfiles.py @@ -0,0 +1,10 @@ +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles +from config.env import UploadConfig + + +def mount_staticfiles(app: FastAPI): + """ + 挂载静态文件 + """ + app.mount(f'{UploadConfig.UPLOAD_PREFIX}', StaticFiles(directory=f'{UploadConfig.UPLOAD_PATH}'), name='profile') diff --git a/ruoyi-fastapi-backend/utils/__init__.py b/ruoyi-fastapi-backend/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ruoyi-fastapi-backend/utils/common_util.py b/ruoyi-fastapi-backend/utils/common_util.py new file mode 100644 index 0000000..b56000e --- /dev/null +++ b/ruoyi-fastapi-backend/utils/common_util.py @@ -0,0 +1,274 @@ +import io +import os +import pandas as pd +import re +from openpyxl import Workbook +from openpyxl.styles import Alignment, PatternFill +from openpyxl.utils import get_column_letter +from openpyxl.worksheet.datavalidation import DataValidation +from sqlalchemy.engine.row import Row +from sqlalchemy.orm.collections import InstrumentedList +from typing import Any, Dict, List, Literal, Union +from config.database import Base +from config.env import CachePathConfig + + +def worship(): + print(""" +//////////////////////////////////////////////////////////////////// +// _ooOoo_ // +// o8888888o // +// 88" . "88 // +// (| ^_^ |) // +// O\ = /O // +// ____/`---'\____ // +// .' \\| |// `. // +// / \\||| : |||// \ // +// / _||||| -:- |||||- \ // +// | | \\\ - /// | | // +// | \_| ''\---/'' | | // +// \ .-\__ `-` ___/-. / // +// ___`. .' /--.--\ `. . ___ // +// ."" '< `.___\_<|>_/___.' >'"". // +// | | : `- \`.;`\ _ /`;.`/ - ` : | | // +// \ \ `-. \_ __\ /__ _/ .-` / / // +// ========`-.____`-.___\_____/___.-`____.-'======== // +// `=---=' // +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // +// 佛祖保佑 永不宕机 永无BUG // +//////////////////////////////////////////////////////////////////// + """) + + +class SqlalchemyUtil: + """ + sqlalchemy工具类 + """ + + @classmethod + def base_to_dict( + cls, obj: Union[Base, Dict], transform_case: Literal['no_case', 'snake_to_camel', 'camel_to_snake'] = 'no_case' + ): + """ + 将sqlalchemy模型对象转换为字典 + + :param obj: sqlalchemy模型对象或普通字典 + :param transform_case: 转换得到的结果形式,可选的有'no_case'(不转换)、'snake_to_camel'(下划线转小驼峰)、'camel_to_snake'(小驼峰转下划线),默认为'no_case' + :return: 字典结果 + """ + if isinstance(obj, Base): + base_dict = obj.__dict__.copy() + base_dict.pop('_sa_instance_state', None) + for name, value in base_dict.items(): + if isinstance(value, InstrumentedList): + base_dict[name] = cls.serialize_result(value, 'snake_to_camel') + elif isinstance(obj, dict): + base_dict = obj.copy() + if transform_case == 'snake_to_camel': + return {CamelCaseUtil.snake_to_camel(k): v for k, v in base_dict.items()} + elif transform_case == 'camel_to_snake': + return {SnakeCaseUtil.camel_to_snake(k): v for k, v in base_dict.items()} + + return base_dict + + @classmethod + def serialize_result( + cls, result: Any, transform_case: Literal['no_case', 'snake_to_camel', 'camel_to_snake'] = 'no_case' + ): + """ + 将sqlalchemy查询结果序列化 + + :param result: sqlalchemy查询结果 + :param transform_case: 转换得到的结果形式,可选的有'no_case'(不转换)、'snake_to_camel'(下划线转小驼峰)、'camel_to_snake'(小驼峰转下划线),默认为'no_case' + :return: 序列化结果 + """ + if isinstance(result, (Base, dict)): + return cls.base_to_dict(result, transform_case) + elif isinstance(result, list): + return [cls.serialize_result(row, transform_case) for row in result] + elif isinstance(result, Row): + if all([isinstance(row, Base) for row in result]): + return [cls.base_to_dict(row, transform_case) for row in result] + elif any([isinstance(row, Base) for row in result]): + return [cls.serialize_result(row, transform_case) for row in result] + else: + result_dict = result._asdict() + if transform_case == 'snake_to_camel': + return {CamelCaseUtil.snake_to_camel(k): v for k, v in result_dict.items()} + elif transform_case == 'camel_to_snake': + return {SnakeCaseUtil.camel_to_snake(k): v for k, v in result_dict.items()} + return result_dict + return result + + +class CamelCaseUtil: + """ + 下划线形式(snake_case)转小驼峰形式(camelCase)工具方法 + """ + + @classmethod + def snake_to_camel(cls, snake_str: str): + """ + 下划线形式字符串(snake_case)转换为小驼峰形式字符串(camelCase) + + :param snake_str: 下划线形式字符串 + :return: 小驼峰形式字符串 + """ + # 分割字符串 + words = snake_str.split('_') + # 小驼峰命名,第一个词首字母小写,其余词首字母大写 + return words[0] + ''.join(word.capitalize() for word in words[1:]) + + @classmethod + def transform_result(cls, result: Any): + """ + 针对不同类型将下划线形式(snake_case)批量转换为小驼峰形式(camelCase)方法 + + :param result: 输入数据 + :return: 小驼峰形式结果 + """ + return SqlalchemyUtil.serialize_result(result=result, transform_case='snake_to_camel') + + +class SnakeCaseUtil: + """ + 小驼峰形式(camelCase)转下划线形式(snake_case)工具方法 + """ + + @classmethod + def camel_to_snake(cls, camel_str: str): + """ + 小驼峰形式字符串(camelCase)转换为下划线形式字符串(snake_case) + + :param camel_str: 小驼峰形式字符串 + :return: 下划线形式字符串 + """ + # 在大写字母前添加一个下划线,然后将整个字符串转为小写 + words = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', camel_str) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', words).lower() + + @classmethod + def transform_result(cls, result: Any): + """ + 针对不同类型将下划线形式(snake_case)批量转换为小驼峰形式(camelCase)方法 + + :param result: 输入数据 + :return: 小驼峰形式结果 + """ + return SqlalchemyUtil.serialize_result(result=result, transform_case='camel_to_snake') + + +def bytes2human(n, format_str='%(value).1f%(symbol)s'): + """Used by various scripts. See: + http://goo.gl/zeJZl + + >>> bytes2human(10000) + '9.8K' + >>> bytes2human(100001221) + '95.4M' + """ + symbols = ('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB') + prefix = {} + for i, s in enumerate(symbols[1:]): + prefix[s] = 1 << (i + 1) * 10 + for symbol in reversed(symbols[1:]): + if n >= prefix[symbol]: + value = float(n) / prefix[symbol] + return format_str % locals() + return format_str % dict(symbol=symbols[0], value=n) + + +def bytes2file_response(bytes_info): + yield bytes_info + + +def export_list2excel(list_data: List): + """ + 工具方法:将需要导出的list数据转化为对应excel的二进制数据 + + :param list_data: 数据列表 + :return: 字典信息对应excel的二进制数据 + """ + df = pd.DataFrame(list_data) + binary_data = io.BytesIO() + df.to_excel(binary_data, index=False, engine='openpyxl') + binary_data = binary_data.getvalue() + + return binary_data + + +def get_excel_template(header_list: List, selector_header_list: List, option_list: List[dict]): + """ + 工具方法:将需要导出的list数据转化为对应excel的二进制数据 + + :param header_list: 表头数据列表 + :param selector_header_list: 需要设置为选择器格式的表头数据列表 + :param option_list: 选择器格式的表头预设的选项列表 + :return: 模板excel的二进制数据 + """ + # 创建Excel工作簿 + wb = Workbook() + # 选择默认的活动工作表 + ws = wb.active + + # 设置表头文字 + headers = header_list + + # 设置表头背景样式为灰色,前景色为白色 + header_fill = PatternFill(start_color='ababab', end_color='ababab', fill_type='solid') + + # 将表头写入第一行 + for col_num, header in enumerate(headers, 1): + cell = ws.cell(row=1, column=col_num) + cell.value = header + cell.fill = header_fill + # 设置列宽度为16 + ws.column_dimensions[chr(64 + col_num)].width = 12 + # 设置水平居中对齐 + cell.alignment = Alignment(horizontal='center') + + # 设置选择器的预设选项 + options = option_list + + # 获取selector_header的字母索引 + for selector_header in selector_header_list: + column_selector_header_index = headers.index(selector_header) + 1 + + # 创建数据有效性规则 + header_option = [] + for option in options: + if option.get(selector_header): + header_option = option.get(selector_header) + dv = DataValidation(type='list', formula1=f'"{",".join(header_option)}"') + # 设置数据有效性规则的起始单元格和结束单元格 + dv.add( + f'{get_column_letter(column_selector_header_index)}2:{get_column_letter(column_selector_header_index)}1048576' + ) + # 添加数据有效性规则到工作表 + ws.add_data_validation(dv) + + # 保存Excel文件为字节类型的数据 + file = io.BytesIO() + wb.save(file) + file.seek(0) + + # 读取字节数据 + excel_data = file.getvalue() + + return excel_data + + +def get_filepath_from_url(url: str): + """ + 工具方法:根据请求参数获取文件路径 + + :param url: 请求参数中的url参数 + :return: 文件路径 + """ + file_info = url.split('?')[1].split('&') + task_id = file_info[0].split('=')[1] + file_name = file_info[1].split('=')[1] + task_path = file_info[2].split('=')[1] + filepath = os.path.join(CachePathConfig.PATH, task_path, task_id, file_name) + + return filepath diff --git a/ruoyi-fastapi-backend/utils/compreface_util.py b/ruoyi-fastapi-backend/utils/compreface_util.py new file mode 100644 index 0000000..0dbd1c2 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/compreface_util.py @@ -0,0 +1,160 @@ +from config.env import ComprefaceConfig +from compreface import CompreFace +from compreface.service import RecognitionService +from compreface.collections import FaceCollection +from compreface.common.typed_dict import AllOptionsDict, check_fields_by_name + + +import httpx +import json +import os + + + +RECOGNITION_ROOT_API: str = '/api/v1/recognition' + +RECOGNIZE_API: str = RECOGNITION_ROOT_API + '/recognize' +RECOGNIZE_CRUD_API: str = RECOGNITION_ROOT_API + '/faces' +SUBJECTS_CRUD_API: str = RECOGNITION_ROOT_API + '/subjects' + +DETECTION_API: str = '/api/v1/detection/detect' + +VERIFICATION_API: str = '/api/v1/verification' + + + + +compre_face: CompreFace = CompreFace(ComprefaceConfig.COMPREFACE_BASE_URL, ComprefaceConfig.COMPERFACE_BASE_PORT) +# recognition: RecognitionService = compre_face.init_face_recognition(ComprefaceConfig.COMPREFACE_API_KEY) +# face_collection: FaceCollection = recognition.get_face_collection() + + + +class ComprefaceUtil: + + api_key: str = ComprefaceConfig.COMPREFACE_API_KEY + api_key_detection: str = ComprefaceConfig.COMPREFACE_API_KEY_DETECTION + @classmethod + async def send_request(cls, method: str, url: str, headers: dict, body: str | None=None, data=None, files=None ,json=True): + + async with httpx.AsyncClient(verify=False) as client: + response = await client.request( + method, url, headers=headers, content=body, data=data, files=files, timeout=5 # timeout 单位秒 + ) + if response.status_code in (301, 302, 303, 307, 308): + location = response.headers.get("Location") + # 可能需要处理成 GET 请求或保留原方法 + response = await client.get(location, headers=headers) + response.raise_for_status() + if json: + return response.json() + return response + + # 添加人脸到指定分组下, subject 为分组名称 + @classmethod + async def face_addition(cls, image: str = '' or bytes, subject: str = '') -> dict: + client_url: str = RECOGNIZE_CRUD_API + url: str = ComprefaceConfig.COMPREFACE_BASE_URL + ':' + ComprefaceConfig.COMPERFACE_BASE_PORT + client_url + '?subject=' + subject + # Validation loop and adding fields to the url. + m = await multipart_constructor(image) + result = await cls.send_request("post" + , url + , headers={'x-api-key': cls.api_key} + , files=m) + return result + # 人脸识别 + @classmethod + async def face_recognition(cls, image: str = '' or bytes, options: AllOptionsDict = {}) -> dict: + + client_url: str = RECOGNIZE_API + url: str = ComprefaceConfig.COMPREFACE_BASE_URL + ':' + ComprefaceConfig.COMPERFACE_BASE_PORT + client_url+"?" + # url: str = "http://10.0.0.202:8000/api/v1/recognition/recognize?face_plugins=landmarks&face_plugins=gender&face_plugins=age&face_plugins=pose" + + for key in options.keys(): + # Checks fields with necessary rules. + # key - key field by options. + check_fields_by_name(key, options[key]) + url += '&' + key + "=" + str(options[key]) + + m = await multipart_constructor(image) + result = await cls.send_request("post" + , url + , headers={'x-api-key': cls.api_key} + , files=m) + return result + + # 人脸检测 + @classmethod + async def face_detection(cls, image: str = '' or bytes, options: AllOptionsDict = {}) -> dict: + client_url: str = DETECTION_API + url: str = ComprefaceConfig.COMPREFACE_BASE_URL + ':' + ComprefaceConfig.COMPERFACE_BASE_PORT + client_url+"?" + for key in options.keys(): + # Checks fields with necessary rules. + # key - key field by options. + # check_fields_by_name(key, options[key]) + url += '&' + key + "=" + str(options[key]) + m = await multipart_constructor(image) + result = await cls.send_request("post" + , url + , headers={'x-api-key': cls.api_key_detection} + , files=m) + return result + + # 获取所有subjects + @classmethod + async def get_all_subjects(cls) -> dict: + """获取所有已注册的人脸subjects + + Returns: + dict: 包含subjects列表的字典 + """ + client_url: str = SUBJECTS_CRUD_API + url: str = ComprefaceConfig.COMPREFACE_BASE_URL + ':' + ComprefaceConfig.COMPERFACE_BASE_PORT + client_url + result = await cls.send_request("get" + , url + , headers={'x-api-key': cls.api_key}) + return result + + # 删除指定的subject + @classmethod + async def delete_subject(cls, subject: str) -> dict: + """删除指定的subject及其所有人脸数据 + + Args: + subject (str): 要删除的subject名称 + + Returns: + dict: 删除操作的结果 + """ + client_url: str = SUBJECTS_CRUD_API + '/' + subject + url: str = ComprefaceConfig.COMPREFACE_BASE_URL + ':' + ComprefaceConfig.COMPERFACE_BASE_PORT + client_url + result = await cls.send_request("delete" + , url + , headers={'x-api-key': cls.api_key}) + return result + +async def get_file(image: str = '' or bytes): + if not os.path.isfile(image): + if type(image) != bytes: + async with httpx.AsyncClient() as client: + response = await client.get(image, timeout=5) + file = response.content + else: + file = image + file = ('image.jpg', file) + else: + name_img: str = os.path.basename(image) + file = (name_img, open(image, 'rb')) + return file + + +async def multipart_constructor(image: str = '' or bytes): + + return {"file": await get_file(image)} + + +# def multipart_constructor_with_two_images(source_image: str = '' or bytes, target_image: str = '' or bytes): +# return MultipartEncoder( +# fields={'source_image': get_file( +# source_image), 'target_image': get_file(target_image)} +# ) \ No newline at end of file diff --git a/ruoyi-fastapi-backend/utils/cron_util.py b/ruoyi-fastapi-backend/utils/cron_util.py new file mode 100644 index 0000000..6232962 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/cron_util.py @@ -0,0 +1,172 @@ +import re +from datetime import datetime + + +class CronUtil: + """ + Cron表达式工具类 + """ + + @classmethod + def __valid_range(cls, search_str: str, start_range: int, end_range: int): + match = re.match(r'^(\d+)-(\d+)$', search_str) + if match: + start, end = int(match.group(1)), int(match.group(2)) + return start_range <= start < end <= end_range + return False + + @classmethod + def __valid_sum( + cls, search_str: str, start_range_a: int, start_range_b: int, end_range_a: int, end_range_b: int, sum_range: int + ): + match = re.match(r'^(\d+)/(\d+)$', search_str) + if match: + start, end = int(match.group(1)), int(match.group(2)) + return ( + start_range_a <= start <= start_range_b + and end_range_a <= end <= end_range_b + and start + end <= sum_range + ) + return False + + @classmethod + def validate_second_or_minute(cls, second_or_minute: str): + """ + 校验秒或分钟值是否正确 + + :param second_or_minute: 秒或分钟值 + :return: 校验结果 + """ + if ( + second_or_minute == '*' + or ('-' in second_or_minute and cls.__valid_range(second_or_minute, 0, 59)) + or ('/' in second_or_minute and cls.__valid_sum(second_or_minute, 0, 58, 1, 59, 59)) + or re.match(r'^(?:[0-5]?\d|59)(?:,[0-5]?\d|59)*$', second_or_minute) + ): + return True + return False + + @classmethod + def validate_hour(cls, hour: str): + """ + 校验小时值是否正确 + + :param hour: 小时值 + :return: 校验结果 + """ + if ( + hour == '*' + or ('-' in hour and cls.__valid_range(hour, 0, 23)) + or ('/' in hour and cls.__valid_sum(hour, 0, 22, 1, 23, 23)) + or re.match(r'^(?:0|[1-9]|1\d|2[0-3])(?:,(?:0|[1-9]|1\d|2[0-3]))*$', hour) + ): + return True + return False + + @classmethod + def validate_day(cls, day: str): + """ + 校验日值是否正确 + + :param day: 日值 + :return: 校验结果 + """ + if ( + day in ['*', '?', 'L'] + or ('-' in day and cls.__valid_range(day, 1, 31)) + or ('/' in day and cls.__valid_sum(day, 1, 30, 1, 30, 31)) + or ('W' in day and re.match(r'^(?:[1-9]|1\d|2\d|3[01])W$', day)) + or re.match(r'^(?:0|[1-9]|1\d|2[0-9]|3[0-1])(?:,(?:0|[1-9]|1\d|2[0-9]|3[0-1]))*$', day) + ): + return True + return False + + @classmethod + def validate_month(cls, month: str): + """ + 校验月值是否正确 + + :param month: 月值 + :return: 校验结果 + """ + if ( + month == '*' + or ('-' in month and cls.__valid_range(month, 1, 12)) + or ('/' in month and cls.__valid_sum(month, 1, 11, 1, 11, 12)) + or re.match(r'^(?:0|[1-9]|1[0-2])(?:,(?:0|[1-9]|1[0-2]))*$', month) + ): + return True + return False + + @classmethod + def validate_week(cls, week: str): + """ + 校验周值是否正确 + + :param week: 周值 + :return: 校验结果 + """ + if ( + week in ['*', '?'] + or ('-' in week and cls.__valid_range(week, 1, 7)) + or ('#' in week and re.match(r'^[1-7]#[1-4]$', week)) + or ('L' in week and re.match(r'^[1-7]L$', week)) + or re.match(r'^[1-7](?:(,[1-7]))*$', week) + ): + return True + return False + + @classmethod + def validate_year(cls, year: str): + """ + 校验年值是否正确 + + :param year: 年值 + :return: 校验结果 + """ + current_year = int(datetime.now().year) + future_years = [current_year + i for i in range(9)] + if ( + year == '*' + or ('-' in year and cls.__valid_range(year, current_year, 2099)) + or ('/' in year and cls.__valid_sum(year, current_year, 2098, 1, 2099 - current_year, 2099)) + or ('#' in year and re.match(r'^[1-7]#[1-4]$', year)) + or ('L' in year and re.match(r'^[1-7]L$', year)) + or ( + (len(year) == 4 or ',' in year) + and all(int(item) in future_years and current_year <= int(item) <= 2099 for item in year.split(',')) + ) + ): + return True + return False + + @classmethod + def validate_cron_expression(cls, cron_expression: str): + """ + 校验Cron表达式是否正确 + + :param cron_expression: Cron表达式 + :return: 校验结果 + """ + values = cron_expression.split() + if len(values) != 6 and len(values) != 7: + return False + second_validation = cls.validate_second_or_minute(values[0]) + minute_validation = cls.validate_second_or_minute(values[1]) + hour_validation = cls.validate_hour(values[2]) + day_validation = cls.validate_day(values[3]) + month_validation = cls.validate_month(values[4]) + week_validation = cls.validate_week(values[5]) + validation = ( + second_validation + and minute_validation + and hour_validation + and day_validation + and month_validation + and week_validation + ) + if len(values) == 6: + return validation + if len(values) == 7: + year_validation = cls.validate_year(values[6]) + return validation and year_validation diff --git a/ruoyi-fastapi-backend/utils/deepseek_client.py b/ruoyi-fastapi-backend/utils/deepseek_client.py new file mode 100644 index 0000000..3068960 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/deepseek_client.py @@ -0,0 +1,137 @@ +from openai import AsyncOpenAI +from typing import Any, Dict, List, Optional +from config.env import DeepSeekConfig +from utils.log_util import logger + + +class DeepSeekAPIError(Exception): + """DeepSeek API调用异常""" + + def __init__(self, status_code: int, message: str): + self.status_code = status_code + self.message = message + super().__init__(f"DeepSeek API error {status_code}: {message}") + + +class DeepSeekAPIClient: + """DeepSeek API客户端 - 用于调用DeepSeek大语言模型 + 使用官方推荐的OpenAI SDK进行调用 + """ + + def __init__(self, base_url: str = None, api_key: str = None, timeout: float = 30.0): + self.base_url = base_url or DeepSeekConfig.DEEPSEEK_API_BASE + self.base_url = self.base_url.rstrip('/') if self.base_url else 'https://api.deepseek.com' + self.api_key = api_key or DeepSeekConfig.DEEPSEEK_API_KEY + self.timeout = timeout + self.model = DeepSeekConfig.DEEPSEEK_MODEL + + # 初始化OpenAI客户端 + if not self.api_key: + raise DeepSeekAPIError(401, 'DeepSeek API密钥未配置') + + self.client = AsyncOpenAI( + api_key=self.api_key, + base_url=self.base_url, + timeout=self.timeout + ) + + async def chat_completion( + self, + messages: List[Dict[str, str]], + model: str = None, + temperature: float = 0.7, + max_tokens: int = 1024, + stream: bool = False, + ) -> Dict[str, Any]: + """调用DeepSeek聊天补全API + + Args: + messages: 聊天消息列表,格式为 [{"role": "user", "content": "你的问题"}] + model: 模型名称,默认使用配置中的模型 + temperature: 温度参数,控制生成内容的随机性 + max_tokens: 最大生成token数 + stream: 是否流式返回 + + Returns: + API返回的JSON响应 + + Raises: + DeepSeekAPIError: API调用失败时抛出 + """ + model = model or self.model + + try: + response = await self.client.chat.completions.create( + model=model, + messages=messages, + temperature=temperature, + max_tokens=max_tokens, + stream=stream + ) + + # 转换为字典格式返回,保持与原有接口兼容 + if not stream: + return response.model_dump() + else: + return response # 流式响应直接返回 + + except Exception as exc: + logger.error(f"DeepSeek API调用失败: {str(exc)}") + raise DeepSeekAPIError(500, str(exc)) from exc + + async def chat( + self, + question: str, + context: str = None, + model: str = None, + temperature: float = 0.7, + max_tokens: int = 1024, + ) -> str: + """简化的聊天接口 + + Args: + question: 用户问题 + context: 上下文信息(可选) + model: 模型名称 + temperature: 温度参数 + max_tokens: 最大生成token数 + + Returns: + 生成的回答 + + Raises: + DeepSeekAPIError: API调用失败时抛出 + """ + # 构建消息列表 + messages = [] + + # 如果有上下文,添加到系统消息中 + if context: + messages.append({ + "role": "system", + "content": f"基于以下上下文信息回答用户问题:\n{context}" + }) + + # 添加用户问题 + messages.append({ + "role": "user", + "content": question + }) + + # 调用API + response = await self.chat_completion( + messages=messages, + model=model, + temperature=temperature, + max_tokens=max_tokens, + stream=False + ) + + # 提取回答 + answer = response.get('choices', [{}])[0].get('message', {}).get('content', '') + + if not answer: + logger.error(f"DeepSeek API返回空回答: {response}") + raise DeepSeekAPIError(200, 'DeepSeek API返回空回答') + + return answer.strip() diff --git a/ruoyi-fastapi-backend/utils/excel_util.py b/ruoyi-fastapi-backend/utils/excel_util.py new file mode 100644 index 0000000..cf19119 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/excel_util.py @@ -0,0 +1,118 @@ +import io +import pandas as pd +from openpyxl import Workbook +from openpyxl.styles import Alignment, PatternFill +from openpyxl.utils import get_column_letter +from openpyxl.worksheet.datavalidation import DataValidation +from typing import Dict, List + + +class ExcelUtil: + """ + Excel操作类 + """ + + @classmethod + def __mapping_list(cls, list_data: List, mapping_dict: Dict): + """ + 工具方法:将list数据中的字段名映射为对应的中文字段名 + + :param list_data: 数据列表 + :param mapping_dict: 映射字典 + :return: 映射后的数据列表 + """ + mapping_data = [{mapping_dict.get(key): item.get(key) for key in mapping_dict} for item in list_data] + + return mapping_data + + @classmethod + def export_list2excel(cls, list_data: List, mapping_dict: Dict): + """ + 工具方法:将需要导出的list数据转化为对应excel的二进制数据 + + :param list_data: 数据列表 + :param mapping_dict: 映射字典 + :return: list数据对应excel的二进制数据 + """ + print("-" * 100, '\n', list_data) + + new_t = list() + # list_data = dict(list_data) + for item in list_data: + t = dict(item) + new_t.append(t) + print(t) + # for k, v in item.items(): + # print(k, v) + # print(item.get('recordId')) + # print(item.get()) + # mapping_data = cls.__mapping_list(list_data, mapping_dict) + mapping_data = cls.__mapping_list(new_t, mapping_dict) + df = pd.DataFrame(mapping_data) + print("-"*100,'\n', df) + binary_data = io.BytesIO() + df.to_excel(binary_data, index=False, engine='openpyxl') + binary_data = binary_data.getvalue() + + return binary_data + + @classmethod + def get_excel_template(cls, header_list: List, selector_header_list: List, option_list: List[Dict]): + """ + 工具方法:将需要导出的list数据转化为对应excel的二进制数据 + + :param header_list: 表头数据列表 + :param selector_header_list: 需要设置为选择器格式的表头数据列表 + :param option_list: 选择器格式的表头预设的选项列表 + :return: 模板excel的二进制数据 + """ + # 创建Excel工作簿 + wb = Workbook() + # 选择默认的活动工作表 + ws = wb.active + + # 设置表头文字 + headers = header_list + + # 设置表头背景样式为灰色,前景色为白色 + header_fill = PatternFill(start_color='ababab', end_color='ababab', fill_type='solid') + + # 将表头写入第一行 + for col_num, header in enumerate(headers, 1): + cell = ws.cell(row=1, column=col_num) + cell.value = header + cell.fill = header_fill + # 设置列宽度为16 + ws.column_dimensions[chr(64 + col_num)].width = 12 + # 设置水平居中对齐 + cell.alignment = Alignment(horizontal='center') + + # 设置选择器的预设选项 + options = option_list + + # 获取selector_header的字母索引 + for selector_header in selector_header_list: + column_selector_header_index = headers.index(selector_header) + 1 + + # 创建数据有效性规则 + header_option = [] + for option in options: + if option.get(selector_header): + header_option = option.get(selector_header) + dv = DataValidation(type='list', formula1=f'"{",".join(header_option)}"') + # 设置数据有效性规则的起始单元格和结束单元格 + dv.add( + f'{get_column_letter(column_selector_header_index)}2:{get_column_letter(column_selector_header_index)}1048576' + ) + # 添加数据有效性规则到工作表 + ws.add_data_validation(dv) + + # 保存Excel文件为字节类型的数据 + file = io.BytesIO() + wb.save(file) + file.seek(0) + + # 读取字节数据 + excel_data = file.getvalue() + + return excel_data diff --git a/ruoyi-fastapi-backend/utils/gen_util.py b/ruoyi-fastapi-backend/utils/gen_util.py new file mode 100644 index 0000000..355e5d0 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/gen_util.py @@ -0,0 +1,223 @@ +import re +from datetime import datetime +from typing import List +from config.constant import GenConstant +from config.env import GenConfig +from module_generator.entity.vo.gen_vo import GenTableColumnModel, GenTableModel +from utils.string_util import StringUtil + + +class GenUtils: + """代码生成器工具类""" + + @classmethod + def init_table(cls, gen_table: GenTableModel, oper_name: str) -> None: + """ + 初始化表信息 + + param gen_table: 业务表对象 + param oper_name: 操作人 + :return: + """ + gen_table.class_name = cls.convert_class_name(gen_table.table_name) + gen_table.package_name = GenConfig.package_name + gen_table.module_name = cls.get_module_name(GenConfig.package_name) + gen_table.business_name = cls.get_business_name(gen_table.table_name) + gen_table.function_name = cls.replace_text(gen_table.table_comment) + gen_table.function_author = GenConfig.author + gen_table.create_by = oper_name + gen_table.create_time = datetime.now() + gen_table.update_by = oper_name + gen_table.update_time = datetime.now() + + @classmethod + def init_column_field(cls, column: GenTableColumnModel, table: GenTableModel) -> None: + """ + 初始化列属性字段 + + param column: 业务表字段对象 + param table: 业务表对象 + :return: + """ + data_type = cls.get_db_type(column.column_type) + column_name = column.column_name + column.table_id = table.table_id + column.create_by = table.create_by + # 设置Python字段名 + column.python_field = cls.to_camel_case(column_name) + # 设置默认类型 + column.python_type = StringUtil.get_mapping_value_by_key_ignore_case( + GenConstant.DB_TO_PYTHON_TYPE_MAPPING, data_type + ) + column.query_type = GenConstant.QUERY_EQ + + if cls.arrays_contains(GenConstant.COLUMNTYPE_STR, data_type) or cls.arrays_contains( + GenConstant.COLUMNTYPE_TEXT, data_type + ): + # 字符串长度超过500设置为文本域 + column_length = cls.get_column_length(column.column_type) + html_type = ( + GenConstant.HTML_TEXTAREA + if column_length >= 500 or cls.arrays_contains(GenConstant.COLUMNTYPE_TEXT, data_type) + else GenConstant.HTML_INPUT + ) + column.html_type = html_type + elif cls.arrays_contains(GenConstant.COLUMNTYPE_TIME, data_type): + column.html_type = GenConstant.HTML_DATETIME + elif cls.arrays_contains(GenConstant.COLUMNTYPE_NUMBER, data_type): + column.html_type = GenConstant.HTML_INPUT + + # 插入字段(默认所有字段都需要插入) + column.is_insert = GenConstant.REQUIRE + + # 编辑字段 + if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_EDIT, column_name) and not column.pk: + column.is_edit = GenConstant.REQUIRE + # 列表字段 + if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_LIST, column_name) and not column.pk: + column.is_list = GenConstant.REQUIRE + # 查询字段 + if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_QUERY, column_name) and not column.pk: + column.is_query = GenConstant.REQUIRE + + # 查询字段类型 + if column_name.lower().endswith('name'): + column.query_type = GenConstant.QUERY_LIKE + # 状态字段设置单选框 + if column_name.lower().endswith('status'): + column.html_type = GenConstant.HTML_RADIO + # 类型&性别字段设置下拉框 + elif column_name.lower().endswith('type') or column_name.lower().endswith('sex'): + column.html_type = GenConstant.HTML_SELECT + # 图片字段设置图片上传控件 + elif column_name.lower().endswith('image'): + column.html_type = GenConstant.HTML_IMAGE_UPLOAD + # 文件字段设置文件上传控件 + elif column_name.lower().endswith('file'): + column.html_type = GenConstant.HTML_FILE_UPLOAD + # 内容字段设置富文本控件 + elif column_name.lower().endswith('content'): + column.html_type = GenConstant.HTML_EDITOR + + column.create_by = table.create_by + column.create_time = datetime.now() + column.update_by = table.update_by + column.update_time = datetime.now() + + @classmethod + def arrays_contains(cls, arr: List[str], target_value: str) -> bool: + """ + 校验数组是否包含指定值 + + param arr: 数组 + param target_value: 需要校验的值 + :return: 校验结果 + """ + return target_value in arr + + @classmethod + def get_module_name(cls, package_name: str) -> str: + """ + 获取模块名 + + param package_name: 包名 + :return: 模块名 + """ + return package_name.split('.')[-1] + + @classmethod + def get_business_name(cls, table_name: str) -> str: + """ + 获取业务名 + + param table_name: 业务表名 + :return: 业务名 + """ + return table_name.split('_')[-1] + + @classmethod + def convert_class_name(cls, table_name: str) -> str: + """ + 表名转换成Python类名 + + param table_name: 业务表名 + :return: Python类名 + """ + auto_remove_pre = GenConfig.auto_remove_pre + table_prefix = GenConfig.table_prefix + if auto_remove_pre and table_prefix: + search_list = table_prefix.split(',') + table_name = cls.replace_first(table_name, search_list) + return StringUtil.convert_to_camel_case(table_name) + + @classmethod + def replace_first(cls, replacement: str, search_list: List[str]) -> str: + """ + 批量替换前缀 + + param replacement: 需要被替换的字符串 + param search_list: 可替换的字符串列表 + :return: 替换后的字符串 + """ + for search_string in search_list: + if replacement.startswith(search_string): + return replacement.replace(search_string, '', 1) + return replacement + + @classmethod + def replace_text(cls, text: str) -> str: + """ + 关键字替换 + + param text: 需要被替换的字符串 + :return: 替换后的字符串 + """ + return re.sub(r'(?:表|若依)', '', text) + + @classmethod + def get_db_type(cls, column_type: str) -> str: + """ + 获取数据库类型字段 + + param column_type: 字段类型 + :return: 数据库类型 + """ + if '(' in column_type: + return column_type.split('(')[0] + return column_type + + @classmethod + def get_column_length(cls, column_type: str) -> int: + """ + 获取字段长度 + + param column_type: 字段类型 + :return: 字段长度 + """ + if '(' in column_type: + length = len(column_type.split('(')[1].split(')')[0]) + return length + return 0 + + @classmethod + def split_column_type(cls, column_type: str) -> List[str]: + """ + 拆分列类型 + + param column_type: 字段类型 + :return: 拆分结果 + """ + if '(' in column_type and ')' in column_type: + return column_type.split('(')[1].split(')')[0].split(',') + return [] + + @classmethod + def to_camel_case(cls, text: str) -> str: + """ + 将字符串转换为驼峰命名 + + param text: 需要转换的字符串 + :return: 驼峰命名 + """ + parts = text.split('_') + return parts[0] + ''.join(word.capitalize() for word in parts[1:]) diff --git a/ruoyi-fastapi-backend/utils/haikang_util.py b/ruoyi-fastapi-backend/utils/haikang_util.py new file mode 100644 index 0000000..3b332cb --- /dev/null +++ b/ruoyi-fastapi-backend/utils/haikang_util.py @@ -0,0 +1,1056 @@ +import hashlib +import hmac +import json +import base64 +import requests +import httpx +import os +from fastapi import HTTPException +from datetime import datetime, timezone +from email.utils import format_datetime +from urllib.parse import urlparse +from dateutil.relativedelta import relativedelta + +from config.env import HaiKangConfig +from utils.log_util import logger + + +class HaikangUtil: + """ + 海康平台工具类 + """ + + # def __init__(self): + # self.HAIKANG_URL = HaiKangConfig.HAIKANG_URL + # self.HAIKANG_PORT = HaiKangConfig.HAIKANG_PORT + # self.HAIKANG_AK = HaiKangConfig.HAIKANG_AK + # self.HAIKANG_SK = HaiKangConfig.HAIKANG_SK + # 通用请求头 + @classmethod + def build_signed_headers(cls, method, url, body, app_key, app_secret): + """ + 返回一个 dict,包含所有签名请求所需的 header。 + + 参数: + - method: HTTP 方法,例如 "POST" + - url: URL完整url(代码中提取相对位置, 保留 path 和 query) + - body: 请求主体字符串(如 JSON),可为空。 + - app_key: AK + - app_secret: SK + """ + # 1. 基本 headers + accept = "*/*" + content_type = "application/json" + + # 2. 计算 MD5(可选,如果 body 存在) + content_md5 = "" + if body: + + # .digest()返回原始二进制md5 + md5_digest = hashlib.md5(body.encode("utf-8")).digest() + content_md5 = base64.b64encode(md5_digest).decode("utf-8") + + # 3. 生成 Date header(HTTP 规范格式) + now = datetime.now(timezone.utc) + date = format_datetime(now, usegmt=True) + + # 4. 构造 httpHeaders 部分 + http_headers_str = "\n".join( + [method.upper(), accept, content_md5, content_type, date, ""] + ) + + # 5. 自定义 headers 部分 + custom_headers_str = f"x-ca-key:{app_key}\n" + + # 6. 拼接 path + query + parsed = urlparse(url) + path_and_query = parsed.path + if parsed.query: + path_and_query += "?" + parsed.query + + # 7. 构造签名字符串 + string_to_sign = http_headers_str + custom_headers_str + path_and_query + + # 8. 使用 HmacSHA256 + Base64 签名 + h = hmac.new( + app_secret.encode("utf-8"), string_to_sign.encode("utf-8"), hashlib.sha256 + ) + signature = base64.b64encode(h.digest()).decode("utf-8") + + # 9. 返回完整 headers + headers = { + "Accept": accept, + "Content-MD5": content_md5, + "Content-Type": content_type, + "Date": date, + "X-Ca-Key": app_key, + "X-Ca-Signature": signature, + "X-Ca-Signature-Headers": "x-ca-key", + } + return headers + + # 发送请求 + @classmethod + async def send_request(cls, method: str, url: str, headers: dict, body: str | None, json=True): + + + + async with httpx.AsyncClient(verify=False) as client: + response = await client.request( + method, url, headers=headers, content=body, timeout=5 # timeout 单位秒 + ) + if response.status_code in (301, 302, 303, 307, 308): + location = response.headers.get("Location") + # 可能需要处理成 GET 请求或保留原方法 + response = await client.get(location, headers=headers) + response.raise_for_status() + # if response.status_code >= 400: + # logger.error(f"发送请求失败: {url} , {response.status_code} {response.text}") + # raise HTTPException(status_code=response.status_code, detail=response.text) + if json: + return response.json() + return response + + # 获取access_token + @classmethod + async def get_access_token(cls): + """获取access_token + + Returns: + _type_: json + """ + + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_ACCESS_TOKEN_URL}" + headers = cls.build_signed_headers( + "POST", url, None, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + back = await cls.send_request("POST", url, headers, None) + + if back["code"] == "0": + # logger.info("获取access_token成功") + return [True, back] + # return back["data"]["access_token"],back["data"]["token_type"] ,back["data"]["expires_in"] + else: + # logger.error("获取access_token失败", back["code"], back["msg"]) + return [False, back] + + # 查询门禁点列表v2 + @classmethod + async def get_door_list_v2(cls, pageNo: int = 1, pageSize: int = 10): + """获取门禁点列表v2 + + Args: + pageNo (int, optional): 页码. Defaults to 1. + pageSize (int, optional): 每页个数. Defaults to 10. + + Returns: + _type_: _description_ + """ + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_DOOR_SEARCH}" + + body_dict = { + "pageNo": max(pageNo, 1), + "pageSize": min(pageSize, 999), + } + body_json = json.dumps(body_dict, separators=(",", ":")) + + headers = cls.build_signed_headers( + "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + + back = await cls.send_request("POST", url, headers, body_json) + + if back["code"] == "0": + # logger.info("查询门禁点列表成功") + return [True, back["data"]] + else: + # logger.error("查询门禁点列表失败 ", back["code"], back["msg"]) + return [False, back["code"], back["msg"]] + + # 查询门禁状态 + @classmethod + async def get_door_status(cls, door_index_codes: list): + """查询门禁状态 + + Args: + door_index_codes (list): 门禁点唯一标识 + """ + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_DOOR_STATES_URL}" + + body_dict = {"doorIndexCodes": door_index_codes} + body_json = json.dumps(body_dict, separators=(",", ":")) + + headers = cls.build_signed_headers( + "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + + back = await cls.send_request("POST", url, headers, body_json) + + if back["code"] == "0": + # logger.info("查询门禁状态成功") + return [ + True, + back["data"]["authDoorList"], + back["data"]["noAuthDoorIndexCodeList"], + ] + else: + # logger.error("查询门禁状态失败 ", back["code"], back["msg"]) + return [False, back["code"], back["msg"]] + + # 门禁点反控 + @classmethod + async def door_do_control(cls, door_index_code: list, control_type: int): + """门禁点反控 + + Args: + door_index_code (list): 门禁点唯一标识 + control_type (int): 操作类型 0-常开, 1-门闭, 2-门开, 3-常闭, 不允许长闭 + """ + if control_type not in [0, 1, 2]: + # logger.error("control_type参数错误 ", control_type) + return [False, 400, "control_type参数错误"] + + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_DOOR_DOCONTROL_URL}" + body_dict = {"doorIndexCodes": door_index_code, "controlType": control_type} + body_json = json.dumps(body_dict, separators=(",", ":")) + + headers = cls.build_signed_headers( + "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + + back = await cls.send_request("POST", url, headers, body_json) + + """ + { + "code": "0", + "msg": "success", + "data": [ + { + "doorIndexCode": "2c95c028a809448f962a969e3ab34f", + "controlResultCode": 0, # 0表示反控成功, 其他表示失败, 门禁控制是否成功主要看这个值 + "controlResultDesc": "success", + } + ], + }, + """ + + if back["code"] == "0": + # logger.info( + # f"执行门禁控制接口成功 door_index_code:{door_index_code} control_type:{control_type} " + # ) + return [True, back["data"]] + else: + # logger.error(f"执行门禁控制失败 ", back["code"], back["msg"]) + return [False, back["code"], back["msg"]] + + @classmethod + # 查询门禁点事件v2 + async def query_door_events_v2(cls, door_index_code: list, **args): + """查询门禁点事件v2 + + Args: + door_index_code (str): 门禁唯一标识 + """ + + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_DOOR_ENVENTS_URL}" + + # current_time = datetime.now(timezone.utc) + # three_months_ago = current_time - relativedelta(months=3) + + # # ISO8601 时间格式 + # current_time_iso = current_time.isoformat() + # three_months_ago_iso = three_months_ago.isoformat() + + body_dict = { + "pageNo": max(args.get("pageNo", 1), 1), + "pageSize": min(args.get("pageSize", 10), 999), + "doorIndexCode": door_index_code, + # 排序字段 + "sort": "eventTime", + # 倒序返回 + "order": "desc", + } + + if args.get("startTime") and args.get('startTime') is not None: + body_dict["startTime"] = args.get("startTime") + if args.get("endTime") and args.get('endTime') is not None: + body_dict["endTime"] = args.get("endTime") + if args.get("eventType") and args.get('eventType') is not None: + body_dict["eventType"] = args.get("eventType") + if args.get("personName") and args.get('personName') is not None: + body_dict["personName"] = args.get("personName") + + body_json = json.dumps(body_dict, separators=(",", ":")) + + headers = cls.build_signed_headers( + "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + + back = await cls.send_request("POST", url, headers, body_json) + + if back["code"] == "0": + # logger.info(f"获取门禁事件成功") + return [True, back["data"]] + else: + # logger.error(f"获取门禁事件失败 ", back["code"], back["msg"]) + return [False, back["code"], back["msg"]] + + # 查看门禁在线状态 + @classmethod + async def door_online_status(cls, indexCodes: list): + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_DOOR_ONLINE_STATUS}" + + body_dict = { + "indexCodes": indexCodes + } + + body_json = json.dumps(body_dict, separators=(",", ":")) + + headers = cls.build_signed_headers( + "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + + back = await cls.send_request("POST", url, headers, body_json) + + if back["code"] == "0": + # logger.info(f"获取门禁在线状态成功") + return [True, back["data"]] + else: + # logger.error(f"获取门禁在线状态失败 ", back["code"], back["msg"]) + return [False, back["code"], back["msg"]] + + # 人脸分组1vN检索 + @classmethod + async def face_group_1vN_search( + cls, + facePicBinaryData: str, # base64编码后的字符串 + pageNo: int = 1, + pageSize: int = 20, + searchNum: int = 99, + minSimilarity: int = 50, + faceGroupIndexCodes: list[str] = None, + ): + """人脸分组1vN检索 + + Args: + facePicBinaryData (str): 图片二值化后,base64编码的字符串 + pageSize (int, optional): 每页个数 Defaults to 20. + searchNum (int, optional): 最大搜索返回数. Defaults to 99. + minSimilarity (int, optional): 最小相似度. Defaults to 50. + faceGroupIndexCodes (list[str], optional): 查询人脸分组. Defaults to None. + + Returns: + _type_: _description_ + """ + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_APPLICATION_ONETOMANY_URL}" + + body_dict = { + "facePicBinaryData": facePicBinaryData, + "pageNo": pageNo, + "pageSize": pageSize, + "searchNum": searchNum, + "minSimilarity": minSimilarity, + "faceGroupIndexCodes": faceGroupIndexCodes, + } + + body_json = json.dumps(body_dict, separators=(",", ":")) + + headers = cls.build_signed_headers( + "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + + back = await cls.send_request("POST", url, headers, body_json) + + if back["code"] == "0": + # logger.info(f"获取人脸分组检索成功") + return [True, back["data"]] + else: + # logger.error(f"获取人脸分组检索失败 ", back["code"], back["msg"]) + return [False, back["code"], back["msg"]] + + # 人脸评分 + @classmethod + async def face_picture_check( + cls, + facePicBinaryData: str, # + ): + """人脸评分 + + Args: + facePicBinaryData (str): 人脸图的二进制数据经过Base64编码后的字符串 + + Returns: + _type_: _description_ + """ + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_PICTURE_CHECK_URL}" + + body_dict = {"facePicBinaryData": facePicBinaryData} + + body_json = json.dumps(body_dict, separators=(",", ":")) + + headers = cls.build_signed_headers( + "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + + back = await cls.send_request("POST", url, headers, body_json) + + if back["code"] == "0": + # logger.info(f"获取人脸评分成功") + return [True, back["data"]] + else: + # logger.error(f"获取人脸评分失败 ", back["code"], back["msg"]) + return [False, back["code"], back["msg"]] + + # 按条件查询人员识别事件 + @classmethod + async def get_face_capture_list_event(cls, startTime, endTime): + """按条件查询人员识别事件, 暂无实现. + + Args: + startTime (_type_): _description_ + endTime (_type_): _description_ + """ + pass + + # 按条件查询人脸分组 + @classmethod + async def get_face_group(cls): + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_FACE_GROUP_URL}" + + headers = cls.build_signed_headers( + "POST", url, json.dumps({}), HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + + back = await cls.send_request("POST", url, headers, json.dumps({})) + + if back["code"] == "0": + # logger.info(f"按条件查询人脸成功") + return [True, back["data"]] + else: + # logger.error(f"按条件查询人脸失败 ", back["code"], back["msg"]) + return [False, back["code"], back["msg"]] + + # 查询访客预约记录, visitorStatus: 0 待审核, 1 正常, 2 迟到, 3 失效, 4 审核退回, 9 审核失效, 10 邀约中, 11 邀约失效 + @classmethod + async def query_visitor_record( + cls, **args + ): + """查询访客预约记录 + + Args: + pageNo (int, optional): 页码. Defaults to 1. + pageSize (int, optional): 每页个数. Defaults to 10. + visitorStatus (int, optional): 访客状态. Defaults to 1. + + Returns: + _type_: _description_ + """ + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_VISITOR_RECORD_SEARCH}" + body_dict = args + + body_json = json.dumps(body_dict, separators=(",", ":")) + + headers = cls.build_signed_headers( + "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + + back = await cls.send_request("POST", url, headers, body_json) + + if back["code"] == "0": + logger.info(f"查询访客预约记录成功") + return [True, back["data"]] + else: + logger.error(f"查询访客预约记录失败 ", back["code"], back["msg"]) + return [False, back["code"], back["msg"]] + + # 获取访客记录中的图片 + @classmethod + async def query_visitor_record_pictures(cls, svrIndexCode: str, picUri: str, save_path:str): + """获取访客记录中的图片 + + Args: + visitorRecordIndexCode (str): 访客记录索引码 + + Returns: + _type_: _description_ + """ + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_VISITOR_RECORD_PICTURES}" + + body_dict = { + "svrIndexCode": svrIndexCode, + "picUri": picUri, + } + + body_json = json.dumps(body_dict, separators=(",", ":")) + + headers = cls.build_signed_headers( + "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + + back = await cls.send_request("POST", url, headers, body_json, False) + + os.makedirs(save_path, exist_ok=True) + + name = save_path.split('/')[-1] + save_path = os.path.join(save_path, name+"_访客"+".jpg") + + with open(save_path, 'wb') as f: + for chunk in back.iter_bytes(chunk_size=8192): + if chunk: # 忽略 keep-alive 的空 chunk + f.write(chunk) + return [save_path, name] + + + + + # 获取人员列表 + @classmethod + async def get_person_list(cls, pageNo: int = 1, pageSize: int = 10): + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_PERSON_LIST}" + body_dict = { + "pageNo": pageNo, + "pageSize": pageSize, + } + body_json = json.dumps(body_dict, separators=(",", ":")) + + headers = cls.build_signed_headers( + "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + + back = await cls.send_request("POST", url, headers, body_json) + + if back["code"] == "0": + return [True, back["data"]] + else: + return [False, back["code"], back["msg"]] + + # 提取人员图片 + @classmethod + async def get_person_picture(cls, serverIndexCode, picUri, save_path): + + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_GET_PERSON_PICTURE}" + body_dict = { + "serverIndexCode": serverIndexCode, + "picUri": picUri, + } + + body_json = json.dumps(body_dict, separators=(",", ":")) + + headers = cls.build_signed_headers( + "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + + back = await cls.send_request("POST", url, headers, body_json, False) + + os.makedirs(save_path, exist_ok=True) + save_path = os.path.join(save_path, picUri.split('/')[-1]+".jpg") + + with open(save_path, 'wb') as f: + for chunk in back.iter_bytes(chunk_size=8192): + if chunk: # 忽略 keep-alive 的空 chunk + f.write(chunk) + + + # 单个添加人脸分组 + @classmethod + async def face_group_addition(cls, name, description): + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_FACE_GROUP_ADDITION}" + body_dict = { + "name": name, + "description": description, + } + body_json = json.dumps(body_dict, separators=(",", ":")) + headers = cls.build_signed_headers( + "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + + back = await cls.send_request("POST", url, headers, body_json) + + if back["code"] == "0": + return [True, back["data"]] + else: + return [False, back["code"], back["msg"]] + + # 向分组中单个添加人脸图片 + @classmethod + async def face_single_addition(cls, faceGroupIndexCode: str, faceInfo: dict, facePic: dict): + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_FACE_SIGLE_ADDITION}" + + + body_dict = { + "faceGroupIndexCode": faceGroupIndexCode, + "faceInfo": { + "name": "测1" + }, + "facePic": { + #"facePicBinaryData": facePic["facePicBinaryData"] + "facePicUrl":"https://pic-image.yesky.com/uploadImages/newPic/2023/271/38/A3A130JG06X2.png" + } + } + body_json = json.dumps(body_dict, separators=(",", ":")) + + # print(body_json) + + headers = cls.build_signed_headers( + "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + back = await cls.send_request("POST", url, headers, body_json) + if back["code"] == "0": + return [True, back["data"]] + else: + return [False, back["code"], back["msg"]] + + # 批量删除人脸 + @classmethod + async def face_delete(cls, faceGroupIndexCode: str, indexCodes: list): + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_FACE_DELETE}" + + body_dict = { + "faceGroupIndexCode": faceGroupIndexCode, + "indexCodes": indexCodes, + } + body_json = json.dumps(body_dict, separators=(",", ":")) + headers = cls.build_signed_headers( + "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + back = await cls.send_request("POST", url, headers, body_json) + if back["code"] == "0": + return [True, back["data"]] + else: + return [False, back["code"], back["msg"]] + """ + 海康平台工具类 + """ + + # def __init__(self): + # self.HAIKANG_URL = HaiKangConfig.HAIKANG_URL + # self.HAIKANG_PORT = HaiKangConfig.HAIKANG_PORT + # self.HAIKANG_AK = HaiKangConfig.HAIKANG_AK + # self.HAIKANG_SK = HaiKangConfig.HAIKANG_SK + # 通用请求头 + @classmethod + def build_signed_headers(cls, method, url, body, app_key, app_secret): + """ + 返回一个 dict,包含所有签名请求所需的 header。 + + 参数: + - method: HTTP 方法,例如 "POST" + - url: URL完整url(代码中提取相对位置, 保留 path 和 query) + - body: 请求主体字符串(如 JSON),可为空。 + - app_key: AK + - app_secret: SK + """ + # 1. 基本 headers + accept = "*/*" + content_type = "application/json" + + # 2. 计算 MD5(可选,如果 body 存在) + content_md5 = "" + if body: + + # .digest()返回原始二进制md5 + md5_digest = hashlib.md5(body.encode("utf-8")).digest() + content_md5 = base64.b64encode(md5_digest).decode("utf-8") + + # 3. 生成 Date header(HTTP 规范格式) + now = datetime.now(timezone.utc) + date = format_datetime(now, usegmt=True) + + # 4. 构造 httpHeaders 部分 + http_headers_str = "\n".join( + [method.upper(), accept, content_md5, content_type, date, ""] + ) + + # 5. 自定义 headers 部分 + custom_headers_str = f"x-ca-key:{app_key}\n" + + # 6. 拼接 path + query + parsed = urlparse(url) + path_and_query = parsed.path + if parsed.query: + path_and_query += "?" + parsed.query + + # 7. 构造签名字符串 + string_to_sign = http_headers_str + custom_headers_str + path_and_query + + # 8. 使用 HmacSHA256 + Base64 签名 + h = hmac.new( + app_secret.encode("utf-8"), string_to_sign.encode("utf-8"), hashlib.sha256 + ) + signature = base64.b64encode(h.digest()).decode("utf-8") + + # 9. 返回完整 headers + headers = { + "Accept": accept, + "Content-MD5": content_md5, + "Content-Type": content_type, + "Date": date, + "X-Ca-Key": app_key, + "X-Ca-Signature": signature, + "X-Ca-Signature-Headers": "x-ca-key", + } + return headers + + # 发送请求 + # @classmethod + # async def send_request(cls, method: str, url: str, headers: dict, body: str | None): + # async with httpx.AsyncClient() as client: + # response = await client.request( + # method, url, headers=headers, content=body, timeout=5 + # ) + # response.raise_for_status() + # # if response.status_code >= 400: + # # logger.error(f"发送请求失败: {url} , {response.status_code} {response.text}") + # # raise HTTPException(status_code=response.status_code, detail=response.text) + # return response.json() + + # 获取access_token + @classmethod + async def get_access_token(cls): + """获取access_token + + Returns: + _type_: json + """ + + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_ACCESS_TOKEN_URL}" + headers = cls.build_signed_headers( + "POST", url, None, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + back = await cls.send_request("POST", url, headers, None) + + if back["code"] == "0": + logger.info("获取access_token成功") + return [True, back] + # return back["data"]["access_token"],back["data"]["token_type"] ,back["data"]["expires_in"] + else: + logger.error("获取access_token失败", back["code"], back["msg"]) + return [False, back] + + # 查询门禁点列表v2 + @classmethod + async def get_door_list_v2(cls, pageNo: int = 1, pageSize: int = 10): + """获取门禁点列表v2 + + Args: + pageNo (int, optional): 页码. Defaults to 1. + pageSize (int, optional): 每页个数. Defaults to 10. + + Returns: + _type_: _description_ + """ + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_DOOR_SEARCH}" + body_dict = { + "pageNo": max(pageNo, 1), + "pageSize": min(pageSize, 999), + } + body_json = json.dumps(body_dict, separators=(",", ":")) + + headers = cls.build_signed_headers( + "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + + back = await cls.send_request("POST", url, headers, body_json) + + if back["code"] == "0": + logger.info("查询门禁点列表成功") + return [True, back["data"]] + else: + logger.error("查询门禁点列表失败 ", back["code"], back["msg"]) + return [False, back["code"], back["msg"]] + + # 查询门禁状态 + @classmethod + async def get_door_status(cls, door_index_codes: list): + """查询门禁状态 + + Args: + door_index_codes (list): 门禁点唯一标识 + """ + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_DOOR_STATES_URL}" + + body_dict = {"doorIndexCodes": door_index_codes} + body_json = json.dumps(body_dict, separators=(",", ":")) + + headers = cls.build_signed_headers( + "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + + back = await cls.send_request("POST", url, headers, body_json) + + if back["code"] == "0": + logger.info("查询门禁状态成功") + return [ + True, + back["data"]["authDoorList"], + back["data"]["noAuthDoorIndexCodeList"], + ] + else: + logger.error("查询门禁状态失败 ", back["code"], back["msg"]) + return [False, back["code"], back["msg"]] + + # 门禁点反控 + @classmethod + async def door_do_control(cls, door_index_code: list, control_type: int): + """门禁点反控 + + Args: + door_index_code (list): 门禁点唯一标识 + control_type (int): 操作类型 0-常开, 1-门闭, 2-门开, 3-常闭, 不允许长闭 + """ + if control_type not in [0, 1, 2]: + logger.error("control_type参数错误 ", control_type) + return [False, 400, "control_type参数错误"] + + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_DOOR_DOCONTROL_URL}" + body_dict = {"doorIndexCodes": door_index_code, "controlType": control_type} + body_json = json.dumps(body_dict, separators=(",", ":")) + + headers = cls.build_signed_headers( + "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + + back = await cls.send_request("POST", url, headers, body_json) + + """ + { + "code": "0", + "msg": "success", + "data": [ + { + "doorIndexCode": "2c95c028a809448f962a969e3ab34f", + "controlResultCode": 0, # 0表示反控成功, 其他表示失败, 门禁控制是否成功主要看这个值 + "controlResultDesc": "success", + } + ], + }, + """ + + if back["code"] == "0": + logger.info( + f"执行门禁控制接口成功 door_index_code:{door_index_code} control_type:{control_type} " + ) + return [True, back["data"]] + else: + logger.error(f"执行门禁控制失败 ", back["code"], back["msg"]) + return [False, back["code"], back["msg"]] + + @classmethod + # 查询门禁点事件v2 + async def query_door_events_v2(cls, door_index_code: list, **args): + """查询门禁点事件v2 + + Args: + door_index_code (str): 门禁唯一标识 + """ + + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_DOOR_ENVENTS_URL}" + + # current_time = datetime.now(timezone.utc) + # three_months_ago = current_time - relativedelta(months=3) + + # # ISO8601 时间格式 + # current_time_iso = current_time.isoformat() + # three_months_ago_iso = three_months_ago.isoformat() + + body_dict = { + "pageNo": max(args.get("pageNo", 1), 1), + "pageSize": min(args.get("pageSize", 10), 999), + "doorIndexCode": door_index_code, + # 排序字段 + "sort": "eventTime", + # 倒序返回 + "order": "desc", + } + + if args.get("startTime") and args.get('startTime') is not None: + body_dict["startTime"] = args.get("startTime") + if args.get("endTime") and args.get('endTime') is not None: + body_dict["endTime"] = args.get("endTime") + if args.get("eventType") and args.get('eventType') is not None: + body_dict["eventType"] = args.get("eventType") + if args.get("personName") and args.get('personName') is not None: + body_dict["personName"] = args.get("personName") + + body_json = json.dumps(body_dict, separators=(",", ":")) + + headers = cls.build_signed_headers( + "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + + back = await cls.send_request("POST", url, headers, body_json) + + if back["code"] == "0": + logger.info(f"获取门禁事件成功") + return [True, back["data"]] + else: + logger.error(f"获取门禁事件失败 ", back["code"], back["msg"]) + return [False, back["code"], back["msg"]] + + # 查看门禁在线状态 + @classmethod + async def door_online_status(cls, indexCodes: list): + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_DOOR_ONLINE_STATUS}" + + body_dict = { + "indexCodes": indexCodes + } + + body_json = json.dumps(body_dict, separators=(",", ":")) + + headers = cls.build_signed_headers( + "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + + back = await cls.send_request("POST", url, headers, body_json) + + if back["code"] == "0": + logger.info(f"获取门禁在线状态成功") + return [True, back["data"]] + else: + logger.error(f"获取门禁在线状态失败 ", back["code"], back["msg"]) + return [False, back["code"], back["msg"]] + + # 人脸分组1vN检索 + @classmethod + async def face_group_1vN_search( + cls, + facePicBinaryData: str, # base64编码后的字符串 + pageNo: int = 1, + pageSize: int = 20, + searchNum: int = 99, + minSimilarity: int = 50, + faceGroupIndexCodes: list[str] = None, + ): + """人脸分组1vN检索 + + Args: + facePicBinaryData (str): 图片二值化后,base64编码的字符串 + pageSize (int, optional): 每页个数 Defaults to 20. + searchNum (int, optional): 最大搜索返回数. Defaults to 99. + minSimilarity (int, optional): 最小相似度. Defaults to 50. + faceGroupIndexCodes (list[str], optional): 查询人脸分组. Defaults to None. + + Returns: + _type_: _description_ + """ + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_APPLICATION_ONETOMANY_URL}" + + body_dict = { + "facePicBinaryData": facePicBinaryData, + "pageNo": pageNo, + "pageSize": pageSize, + "searchNum": searchNum, + "minSimilarity": minSimilarity, + "faceGroupIndexCodes": faceGroupIndexCodes, + } + + body_json = json.dumps(body_dict, separators=(",", ":")) + + headers = cls.build_signed_headers( + "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + + back = await cls.send_request("POST", url, headers, body_json) + + if back["code"] == "0": + logger.info(f"获取人脸分组检索成功") + return [True, back["data"]] + else: + logger.error(f"获取人脸分组检索失败 ", back["code"], back["msg"]) + return [False, back["code"], back["msg"]] + + # 人脸评分 + @classmethod + async def face_picture_check( + cls, + facePicBinaryData: str, # + ): + """人脸评分 + + Args: + facePicBinaryData (str): 人脸图的二进制数据经过Base64编码后的字符串 + + Returns: + _type_: _description_ + """ + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_PICTURE_CHECK_URL}" + + body_dict = {"facePicBinaryData": facePicBinaryData} + + body_json = json.dumps(body_dict, separators=(",", ":")) + + headers = cls.build_signed_headers( + "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + + back = await cls.send_request("POST", url, headers, body_json) + + if back["code"] == "0": + logger.info(f"获取人脸评分成功") + return [True, back["data"]] + else: + logger.error(f"获取人脸评分失败 ", back["code"], back["msg"]) + return [False, back["code"], back["msg"]] + + # 按条件查询人员识别事件 + @classmethod + async def get_face_capture_list_event(cls, startTime, endTime): + """按条件查询人员识别事件, 暂无实现. + + Args: + startTime (_type_): _description_ + endTime (_type_): _description_ + """ + pass + + # 按条件查询人脸分组 + @classmethod + async def get_face_group(cls): + url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_FACE_GROUP_URL}" + + headers = cls.build_signed_headers( + "POST", url, None, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + ) + + back = await cls.send_request("POST", url, headers, None) + + if back["code"] == "0": + logger.info(f"按条件查询人脸成功") + return [True, back["data"]] + else: + logger.error(f"按条件查询人脸失败 ", back["code"], back["msg"]) + return [False, back["code"], back["msg"]] + + # 查询访客预约记录, visitorStatus: 0 待审核, 1 正常, 2 迟到, 3 失效, 4 审核退回, 9 审核失效, 10 邀约中, 11 邀约失效 + # @classmethod + # async def query_visitor_record( + # cls, **args + # ): + # """查询访客预约记录 + + # Args: + # pageNo (int, optional): 页码. Defaults to 1. + # pageSize (int, optional): 每页个数. Defaults to 10. + # visitorStatus (int, optional): 访客状态. Defaults to 1. + + # Returns: + # _type_: _description_ + # """ + # url = f"{HaiKangConfig.HAIKANG_URL}:{HaiKangConfig.HAIKANG_PORT}/artemis{HaiKangConfig.HAIKANG_VISITOR_RECORD_SEARCH}" + # body_dict = args + + # body_json = json.dumps(body_dict, separators=(",", ":")) + + # headers = cls.build_signed_headers( + # "POST", url, body_json, HaiKangConfig.HAIKANG_AK, HaiKangConfig.HAIKANG_SK + # ) + + # back = await cls.send_request("POST", url, headers, body_json) + + # if back["code"] == "0": + # logger.info(f"查询访客预约记录成功") + # return [True, back["data"]] + # else: + # logger.error(f"查询访客预约记录失败 ", back["code"], back["msg"]) + # return [False, back["code"], back["msg"]] diff --git a/ruoyi-fastapi-backend/utils/lang_graph_util.py b/ruoyi-fastapi-backend/utils/lang_graph_util.py new file mode 100644 index 0000000..b915d17 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/lang_graph_util.py @@ -0,0 +1,2 @@ +# pip install -qU "langchain[anthropic]" to call the model + diff --git a/ruoyi-fastapi-backend/utils/log_util.py b/ruoyi-fastapi-backend/utils/log_util.py new file mode 100644 index 0000000..f953f55 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/log_util.py @@ -0,0 +1,60 @@ +import os +import sys +import time +from loguru import logger as _logger +from typing import Dict +from middlewares.trace_middleware import TraceCtx + + +class LoggerInitializer: + def __init__(self): + self.log_path = os.path.join(os.getcwd(), 'logs') + self.__ensure_log_directory_exists() + self.log_path_error = os.path.join(self.log_path, f'{time.strftime("%Y-%m-%d")}_error.log') + + def __ensure_log_directory_exists(self): + """ + 确保日志目录存在,如果不存在则创建 + """ + if not os.path.exists(self.log_path): + os.mkdir(self.log_path) + + @staticmethod + def __filter(log: Dict): + """ + 自定义日志过滤器,添加trace_id + """ + log['trace_id'] = TraceCtx.get_id() + return log + + def init_log(self): + """ + 初始化日志配置 + """ + # 自定义日志格式 + format_str = ( + '{time:YYYY-MM-DD HH:mm:ss.SSS} | ' + '{trace_id} | ' + '{level: <8} | ' + '{name}:{function}:{line} - ' + '{message}' + ) + _logger.remove() + # 移除后重新添加sys.stderr, 目的: 控制台输出与文件日志内容和结构一致 + _logger.add(sys.stderr, filter=self.__filter, format=format_str, enqueue=True) + _logger.add( + self.log_path_error, + filter=self.__filter, + format=format_str, + rotation='50MB', + encoding='utf-8', + enqueue=True, + compression='zip', + ) + + return _logger + + +# 初始化日志处理器 +log_initializer = LoggerInitializer() +logger = log_initializer.init_log() diff --git a/ruoyi-fastapi-backend/utils/match_service.py b/ruoyi-fastapi-backend/utils/match_service.py new file mode 100644 index 0000000..7cf5716 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/match_service.py @@ -0,0 +1,264 @@ +import re +import jieba +from typing import List, Tuple, Optional +from loguru import logger + +class MatchService: + def __init__(self): + self._colloquial_map = { + '咋': '怎么', + '咋办': '怎么办', + '咋样': '怎么样', + '啥': '什么', + '啥子': '什么', + '啥时候': '什么时候', + '啥地方': '什么地方', + '哪儿': '哪里', + '咋个': '怎么', + '咋整': '怎么办', + '咋回事': '怎么回事', + '咋回事儿': '怎么回事', + '咋样': '怎么样', + '啥人': '什么人', + '啥东西': '什么东西', + '啥玩意': '什么东西', + '啥情况': '什么情况', + '咋样': '怎么样', + '咋办': '怎么办', + '咋弄': '怎么办', + '咋整': '怎么办', + '咋回事': '怎么回事', + '咋回事儿': '怎么回事', + '咋样': '怎么样', + '啥': '什么', + '啥子': '什么', + '啥时候': '什么时候', + '啥地方': '什么地方', + '啥人': '什么人', + '啥东西': '什么东西', + '啥玩意': '什么东西', + '啥情况': '什么情况', + '啥事': '什么事', + '啥事儿': '什么事', + '啥时候': '什么时候', + '啥地方': '什么地方', + '哪儿': '哪里', + '哪儿个': '哪个', + '咋': '怎么', + '咋办': '怎么办', + '咋样': '怎么样', + '咋个': '怎么', + '咋整': '怎么办', + '咋弄': '怎么办', + '咋回事': '怎么回事', + '咋回事儿': '怎么回事', + '咋样': '怎么样', + '啥': '什么', + '啥子': '什么', + '啥时候': '什么时候', + '啥地方': '什么地方', + '啥人': '什么人', + '啥东西': '什么东西', + '啥玩意': '什么东西', + '啥情况': '什么情况', + '啥事': '什么事', + '啥事儿': '什么事', + } + + self._stop_words = { + '的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都', '一', + '一个', '上', '也', '很', '到', '说', '要', '去', '你', '会', '着', '没有', + '看', '好', '自己', '这', '那', '吗', '呢', '吧', '啊', '哦', '呀', '嘛', + '啦', '呗', '哇', '嗯', '哎', '唉', '哼', '哦', '噢', '哎哟', '哎呀', + '请问', '请问一下', '请问一下', '请问一下', '我想知道', '我想问', + '帮我', '帮我看', '帮我看一下', '帮我查', '帮我查一下', + '能不能', '可不可以', '是否', '有没有', '是不是', + '麻烦', '麻烦问一下', '麻烦您', '麻烦帮我', + '请问', '请问一下', '请问问', + '我们', '你们', '他们', '她们', '它们', + '这个', '那个', '这些', '那些', + '这种', '那种', '这类', '那类', + '嗨', '哈喽', '哈罗', + '早上好', '上午好', '下午好', '晚上好', + 'hi', 'hello', 'hey', '嗨', '哈喽', + '怎么样', '如何', '怎样', + '关于', '对于', '至于', + '以及', '或者', '还是', + '因为', '所以', '但是', '不过', + '如果', '要是', '假如', + '虽然', '尽管', + '而且', '并且', '同时', + '然后', '接着', '之后', + '首先', '其次', '最后', + '总之', '总的来说', + '吗', '呢', '吧', '啊', '哦', '呀', '嘛', '啦', '呗', '哇', '嗯', '哎', '唉', '哼', '噢', + '哎哟', '哎呀', '哎嘿', '哎呦', '哎呀', + '呀', '耶', '哟', '喔', '唔', '诶', '咦', '嗯哼', '哼哼', '呵呵', '嘿嘿', + '嘻嘻', '哈哈', '哇塞', '哇哦', '天哪', '天啊', '哎呀呀', + '呃', '额', '嗯嗯', '哦哦', '啊啊', '嗯啊', '哦啊', + } + + self._question_words = { + '什么', '怎么', '如何', '为什么', '哪儿', '哪里', '哪个', '谁', + '多少', '几', '何时', '什么时候', '怎样', '怎么样', + '是否', '有没有', '是不是', '能不能', '可不可以', + } + + jieba.setLogLevel(jieba.logging.INFO) + + def preprocess_text(self, text: str) -> str: + if not text: + return '' + + text = text.strip() + + text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9,。?!、;:""''()【】《》\s]', '', text) + + colloquial_fillers = ['嗯', '呃', '额'] + for filler in colloquial_fillers: + text = re.sub(f'{filler}+', filler, text) + + trailing_fillers = ['嗯', '哦', '啊', '呀', '啦', '呗', '嘛', '吧', '噢', '呃', '额', '哈'] + for filler in trailing_fillers: + text = re.sub(f'{filler}\\s*$', '', text) + text = re.sub(f'{filler}$', '', text) + + text = re.sub(r'\s+', ' ', text) + + for colloquial, standard in self._colloquial_map.items(): + text = text.replace(colloquial, standard) + + text = re.sub(r'[,。?!、;:""''()【】《》]', ' ', text) + text = re.sub(r'\s+', ' ', text) + + return text.strip() + + def extract_keywords(self, text: str, top_k: int = 10) -> List[str]: + if not text: + return [] + + text = self.preprocess_text(text) + words = jieba.lcut(text) + + keywords = [] + for word in words: + word = word.strip() + if len(word) < 2: + continue + if word in self._stop_words: + continue + if word.isdigit(): + continue + keywords.append(word) + + question_keywords = [] + for word in words: + if word in self._question_words: + question_keywords.append(word) + + keywords = list(set(keywords)) + + word_freq = {} + for word in words: + if word in keywords: + word_freq[word] = word_freq.get(word, 0) + 1 + + keywords.sort(key=lambda x: word_freq.get(x, 0), reverse=True) + + return keywords[:top_k] + + def calculate_similarity(self, q1: str, q2: str) -> float: + if not q1 or not q2: + return 0.0 + + n1 = self.preprocess_text(q1) + n2 = self.preprocess_text(q2) + + if n1 == n2: + return 1.0 + + kw1 = set(self.extract_keywords(n1)) + kw2 = set(self.extract_keywords(n2)) + + if not kw1 or not kw2: + return 0.0 + + intersection = len(kw1 & kw2) + union = len(kw1 | kw2) + jaccard_sim = intersection / union if union > 0 else 0 + + len_sim = 1 - abs(len(n1) - len(n2)) / max(len(n1), len(n2)) + len_sim = max(0, len_sim) + + similarity = 0.7 * jaccard_sim + 0.3 * len_sim + + return similarity + + def find_best_match(self, query: str, candidates: List[Tuple[str, any]], threshold: float = 0.6) -> Tuple[Optional[any], float]: + if not query or not candidates: + return None, 0.0 + + best_match = None + best_similarity = 0.0 + + for candidate_text, candidate_data in candidates: + similarity = self.calculate_similarity(query, candidate_text) + if similarity > best_similarity: + best_similarity = similarity + best_match = candidate_data + if best_similarity >= threshold: + return best_match, best_similarity + + return None, best_similarity + + def decompose_question(self, question: str) -> List[str]: + if not question: + return [] + + question = self.preprocess_text(question) + + separators = ['?', '?', '。', '.', ';', ';', ',', ',', '和', '以及', '还有', '另外'] + + sub_questions = [question] + for sep in separators: + new_sub_questions = [] + for sq in sub_questions: + parts = sq.split(sep) + new_sub_questions.extend(parts) + sub_questions = new_sub_questions + + sub_questions = [sq.strip() for sq in sub_questions if sq.strip()] + + unique_sub_questions = [] + seen = set() + for sq in sub_questions: + if sq not in seen: + seen.add(sq) + unique_sub_questions.append(sq) + + return unique_sub_questions + + def calculate_keyword_coverage(self, query: str, candidate: str) -> float: + if not query or not candidate: + return 0.0 + + kw1 = set(self.extract_keywords(query)) + kw2 = set(self.extract_keywords(candidate)) + + if not kw1: + return 0.0 + + if not kw2: + return 0.0 + + coverage = len(kw1 & kw2) / len(kw1) + + return coverage + +_match_service_instance = None + +def get_match_service() -> MatchService: + global _match_service_instance + if _match_service_instance is None: + _match_service_instance = MatchService() + return _match_service_instance diff --git a/ruoyi-fastapi-backend/utils/message_util.py b/ruoyi-fastapi-backend/utils/message_util.py new file mode 100644 index 0000000..3d3eb51 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/message_util.py @@ -0,0 +1,5 @@ +from utils.log_util import logger + + +def message_service(sms_code: str): + logger.info(f'短信验证码为{sms_code}') diff --git a/ruoyi-fastapi-backend/utils/page_util.py b/ruoyi-fastapi-backend/utils/page_util.py new file mode 100644 index 0000000..dfe8a31 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/page_util.py @@ -0,0 +1,116 @@ +import math +from pydantic import BaseModel, ConfigDict +from pydantic.alias_generators import to_camel +from sqlalchemy import func, select, Select +from sqlalchemy.ext.asyncio import AsyncSession +from typing import Optional, List +from utils.common_util import CamelCaseUtil + + +class PageResponseModel(BaseModel): + """ + 列表分页查询返回模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + rows: List = [] + page_num: Optional[int] = None + page_size: Optional[int] = None + total: int + has_next: Optional[bool] = None + + +class PageUtil: + """ + 分页工具类 + """ + + @classmethod + def get_page_obj(cls, data_list: List, page_num: int, page_size: int): + """ + 输入数据列表data_list和分页信息,返回分页数据列表结果 + + :param data_list: 原始数据列表 + :param page_num: 当前页码 + :param page_size: 当前页面数据量 + :return: 分页数据对象 + """ + # 计算起始索引和结束索引 + start = (page_num - 1) * page_size + end = page_num * page_size + + # 根据计算得到的起始索引和结束索引对数据列表进行切片 + paginated_data = data_list[start:end] + has_next = True if math.ceil(len(data_list) / page_size) > page_num else False + + result = PageResponseModel( + rows=paginated_data, pageNum=page_num, pageSize=page_size, total=len(data_list), hasNext=has_next + ) + + return result + + @classmethod + async def paginate(cls, db: AsyncSession, query: Select, page_num: int, page_size: int, is_page: bool = False): + """ + 输入查询语句和分页信息,返回分页数据列表结果 + + :param db: orm对象 + :param query: sqlalchemy查询语句 + :param page_num: 当前页码 + :param page_size: 当前页面数据量 + :param is_page: 是否开启分页 + :return: 分页数据对象 + """ + if is_page: + total = (await db.execute(select(func.count('*')).select_from(query.subquery()))).scalar() + query_result = await db.execute(query.offset((page_num - 1) * page_size).limit(page_size)) + paginated_data = [] + for row in query_result: + if row and len(row) == 1: + paginated_data.append(row[0]) + else: + paginated_data.append(row) + has_next = math.ceil(total / page_size) > page_num + result = PageResponseModel( + rows=CamelCaseUtil.transform_result(paginated_data), + pageNum=page_num, + pageSize=page_size, + total=total, + hasNext=has_next, + ) + else: + query_result = await db.execute(query) + no_paginated_data = [] + for row in query_result: + if row and len(row) == 1: + no_paginated_data.append(row[0]) + else: + no_paginated_data.append(row) + result = CamelCaseUtil.transform_result(no_paginated_data) + + return result + + +def get_page_obj(data_list: List, page_num: int, page_size: int): + """ + 输入数据列表data_list和分页信息,返回分页数据列表结果 + + :param data_list: 原始数据列表 + :param page_num: 当前页码 + :param page_size: 当前页面数据量 + :return: 分页数据对象 + """ + # 计算起始索引和结束索引 + start = (page_num - 1) * page_size + end = page_num * page_size + + # 根据计算得到的起始索引和结束索引对数据列表进行切片 + paginated_data = data_list[start:end] + has_next = True if math.ceil(len(data_list) / page_size) > page_num else False + + result = PageResponseModel( + rows=paginated_data, pageNum=page_num, pageSize=page_size, total=len(data_list), hasNext=has_next + ) + + return result diff --git a/ruoyi-fastapi-backend/utils/pwd_util.py b/ruoyi-fastapi-backend/utils/pwd_util.py new file mode 100644 index 0000000..86e9c27 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/pwd_util.py @@ -0,0 +1,30 @@ +from passlib.context import CryptContext + +pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto') + + +class PwdUtil: + """ + 密码工具类 + """ + + @classmethod + def verify_password(cls, plain_password, hashed_password): + """ + 工具方法:校验当前输入的密码与数据库存储的密码是否一致 + + :param plain_password: 当前输入的密码 + :param hashed_password: 数据库存储的密码 + :return: 校验结果 + """ + return pwd_context.verify(plain_password, hashed_password) + + @classmethod + def get_password_hash(cls, input_password): + """ + 工具方法:对当前输入的密码进行加密 + + :param input_password: 输入的密码 + :return: 加密成功的密码 + """ + return pwd_context.hash(input_password) diff --git a/ruoyi-fastapi-backend/utils/ragflow_asy_util.py b/ruoyi-fastapi-backend/utils/ragflow_asy_util.py new file mode 100644 index 0000000..2182d91 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/ragflow_asy_util.py @@ -0,0 +1,1081 @@ +import aiohttp +import asyncio +import inspect +import json +from typing import Optional, List, Dict, Any, Union, AsyncGenerator +import os +from pathlib import Path + + +class RAGFlowError(Exception): + """RAGFlow API错误异常""" + def __init__(self, code: int, message: str): + self.code = code + self.message = message + super().__init__(f"Error {code}: {message}") + + +class AsyncRAGFlowClient: + """异步RAGFlow API客户端""" + + def __init__(self, base_url: str, api_key: str, timeout: int = 30, connector_kwargs: Optional[Dict[str, Any]] = None): + """ + 初始化RAGFlow客户端 + + Args: + base_url: RAGFlow服务器地址 + api_key: API密钥 + timeout: 请求超时时间(秒) + """ + try: + self.base_url = base_url.rstrip('/') + except Exception: + self.base_url = base_url + self.api_key = api_key + self.timeout = timeout + self.connector_kwargs = connector_kwargs or { + "limit": 100, + "ssl": False, + "keepalive_timeout": 30, + } + self.headers = { + 'Authorization': f'Bearer {api_key}', + 'Content-Type': 'application/json' + } + self._session = None + + async def __aenter__(self): + """异步上下文管理器入口""" + await self.create_session() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """异步上下文管理器出口""" + await self.close_session() + + async def create_session(self): + """创建aiohttp会话""" + if self._session is None: + timeout = aiohttp.ClientTimeout(total=self.timeout) + connector = aiohttp.TCPConnector(**self.connector_kwargs) + self._session = aiohttp.ClientSession(timeout=timeout, connector=connector) + + async def close_session(self): + """关闭aiohttp会话""" + if self._session: + await self._session.close() + self._session = None + + async def _request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]: + """发送HTTP请求""" + if not self._session: + await self.create_session() + + url = f"{self.base_url}{endpoint}" + + # 处理headers + headers = kwargs.pop('headers', self.headers.copy()) + + async with self._session.request(method, url, headers=headers, **kwargs) as response: + try: + result = await response.json() + except (aiohttp.ContentTypeError, json.JSONDecodeError): + if response.status == 200: + content = await response.read() + return {'code': 0, 'data': content} + else: + text = await response.text() + raise RAGFlowError(response.status, text) + + if result.get('code', 0) != 0: + raise RAGFlowError(result.get('code'), result.get('message', 'Unknown error')) + + return result + + async def _stream_request(self, method: str, endpoint: str, **kwargs) -> AsyncGenerator[Dict[str, Any], None]: + """发送流式HTTP请求 - 处理SSE协议""" + if not self._session: + await self.create_session() + + url = f"{self.base_url}{endpoint}" + headers = kwargs.pop('headers', self.headers.copy()) + + async with self._session.request(method, url, headers=headers, **kwargs) as response: + async for line in response.content: + if line: + line_str = line.decode('utf-8').strip() + + # 处理SSE数据行 + if line_str.startswith('data:'): + # 提取数据部分(去掉 "data: " 前缀) + data_part = line_str[5:].strip() + + # 检查是否为结束标记 + if data_part == '[DONE]': + break + + # 尝试解析JSON + try: + data = json.loads(data_part) + yield data + except json.JSONDecodeError: + # 如果不是有效JSON,作为文本返回 + if data_part: + yield {'content': data_part, 'text': data_part} + + # ==================== + # OpenAI兼容API + # ==================== + + async def create_chat_completion(self, chat_id: str, model: str, messages: List[Dict[str, str]], + stream: bool = False) -> Union[Dict[str, Any], AsyncGenerator[Dict[str, Any], None]]: + """ + 创建聊天完成 + + Args: + chat_id: 聊天ID + model: 模型名称 + messages: 消息列表 + stream: 是否流式返回 + """ + endpoint = f"/api/v1/chats_openai/{chat_id}/chat/completions" + data = { + "model": model, + "messages": messages, + "stream": stream + } + + if stream: + return self._stream_request('POST', endpoint, json=data) + else: + return await self._request('POST', endpoint, json=data) + + async def create_agent_completion(self, agent_id: str, model: str, messages: List[Dict[str, str]], + stream: bool = False) -> Union[Dict[str, Any], AsyncGenerator[Dict[str, Any], None]]: + """ + 创建代理完成 + + Args: + agent_id: 代理ID + model: 模型名称 + messages: 消息列表 + stream: 是否流式返回 + """ + endpoint = f"/api/v1/agents_openai/{agent_id}/chat/completions" + data = { + "model": model, + "messages": messages, + "stream": stream + } + + if stream: + return self._stream_request('POST', endpoint, json=data) + else: + return await self._request('POST', endpoint, json=data) + + # ==================== + # 数据集管理 + # ==================== + + async def create_dataset(self, name: str, avatar: Optional[str] = None, description: Optional[str] = None, + embedding_model: Optional[str] = None, permission: str = "me", + chunk_method: str = "naive", + parser_config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + 创建数据集 + + Args: + name: 数据集名称 + avatar: Base64编码的头像 + description: 描述 + embedding_model: 嵌入模型 + permission: 权限设置 ("me" 或 "team") + chunk_method: 分块方法 + parser_config: 解析器配置 + """ + endpoint = "/api/v1/datasets" + data = { + "name": name, + "permission": permission, + "chunk_method": chunk_method, + } + + if avatar: + data["avatar"] = avatar + if description: + data["description"] = description + if embedding_model: + data["embedding_model"] = embedding_model + if parser_config: + data["parser_config"] = parser_config + + return await self._request('POST', endpoint, json=data) + + async def delete_datasets(self, ids: Optional[List[str]] = None) -> Dict[str, Any]: + """ + 删除数据集 + + Args: + ids: 要删除的数据集ID列表,None表示删除所有 + """ + endpoint = "/api/v1/datasets" + data = {"ids": ids} + return await self._request('DELETE', endpoint, json=data) + + async def update_dataset(self, dataset_id: str, name: Optional[str] = None, + avatar: Optional[str] = None, description: Optional[str] = None, + embedding_model: Optional[str] = None, permission: Optional[str] = None, + chunk_method: Optional[str] = None, pagerank: Optional[int] = None, + parser_config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + 更新数据集 + """ + endpoint = f"/api/v1/datasets/{dataset_id}" + data = {} + + if name is not None: + data["name"] = name + if avatar is not None: + data["avatar"] = avatar + if description is not None: + data["description"] = description + if embedding_model is not None: + data["embedding_model"] = embedding_model + if permission is not None: + data["permission"] = permission + if chunk_method is not None: + data["chunk_method"] = chunk_method + if pagerank is not None: + data["pagerank"] = pagerank + if parser_config is not None: + data["parser_config"] = parser_config + + return await self._request('PUT', endpoint, json=data) + + async def list_datasets(self, page: int = 1, page_size: int = 30, orderby: str = "create_time", + desc: str = "true", name: Optional[str] = None, + dataset_id: Optional[str] = None) -> Dict[str, Any]: + """ + 列出数据集 + """ + endpoint = "/api/v1/datasets" + params = { + "page": page, + "page_size": page_size, + "orderby": orderby, + "desc": desc + } + + if name: + params["name"] = name + if dataset_id: + params["id"] = dataset_id + + return await self._request('GET', endpoint, params=params) + + # ==================== + # 文档管理 + # ==================== + + async def upload_documents(self, dataset_id: str, file_paths: List[str]) -> Dict[str, Any]: + """ + 上传文档到数据集 + + Args: + dataset_id: 数据集ID + file_paths: 文件路径列表 + """ + if not self._session: + await self.create_session() + + endpoint = f"/api/v1/datasets/{dataset_id}/documents" + url = f"{self.base_url}{endpoint}" + + # 准备multipart数据 + data = aiohttp.FormData() + for file_path in file_paths: + if os.path.exists(file_path): + file_name = Path(file_path).name + with open(file_path, 'rb') as f: + data.add_field('file', f.read(), filename=file_name) + + headers = {'Authorization': f'Bearer {self.api_key}'} + + async with self._session.post(url, headers=headers, data=data) as response: + try: + result = await response.json() + if result.get('code', 0) != 0: + raise RAGFlowError(result.get('code'), result.get('message')) + return result + except (aiohttp.ContentTypeError, json.JSONDecodeError): + text = await response.text() + raise RAGFlowError(response.status, text) + + async def upload_documents_bytes(self, dataset_id: str, file_bytes: List) -> Dict[str, Any]: + """ + 上传文档到数据集 + + Args: + dataset_id: 数据集ID + file_name: 文件名 + file_bytes: 文件二进制列表 + """ + if not self._session: + await self.create_session() + + endpoint = f"/api/v1/datasets/{dataset_id}/documents" + url = f"{self.base_url}{endpoint}" + + # 准备multipart数据 + data = aiohttp.FormData() + for file in file_bytes: + filename = getattr(file, 'filename', None) + payload = None + + read_method = getattr(file, 'read', None) + if callable(read_method): + result = read_method() + payload = await result if inspect.isawaitable(result) else result + elif hasattr(file, 'file') and callable(getattr(file.file, 'read', None)): + payload = await asyncio.to_thread(file.file.read) + elif isinstance(file, tuple) and len(file) == 2: + filename = filename or file[0] + payload = file[1] + elif isinstance(file, (bytes, bytearray)): + payload = file + else: + payload = file + + if filename is None: + filename = getattr(file, 'name', 'upload') + + data.add_field('file', payload, filename=str(filename)) + + headers = {'Authorization': f'Bearer {self.api_key}'} + + async with self._session.post(url, headers=headers, data=data) as response: + try: + result = await response.json() + if result.get('code', 0) != 0: + raise RAGFlowError(result.get('code'), result.get('message')) + return result + except (aiohttp.ContentTypeError, json.JSONDecodeError): + text = await response.text() + raise RAGFlowError(response.status, text) + + async def update_document(self, dataset_id: str, document_id: str, name: Optional[str] = None, + meta_fields: Optional[Dict[str, Any]] = None, + chunk_method: Optional[str] = None, + parser_config: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + """ + 更新文档配置 + """ + endpoint = f"/api/v1/datasets/{dataset_id}/documents/{document_id}" + data = {} + + if name is not None: + data["name"] = name + if meta_fields is not None: + data["meta_fields"] = meta_fields + if chunk_method is not None: + data["chunk_method"] = chunk_method + if parser_config is not None: + data["parser_config"] = parser_config + + + return await self._request('PUT', endpoint, json=data) + + async def download_document(self, dataset_id: str, document_id: str, save_path: str) -> None: + """ + 下载文档 + """ + if not self._session: + await self.create_session() + + endpoint = f"/api/v1/datasets/{dataset_id}/documents/{document_id}" + url = f"{self.base_url}{endpoint}" + headers = {'Authorization': f'Bearer {self.api_key}'} + + async with self._session.get(url, headers=headers) as response: + if response.status == 200: + content = await response.read() + with open(save_path, 'wb') as f: + f.write(content) + else: + try: + error = await response.json() + raise RAGFlowError(error.get('code'), error.get('message')) + except (aiohttp.ContentTypeError, json.JSONDecodeError): + text = await response.text() + raise RAGFlowError(response.status, text) + + async def list_documents(self, dataset_id: str, page: int = 1, page_size: int = 30, + orderby: str = "create_time", desc: str = "true", + keywords: Optional[str] = None, document_id: Optional[str] = None, + document_name: Optional[str] = None) -> Dict[str, Any]: + """ + 列出数据集中的文档 + """ + endpoint = f"/api/v1/datasets/{dataset_id}/documents" + params = { + "page": page, + "page_size": page_size, + "orderby": orderby, + "desc": desc + } + + if keywords: + params["keywords"] = keywords + if document_id: + params["id"] = document_id + if document_name: + params["name"] = document_name + + return await self._request('GET', endpoint, params=params) + + async def delete_documents(self, dataset_id: str, ids: Optional[List[str]] = None) -> Dict[str, Any]: + """ + 删除文档 + """ + endpoint = f"/api/v1/datasets/{dataset_id}/documents" + data = {"ids": ids} if ids else {} + return await self._request('DELETE', endpoint, json=data) + + async def parse_documents(self, dataset_id: str, document_ids: List[str]) -> Dict[str, Any]: + """ + 解析文档 + """ + endpoint = f"/api/v1/datasets/{dataset_id}/chunks" + data = {"document_ids": document_ids} + return await self._request('POST', endpoint, json=data) + + async def stop_parsing_documents(self, dataset_id: str, document_ids: List[str]) -> Dict[str, Any]: + """ + 停止解析文档 + """ + endpoint = f"/api/v1/datasets/{dataset_id}/chunks" + data = {"document_ids": document_ids} + return await self._request('DELETE', endpoint, json=data) + + # ==================== + # 分块管理 + # ==================== + + async def add_chunk(self, dataset_id: str, document_id: str, content: str, + important_keywords: Optional[List[str]] = None, + questions: Optional[List[str]] = None) -> Dict[str, Any]: + """ + 添加分块 + """ + endpoint = f"/api/v1/datasets/{dataset_id}/documents/{document_id}/chunks" + data = {"content": content} + + if important_keywords: + data["important_keywords"] = important_keywords + if questions: + data["questions"] = questions + + return await self._request('POST', endpoint, json=data) + + async def list_chunks(self, dataset_id: str, document_id: str, keywords: Optional[str] = None, + page: int = 1, page_size: int = 1024, + chunk_id: Optional[str] = None) -> Dict[str, Any]: + """ + 列出分块 + """ + endpoint = f"/api/v1/datasets/{dataset_id}/documents/{document_id}/chunks" + params = {"page": page, "page_size": page_size} + + if keywords: + params["keywords"] = keywords + if chunk_id: + params["id"] = chunk_id + + return await self._request('GET', endpoint, params=params) + + async def delete_chunks(self, dataset_id: str, document_id: str, + chunk_ids: Optional[List[str]] = None) -> Dict[str, Any]: + """ + 删除分块 + """ + endpoint = f"/api/v1/datasets/{dataset_id}/documents/{document_id}/chunks" + data = {"chunk_ids": chunk_ids} if chunk_ids else {} + return await self._request('DELETE', endpoint, json=data) + + async def update_chunk(self, dataset_id: str, document_id: str, chunk_id: str, + content: Optional[str] = None, + important_keywords: Optional[List[str]] = None, + available: Optional[bool] = None) -> Dict[str, Any]: + """ + 更新分块 + """ + endpoint = f"/api/v1/datasets/{dataset_id}/documents/{document_id}/chunks/{chunk_id}" + data = {} + + if content is not None: + data["content"] = content + if important_keywords is not None: + data["important_keywords"] = important_keywords + if available is not None: + data["available"] = available + + return await self._request('PUT', endpoint, json=data) + + async def retrieve_chunks(self, question: str, dataset_ids: Optional[List[str]] = None, + document_ids: Optional[List[str]] = None, page: int = 1, + page_size: int = 30, similarity_threshold: float = 0.2, + vector_similarity_weight: float = 0.3, top_k: int = 1024, + rerank_id: Optional[str] = None, keyword: bool = False, + highlight: bool = False) -> Dict[str, Any]: + """ + 检索分块 + """ + endpoint = "/api/v1/retrieval" + data = { + "question": question, + "page": page, + "page_size": page_size, + "similarity_threshold": similarity_threshold, + "vector_similarity_weight": vector_similarity_weight, + "top_k": top_k, + "keyword": keyword, + "highlight": highlight + } + + if dataset_ids: + data["dataset_ids"] = dataset_ids + if document_ids: + data["document_ids"] = document_ids + if rerank_id: + data["rerank_id"] = rerank_id + + return await self._request('POST', endpoint, json=data) + + # ==================== + # 聊天助手管理 + # ==================== + + async def create_chat_assistant(self, name: str, avatar: Optional[str] = None, + dataset_ids: Optional[List[str]] = None, + llm: Optional[Dict[str, Any]] = None, + prompt: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + 创建聊天助手 + """ + endpoint = "/api/v1/chats" + data = {"name": name} + + if avatar: + data["avatar"] = avatar + if dataset_ids: + data["dataset_ids"] = dataset_ids + if llm: + data["llm"] = llm + if prompt: + data["prompt"] = prompt + + return await self._request('POST', endpoint, json=data) + + async def update_chat_assistant(self, chat_id: str, name: Optional[str] = None, + avatar: Optional[str] = None, + dataset_ids: Optional[List[str]] = None, + llm: Optional[Dict[str, Any]] = None, + prompt: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + 更新聊天助手 + """ + endpoint = f"/api/v1/chats/{chat_id}" + data = {} + + if name is not None: + data["name"] = name + if avatar is not None: + data["avatar"] = avatar + if dataset_ids is not None: + data["dataset_ids"] = dataset_ids + if llm is not None: + data["llm"] = llm + if prompt is not None: + data["prompt"] = prompt + + return await self._request('PUT', endpoint, json=data) + + async def delete_chat_assistants(self, ids: Optional[List[str]] = None) -> Dict[str, Any]: + """ + 删除聊天助手 + """ + endpoint = "/api/v1/chats" + data = {"ids": ids} if ids else {} + return await self._request('DELETE', endpoint, json=data) + + async def list_chat_assistants(self, page: int = 1, page_size: int = 30, + orderby: str = "create_time", desc: str = "true", + name: Optional[str] = None, + chat_id: Optional[str] = None) -> Dict[str, Any]: + """ + 列出聊天助手 + """ + endpoint = "/api/v1/chats" + params = { + "page": page, + "page_size": page_size, + "orderby": orderby, + "desc": desc + } + + if name: + params["name"] = name + if chat_id: + params["id"] = chat_id + + return await self._request('GET', endpoint, params=params) + + # ==================== + # 会话管理 + # ==================== + + async def create_session_with_chat(self, chat_id: str, name: str, + user_id: Optional[str] = None) -> Dict[str, Any]: + """ + 创建与聊天助手的会话 + """ + endpoint = f"/api/v1/chats/{chat_id}/sessions" + data = {"name": name} + + if user_id: + data["user_id"] = user_id + + return await self._request('POST', endpoint, json=data) + + async def update_chat_session(self, chat_id: str, session_id: str, name: Optional[str] = None, + user_id: Optional[str] = None) -> Dict[str, Any]: + """ + 更新聊天会话 + """ + endpoint = f"/api/v1/chats/{chat_id}/sessions/{session_id}" + data = {} + + if name is not None: + data["name"] = name + if user_id is not None: + data["user_id"] = user_id + + return await self._request('PUT', endpoint, json=data) + + async def list_chat_sessions(self, chat_id: str, page: int = 1, page_size: int = 30, + orderby: str = "create_time", desc: str = "true", + name: Optional[str] = None, session_id: Optional[str] = None, + user_id: Optional[str] = None) -> Dict[str, Any]: + """ + 列出与指定聊天助手相关的聊天会话 + """ + endpoint = f"/api/v1/chats/{chat_id}/sessions" + params = { + "page": page, + "page_size": page_size, + "orderby": orderby, + "desc": desc + } + + if name: + params["name"] = name + if session_id: + params["id"] = session_id + if user_id: + params["user_id"] = user_id + + return await self._request('GET', endpoint, params=params) + + async def delete_chat_sessions(self, chat_id: str, ids: Optional[List[str]] = None) -> Dict[str, Any]: + """ + 删除聊天会话 + """ + endpoint = f"/api/v1/chats/{chat_id}/sessions" + data = {"ids": ids} if ids else {} + return await self._request('DELETE', endpoint, json=data) + + async def converse_with_chat_assistant(self, chat_id: str, question: str, stream: bool = True, + session_id: Optional[str] = None, + user_id: Optional[str] = None) -> Union[Dict[str, Any], AsyncGenerator[Dict[str, Any], None]]: + """ + 与聊天助手对话 + """ + endpoint = f"/api/v1/chats/{chat_id}/completions" + data = {"question": question, "stream": stream} + + if session_id: + data["session_id"] = session_id + + print(f"开始对话: {question} {session_id}") + if user_id: + data["user_id"] = user_id + + if stream: + return self._stream_request('POST', endpoint, json=data) + else: + return await self._request('POST', endpoint, json=data) + + # ==================== + # 代理管理 + # ==================== + + async def create_session_with_agent(self, agent_id: str, user_id: Optional[str] = None, + file_data: Optional[Dict[str, Any]] = None, + **kwargs) -> Dict[str, Any]: + """ + 创建与代理的会话 + """ + if not self._session: + await self.create_session() + + endpoint = f"/api/v1/agents/{agent_id}/sessions" + url = f"{self.base_url}{endpoint}" + params = {} + + if user_id: + params["user_id"] = user_id + + if file_data: + # 处理文件上传 + data = aiohttp.FormData() + + for key, file_path in file_data.items(): + if os.path.exists(file_path): + file_name = Path(file_path).name + with open(file_path, 'rb') as f: + data.add_field(key, f.read(), filename=file_name) + + headers = {'Authorization': f'Bearer {self.api_key}'} + + async with self._session.post(url, headers=headers, data=data, params=params) as response: + try: + result = await response.json() + if result.get('code', 0) != 0: + raise RAGFlowError(result.get('code'), result.get('message')) + return result + except (aiohttp.ContentTypeError, json.JSONDecodeError): + text = await response.text() + raise RAGFlowError(response.status, text) + else: + # 普通JSON请求 + data = kwargs + return await self._request('POST', endpoint, json=data, params=params) + + async def converse_with_agent(self, agent_id: str, question: str, stream: bool = True, + session_id: Optional[str] = None, user_id: Optional[str] = None, + sync_dsl: bool = False, **kwargs) -> Union[Dict[str, Any], AsyncGenerator[Dict[str, Any], None]]: + """ + 与代理对话 + """ + endpoint = f"/api/v1/agents/{agent_id}/completions" + data = {"question": question, "stream": stream, "sync_dsl": sync_dsl} + + if session_id: + data["session_id"] = session_id + if user_id: + data["user_id"] = user_id + + # 添加其他Begin组件参数 + data.update(kwargs) + + if stream: + return self._stream_request('POST', endpoint, json=data) + else: + return await self._request('POST', endpoint, json=data) + + async def list_agent_sessions(self, agent_id: str, page: int = 1, page_size: int = 30, + orderby: str = "create_time", desc: str = "true", + session_id: Optional[str] = None, user_id: Optional[str] = None, + dsl: bool = True) -> Dict[str, Any]: + """ + 列出代理会话 + """ + endpoint = f"/api/v1/agents/{agent_id}/sessions" + params = { + "page": page, + "page_size": page_size, + "orderby": orderby, + "desc": desc, + "dsl": dsl + } + + if session_id: + params["id"] = session_id + if user_id: + params["user_id"] = user_id + + return await self._request('GET', endpoint, params=params) + + async def delete_agent_sessions(self, agent_id: str, ids: Optional[List[str]] = None) -> Dict[str, Any]: + """ + 删除代理会话 + """ + endpoint = f"/api/v1/agents/{agent_id}/sessions" + data = {"ids": ids} if ids else {} + return await self._request('DELETE', endpoint, json=data) + + async def get_related_questions(self, question: str, login_token: str) -> Dict[str, Any]: + """ + 生成相关问题 + """ + endpoint = "/v1/sessions/related_questions" + headers = { + 'Authorization': f'Bearer {login_token}', + 'Content-Type': 'application/json' + } + data = {"question": question} + + return await self._request('POST', endpoint, headers=headers, json=data) + + async def list_agents(self, page: int = 1, page_size: int = 30, orderby: str = "create_time", + desc: str = "true", name: Optional[str] = None, + agent_id: Optional[str] = None) -> Dict[str, Any]: + """ + 列出代理 + """ + endpoint = "/api/v1/agents" + params = { + "page": page, + "page_size": page_size, + "orderby": orderby, + "desc": desc + } + + if name: + params["name"] = name + if agent_id: + params["id"] = agent_id + + return await self._request('GET', endpoint, params=params) + + async def create_agent(self, title: str, description: Optional[str] = None, + dsl: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + 创建代理 + """ + endpoint = "/api/v1/agents" + data = {"title": title} + + if description is not None: + data["description"] = description + if dsl is not None: + data["dsl"] = dsl + + return await self._request('POST', endpoint, json=data) + + async def update_agent(self, agent_id: str, title: Optional[str] = None, + description: Optional[str] = None, + dsl: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + 更新代理 + + Args: + agent_id: 代理ID + title: 新标题 + description: 新描述 + dsl: 新DSL配置 + """ + endpoint = f"/api/v1/agents/{agent_id}" + data = {} + + if title is not None: + data["title"] = title + if description is not None: + data["description"] = description + if dsl is not None: + data["dsl"] = dsl + + return await self._request('PUT', endpoint, json=data) + + async def delete_agent(self, agent_id: str) -> Dict[str, Any]: + """ + 删除代理 + + Args: + agent_id: 代理ID + """ + endpoint = f"/api/v1/agents/{agent_id}" + return await self._request('DELETE', endpoint) + + +# ==================== +# 使用示例 +# ==================== + +async def example_usage(): + """ + 异步RAGFlow SDK使用示例 + """ + # 使用异步上下文管理器 + async with AsyncRAGFlowClient( + base_url="http://10.0.0.202:82", + api_key="ragflow-hlMjRmNzE2ODNiNTExZjA4ZTNlMDI0Mm" + ) as client: + + try: + # 删除数据集 + await client.delete_datasets(ids=["afe1387883bb11f0a0fd0242ac170006"]) + print("删除数据集成功") + + # 1. 创建数据集 + dataset = await client.create_dataset( + name="我的数据集", + description="这是一个测试数据集", + chunk_method="naive" + ) + dataset_id = dataset['data']['id'] + print(f"创建数据集成功: {dataset_id}") + + # 2. 上传文档 + documents = await client.upload_documents( + dataset_id=dataset_id, + file_paths=[ + "/home/admin-root/haotian/康达瑞贝斯机器人后台/ruoyi-fastapi-backend/requirements.txt", + "/home/admin-root/haotian/康达瑞贝斯机器人后台/ruoyi-fastapi-backend/requirements-pg.txt" + ] + ) + print("文档上传成功") + + # 3. 解析文档 + document_ids = [doc['id'] for doc in documents['data']] + await client.parse_documents(dataset_id, document_ids) + print("开始解析文档") + + # 等待解析完成 + await asyncio.sleep(5) + + # 4. 创建聊天助手 + chat_assistant = await client.create_chat_assistant( + name="我的AI助手", + dataset_ids=[dataset_id] + ) + chat_id = chat_assistant['data']['id'] + print(f"创建聊天助手成功: {chat_id}") + + # 5. 创建会话 + session = await client.create_session_with_chat( + chat_id=chat_id, + name="测试会话" + ) + session_id = session['data']['id'] + print(f"创建会话成功: {session_id}") + + # 6. 开始对话(流式) + responses = client.converse_with_chat_assistant( + chat_id=chat_id, + question="你好,请介绍一下自己", + stream=True, + session_id=session_id + ) + + print("AI回复:") + async for response in responses: + if response.get('data') and isinstance(response['data'], dict): + answer = response['data'].get('answer', '') + if answer: + print(answer, end='', flush=True) + print() + + # 7. 检索相关文档块 + chunks = await client.retrieve_chunks( + question="RAGFlow的优势是什么?", + dataset_ids=[dataset_id], + top_k=5, + highlight=True + ) + print(f"检索到 {chunks['data']['total']} 个相关文档块") + + # 8. 列出数据集 + datasets = await client.list_datasets(page=1, page_size=10) + print(f"当前有 {len(datasets['data'])} 个数据集") + + except RAGFlowError as e: + print(f"RAGFlow API错误: {e}") + except Exception as e: + print(f"其他错误: {e}") + + +async def example_usage_1(): + """测试获取列表方法的异步版本""" + + # 使用异步上下文管理器 + async with AsyncRAGFlowClient( + base_url="http://10.0.0.202:82", + api_key="ragflow-hlMjRmNzE2ODNiNTExZjA4ZTNlMDI0Mm" + ) as client: + + try: + # 1. 获取数据集列表 + results_dataset = await client.list_datasets() + print(f"获取数据集列表成功,共有 {len(results_dataset['data'])} 个数据集") + print("数据集ID:\n", [result["id"] for result in results_dataset['data']]) + + # 2. 获取数据集中文档列表 + for result in results_dataset['data']: + print(f"数据集 {result['id']} 的文档列表为:") + results_doc = await client.list_documents(dataset_id=result['id']) + # 文档名称 + print([t["name"] for t in results_doc["data"]["docs"]]) + + except RAGFlowError as e: + print(f"RAGFlow API错误: {e}") + except Exception as e: + print(f"其他错误: {e}") + + +async def batch_operations_example(): + """ + 批量操作示例 - 演示异步并发处理 + """ + async with AsyncRAGFlowClient( + base_url="http://10.0.0.202:82", + api_key="ragflow-hlMjRmNzE2ODNiNTExZjA4ZTNlMDI0Mm" + ) as client: + + try: + # 并发获取数据集和代理列表 + datasets_task = client.list_datasets() + agents_task = client.list_agents() + chats_task = client.list_chat_assistants() + + # 等待所有任务完成 + datasets, agents, chats = await asyncio.gather( + datasets_task, agents_task, chats_task + ) + + print(f"数据集数量: {len(datasets['data'])}") + print(f"代理数量: {len(agents['data'])}") + print(f"聊天助手数量: {len(chats['data'])}") + + # 如果有数据集,并发获取每个数据集的文档 + if datasets['data']: + doc_tasks = [ + client.list_documents(dataset_id=dataset['id']) + for dataset in datasets['data'][:3] # 限制前3个 + ] + + doc_results = await asyncio.gather(*doc_tasks, return_exceptions=True) + + for i, result in enumerate(doc_results): + if isinstance(result, Exception): + print(f"获取数据集 {datasets['data'][i]['id']} 的文档时出错: {result}") + else: + print(f"数据集 {datasets['data'][i]['id']} 有 {len(result['data']['docs'])} 个文档") + + except Exception as e: + print(f"批量操作出错: {e}") + + +# 运行示例的辅助函数 +def run_example(): + """运行异步示例""" + # 可以选择运行不同的示例 + asyncio.run(example_usage_1()) + # asyncio.run(example_usage()) + # asyncio.run(batch_operations_example()) + + +if __name__ == "__main__": + # 运行示例 + run_example() \ No newline at end of file diff --git a/ruoyi-fastapi-backend/utils/ragflow_client_manager.py b/ruoyi-fastapi-backend/utils/ragflow_client_manager.py new file mode 100644 index 0000000..ddad3d7 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/ragflow_client_manager.py @@ -0,0 +1,54 @@ +import asyncio +from typing import Optional + +from config.env import RAGFlowConfig +from utils.ragflow_asy_util import AsyncRAGFlowClient + + +class RAGFlowClientManager: + """管理RAGFlow异步客户端的生命周期,复用底层HTTP会话""" + + _client: Optional[AsyncRAGFlowClient] = None + _lock: asyncio.Lock = asyncio.Lock() + + @classmethod + async def init_client(cls) -> AsyncRAGFlowClient: + async with cls._lock: + if cls._client is None: + cls._client = AsyncRAGFlowClient( + RAGFlowConfig.RAGFLOW_BASE_URL, + RAGFlowConfig.RAGFLOW_API_KEY, + timeout=60, + connector_kwargs={ + "limit": 128, + "ssl": False, + "keepalive_timeout": 45, + }, + ) + await cls._client.create_session() + return cls._client + + @classmethod + async def get_client(cls) -> AsyncRAGFlowClient: + if cls._client is None: + return await cls.init_client() + return cls._client + + @classmethod + async def shutdown_client(cls) -> None: + async with cls._lock: + if cls._client is not None: + await cls._client.close_session() + cls._client = None + + +async def init_ragflow_client() -> AsyncRAGFlowClient: + return await RAGFlowClientManager.init_client() + + +async def get_ragflow_client() -> AsyncRAGFlowClient: + return await RAGFlowClientManager.get_client() + + +async def shutdown_ragflow_client() -> None: + await RAGFlowClientManager.shutdown_client() diff --git a/ruoyi-fastapi-backend/utils/ragflow_util.py b/ruoyi-fastapi-backend/utils/ragflow_util.py new file mode 100644 index 0000000..c6772e4 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/ragflow_util.py @@ -0,0 +1,1187 @@ +import requests +import json +from typing import Optional, List, Dict, Any, Union, Generator +import os + + +class RAGFlowError(Exception): + """RAGFlow API错误异常""" + def __init__(self, code: int, message: str): + self.code = code + self.message = message + super().__init__(f"Error {code}: {message}") + + +class RAGFlowClient: + """RAGFlow API客户端""" + + def __init__(self, base_url: str, api_key: str): + """ + 初始化RAGFlow客户端 + + Args: + base_url: RAGFlow服务器地址 + api_key: API密钥 + """ + self.base_url = base_url.rstrip('/') + self.api_key = api_key + self.headers = { + 'Authorization': f'Bearer {api_key}', + 'Content-Type': 'application/json' + } + + def _request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]: + """发送HTTP请求""" + url = f"{self.base_url}{endpoint}" + + # 处理headers + headers = kwargs.pop('headers', self.headers.copy()) + + response = requests.request(method, url, headers=headers, **kwargs) + + try: + result = response.json() + except json.JSONDecodeError: + if response.status_code == 200: + return {'code': 0, 'data': response.content} + else: + raise RAGFlowError(response.status_code, response.text) + + if result.get('code', 0) != 0: + raise RAGFlowError(result.get('code'), result.get('message', 'Unknown error')) + + return result + + def _stream_request(self, method: str, endpoint: str, **kwargs) -> Generator[Dict[str, Any], None, None]: + """发送流式HTTP请求,修复SSE缓冲问题""" + import time + + # 添加详细的时间戳日志 + server_start_time = time.time() + print(f"[RAG_CLIENT {server_start_time:.3f}] 🔄 开始流式请求: {method} {endpoint}") + + url = f"{self.base_url}{endpoint}" + headers = kwargs.pop('headers', self.headers.copy()) + + # 添加防止缓冲的响应头 + headers.update({ + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'X-Accel-Buffering': 'no' # Nginx禁用缓冲 + }) + + print(f"[RAG_CLIENT {time.time():.3f}] 📤 发送HTTP请求到: {url}") + response = requests.request(method, url, headers=headers, stream=True, **kwargs) + + response_time = time.time() + print(f"[RAG_CLIENT {response_time:.3f}] 📥 HTTP响应接收完成,状态码: {response.status_code}") + print(f"[RAG_CLIENT {response_time:.3f}] ⏱️ 连接建立耗时: {response_time - server_start_time:.3f}s") + + # 立即处理响应,yield每个chunk + chunk_count = 0 + first_chunk_sent = False + + for line in response.iter_lines(): + if line: + line_time = time.time() + line = line.decode('utf-8') + if line.startswith('data:'): + try: + data = json.loads(line[5:].strip()) + + if not first_chunk_sent: + first_chunk_sent = True + print(f"[RAG_CLIENT {line_time:.3f}] 🎯 首块数据开始发送,耗时: {line_time - response_time:.3f}s") + + chunk_count += 1 + print(f"[RAG_CLIENT {line_time:.3f}] 📦 Chunk #{chunk_count} 发送中... (耗时: {line_time - server_start_time:.3f}s)") + yield data + except json.JSONDecodeError: + print(f"[RAG_CLIENT {line_time:.3f}] ❌ JSON解析错误: {line}") + continue + + end_time = time.time() + print(f"[RAG_CLIENT {end_time:.3f}] 🏁 流式请求完成,总耗时: {end_time - server_start_time:.3f}s") + print(f"[RAG_CLIENT {end_time:.3f}] 📊 总共发送chunk数量: {chunk_count}") + + # ==================== + # OpenAI兼容API + # ==================== + + def create_chat_completion(self, chat_id: str, model: str, messages: List[Dict[str, str]], + stream: bool = False) -> Union[Dict[str, Any], Generator[Dict[str, Any], None, None]]: + """ + 创建聊天完成 + + Args: + chat_id: 聊天ID + model: 模型名称 + messages: 消息列表 + stream: 是否流式返回 + """ + endpoint = f"/api/v1/chats_openai/{chat_id}/chat/completions" + data = { + "model": model, + "messages": messages, + "stream": stream + } + + if stream: + return self._stream_request('POST', endpoint, json=data) + else: + return self._request('POST', endpoint, json=data) + + def create_agent_completion(self, agent_id: str, model: str, messages: List[Dict[str, str]], + stream: bool = False) -> Union[Dict[str, Any], Generator[Dict[str, Any], None, None]]: + """ + 创建代理完成 + + Args: + agent_id: 代理ID + model: 模型名称 + messages: 消息列表 + stream: 是否流式返回 + """ + endpoint = f"/api/v1/agents_openai/{agent_id}/chat/completions" + data = { + "model": model, + "messages": messages, + "stream": stream + } + + if stream: + return self._stream_request('POST', endpoint, json=data) + else: + return self._request('POST', endpoint, json=data) + + # ==================== + # 数据集管理 + # ==================== + + def create_dataset(self, name: str, avatar: Optional[str] = None, description: Optional[str] = None, + embedding_model: Optional[str] = None, permission: str = "me", + chunk_method: str = "naive", + parser_config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + 创建数据集 + + Args: + name: 数据集名称 + avatar: Base64编码的头像 + description: 描述 + embedding_model: 嵌入模型 + permission: 权限设置 ("me" 或 "team") + chunk_method: 分块方法 + "naive": General (default) + parser_config + "auto_keywords": int + Defaults to 0 + Minimum: 0 + Maximum: 32 + "auto_questions": int + Defaults to 0 + Minimum: 0 + Maximum: 10 + "chunk_token_num": int + Defaults to 512 + Minimum: 1 + Maximum: 2048 + "delimiter": string + Defaults to "\n". + "html4excel": bool Indicates whether to convert Excel documents into HTML format. + Defaults to false + "layout_recognize": string + Defaults to DeepDOC + "tag_kb_ids": array refer to Use tag set + Must include a list of dataset IDs, where each dataset is parsed using the ​​Tag Chunking Method + "task_page_size": int For PDF only. + Defaults to 12 + Minimum: 1 + "raptor": object RAPTOR-specific settings. + Defaults to: {"use_raptor": false} + "graphrag": object GRAPHRAG-specific settings. + Defaults to: {"use_graphrag": false} + "book": Book + "email": Email + "laws": Laws + "manual": Manual + "one": One + "paper": Paper + "picture": Picture + "presentation": Presentation + "qa": Q&A + "table": Table + "tag": Tag + pagerank: 页面排名 + parser_config: 解析器配置 + """ + endpoint = "/api/v1/datasets" + data = { + "name": name, + "permission": permission, + "chunk_method": chunk_method, + # "pagerank": pagerank + } + + if avatar: + data["avatar"] = avatar + if description: + data["description"] = description + if embedding_model: + data["embedding_model"] = embedding_model + if parser_config: + data["parser_config"] = parser_config + + return self._request('POST', endpoint, json=data) + + def delete_datasets(self, ids: Optional[List[str]] = None) -> Dict[str, Any]: + """ + 删除数据集 + + Args: + ids: 要删除的数据集ID列表,None表示删除所有 + """ + endpoint = "/api/v1/datasets" + data = {"ids": ids} + return self._request('DELETE', endpoint, json=data) + + def update_dataset(self, dataset_id: str, name: Optional[str] = None, + avatar: Optional[str] = None, description: Optional[str] = None, + embedding_model: Optional[str] = None, permission: Optional[str] = None, + chunk_method: Optional[str] = None, pagerank: Optional[int] = None, + parser_config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + 更新数据集 + + Args: + dataset_id: 数据集ID + name: 新名称 + avatar: 新头像 + description: 新描述 + embedding_model: 新嵌入模型 + permission: 新权限设置 + chunk_method: 新分块方法 + pagerank: 新页面排名 + parser_config: 新解析器配置 + """ + endpoint = f"/api/v1/datasets/{dataset_id}" + data = {} + + if name is not None: + data["name"] = name + if avatar is not None: + data["avatar"] = avatar + if description is not None: + data["description"] = description + if embedding_model is not None: + data["embedding_model"] = embedding_model + if permission is not None: + data["permission"] = permission + if chunk_method is not None: + data["chunk_method"] = chunk_method + if pagerank is not None: + data["pagerank"] = pagerank + if parser_config is not None: + data["parser_config"] = parser_config + + return self._request('PUT', endpoint, json=data) + + def list_datasets(self, page: int = 1, page_size: int = 30, orderby: str = "create_time", + desc: bool = True, name: Optional[str] = None, + dataset_id: Optional[str] = None) -> Dict[str, Any]: + """ + 列出数据集 + + Args: + page: 页码 + page_size: 每页数量 + orderby: 排序字段 + desc: 是否降序 + name: 筛选名称 + dataset_id: 筛选ID + """ + endpoint = "/api/v1/datasets" + params = { + "page": page, + "page_size": page_size, + "orderby": orderby, + "desc": desc + } + + if name: + params["name"] = name + if dataset_id: + params["id"] = dataset_id + + return self._request('GET', endpoint, params=params) + + # ==================== + # 文档管理 + # ==================== + + def upload_documents(self, dataset_id: str, file_paths: List[str]) -> Dict[str, Any]: + """ + 上传文档到数据集 + + Args: + dataset_id: 数据集ID + file_paths: 文件路径列表 + """ + endpoint = f"/api/v1/datasets/{dataset_id}/documents" + + files = [] + for file_path in file_paths: + if os.path.exists(file_path): + files.append(('file', open(file_path, 'rb'))) + + headers = { + 'Authorization': f'Bearer {self.api_key}', + } + + try: + response = requests.post(f"{self.base_url}{endpoint}", + headers=headers, files=files) + result = response.json() + + if result.get('code', 0) != 0: + raise RAGFlowError(result.get('code'), result.get('message')) + + return result + finally: + for _, file_obj in files: + file_obj.close() + + def update_document(self, dataset_id: str, document_id: str, name: Optional[str] = None, + meta_fields: Optional[Dict[str, Any]] = None, + chunk_method: Optional[str] = None, + parser_config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + 更新文档配置 + + Args: + dataset_id: 数据集ID + document_id: 文档ID + name: 新名称 + meta_fields: 元字段 + chunk_method: 分块方法 + parser_config: 解析器配置 + """ + endpoint = f"/api/v1/datasets/{dataset_id}/documents/{document_id}" + data = {} + + if name is not None: + data["name"] = name + if meta_fields is not None: + data["meta_fields"] = meta_fields + if chunk_method is not None: + data["chunk_method"] = chunk_method + if parser_config is not None: + data["parser_config"] = parser_config + + return self._request('PUT', endpoint, json=data) + + def download_document(self, dataset_id: str, document_id: str, save_path: str) -> None: + """ + 下载文档 + + Args: + dataset_id: 数据集ID + document_id: 文档ID + save_path: 保存路径 + """ + endpoint = f"/api/v1/datasets/{dataset_id}/documents/{document_id}" + headers = {'Authorization': f'Bearer {self.api_key}'} + + response = requests.get(f"{self.base_url}{endpoint}", headers=headers) + + if response.status_code == 200: + with open(save_path, 'wb') as f: + f.write(response.content) + else: + try: + error = response.json() + raise RAGFlowError(error.get('code'), error.get('message')) + except json.JSONDecodeError: + raise RAGFlowError(response.status_code, response.text) + + def list_documents(self, dataset_id: str, page: int = 1, page_size: int = 30, + orderby: str = "create_time", desc: bool = True, + keywords: Optional[str] = None, document_id: Optional[str] = None, + document_name: Optional[str] = None) -> Dict[str, Any]: + """ + 列出数据集中的文档 + + Args: + dataset_id: 数据集ID + page: 页码 + page_size: 每页数量 + orderby: 排序字段 + desc: 是否降序 + keywords: 关键词搜索 + document_id: 筛选文档ID + document_name: 筛选文档名称 + """ + endpoint = f"/api/v1/datasets/{dataset_id}/documents" + params = { + "page": page, + "page_size": page_size, + "orderby": orderby, + "desc": desc + } + + if keywords: + params["keywords"] = keywords + if document_id: + params["id"] = document_id + if document_name: + params["name"] = document_name + + return self._request('GET', endpoint, params=params) + + def delete_documents(self, dataset_id: str, ids: Optional[List[str]] = None) -> Dict[str, Any]: + """ + 删除文档 + + Args: + dataset_id: 数据集ID + ids: 要删除的文档ID列表 + """ + endpoint = f"/api/v1/datasets/{dataset_id}/documents" + data = {"ids": ids} if ids else {} + return self._request('DELETE', endpoint, json=data) + + def parse_documents(self, dataset_id: str, document_ids: List[str]) -> Dict[str, Any]: + """ + 解析文档 + + Args: + dataset_id: 数据集ID + document_ids: 要解析的文档ID列表 + """ + endpoint = f"/api/v1/datasets/{dataset_id}/chunks" + data = {"document_ids": document_ids} + return self._request('POST', endpoint, json=data) + + def stop_parsing_documents(self, dataset_id: str, document_ids: List[str]) -> Dict[str, Any]: + """ + 停止解析文档 + + Args: + dataset_id: 数据集ID + document_ids: 要停止解析的文档ID列表 + """ + endpoint = f"/api/v1/datasets/{dataset_id}/chunks" + data = {"document_ids": document_ids} + return self._request('DELETE', endpoint, json=data) + + # ==================== + # 分块管理 + # ==================== + + def add_chunk(self, dataset_id: str, document_id: str, content: str, + important_keywords: Optional[List[str]] = None, + questions: Optional[List[str]] = None) -> Dict[str, Any]: + """ + 添加分块 + + Args: + dataset_id: 数据集ID + document_id: 文档ID + content: 分块内容 + important_keywords: 重要关键词 + questions: 问题列表 + """ + endpoint = f"/api/v1/datasets/{dataset_id}/documents/{document_id}/chunks" + data = {"content": content} + + if important_keywords: + data["important_keywords"] = important_keywords + if questions: + data["questions"] = questions + + return self._request('POST', endpoint, json=data) + + def list_chunks(self, dataset_id: str, document_id: str, keywords: Optional[str] = None, + page: int = 1, page_size: int = 1024, + chunk_id: Optional[str] = None) -> Dict[str, Any]: + """ + 列出分块 + + Args: + dataset_id: 数据集ID + document_id: 文档ID + keywords: 关键词搜索 + page: 页码 + page_size: 每页数量 + chunk_id: 分块ID筛选 + """ + endpoint = f"/api/v1/datasets/{dataset_id}/documents/{document_id}/chunks" + params = {"page": page, "page_size": page_size} + + if keywords: + params["keywords"] = keywords + if chunk_id: + params["id"] = chunk_id + + return self._request('GET', endpoint, params=params) + + def delete_chunks(self, dataset_id: str, document_id: str, + chunk_ids: Optional[List[str]] = None) -> Dict[str, Any]: + """ + 删除分块 + + Args: + dataset_id: 数据集ID + document_id: 文档ID + chunk_ids: 要删除的分块ID列表 + """ + endpoint = f"/api/v1/datasets/{dataset_id}/documents/{document_id}/chunks" + data = {"chunk_ids": chunk_ids} if chunk_ids else {} + return self._request('DELETE', endpoint, json=data) + + def update_chunk(self, dataset_id: str, document_id: str, chunk_id: str, + content: Optional[str] = None, + important_keywords: Optional[List[str]] = None, + available: Optional[bool] = None) -> Dict[str, Any]: + """ + 更新分块 + + Args: + dataset_id: 数据集ID + document_id: 文档ID + chunk_id: 分块ID + content: 新内容 + important_keywords: 重要关键词 + available: 可用状态 + """ + endpoint = f"/api/v1/datasets/{dataset_id}/documents/{document_id}/chunks/{chunk_id}" + data = {} + + if content is not None: + data["content"] = content + if important_keywords is not None: + data["important_keywords"] = important_keywords + if available is not None: + data["available"] = available + + return self._request('PUT', endpoint, json=data) + + def retrieve_chunks(self, question: str, dataset_ids: Optional[List[str]] = None, + document_ids: Optional[List[str]] = None, page: int = 1, + page_size: int = 30, similarity_threshold: float = 0.2, + vector_similarity_weight: float = 0.3, top_k: int = 1024, + rerank_id: Optional[str] = None, keyword: bool = False, + highlight: bool = False) -> Dict[str, Any]: + """ + 检索分块 + + Args: + question: 查询问题 + dataset_ids: 数据集ID列表 + document_ids: 文档ID列表 + page: 页码 + page_size: 每页数量 + similarity_threshold: 相似度阈值 + vector_similarity_weight: 向量相似度权重 + top_k: TopK数量 + rerank_id: 重排序模型ID + keyword: 是否启用关键词匹配 + highlight: 是否高亮显示 + """ + endpoint = "/api/v1/retrieval" + data = { + "question": question, + "page": page, + "page_size": page_size, + "similarity_threshold": similarity_threshold, + "vector_similarity_weight": vector_similarity_weight, + "top_k": top_k, + "keyword": keyword, + "highlight": highlight + } + + if dataset_ids: + data["dataset_ids"] = dataset_ids + if document_ids: + data["document_ids"] = document_ids + if rerank_id: + data["rerank_id"] = rerank_id + + return self._request('POST', endpoint, json=data) + + # ==================== + # 聊天助手管理 + # ==================== + + def create_chat_assistant(self, name: str, avatar: Optional[str] = None, + dataset_ids: Optional[List[str]] = None, + llm: Optional[Dict[str, Any]] = None, + prompt: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + 创建聊天助手 + + Args: + name: 助手名称 + avatar: Base64编码头像 + dataset_ids: 关联的数据集ID列表 + llm: LLM配置 + prompt: 提示配置 + """ + endpoint = "/api/v1/chats" + data = {"name": name} + + if avatar: + data["avatar"] = avatar + if dataset_ids: + data["dataset_ids"] = dataset_ids + if llm: + data["llm"] = llm + if prompt: + data["prompt"] = prompt + + return self._request('POST', endpoint, json=data) + + def update_chat_assistant(self, chat_id: str, name: Optional[str] = None, + avatar: Optional[str] = None, + dataset_ids: Optional[List[str]] = None, + llm: Optional[Dict[str, Any]] = None, + prompt: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + 更新聊天助手 + + Args: + chat_id: 聊天助手ID + name: 新名称 + avatar: 新头像 + dataset_ids: 新数据集ID列表 + llm: 新LLM配置 + prompt: 新提示配置 + """ + endpoint = f"/api/v1/chats/{chat_id}" + data = {} + + if name is not None: + data["name"] = name + if avatar is not None: + data["avatar"] = avatar + if dataset_ids is not None: + data["dataset_ids"] = dataset_ids + if llm is not None: + data["llm"] = llm + if prompt is not None: + data["prompt"] = prompt + + return self._request('PUT', endpoint, json=data) + + def delete_chat_assistants(self, ids: Optional[List[str]] = None) -> Dict[str, Any]: + """ + 删除聊天助手 + + Args: + ids: 要删除的聊天助手ID列表 + """ + endpoint = "/api/v1/chats" + data = {"ids": ids} if ids else {} + return self._request('DELETE', endpoint, json=data) + + def list_chat_assistants(self, page: int = 1, page_size: int = 30, + orderby: str = "create_time", desc: bool = True, + name: Optional[str] = None, + chat_id: Optional[str] = None) -> Dict[str, Any]: + """ + 列出聊天助手 + + Args: + page: 页码 + page_size: 每页数量 + orderby: 排序字段 + desc: 是否降序 + name: 筛选名称 + chat_id: 筛选ID + """ + endpoint = "/api/v1/chats" + params = { + "page": page, + "page_size": page_size, + "orderby": orderby, + "desc": desc + } + + if name: + params["name"] = name + if chat_id: + params["id"] = chat_id + + return self._request('GET', endpoint, params=params) + + # ==================== + # 会话管理 + # ==================== + + def create_session_with_chat(self, chat_id: str, name: str, + user_id: Optional[str] = None) -> Dict[str, Any]: + """ + 创建与聊天助手的会话 + + Args: + chat_id: 聊天助手ID + name: 会话名称 + user_id: 可选的用户定义ID + """ + endpoint = f"/api/v1/chats/{chat_id}/sessions" + data = {"name": name} + + if user_id: + data["user_id"] = user_id + + return self._request('POST', endpoint, json=data) + + def update_chat_session(self, chat_id: str, session_id: str, name: Optional[str] = None, + user_id: Optional[str] = None) -> Dict[str, Any]: + """ + 更新聊天会话 + + Args: + chat_id: 聊天助手ID + session_id: 会话ID + name: 新名称 + user_id: 新用户ID + """ + endpoint = f"/api/v1/chats/{chat_id}/sessions/{session_id}" + data = {} + + if name is not None: + data["name"] = name + if user_id is not None: + data["user_id"] = user_id + + return self._request('PUT', endpoint, json=data) + + def list_chat_sessions(self, chat_id: str, page: int = 1, page_size: int = 30, + orderby: str = "create_time", desc: bool = True, + name: Optional[str] = None, session_id: Optional[str] = None, + user_id: Optional[str] = None) -> Dict[str, Any]: + """ + 列出与指定聊天助手相关的聊天会话 + + Args: + chat_id: 聊天助手ID + page: 页码 + page_size: 每页数量 + orderby: 排序字段 + desc: 是否降序 + name: 筛选名称 + session_id: 筛选会话ID + user_id: 筛选用户ID + """ + endpoint = f"/api/v1/chats/{chat_id}/sessions" + params = { + "page": page, + "page_size": page_size, + "orderby": orderby, + "desc": desc + } + + if name: + params["name"] = name + if session_id: + params["id"] = session_id + if user_id: + params["user_id"] = user_id + + return self._request('GET', endpoint, params=params) + + def delete_chat_sessions(self, chat_id: str, ids: Optional[List[str]] = None) -> Dict[str, Any]: + """ + 删除聊天会话 + + Args: + chat_id: 聊天助手ID + ids: 要删除的会话ID列表 + """ + endpoint = f"/api/v1/chats/{chat_id}/sessions" + data = {"ids": ids} if ids else {} + return self._request('DELETE', endpoint, json=data) + + def converse_with_chat_assistant(self, chat_id: str, question: str, stream: bool = True, + session_id: Optional[str] = None, + user_id: Optional[str] = None) -> Union[Dict[str, Any], Generator[Dict[str, Any], None, None]]: + """ + 与聊天助手对话 + + Args: + chat_id: 聊天助手ID + question: 问题 + stream: 是否流式返回 + session_id: 会话ID + user_id: 用户ID + """ + endpoint = f"/api/v1/chats/{chat_id}/completions" + data = {"question": question, "stream": stream} + + if session_id: + data["session_id"] = session_id + if user_id: + data["user_id"] = user_id + + if stream: + return self._stream_request('POST', endpoint, json=data) + else: + return self._request('POST', endpoint, json=data) + + # ==================== + # 代理管理 + # ==================== + + def create_session_with_agent(self, agent_id: str, user_id: Optional[str] = None, + file_data: Optional[Dict[str, Any]] = None, + **kwargs) -> Dict[str, Any]: + """ + 创建与代理的会话 + + Args: + agent_id: 代理ID + user_id: 用户ID + file_data: 文件数据(当Begin组件需要文件参数时) + **kwargs: 其他Begin组件需要的参数 + """ + endpoint = f"/api/v1/agents/{agent_id}/sessions" + params = {} + + if user_id: + params["user_id"] = user_id + + if file_data: + # 处理文件上传 + headers = {'Authorization': f'Bearer {self.api_key}'} + files = {} + + for key, file_path in file_data.items(): + if os.path.exists(file_path): + files[key] = open(file_path, 'rb') + + try: + response = requests.post(f"{self.base_url}{endpoint}", + headers=headers, files=files, params=params) + result = response.json() + + if result.get('code', 0) != 0: + raise RAGFlowError(result.get('code'), result.get('message')) + + return result + finally: + for file_obj in files.values(): + file_obj.close() + else: + # 普通JSON请求 + data = kwargs + return self._request('POST', endpoint, json=data, params=params) + + def converse_with_agent(self, agent_id: str, question: str, stream: bool = True, + session_id: Optional[str] = None, user_id: Optional[str] = None, + sync_dsl: bool = False, **kwargs) -> Union[Dict[str, Any], Generator[Dict[str, Any], None, None]]: + """ + 与代理对话 + + Args: + agent_id: 代理ID + question: 问题 + stream: 是否流式返回 + session_id: 会话ID + user_id: 用户ID + sync_dsl: 是否同步DSL变更到现有会话 + **kwargs: Begin组件需要的其他参数 + """ + endpoint = f"/api/v1/agents/{agent_id}/completions" + data = {"question": question, "stream": stream, "sync_dsl": sync_dsl} + + if session_id: + data["session_id"] = session_id + if user_id: + data["user_id"] = user_id + + # 添加其他Begin组件参数 + data.update(kwargs) + + if stream: + return self._stream_request('POST', endpoint, json=data) + else: + return self._request('POST', endpoint, json=data) + + def list_agent_sessions(self, agent_id: str, page: int = 1, page_size: int = 30, + orderby: str = "create_time", desc: bool = True, + session_id: Optional[str] = None, user_id: Optional[str] = None, + dsl: bool = True) -> Dict[str, Any]: + """ + 列出代理会话 + + Args: + agent_id: 代理ID + page: 页码 + page_size: 每页数量 + orderby: 排序字段 + desc: 是否降序 + session_id: 筛选会话ID + user_id: 筛选用户ID + dsl: 是否在响应中包含dsl字段 + """ + endpoint = f"/api/v1/agents/{agent_id}/sessions" + params = { + "page": page, + "page_size": page_size, + "orderby": orderby, + "desc": desc, + "dsl": dsl + } + + if session_id: + params["id"] = session_id + if user_id: + params["user_id"] = user_id + + return self._request('GET', endpoint, params=params) + + def delete_agent_sessions(self, agent_id: str, ids: Optional[List[str]] = None) -> Dict[str, Any]: + """ + 删除代理会话 + + Args: + agent_id: 代理ID + ids: 要删除的会话ID列表 + """ + endpoint = f"/api/v1/agents/{agent_id}/sessions" + data = {"ids": ids} if ids else {} + return self._request('DELETE', endpoint, json=data) + + def get_related_questions(self, question: str, login_token: str) -> Dict[str, Any]: + """ + 生成相关问题 + 注意:此API需要登录令牌而不是API密钥 + + Args: + question: 原始问题 + login_token: 登录令牌 + """ + endpoint = "/v1/sessions/related_questions" + headers = { + 'Authorization': f'Bearer {login_token}', + 'Content-Type': 'application/json' + } + data = {"question": question} + + return self._request('POST', endpoint, headers=headers, json=data) + + # ==================== + # 代理管理 + # ==================== + + def list_agents(self, page: int = 1, page_size: int = 30, orderby: str = "create_time", + desc: bool = True, name: Optional[str] = None, + agent_id: Optional[str] = None) -> Dict[str, Any]: + """ + 列出代理 + + Args: + page: 页码 + page_size: 每页数量 + orderby: 排序字段 + desc: 是否降序 + name: 筛选名称 + agent_id: 筛选ID + """ + endpoint = "/api/v1/agents" + params = { + "page": page, + "page_size": page_size, + "orderby": orderby, + "desc": desc + } + + if name: + params["name"] = name + if agent_id: + params["id"] = agent_id + + return self._request('GET', endpoint, params=params) + + def create_agent(self, title: str, description: Optional[str] = None, + dsl: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + 创建代理 + + Args: + title: 代理标题 + description: 代理描述 + dsl: Canvas DSL对象 + """ + endpoint = "/api/v1/agents" + data = {"title": title} + + if description is not None: + data["description"] = description + if dsl is not None: + data["dsl"] = dsl + + return self._request('POST', endpoint, json=data) + + def update_agent(self, agent_id: str, title: Optional[str] = None, + description: Optional[str] = None, + dsl: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + 更新代理 + + Args: + agent_id: 代理ID + title: 新标题 + description: 新描述 + dsl: 新DSL配置 + """ + endpoint = f"/api/v1/agents/{agent_id}" + data = {} + + if title is not None: + data["title"] = title + if description is not None: + data["description"] = description + if dsl is not None: + data["dsl"] = dsl + + return self._request('PUT', endpoint, json=data) + + def delete_agent(self, agent_id: str) -> Dict[str, Any]: + """ + 删除代理 + + Args: + agent_id: 代理ID + """ + endpoint = f"/api/v1/agents/{agent_id}" + return self._request('DELETE', endpoint) + + +# ==================== +# 使用示例 +# ==================== + +def example_usage(): + """ + RAGFlow SDK使用示例 + """ + # 初始化客户端 + client = RAGFlowClient( + base_url="http://10.0.0.202:82", + api_key="ragflow-hlMjRmNzE2ODNiNTExZjA4ZTNlMDI0Mm" + ) + + try: + # 删除数据集 + client.delete_datasets(ids=["afe1387883bb11f0a0fd0242ac170006"]) + print("删除数据集成功") + + # 1. 创建数据集, 都是默认配置 + dataset = client.create_dataset( + name="我的数据集", + description="这是一个测试数据集", + chunk_method="naive" + ) + dataset_id = dataset['data']['id'] + print(f"创建数据集成功: {dataset_id}") + + # 2. 上传文档 + documents = client.upload_documents( + dataset_id=dataset_id, + file_paths=[ + "/home/admin-root/haotian/康达瑞贝斯机器人后台/ruoyi-fastapi-backend/requirements.txt", + "/home/admin-root/haotian/康达瑞贝斯机器人后台/ruoyi-fastapi-backend/requirements-pg.txt" + ] + ) + print("文档上传成功") + + # 3. 解析文档 + document_ids = [doc['id'] for doc in documents['data']] + client.parse_documents(dataset_id, document_ids) + print("开始解析文档") + + import time + time.sleep(5) + + # 4. 创建聊天助手 + chat_assistant = client.create_chat_assistant( + name="我的AI助手", + dataset_ids=[dataset_id] + ) + chat_id = chat_assistant['data']['id'] + print(f"创建聊天助手成功: {chat_id}") + + # 5. 创建会话 + session = client.create_session_with_chat( + chat_id=chat_id, + name="测试会话" + ) + session_id = session['data']['id'] + print(f"创建会话成功: {session_id}") + + # 6. 开始对话(流式) + responses = client.converse_with_chat_assistant( + chat_id=chat_id, + question="你好,请介绍一下自己", + stream=True, + session_id=session_id + ) + + print("AI回复:") + for response in responses: + if response.get('data') and isinstance(response['data'], dict): + answer = response['data'].get('answer', '') + if answer: + print(answer, end='', flush=True) + print() + + # 7. 检索相关文档块 + chunks = client.retrieve_chunks( + question="RAGFlow的优势是什么?", + dataset_ids=[dataset_id], + top_k=5, + highlight=True + ) + print(f"检索到 {chunks['data']['total']} 个相关文档块") + + # 8. 列出数据集 + datasets = client.list_datasets(page=1, page_size=10) + print(f"当前有 {len(datasets['data'])} 个数据集") + + except RAGFlowError as e: + print(f"RAGFlow API错误: {e}") + except Exception as e: + print(f"其他错误: {e}") + + +def example_usage_1(): + """ 测试获取列表方法 + """ + + # 初始化客户端 + client = RAGFlowClient( + base_url="http://10.0.0.202:82", + api_key="ragflow-hlMjRmNzE2ODNiNTExZjA4ZTNlMDI0Mm" + ) + + # 1. 获取数据集列表 + results_dataset = client.list_datasets() + print(f"获取数据集列表成功,共有 {len(results_dataset['data'])} 个数据集") + print("数据集为id:\n", [result["id"] for result in results_dataset['data']]) + # 2. 获取数据集中文档列表 + for result in results_dataset['data']: + print(f"数据集 {result['id']} 的文档列表为:") + results_doc = client.list_documents(dataset_id=result['id']) + # 文档名称 + print([t["name"] for t in results_doc["data"]["docs"]]) + + # results = client.list_documents(dataset_id="d01") + + +if __name__ == "__main__": + # 运行示例 + # example_usage() + + example_usage_1() \ No newline at end of file diff --git a/ruoyi-fastapi-backend/utils/response_util.py b/ruoyi-fastapi-backend/utils/response_util.py new file mode 100644 index 0000000..4ed77ec --- /dev/null +++ b/ruoyi-fastapi-backend/utils/response_util.py @@ -0,0 +1,269 @@ +from datetime import datetime +from fastapi import status +from fastapi.encoders import jsonable_encoder +from fastapi.responses import JSONResponse, Response, StreamingResponse +from pydantic import BaseModel +from starlette.background import BackgroundTask +from typing import Any, Dict, Mapping, Optional +from config.constant import HttpStatusConstant + + +class ResponseUtil: + """ + 响应工具类 + """ + + @classmethod + def success( + cls, + msg: str = '操作成功', + data: Optional[Any] = None, + rows: Optional[Any] = None, + dict_content: Optional[Dict] = None, + model_content: Optional[BaseModel] = None, + headers: Optional[Mapping[str, str]] = None, + media_type: Optional[str] = None, + background: Optional[BackgroundTask] = None, + ) -> Response: + """ + 成功响应方法 + + :param msg: 可选,自定义成功响应信息 + :param data: 可选,成功响应结果中属性为data的值 + :param rows: 可选,成功响应结果中属性为rows的值 + :param dict_content: 可选,dict类型,成功响应结果中自定义属性的值 + :param model_content: 可选,BaseModel类型,成功响应结果中自定义属性的值 + :param headers: 可选,响应头信息 + :param media_type: 可选,响应结果媒体类型 + :param background: 可选,响应返回后执行的后台任务 + :return: 成功响应结果 + """ + result = {'code': HttpStatusConstant.SUCCESS, 'msg': msg} + + if data is not None: + result['data'] = data + if rows is not None: + result['rows'] = rows + if dict_content is not None: + result.update(dict_content) + if model_content is not None: + result.update(model_content.model_dump(by_alias=True)) + + result.update({'success': True, 'time': datetime.now()}) + + return JSONResponse( + status_code=status.HTTP_200_OK, + content=jsonable_encoder(result), + headers=headers, + media_type=media_type, + background=background, + ) + + @classmethod + def failure( + cls, + msg: str = '操作失败', + data: Optional[Any] = None, + rows: Optional[Any] = None, + dict_content: Optional[Dict] = None, + model_content: Optional[BaseModel] = None, + headers: Optional[Mapping[str, str]] = None, + media_type: Optional[str] = None, + background: Optional[BackgroundTask] = None, + ) -> Response: + """ + 失败响应方法 + + :param msg: 可选,自定义失败响应信息 + :param data: 可选,失败响应结果中属性为data的值 + :param rows: 可选,失败响应结果中属性为rows的值 + :param dict_content: 可选,dict类型,失败响应结果中自定义属性的值 + :param model_content: 可选,BaseModel类型,失败响应结果中自定义属性的值 + :param headers: 可选,响应头信息 + :param media_type: 可选,响应结果媒体类型 + :param background: 可选,响应返回后执行的后台任务 + :return: 失败响应结果 + """ + result = {'code': HttpStatusConstant.WARN, 'msg': msg} + + if data is not None: + result['data'] = data + if rows is not None: + result['rows'] = rows + if dict_content is not None: + result.update(dict_content) + if model_content is not None: + result.update(model_content.model_dump(by_alias=True)) + + result.update({'success': False, 'time': datetime.now()}) + + return JSONResponse( + status_code=status.HTTP_200_OK, + content=jsonable_encoder(result), + headers=headers, + media_type=media_type, + background=background, + ) + + @classmethod + def unauthorized( + cls, + msg: str = '登录信息已过期,访问系统资源失败', + data: Optional[Any] = None, + rows: Optional[Any] = None, + dict_content: Optional[Dict] = None, + model_content: Optional[BaseModel] = None, + headers: Optional[Mapping[str, str]] = None, + media_type: Optional[str] = None, + background: Optional[BackgroundTask] = None, + ) -> Response: + """ + 未认证响应方法 + + :param msg: 可选,自定义未认证响应信息 + :param data: 可选,未认证响应结果中属性为data的值 + :param rows: 可选,未认证响应结果中属性为rows的值 + :param dict_content: 可选,dict类型,未认证响应结果中自定义属性的值 + :param model_content: 可选,BaseModel类型,未认证响应结果中自定义属性的值 + :param headers: 可选,响应头信息 + :param media_type: 可选,响应结果媒体类型 + :param background: 可选,响应返回后执行的后台任务 + :return: 未认证响应结果 + """ + result = {'code': HttpStatusConstant.UNAUTHORIZED, 'msg': msg} + + if data is not None: + result['data'] = data + if rows is not None: + result['rows'] = rows + if dict_content is not None: + result.update(dict_content) + if model_content is not None: + result.update(model_content.model_dump(by_alias=True)) + + result.update({'success': False, 'time': datetime.now()}) + + return JSONResponse( + status_code=status.HTTP_200_OK, + content=jsonable_encoder(result), + headers=headers, + media_type=media_type, + background=background, + ) + + @classmethod + def forbidden( + cls, + msg: str = '该用户无此接口权限', + data: Optional[Any] = None, + rows: Optional[Any] = None, + dict_content: Optional[Dict] = None, + model_content: Optional[BaseModel] = None, + headers: Optional[Mapping[str, str]] = None, + media_type: Optional[str] = None, + background: Optional[BackgroundTask] = None, + ) -> Response: + """ + 未授权响应方法 + + :param msg: 可选,自定义未授权响应信息 + :param data: 可选,未授权响应结果中属性为data的值 + :param rows: 可选,未授权响应结果中属性为rows的值 + :param dict_content: 可选,dict类型,未授权响应结果中自定义属性的值 + :param model_content: 可选,BaseModel类型,未授权响应结果中自定义属性的值 + :param headers: 可选,响应头信息 + :param media_type: 可选,响应结果媒体类型 + :param background: 可选,响应返回后执行的后台任务 + :return: 未授权响应结果 + """ + result = {'code': HttpStatusConstant.FORBIDDEN, 'msg': msg} + + if data is not None: + result['data'] = data + if rows is not None: + result['rows'] = rows + if dict_content is not None: + result.update(dict_content) + if model_content is not None: + result.update(model_content.model_dump(by_alias=True)) + + result.update({'success': False, 'time': datetime.now()}) + + return JSONResponse( + status_code=status.HTTP_200_OK, + content=jsonable_encoder(result), + headers=headers, + media_type=media_type, + background=background, + ) + + @classmethod + def error( + cls, + msg: str = '接口异常', + data: Optional[Any] = None, + rows: Optional[Any] = None, + dict_content: Optional[Dict] = None, + model_content: Optional[BaseModel] = None, + headers: Optional[Mapping[str, str]] = None, + media_type: Optional[str] = None, + background: Optional[BackgroundTask] = None, + ) -> Response: + """ + 错误响应方法 + + :param msg: 可选,自定义错误响应信息 + :param data: 可选,错误响应结果中属性为data的值 + :param rows: 可选,错误响应结果中属性为rows的值 + :param dict_content: 可选,dict类型,错误响应结果中自定义属性的值 + :param model_content: 可选,BaseModel类型,错误响应结果中自定义属性的值 + :param headers: 可选,响应头信息 + :param media_type: 可选,响应结果媒体类型 + :param background: 可选,响应返回后执行的后台任务 + :return: 错误响应结果 + """ + result = {'code': HttpStatusConstant.ERROR, 'msg': msg} + + if data is not None: + result['data'] = data + if rows is not None: + result['rows'] = rows + if dict_content is not None: + result.update(dict_content) + if model_content is not None: + result.update(model_content.model_dump(by_alias=True)) + + result.update({'success': False, 'time': datetime.now()}) + + return JSONResponse( + status_code=status.HTTP_200_OK, + content=jsonable_encoder(result), + headers=headers, + media_type=media_type, + background=background, + ) + + @classmethod + def streaming( + cls, + *, + data: Any = None, + headers: Optional[Mapping[str, str]] = None, + media_type: Optional[str] = None, + background: Optional[BackgroundTask] = None, + ) -> Response: + """ + 流式响应方法 + + :param data: 流式传输的内容 + :param headers: 可选,响应头信息 + :param media_type: 可选,响应结果媒体类型 + :param background: 可选,响应返回后执行的后台任务 + :return: 流式响应结果 + """ + return StreamingResponse( + status_code=status.HTTP_200_OK, content=data, headers=headers, media_type=media_type, background=background + ) + + + diff --git a/ruoyi-fastapi-backend/utils/semantic_cache_service.py b/ruoyi-fastapi-backend/utils/semantic_cache_service.py new file mode 100644 index 0000000..9785a8f --- /dev/null +++ b/ruoyi-fastapi-backend/utils/semantic_cache_service.py @@ -0,0 +1,435 @@ +""" +语义缓存服务 - 基于问答历史的缓存方案 + +功能: +1. 记录用户通过RAG返回的对话历史(问题+答案) +2. 当用户后续提问时,基于语义先搜索缓存 +3. 如果命中缓存,直接返回答案 +4. 如果未命中,调用RAG,并将结果存入缓存 + +技术方案: +- 使用Redis存储问答对(问题文本 + 答案文本) +- 使用简单的文本相似度算法(因为没有现成的Embedding模型) +- 缓存键:rag:cache:{chat_id}:{question_hash} +- 支持多级匹配:精确匹配 → 模糊匹配 + +作者:AI Assistant +日期:2024 +""" + +from utils.log_util import logger + +import hashlib +import json +import time +import re +from typing import Optional, Tuple, Dict, Any, List +from dataclasses import dataclass, asdict +from .match_service import get_match_service + + +@dataclass +class CacheEntry: + """缓存条目""" + question: str + answer: str + created_at: float + hit_count: int = 0 + chat_id: str = "" + + def to_dict(self) -> dict: + return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> 'CacheEntry': + return cls(**data) + + +class SemanticCacheService: + """ + 语义缓存服务 + + 支持三种匹配策略: + 1. 精确匹配:问题完全相同 + 2. 关键语义匹配:提取问题核心关键词,判断语义相似 + 3. 模糊匹配:计算文本相似度 + """ + + # 缓存配置 + CACHE_PREFIX = "rag:semantic:cache" + MAX_CACHE_SIZE = 1000 # 每个chat_id最大缓存条数 + SIMILARITY_THRESHOLD = 0.6 # 相似度阈值 + CACHE_TTL_HOURS = 720 # 缓存过期时间(小时,一个月) + + def __init__(self, redis_client=None): + """ + 初始化语义缓存服务 + + Args: + redis_client: Redis客户端实例,如果为None则使用全局redis + """ + self.redis = redis_client + self._default_redis = None + + def _get_redis(self): + """获取Redis客户端""" + if self.redis is not None: + return self.redis + if self._default_redis is None: + from config.get_redis import RedisUtil + # 注意:这里需要异步获取,实际使用时应该传入redis实例 + logger.warning("未提供Redis客户端,语义缓存功能不可用") + return self._default_redis + + def _build_cache_key(self, chat_id: str, question_hash: str) -> str: + """构建缓存键""" + return f"{self.CACHE_PREFIX}:{chat_id}:{question_hash}" + + def _normalize_question(self, question: str) -> str: + """标准化问题文本""" + match_service = get_match_service() + return match_service.preprocess_text(question) + + def _extract_keywords(self, question: str) -> List[str]: + """提取问题关键词(用于语义匹配)""" + match_service = get_match_service() + return match_service.extract_keywords(question) + + def _calculate_text_similarity(self, q1: str, q2: str) -> float: + """计算两个问题的文本相似度""" + match_service = get_match_service() + return match_service.calculate_similarity(q1, q2) + + async def lookup( + self, + chat_id: str, + question: str, + redis_client=None + ) -> Optional[Tuple[str, float]]: + """ + 在缓存中查找相似问题 + + Args: + chat_id: 聊天会话ID + question: 用户问题 + redis_client: Redis客户端(可选) + + Returns: + 如果命中缓存,返回 (答案, 相似度) + 如果未命中,返回 None + """ + redis = redis_client or self._get_redis() + if redis is None: + return None + + try: + # 1. 精确匹配 + normalized = question.lower().strip() + question_hash = hashlib.md5(normalized.encode('utf-8')).hexdigest()[:16] + exact_key = self._build_cache_key(chat_id, question_hash) + + logger.info(f"[SemanticCache] 精确查找 | chat_id={chat_id} | question={question} | hash={question_hash} | key={exact_key}") + + cached_data = await redis.get(exact_key) + if cached_data: + entry = CacheEntry.from_dict(json.loads(cached_data)) + entry.hit_count += 1 + # 更新命中次数(异步,不阻塞返回) + await redis.set(exact_key, json.dumps(entry.to_dict()), ex=self.CACHE_TTL_HOURS * 3600) + logger.info(f"[SemanticCache] 精确命中: {question[:30]}...") + return entry.answer, 1.0 + + # 2. 语义模糊匹配(扫描同chat_id的所有缓存) + # 获取该chat_id的所有缓存键 + pattern = f"{self.CACHE_PREFIX}:{chat_id}:*" + cache_keys = await redis.keys(pattern) + + if not cache_keys: + return None + + # 遍历缓存,计算相似度 + best_match = None + best_match_key = None # 记录最佳匹配的key + best_similarity = 0.0 + + for key in cache_keys: + # 跳过刚检查过的精确匹配键 + if key == exact_key: + continue + + cached_data = await redis.get(key) + if not cached_data: + continue + + try: + entry = CacheEntry.from_dict(json.loads(cached_data)) + similarity = self._calculate_text_similarity(question, entry.question) + + if similarity > best_similarity: + best_similarity = similarity + best_match = entry + best_match_key = key # 保存正确的key + + except (json.JSONDecodeError, KeyError) as e: + logger.warning(f"[SemanticCache] 解析缓存失败: {key}, {e}") + continue + + # 判断是否达到相似度阈值 + if best_match and best_similarity >= self.SIMILARITY_THRESHOLD: + logger.info(f"[SemanticCache] 语义命中 (相似度={best_similarity:.2f}): {question[:30]}...") + # 更新命中次数 + best_match.hit_count += 1 + await redis.set(best_match_key, json.dumps(best_match.to_dict()), ex=self.CACHE_TTL_HOURS * 3600) + return best_match.answer, best_similarity + + return None + + except Exception as e: + logger.error(f"[SemanticCache] 查找失败: {e}") + return None + + async def store( + self, + chat_id: str, + question: str, + answer: str, + redis_client=None + ) -> bool: + """ + 将问答对存入缓存 + + Args: + chat_id: 聊天会话ID + question: 用户问题 + answer: RAG返回的答案 + redis_client: Redis客户端(可选) + + Returns: + 是否存储成功 + """ + redis = redis_client or self._get_redis() + if redis is None: + return False + + try: + if not answer or len(answer.strip()) < 10: + logger.info(f"[SemanticCache] 答案太短,不缓存: {answer[:20]}...") + return False + + normalized = question.lower().strip() + question_hash = hashlib.md5(normalized.encode('utf-8')).hexdigest()[:16] + cache_key = self._build_cache_key(chat_id, question_hash) + + logger.info(f"[SemanticCache] 存储缓存 | chat_id={chat_id} | question={question} | hash={question_hash} | key={cache_key}") + + entry = CacheEntry( + question=question, + answer=answer, + created_at=time.time(), + hit_count=0, + chat_id=chat_id + ) + + existing = await redis.get(cache_key) + if existing: + logger.debug(f"[SemanticCache] 问题已存在,跳过: {question[:30]}...") + return True + + await self._cleanup_old_cache(chat_id, redis) + + await redis.set( + cache_key, + json.dumps(entry.to_dict()), + ex=self.CACHE_TTL_HOURS * 3600 + ) + + logger.info(f"[SemanticCache] 已缓存 | key={cache_key} | question={question[:30]}...") + + verify = await redis.get(cache_key) + logger.info(f"[SemanticCache] 存储验证 | key={cache_key} | found={verify is not None}") + return True + + except Exception as e: + logger.error(f"[SemanticCache] 存储失败: {e}") + return False + + async def _cleanup_old_cache(self, chat_id: str, redis_client=None) -> int: + """ + 清理旧缓存(当缓存过多时) + + 策略:删除最久未命中(创建时间最早)的缓存 + + Returns: + 删除的缓存数量 + """ + redis = redis_client or self._get_redis() + if redis is None: + return 0 + + try: + pattern = f"{self.CACHE_PREFIX}:{chat_id}:*" + cache_keys = await redis.keys(pattern) + + if len(cache_keys) <= self.MAX_CACHE_SIZE: + return 0 + + cache_info = [] + for key in cache_keys: + data = await redis.get(key) + if data: + try: + entry = CacheEntry.from_dict(json.loads(data)) + cache_info.append((key, entry.created_at)) + except: + pass + + cache_info.sort(key=lambda x: x[1]) + delete_count = len(cache_keys) - self.MAX_CACHE_SIZE + + deleted = 0 + for key, _ in cache_info[:delete_count]: + await redis.delete(key) + deleted += 1 + + if deleted > 0: + logger.info(f"[SemanticCache] 清理了 {deleted} 条旧缓存") + + return deleted + + except Exception as e: + logger.error(f"[SemanticCache] 清理失败: {e}") + return 0 + + async def clear(self, chat_id: str = None, redis_client=None) -> bool: + """ + 清除缓存 + + Args: + chat_id: 要清除的chat_id,如果为None则清除所有 + redis_client: Redis客户端(可选) + + Returns: + 是否清除成功 + """ + redis = redis_client or self._get_redis() + if redis is None: + return False + + try: + if chat_id: + pattern = f"{self.CACHE_PREFIX}:{chat_id}:*" + else: + pattern = f"{self.CACHE_PREFIX}:*" + + cache_keys = await redis.keys(pattern) + + if cache_keys: + await redis.delete(*cache_keys) + logger.info(f"[SemanticCache] 清除 {len(cache_keys)} 条缓存") + + return True + + except Exception as e: + logger.error(f"[SemanticCache] 清除失败: {e}") + return False + + async def get_stats(self, chat_id: str = None, redis_client=None) -> Dict[str, Any]: + """ + 获取缓存统计信息 + + Args: + chat_id: 要统计的chat_id,如果为None则统计所有 + redis_client: Redis客户端(可选) + + Returns: + 统计信息字典 + """ + redis = redis_client or self._get_redis() + if redis is None: + return {} + + try: + if chat_id: + pattern = f"{self.CACHE_PREFIX}:{chat_id}:*" + else: + pattern = f"{self.CACHE_PREFIX}:*" + + cache_keys = await redis.keys(pattern) + + stats = { + "total_entries": len(cache_keys), + "chat_id": chat_id or "all", + "config": { + "max_cache_size": self.MAX_CACHE_SIZE, + "similarity_threshold": self.SIMILARITY_THRESHOLD, + "cache_ttl_hours": self.CACHE_TTL_HOURS + } + } + + # 统计命中次数分布 + if cache_keys: + total_hits = 0 + for key in cache_keys[:100]: # 只采样前100个 + data = await redis.get(key) + if data: + try: + entry = CacheEntry.from_dict(json.loads(data)) + total_hits += entry.hit_count + except: + pass + + stats["total_hits"] = total_hits + stats["avg_hits_per_entry"] = total_hits / min(len(cache_keys), 100) + + return stats + + except Exception as e: + logger.error(f"[SemanticCache] 统计失败: {e}") + return {} + + +# 全局服务实例 +_semantic_cache_service: Optional[SemanticCacheService] = None + + +def get_semantic_cache_service() -> SemanticCacheService: + """获取语义缓存服务单例""" + global _semantic_cache_service + if _semantic_cache_service is None: + _semantic_cache_service = SemanticCacheService() + return _semantic_cache_service + + +# 便捷函数 +async def lookup_question(chat_id: str, question: str, redis_client=None) -> Optional[Tuple[str, float]]: + """查找缓存问题""" + try: + logger.info(f"[SemanticCache.lookup_question] 🔍 函数被调用 | chat_id={chat_id} | question={question} | redis_client={redis_client is not None}") + service = get_semantic_cache_service() + logger.info(f"[SemanticCache.lookup_question] 获取服务实例完成 | service={service is not None}") + result = await service.lookup(chat_id, question, redis_client) + logger.info(f"[SemanticCache.lookup_question] 返回 | result={result is not None}") + if result: + logger.info(f"[SemanticCache.lookup_question] 命中结果 | answer_length={len(result[0])} | similarity={result[1]:.2f}") + return result + except Exception as e: + logger.error(f"[SemanticCache.lookup_question] 异常: {e}") + return None + + +async def store_qa_pair(chat_id: str, question: str, answer: str, redis_client=None) -> bool: + """存储问答对""" + service = get_semantic_cache_service() + return await service.store(chat_id, question, answer, redis_client) + + +def get_question_hash(question: str) -> str: + """计算问题的hash值(公共函数,确保存储和查找一致)""" + normalized = question.lower().strip() + return hashlib.md5(normalized.encode('utf-8')).hexdigest()[:16] + + +async def clear_cache(chat_id: str = None, redis_client=None) -> bool: + """清除缓存""" + service = get_semantic_cache_service() + return await service.clear(chat_id, redis_client) diff --git a/ruoyi-fastapi-backend/utils/static_qa_service.py b/ruoyi-fastapi-backend/utils/static_qa_service.py new file mode 100644 index 0000000..8576318 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/static_qa_service.py @@ -0,0 +1,153 @@ +import json +import os +from typing import List, Dict, Optional, Tuple +from loguru import logger +from .match_service import get_match_service + +class StaticQAService: + def __init__(self, config_path: str = None): + if config_path is None: + backend_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + config_path = os.path.join(backend_path, 'config', 'static_qa.json') + + self.config_path = config_path + self.match_service = get_match_service() + self._qa_pairs: List[Dict] = [] + self._flattened_pairs: List[Tuple[str, Dict]] = [] + self._load_qa_data() + + def _load_qa_data(self): + try: + if not os.path.exists(self.config_path): + logger.warning(f'[StaticQA] 配置文件不存在: {self.config_path}') + self._qa_pairs = [] + self._flattened_pairs = [] + return + + with open(self.config_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + self._qa_pairs = data.get('qa_pairs', []) + self._build_flattened_pairs() + + logger.info(f'[StaticQA] 加载了 {len(self._qa_pairs)} 个问答对,{len(self._flattened_pairs)} 个问题变体') + + except Exception as e: + logger.error(f'[StaticQA] 加载配置文件失败: {e}') + self._qa_pairs = [] + self._flattened_pairs = [] + + def _build_flattened_pairs(self): + self._flattened_pairs = [] + + for qa_pair in self._qa_pairs: + main_question = qa_pair.get('question', '') + answer = qa_pair.get('answer', '') + category = qa_pair.get('category', '') + priority = qa_pair.get('priority', 0) + + if not main_question or not answer: + continue + + qa_data = { + 'question': main_question, + 'answer': answer, + 'category': category, + 'priority': priority, + 'source': 'static_qa' + } + + self._flattened_pairs.append((main_question, qa_data)) + + sub_questions = qa_pair.get('sub_questions', []) + for sub_q in sub_questions: + if sub_q: + sub_qa_data = qa_data.copy() + sub_qa_data['is_sub_question'] = True + self._flattened_pairs.append((sub_q, sub_qa_data)) + + variations = qa_pair.get('variations', []) + for var in variations: + if var: + var_qa_data = qa_data.copy() + var_qa_data['is_variation'] = True + self._flattened_pairs.append((var, var_qa_data)) + + def reload(self): + self._load_qa_data() + + def find_match(self, question: str, threshold: float = 0.70) -> Tuple[Optional[Dict], float]: + if not question or not self._flattened_pairs: + return None, 0.0 + + decomposed_questions = self.match_service.decompose_question(question) + + if len(decomposed_questions) > 1: + logger.info(f'[StaticQA] 问题分解为 {len(decomposed_questions)} 个子问题: {decomposed_questions}') + + best_match = None + best_similarity = 0.0 + + for sub_q in decomposed_questions: + match, similarity = self.match_service.find_best_match( + sub_q, + self._flattened_pairs, + threshold=threshold + ) + + if match and similarity > best_similarity: + best_match = match + best_similarity = similarity + + if best_match: + logger.info(f'[StaticQA] 找到匹配 (相似度={best_similarity:.2f}): {best_match.get("question")}') + + return best_match, best_similarity + + def find_all_matches(self, question: str, threshold: float = 0.70, max_results: int = 5) -> List[Tuple[Dict, float]]: + if not question or not self._flattened_pairs: + return [] + + decomposed_questions = self.match_service.decompose_question(question) + + all_matches = [] + + for sub_q in decomposed_questions: + for candidate_text, candidate_data in self._flattened_pairs: + similarity = self.match_service.calculate_similarity(sub_q, candidate_text) + + if similarity >= threshold: + all_matches.append((candidate_data, similarity)) + + all_matches.sort(key=lambda x: (x[1], x[0].get('priority', 0)), reverse=True) + + return all_matches[:max_results] + + def get_all_categories(self) -> List[str]: + categories = set() + for qa_pair in self._qa_pairs: + category = qa_pair.get('category', '') + if category: + categories.add(category) + return sorted(list(categories)) + + def get_qa_by_category(self, category: str) -> List[Dict]: + result = [] + for qa_pair in self._qa_pairs: + if qa_pair.get('category') == category: + result.append(qa_pair) + return result + + def get_qa_count(self) -> int: + return len(self._qa_pairs) + + def get_question_count(self) -> int: + return len(self._flattened_pairs) + +_static_qa_service_instance = None + +def get_static_qa_service(config_path: str = None) -> StaticQAService: + global _static_qa_service_instance + if _static_qa_service_instance is None: + _static_qa_service_instance = StaticQAService(config_path) + return _static_qa_service_instance diff --git a/ruoyi-fastapi-backend/utils/string_util.py b/ruoyi-fastapi-backend/utils/string_util.py new file mode 100644 index 0000000..7196bcf --- /dev/null +++ b/ruoyi-fastapi-backend/utils/string_util.py @@ -0,0 +1,170 @@ +from typing import Dict, List +from config.constant import CommonConstant + + +class StringUtil: + """ + 字符串工具类 + """ + + @classmethod + def is_blank(cls, string: str) -> bool: + """ + 校验字符串是否为''或全空格 + + :param string: 需要校验的字符串 + :return: 校验结果 + """ + if string is None: + return False + str_len = len(string) + if str_len == 0: + return True + else: + for i in range(str_len): + if string[i] != ' ': + return False + return True + + @classmethod + def is_empty(cls, string) -> bool: + """ + 校验字符串是否为''或None + + :param string: 需要校验的字符串 + :return: 校验结果 + """ + return string is None or len(string) == 0 + + @classmethod + def is_not_empty(cls, string: str) -> bool: + """ + 校验字符串是否不是''和None + + :param string: 需要校验的字符串 + :return: 校验结果 + """ + return not cls.is_empty(string) + + @classmethod + def is_http(cls, link: str): + """ + 判断是否为http(s)://开头 + + :param link: 链接 + :return: 是否为http(s)://开头 + """ + return link.startswith(CommonConstant.HTTP) or link.startswith(CommonConstant.HTTPS) + + @classmethod + def contains_ignore_case(cls, search_str: str, compare_str: str): + """ + 查找指定字符串是否包含指定字符串同时忽略大小写 + + :param search_str: 查找的字符串 + :param compare_str: 比对的字符串 + :return: 查找结果 + """ + if compare_str and search_str: + return compare_str.lower() in search_str.lower() + return False + + @classmethod + def contains_any_ignore_case(cls, search_str: str, compare_str_list: List[str]): + """ + 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时忽略大小写 + + :param search_str: 查找的字符串 + :param compare_str_list: 比对的字符串列表 + :return: 查找结果 + """ + if search_str and compare_str_list: + return any([cls.contains_ignore_case(search_str, compare_str) for compare_str in compare_str_list]) + return False + + @classmethod + def equals_ignore_case(cls, search_str: str, compare_str: str): + """ + 比较两个字符串是否相等同时忽略大小写 + + :param search_str: 查找的字符串 + :param compare_str: 比对的字符串 + :return: 比较结果 + """ + if search_str and compare_str: + return search_str.lower() == compare_str.lower() + return False + + @classmethod + def equals_any_ignore_case(cls, search_str: str, compare_str_list: List[str]): + """ + 比较指定字符串是否与指定字符串列表中的任意一个字符串相等同时忽略大小写 + + :param search_str: 查找的字符串 + :param compare_str_list: 比对的字符串列表 + :return: 比较结果 + """ + if search_str and compare_str_list: + return any([cls.equals_ignore_case(search_str, compare_str) for compare_str in compare_str_list]) + return False + + @classmethod + def startswith_case(cls, search_str: str, compare_str: str): + """ + 查找指定字符串是否以指定字符串开头 + + :param search_str: 查找的字符串 + :param compare_str: 比对的字符串 + :return: 查找结果 + """ + if compare_str and search_str: + return search_str.startswith(compare_str) + return False + + @classmethod + def startswith_any_case(cls, search_str: str, compare_str_list: List[str]): + """ + 查找指定字符串是否以指定字符串列表中的任意一个字符串开头 + + :param search_str: 查找的字符串 + :param compare_str_list: 比对的字符串列表 + :return: 查找结果 + """ + if search_str and compare_str_list: + return any([cls.startswith_case(search_str, compare_str) for compare_str in compare_str_list]) + return False + + @classmethod + def convert_to_camel_case(cls, name: str) -> str: + """ + 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串 + + :param name: 转换前的下划线大写方式命名的字符串 + :return: 转换后的驼峰式命名的字符串 + """ + if not name: + return '' + if '_' not in name: + return name[0].upper() + name[1:] + parts = name.split('_') + result = [] + for part in parts: + if not part: + continue + result.append(part[0].upper() + part[1:].lower()) + return ''.join(result) + + @classmethod + def get_mapping_value_by_key_ignore_case(cls, mapping: Dict[str, str], key: str) -> str: + """ + 根据忽略大小写的键获取字典中的对应的值 + + param mapping: 字典 + param key: 字典的键 + :return: 字典键对应的值 + """ + for k, v in mapping.items(): + if key.lower() == k.lower(): + return v + + return '' diff --git a/ruoyi-fastapi-backend/utils/template_util.py b/ruoyi-fastapi-backend/utils/template_util.py new file mode 100644 index 0000000..6765bd0 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/template_util.py @@ -0,0 +1,470 @@ +import json +import os +from datetime import datetime +from jinja2 import Environment, FileSystemLoader +from typing import Dict, List, Set +from config.constant import GenConstant +from config.env import DataBaseConfig +from exceptions.exception import ServiceWarning +from module_generator.entity.vo.gen_vo import GenTableModel, GenTableColumnModel +from utils.common_util import CamelCaseUtil, SnakeCaseUtil +from utils.string_util import StringUtil + + +class TemplateInitializer: + """ + 模板引擎初始化类 + """ + + @classmethod + def init_jinja2(cls): + """ + 初始化 Jinja2 模板引擎 + + :return: Jinja2 环境对象 + """ + try: + template_dir = os.path.join(os.getcwd(), 'module_generator', 'templates') + env = Environment( + loader=FileSystemLoader(template_dir), + keep_trailing_newline=True, + trim_blocks=True, + lstrip_blocks=True, + ) + env.filters.update( + { + 'camel_to_snake': SnakeCaseUtil.camel_to_snake, + 'snake_to_camel': CamelCaseUtil.snake_to_camel, + 'get_sqlalchemy_type': TemplateUtils.get_sqlalchemy_type, + } + ) + return env + except Exception as e: + raise RuntimeError(f'初始化Jinja2模板引擎失败: {e}') + + +class TemplateUtils: + """ + 模板工具类 + """ + + # 项目路径 + FRONTEND_PROJECT_PATH = 'frontend' + BACKEND_PROJECT_PATH = 'backend' + DEFAULT_PARENT_MENU_ID = '3' + + @classmethod + def prepare_context(cls, gen_table: GenTableModel): + """ + 准备模板变量 + + :param gen_table: 生成表的配置信息 + :return: 模板上下文字典 + """ + if not gen_table.options: + raise ServiceWarning(message='请先完善生成配置信息') + class_name = gen_table.class_name + module_name = gen_table.module_name + business_name = gen_table.business_name + package_name = gen_table.package_name + tpl_category = gen_table.tpl_category + function_name = gen_table.function_name + + context = { + 'tplCategory': tpl_category, + 'tableName': gen_table.table_name, + 'functionName': function_name if StringUtil.is_not_empty(function_name) else '【请填写功能名称】', + 'ClassName': class_name, + 'className': class_name.lower(), + 'moduleName': module_name, + 'BusinessName': business_name.capitalize(), + 'businessName': business_name, + 'basePackage': cls.get_package_prefix(package_name), + 'packageName': package_name, + 'author': gen_table.function_author, + 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'pkColumn': gen_table.pk_column, + 'doImportList': cls.get_do_import_list(gen_table), + 'voImportList': cls.get_vo_import_list(gen_table), + 'permissionPrefix': cls.get_permission_prefix(module_name, business_name), + 'columns': gen_table.columns, + 'table': gen_table, + 'dicts': cls.get_dicts(gen_table), + 'dbType': DataBaseConfig.db_type, + 'column_not_add_show': GenConstant.COLUMNNAME_NOT_ADD_SHOW, + 'column_not_edit_show': GenConstant.COLUMNNAME_NOT_EDIT_SHOW, + } + + # 设置菜单、树形结构、子表的上下文 + cls.set_menu_context(context, gen_table) + if tpl_category == GenConstant.TPL_TREE: + cls.set_tree_context(context, gen_table) + if tpl_category == GenConstant.TPL_SUB: + cls.set_sub_context(context, gen_table) + + return context + + @classmethod + def set_menu_context(cls, context: Dict, gen_table: GenTableModel): + """ + 设置菜单上下文 + + :param context: 模板上下文字典 + :param gen_table: 生成表的配置信息 + :return: 新的模板上下文字典 + """ + options = gen_table.options + params_obj = json.loads(options) + context['parentMenuId'] = cls.get_parent_menu_id(params_obj) + + @classmethod + def set_tree_context(cls, context: Dict, gen_table: GenTableModel): + """ + 设置树形结构上下文 + + :param context: 模板上下文字典 + :param gen_table: 生成表的配置信息 + :return: 新的模板上下文字典 + """ + options = gen_table.options + params_obj = json.loads(options) + context['treeCode'] = cls.get_tree_code(params_obj) + context['treeParentCode'] = cls.get_tree_parent_code(params_obj) + context['treeName'] = cls.get_tree_name(params_obj) + context['expandColumn'] = cls.get_expand_column(gen_table) + + @classmethod + def set_sub_context(cls, context: Dict, gen_table: GenTableModel): + """ + 设置子表上下文 + + :param context: 模板上下文字典 + :param gen_table: 生成表的配置信息 + :return: 新的模板上下文字典 + """ + sub_table = gen_table.sub_table + sub_table_name = gen_table.sub_table_name + sub_table_fk_name = gen_table.sub_table_fk_name + sub_class_name = sub_table.class_name + sub_table_fk_class_name = StringUtil.convert_to_camel_case(sub_table_fk_name) + context['subTable'] = sub_table + context['subTableName'] = sub_table_name + context['subTableFkName'] = sub_table_fk_name + context['subTableFkClassName'] = sub_table_fk_class_name + context['subTableFkclassName'] = sub_table_fk_class_name.lower() + context['subClassName'] = sub_class_name + context['subclassName'] = sub_class_name.lower() + + @classmethod + def get_template_list(cls, tpl_category: str, tpl_web_type: str): + """ + 获取模板列表 + + :param tpl_category: 生成模板类型 + :param tpl_web_type: 前端类型 + :return: 模板列表 + """ + use_web_type = 'vue' + if tpl_web_type == 'element-plus': + use_web_type = 'vue/v3' + templates = [ + 'python/controller.py.jinja2', + 'python/dao.py.jinja2', + 'python/do.py.jinja2', + 'python/service.py.jinja2', + 'python/vo.py.jinja2', + 'sql/sql.jinja2', + 'js/api.js.jinja2', + ] + if tpl_category == GenConstant.TPL_CRUD: + templates.append(f'{use_web_type}/index.vue.jinja2') + elif tpl_category == GenConstant.TPL_TREE: + templates.append(f'{use_web_type}/index-tree.vue.jinja2') + elif tpl_category == GenConstant.TPL_SUB: + templates.append(f'{use_web_type}/index.vue.jinja2') + # templates.append('python/sub-domain.python.jinja2') + return templates + + @classmethod + def get_file_name(cls, template: List[str], gen_table: GenTableModel): + """ + 根据模板生成文件名 + + :param template: 模板列表 + :param gen_table: 生成表的配置信息 + :return: 模板生成文件名 + """ + package_name = gen_table.package_name + module_name = gen_table.module_name + business_name = gen_table.business_name + + vue_path = cls.FRONTEND_PROJECT_PATH + python_path = f'{cls.BACKEND_PROJECT_PATH}/{package_name.replace(".", "/")}' + + if 'controller.py.jinja2' in template: + return f'{python_path}/controller/{business_name}_controller.py' + elif 'dao.py.jinja2' in template: + return f'{python_path}/dao/{business_name}_dao.py' + elif 'do.py.jinja2' in template: + return f'{python_path}/entity/do/{business_name}_do.py' + elif 'service.py.jinja2' in template: + return f'{python_path}/service/{business_name}_service.py' + elif 'vo.py.jinja2' in template: + return f'{python_path}/entity/vo/{business_name}_vo.py' + elif 'sql.jinja2' in template: + return f'{cls.BACKEND_PROJECT_PATH}/sql/{business_name}_menu.sql' + elif 'api.js.jinja2' in template: + return f'{vue_path}/api/{module_name}/{business_name}.js' + elif 'index.vue.jinja2' in template or 'index-tree.vue.jinja2' in template: + return f'{vue_path}/views/{module_name}/{business_name}/index.vue' + return '' + + @classmethod + def get_package_prefix(cls, package_name: str): + """ + 获取包前缀 + + :param package_name: 包名 + :return: 包前缀 + """ + return package_name[: package_name.rfind('.')] + + @classmethod + def get_vo_import_list(cls, gen_table: GenTableModel): + """ + 获取vo模板导入包列表 + + :param gen_table: 生成表的配置信息 + :return: 导入包列表 + """ + columns = gen_table.columns or [] + import_list = set() + for column in columns: + if column.python_type in GenConstant.TYPE_DATE: + import_list.add(f'from datetime import {column.python_type}') + elif column.python_type == GenConstant.TYPE_DECIMAL: + import_list.add('from decimal import Decimal') + if gen_table.sub: + sub_columns = gen_table.sub_table.columns or [] + for sub_column in sub_columns: + if sub_column.python_type in GenConstant.TYPE_DATE: + import_list.add(f'from datetime import {sub_column.python_type}') + elif sub_column.python_type == GenConstant.TYPE_DECIMAL: + import_list.add('from decimal import Decimal') + return cls.merge_same_imports(list(import_list), 'from datetime import') + + @classmethod + def get_do_import_list(cls, gen_table: GenTableModel): + """ + 获取do模板导入包列表 + + :param gen_table: 生成表的配置信息 + :return: 导入包列表 + """ + columns = gen_table.columns or [] + import_list = set() + import_list.add('from sqlalchemy import Column') + for column in columns: + data_type = cls.get_db_type(column.column_type) + if data_type in GenConstant.COLUMNTYPE_GEOMETRY: + import_list.add('from geoalchemy2 import Geometry') + import_list.add( + f'from sqlalchemy import {StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY_TYPE_MAPPING, data_type)}' + ) + if gen_table.sub: + import_list.add('from sqlalchemy import ForeignKey') + sub_columns = gen_table.sub_table.columns or [] + for sub_column in sub_columns: + data_type = cls.get_db_type(sub_column.column_type) + import_list.add( + f'from sqlalchemy import {StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY_TYPE_MAPPING, data_type)}' + ) + return cls.merge_same_imports(list(import_list), 'from sqlalchemy import') + + @classmethod + def get_db_type(cls, column_type: str) -> str: + """ + 获取数据库类型字段 + + param column_type: 字段类型 + :return: 数据库类型 + """ + if '(' in column_type: + return column_type.split('(')[0] + return column_type + + @classmethod + def merge_same_imports(cls, imports: List[str], import_start: str) -> List[str]: + """ + 合并相同的导入语句 + + :param imports: 导入语句列表 + :param import_start: 导入语句的起始字符串 + :return: 合并后的导入语句列表 + """ + merged_imports = [] + _imports = [] + for import_stmt in imports: + if import_stmt.startswith(import_start): + imported_items = import_stmt.split('import')[1].strip() + _imports.extend(imported_items.split(', ')) + else: + merged_imports.append(import_stmt) + + if _imports: + merged_datetime_import = f'{import_start} {", ".join(_imports)}' + merged_imports.append(merged_datetime_import) + + return merged_imports + + @classmethod + def get_dicts(cls, gen_table: GenTableModel): + """ + 获取字典列表 + + :param gen_table: 生成表的配置信息 + :return: 字典列表 + """ + columns = gen_table.columns or [] + dicts = set() + cls.add_dicts(dicts, columns) + if gen_table.sub_table is not None: + cls.add_dicts(dicts, gen_table.sub_table.columns) + return ', '.join(dicts) + + @classmethod + def add_dicts(cls, dicts: Set[str], columns: List[GenTableColumnModel]): + """ + 添加字典列表 + + :param dicts: 字典列表 + :param columns: 字段列表 + :return: 新的字典列表 + """ + for column in columns: + if ( + not column.super_column + and StringUtil.is_not_empty(column.dict_type) + and StringUtil.equals_any_ignore_case( + column.html_type, [GenConstant.HTML_SELECT, GenConstant.HTML_RADIO, GenConstant.HTML_CHECKBOX] + ) + ): + dicts.add(f"'{column.dict_type}'") + + @classmethod + def get_permission_prefix(cls, module_name: str, business_name: str): + """ + 获取权限前缀 + + :param module_name: 模块名 + :param business_name: 业务名 + :return: 权限前缀 + """ + return f'{module_name}:{business_name}' + + @classmethod + def get_parent_menu_id(cls, params_obj: Dict): + """ + 获取上级菜单ID + + :param params_obj: 菜单参数字典 + :return: 上级菜单ID + """ + if params_obj and params_obj.get(GenConstant.PARENT_MENU_ID): + return params_obj.get(GenConstant.PARENT_MENU_ID) + return cls.DEFAULT_PARENT_MENU_ID + + @classmethod + def get_tree_code(cls, params_obj: Dict): + """ + 获取树编码 + + :param params_obj: 菜单参数字典 + :return: 树编码 + """ + if GenConstant.TREE_CODE in params_obj: + return cls.to_camel_case(params_obj.get(GenConstant.TREE_CODE)) + return '' + + @classmethod + def get_tree_parent_code(cls, params_obj: Dict): + """ + 获取树父编码 + + :param params_obj: 菜单参数字典 + :return: 树父编码 + """ + if GenConstant.TREE_PARENT_CODE in params_obj: + return cls.to_camel_case(params_obj.get(GenConstant.TREE_PARENT_CODE)) + return '' + + @classmethod + def get_tree_name(cls, params_obj: Dict): + """ + 获取树名称 + + :param params_obj: 菜单参数字典 + :return: 树名称 + """ + if GenConstant.TREE_NAME in params_obj: + return cls.to_camel_case(params_obj.get(GenConstant.TREE_NAME)) + return '' + + @classmethod + def get_expand_column(cls, gen_table: GenTableModel): + """ + 获取展开列 + + :param gen_table: 生成表的配置信息 + :return: 展开列 + """ + options = gen_table.options + params_obj = json.loads(options) + tree_name = params_obj.get(GenConstant.TREE_NAME) + num = 0 + for column in gen_table.columns or []: + if column.list: + num += 1 + if column.column_name == tree_name: + break + return num + + @classmethod + def to_camel_case(cls, text: str) -> str: + """ + 将字符串转换为驼峰命名 + + :param text: 待转换的字符串 + :return: 转换后的驼峰命名字符串 + """ + parts = text.split('_') + return parts[0] + ''.join(word.capitalize() for word in parts[1:]) + + @classmethod + def get_sqlalchemy_type(cls, column_type: str): + """ + 获取SQLAlchemy类型 + + :param column_type: 列类型 + :return: SQLAlchemy类型 + """ + if '(' in column_type: + column_type_list = column_type.split('(') + if column_type_list[0] in GenConstant.COLUMNTYPE_STR: + sqlalchemy_type = ( + StringUtil.get_mapping_value_by_key_ignore_case( + GenConstant.DB_TO_SQLALCHEMY_TYPE_MAPPING, column_type_list[0] + ) + + '(' + + column_type_list[1] + ) + else: + sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case( + GenConstant.DB_TO_SQLALCHEMY_TYPE_MAPPING, column_type_list[0] + ) + else: + sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case( + GenConstant.DB_TO_SQLALCHEMY_TYPE_MAPPING, column_type + ) + + return sqlalchemy_type diff --git a/ruoyi-fastapi-backend/utils/time_format_util.py b/ruoyi-fastapi-backend/utils/time_format_util.py new file mode 100644 index 0000000..bfb0481 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/time_format_util.py @@ -0,0 +1,141 @@ +from copy import deepcopy +from datetime import datetime +from dateutil.parser import parse +from typing import Dict, List, Union + + +def object_format_datetime(obj): + """ + :param obj: 输入一个对象 + :return:对目标对象所有datetime类型的属性格式化 + """ + for attr in dir(obj): + value = getattr(obj, attr) + if isinstance(value, datetime): + setattr(obj, attr, value.strftime('%Y-%m-%d %H:%M:%S')) + return obj + + +def list_format_datetime(lst): + """ + :param lst: 输入一个嵌套对象的列表 + :return: 对目标列表中所有对象的datetime类型的属性格式化 + """ + for obj in lst: + for attr in dir(obj): + value = getattr(obj, attr) + if isinstance(value, datetime): + setattr(obj, attr, value.strftime('%Y-%m-%d %H:%M:%S')) + return lst + + +def format_datetime_dict_list(dicts): + """ + 递归遍历嵌套字典,并将 datetime 值转换为字符串格式 + + :param dicts: 输入一个嵌套字典的列表 + :return: 对目标列表中所有字典的datetime类型的属性格式化 + """ + result = [] + + for item in dicts: + new_item = {} + for k, v in item.items(): + if isinstance(v, dict): + # 递归遍历子字典 + new_item[k] = format_datetime_dict_list([v])[0] + elif isinstance(v, datetime): + # 如果值是 datetime 类型,则格式化为字符串 + new_item[k] = v.strftime('%Y-%m-%d %H:%M:%S') + else: + # 否则保留原始值 + new_item[k] = v + result.append(new_item) + + return result + + +class TimeFormatUtil: + """ + 时间格式化工具类 + """ + + @classmethod + def format_time(cls, time_info: Union[str, datetime], format: str = '%Y-%m-%d %H:%M:%S'): + """ + 格式化时间字符串或datetime对象为指定格式 + + :param time_info: 时间字符串或datetime对象 + :param format: 格式化格式,默认为'%Y-%m-%d %H:%M:%S' + :return: 格式化后的时间字符串 + """ + if isinstance(time_info, datetime): + format_date = time_info.strftime(format) + else: + try: + date = parse(time_info) + format_date = date.strftime(format) + except Exception: + format_date = time_info + + return format_date + + @classmethod + def parse_date(cls, time_str: str): + """ + 解析时间字符串提取日期部分 + + :param time_str: 时间字符串 + :return: 日期部分 + """ + try: + dt = parse(time_str) + return dt.date() + except Exception: + return time_str + + @classmethod + def format_time_dict(cls, time_dict: Dict, format: str = '%Y-%m-%d %H:%M:%S'): + """ + 格式化时间字典 + + :param time_dict: 时间字典 + :param format: 格式化格式,默认为'%Y-%m-%d %H:%M:%S' + :return: 格式化后的时间字典 + """ + copy_time_dict = deepcopy(time_dict) + for k, v in copy_time_dict.items(): + if isinstance(v, (str, datetime)): + copy_time_dict[k] = cls.format_time(v, format) + elif isinstance(v, dict): + copy_time_dict[k] = cls.format_time_dict(v, format) + elif isinstance(v, list): + copy_time_dict[k] = cls.format_time_list(v, format) + else: + copy_time_dict[k] = v + + return copy_time_dict + + @classmethod + def format_time_list(cls, time_list: List, format: str = '%Y-%m-%d %H:%M:%S'): + """ + 格式化时间列表 + + :param time_list: 时间列表 + :param format: 格式化格式,默认为'%Y-%m-%d %H:%M:%S' + :return: 格式化后的时间列表 + """ + format_time_list = [] + for item in time_list: + if isinstance(item, (str, datetime)): + format_item = cls.format_time(item, format) + elif isinstance(item, dict): + format_item = cls.format_time_dict(item, format) + elif isinstance(item, list): + format_item = cls.format_time_list(item, format) + else: + format_item = item + + format_time_list.append(format_item) + + return format_time_list diff --git a/ruoyi-fastapi-backend/utils/unitree_util.py b/ruoyi-fastapi-backend/utils/unitree_util.py new file mode 100644 index 0000000..151e81e --- /dev/null +++ b/ruoyi-fastapi-backend/utils/unitree_util.py @@ -0,0 +1 @@ +# from unitree_sdk2py.core.channel import ChannelSubscriber, ChannelFactoryInitialize \ No newline at end of file diff --git a/ruoyi-fastapi-backend/utils/upload_util.py b/ruoyi-fastapi-backend/utils/upload_util.py new file mode 100644 index 0000000..726789e --- /dev/null +++ b/ruoyi-fastapi-backend/utils/upload_util.py @@ -0,0 +1,105 @@ +import os +import random +from datetime import datetime +from fastapi import UploadFile +from config.env import UploadConfig + + +class UploadUtil: + """ + 上传工具类 + """ + + @classmethod + def generate_random_number(cls): + """ + 生成3位数字构成的字符串 + + :return: 3位数字构成的字符串 + """ + random_number = random.randint(1, 999) + + return f'{random_number:03}' + + @classmethod + def check_file_exists(cls, filepath: str): + """ + 检查文件是否存在 + + :param filepath: 文件路径 + :return: 校验结果 + """ + return os.path.exists(filepath) + + @classmethod + def check_file_extension(cls, file: UploadFile): + """ + 检查文件后缀是否合法 + + :param file: 文件对象 + :return: 校验结果 + """ + file_extension = file.filename.rsplit('.', 1)[-1] + if file_extension in UploadConfig.DEFAULT_ALLOWED_EXTENSION: + return True + return False + + @classmethod + def check_file_timestamp(cls, filename: str): + """ + 校验文件时间戳是否合法 + + :param filename: 文件名称 + :return: 校验结果 + """ + timestamp = filename.rsplit('.', 1)[0].split('_')[-1].split(UploadConfig.UPLOAD_MACHINE)[0] + try: + datetime.strptime(timestamp, '%Y%m%d%H%M%S') + return True + except ValueError: + return False + + @classmethod + def check_file_machine(cls, filename: str): + """ + 校验文件机器码是否合法 + + :param filename: 文件名称 + :return: 校验结果 + """ + if filename.rsplit('.', 1)[0][-4] == UploadConfig.UPLOAD_MACHINE: + return True + return False + + @classmethod + def check_file_random_code(cls, filename: str): + """ + 校验文件随机码是否合法 + + :param filename: 文件名称 + :return: 校验结果 + """ + valid_code_list = [f'{i:03}' for i in range(1, 999)] + if filename.rsplit('.', 1)[0][-3:] in valid_code_list: + return True + return False + + @classmethod + def generate_file(cls, filepath: str): + """ + 根据文件生成二进制数据 + + :param filepath: 文件路径 + :yield: 二进制数据 + """ + with open(filepath, 'rb') as response_file: + yield from response_file + + @classmethod + def delete_file(cls, filepath: str): + """ + 根据文件路径删除对应文件 + + :param filepath: 文件路径 + """ + os.remove(filepath)