diff --git a/application/services/article.py b/application/services/article.py index 07c8269..826c53f 100644 --- a/application/services/article.py +++ b/application/services/article.py @@ -1,495 +1,8 @@ """ -文章管理服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了文章管理相关的所有业务逻辑。 -包括文章的分页查询、详情查询、添加、更新、删除、状态设置、点击率统计等功能。 - -命名规范: -- 函数参数使用驼峰命名(如 articleId, pageNo) -- 表单字段使用下划线命名(如 title, category_id)- 与数据库字段保持一致 -- 返回数据使用驼峰命名(如 createUser, updateTime)- 与前端规范保持一致 -- 私有函数以下划线开头(如 _apply_filters, _build_page_data) - -功能模块: -1. 分页查询:支持按标题模糊搜索、分类筛选、状态筛选,按sort字段排序 -2. 详情查询:根据ID获取单个文章的详细信息 -3. 数据维护:文章的添加、更新、删除(支持批量) -4. 状态管理:单独更新文章的启用/下架状态 -5. 点击统计:增加文章点击量 -6. 文章列表:获取所有文章列表(用于下拉选择等场景) +服务已迁移至 src.modules.cms.article.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging -from datetime import datetime, timezone - -from flask import request -from sqlalchemy import and_ - -from application.constants.common import PAGE_SIZE -from application.constants.dict import ARTICLE_STATUS_LIST -from application.forms.article import ArticleForm, ArticleStatusForm -from application.models.article import Article -from config import FLASK_FILE_URL -from extensions import db -from utils import R, regular -from utils.case_converter import convert_camel_to_snake -from utils.model_utils import models_to_list, model_to_dict -from utils.request import parse_request_body, parse_id_list, like_contains -from utils.rich_text import get_file_url, save_file, save_content -from utils.security import get_username - -# 获取日志记录器 -logger = logging.getLogger(__name__) - - -# ============================================================ -# 文章分页查询 -# ============================================================ - -def get_article_page(): - """ - 查询文章分页数据 - - Args: - request: Flask request对象,包含查询参数 - - pageNo: 当前页码(可选,默认为1) - - pageSize: 每页记录数(可选,默认为PAGE_SIZE) - - title: 文章标题(可选,模糊查询) - - category_id: 分类ID(可选,精确查询) - - status: 文章状态(可选,精确查询) - - startTime: 开始时间(可选) - - endTime: 结束时间(可选) - - Returns: - R: 包含分页数据的JSON响应,格式为: - { - "records": 文章列表, - "total": 总记录数, - "size": 每页大小, - "current": 当前页码, - "pages": 总页数 - } - """ - try: - # 获取分页参数 - page_no = int(request.args.get('pageNo', 1)) - page_size = int(request.args.get('pageSize', PAGE_SIZE)) - - # 应用筛选条件 - query = _apply_filters() - - # 记录总数 - total = query.count() - # 计算总页数 - pages = (total + page_size - 1) // page_size if total > 0 else 0 - - # 分页查询 - article_list = query.order_by(Article.sort.asc(), Article.create_time.desc()) \ - .limit(page_size) \ - .offset((page_no - 1) * page_size) \ - .all() - - # 构建返回数据 - page_data = _build_page_data(article_list, total, page_size, page_no, pages) - return R.ok(data=page_data) - - except Exception as e: - logger.error(f"查询文章分页数据异常: {str(e)}") - return R.failed(msg="查询失败,请稍后重试") - - -def _apply_filters(): - """ - 应用查询筛选条件 - - 根据请求参数动态构建查询条件: - - 自动过滤已删除记录(is_delete == 0) - - 支持文章标题模糊查询 - - 支持分类ID精确查询 - - 支持文章状态精确查询 - - 支持时间范围查询 - - Returns: - Query: 应用筛选后的SQLAlchemy查询对象 - """ - # 基础查询:过滤已删除记录 - query = Article.query.filter(Article.is_delete == 0) - - # 文章标题模糊筛选 - title = request.args.get('title') - if title: - query = query.filter(Article.title.like(like_contains(title), escape='\\')) - - # 分类ID精确筛选 - category_id = request.args.get('categoryId') - if category_id: - query = query.filter(Article.category_id == category_id) - - # 文章状态精确筛选 - status = request.args.get('status') - if status: - query = query.filter(Article.status == status) - - # 时间范围筛选 - start_time = request.args.get('startTime') - end_time = request.args.get('endTime') - - if start_time: - try: - start_date = datetime.strptime(start_time, '%Y-%m-%d %H:%M:%S') - query = query.filter(Article.create_time >= start_date) - except ValueError: - pass - - if end_time: - try: - end_date = datetime.strptime(end_time, '%Y-%m-%d %H:%M:%S') - query = query.filter(Article.create_time <= end_date) - except ValueError: - pass - - return query - - -def _build_page_data(article_list, total, page_size, page_no, pages): - """ - 构建分页返回数据 - - Args: - article_list: 文章对象列表 - total: 总记录数 - page_size: 每页大小 - page_no: 当前页码 - pages: 总页数 - - Returns: - dict: 分页数据,包含 records、total、size、current、pages - """ - # 将模型对象列表转换为字典列表 - records = models_to_list(article_list) - - # 为每条记录添加状态文本映射 - for record in records: - # 文章封面 - record['cover'] = get_file_url(record.get('cover')) - # 文章状态 - record['statusText'] = ARTICLE_STATUS_LIST.get(record.get('status')) - - return { - 'records': records, - 'total': total, - 'size': page_size, - 'current': page_no, - 'pages': pages - } - - -# ============================================================ -# 文章详情查询 -# ============================================================ - -def get_article_detail(article_id): - """ - 根据ID查询文章详情(返回字典格式) - - 此方法调用 get_info 获取模型对象,然后转换为字典格式返回。 - 适用于前端接口调用。 - - Args: - article_id: 文章ID - - Returns: - dict/None: 文章详情字典,不存在时返回 None - """ - try: - # 复用 get_info 方法查询文章 - article = get_info(article_id) - if not article: - return None - # 转换为字典格式 - record = model_to_dict(article) - - # 图片封面 - record['cover'] = get_file_url(record['cover']) - - # 处理图集(逗号分隔转URL列表) - images = record.get('images') - if images: - # 将逗号分隔的路径转换为URL列表 - image_paths = [path.strip() for path in images.split(',') if path.strip()] - record['imagesList'] = [get_file_url(path) for path in image_paths] - else: - record['imagesList'] = [] - - # 富文本处理 - content = record.get('content') - if content: - record['content'] = content.replace("[IMG_URL]", FLASK_FILE_URL) - - # 文章状态 - record['statusText'] = ARTICLE_STATUS_LIST.get(record.get('status')) - - return record - - except Exception as e: - logger.error(f"查询文章详情异常,ID: {article_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 文章信息查询(根据ID)- 核心查询方法 -# ============================================================ - -def get_info(article_id: int): - """ - 根据ID查询文章信息(返回模型对象) - - 这是一个通用的文章查询方法,返回文章模型对象而非字典。 - 与 get_article_detail 的区别: - - get_article_detail: 返回字典格式,用于前端接口 - - get_info: 返回模型对象,用于内部服务调用 - - 此方法被以下方法复用: - - get_article_detail: 获取详情字典 - - update_article: 验证文章是否存在 - - update_article_status: 验证文章是否存在 - - increment_click: 增加点击量 - - Args: - article_id: 文章ID - - Returns: - Article/None: 文章模型对象,不存在或已删除时返回 None - - Example: - article = get_info(article_id) - if article: - print(article.title) - """ - # 参数有效性验证 - if not article_id or article_id <= 0: - return None - - try: - # 查询未删除的文章 - article = Article.query.filter( - and_(Article.id == article_id, Article.is_delete == 0) - ).first() - return article - except Exception as e: - logger.error(f"查询文章信息异常,ID: {article_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 文章添加 -# ============================================================ - -def add_article(): - """ - 添加文章 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证 - 3. 处理images字段(JSON序列化) - 4. 创建文章对象并保存到数据库 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = ArticleForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - # 文章封面 - cover = form.data.get('cover') - if cover: - cover = save_file(cover, "article") - form.cover.data = cover - - # 文章图集(逗号分隔的图片路径) - images = form.data.get('images') - if images: - image_paths = [img.strip() for img in images.split(',') if img.strip()] - processed_paths = [] - for img in image_paths: - image = save_file(img, "article") - processed_paths.append(image) - # 处理后的地址逗号分隔 - form.images.data = ','.join(processed_paths) - - # 文章内容 - title = form.data.get('title', '') - content = form.data.get('content', '') - if content: - form.content.data = save_content(content, title, "article") - - # 创建文章 - article = Article(**form.data) - article.create_user = get_username() - article.save() - - return R.ok(msg="添加成功") - - except Exception as e: - logger.error(f"添加文章异常: {str(e)}") - return R.failed(msg="添加文章失败,请稍后重试") - - -# ============================================================ -# 文章更新 -# ============================================================ - -def update_article(): - """ - 更新文章 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证 - 3. 验证文章是否存在(复用 get_info) - 4. 处理images字段 - 5. 更新文章信息 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = ArticleForm(data=json_data) - if not form.validate(): - return R.failed(msg=regular.get_err(form)) - - # 获取文章ID - article_id = form.data.get('id') - if not article_id or int(article_id) <= 0: - return R.failed("文章ID无效") - article_id = int(article_id) - - # 复用 get_info 方法验证文章是否存在 - article = get_info(article_id) - if not article: - return R.failed("文章不存在") - - # 文章封面 - cover = form.data.get('cover') - if cover: - cover = save_file(cover, "article") - form.cover.data = cover - - # 文章图集(逗号分隔的图片路径) - images = form.data.get('images') - if images: - image_paths = [img.strip() for img in images.split(',') if img.strip()] - processed_paths = [] - for img in image_paths: - image = save_file(img, "article") - processed_paths.append(image) - # 处理后的地址逗号分隔 - form.images.data = ','.join(processed_paths) - - # 文章内容 - title = form.data.get('title', '') - content = form.data.get('content', '') - if content: - form.content.data = save_content(content, title, "article") - - # 更新文章信息(移除ID,避免更新主键) - form_data = convert_camel_to_snake(form.data.copy()) - form_data.pop('id', None) - - form_data['update_time'] = datetime.now(tz=timezone.utc) - form_data['update_user'] = get_username() - - result = Article.query.filter_by(id=article_id).update(form_data) - - if not result: - return R.failed("更新失败") - - return R.ok(msg="更新成功") - - except Exception as e: - logger.error(f"更新文章异常: {str(e)}") - return R.failed(msg="更新文章失败,请稍后重试") - - -# ============================================================ -# 文章删除 -# ============================================================ - -def delete_articles(article_ids): - """ - 删除文章(批量删除) - - 删除 - 支持逗号分隔的多个ID批量删除。 - - Args: - article_ids: 文章ID字符串,多个ID用逗号分隔,如 "1,2,3" - - Returns: - R: 操作结果 - """ - return Article.batch_delete_r(article_ids) - - -# ============================================================ -# 文章状态设置 -# ============================================================ - -def update_article_status(): - """ - 设置文章状态 - - 单独更新文章的启用/下架状态,不涉及其他字段。 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = ArticleStatusForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - article_id = int(form.data['id']) - status = int(form.data['status']) - - # 复用 get_info 方法验证文章是否存在 - article = get_info(article_id) - if not article: - return R.failed("文章不存在") - - # 更新状态 - update_data = {"status": status, "update_time": datetime.now(tz=timezone.utc), "update_user": get_username()} - result = Article.query.filter_by(id=article_id).update(update_data) - - if not result: - return R.failed("设置失败") - - return R.ok(msg="设置成功") - - except Exception as e: - logger.error(f"设置文章状态异常: {str(e)}") - return R.failed(msg="设置文章状态失败,请稍后重试") +from src.modules.cms.article.service import * # noqa: F401,F403 diff --git a/application/services/category.py b/application/services/category.py index a650433..27103dd 100644 --- a/application/services/category.py +++ b/application/services/category.py @@ -1,341 +1,8 @@ """ -分类管理服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了分类管理相关的所有业务逻辑。 -包括分类的列表查询、详情查询、添加、更新、删除、树形数据获取等功能。 - -命名规范: -- 函数参数使用驼峰命名(如 categoryId, pageNo) -- 表单字段使用下划线命名(如 category_name, parent_id)- 与数据库字段保持一致 -- 返回数据使用驼峰命名(如 createUser, updateTime)- 与前端规范保持一致 -- 私有函数以下划线开头(如 _apply_filters) - -功能模块: -1. 列表查询:支持按名称模糊搜索,按sort字段排序 -2. 详情查询:根据ID获取单个分类的详细信息 -3. 数据维护:分类的添加、更新、删除(支持批量) -4. 树形数据:获取所有分类数据(用于树形展示、下拉选择等场景) -5. 获取子分类:根据父级ID获取子分类列表 -6. 获取分类树:构建完整的分类树结构 +服务已迁移至 src.modules.cms.category.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging - -from datetime import datetime, timezone -from flask import request -from sqlalchemy import and_ - -from application.forms.category import CategoryForm -from application.models.category import Category -from extensions import db -from utils import R, regular -from utils.case_converter import convert_camel_to_snake -from utils.request import parse_request_body, parse_id_list, like_contains -from utils.model_utils import models_to_list, model_to_dict -from utils.security import get_username - -# 获取日志记录器 -logger = logging.getLogger(__name__) - - -# ============================================================ -# 分类列表查询 -# ============================================================ - -def get_category_list(): - """ - 查询分类列表(无分页) - - 用于分类管理页面的列表展示,支持按名称模糊搜索。 - - Args: - request: Flask request对象,包含查询参数 - - category_name: 分类名称(可选,模糊查询) - - type: 分类类型(可选,精确查询) - - Returns: - R: 包含分类列表的JSON响应 - """ - try: - # 应用筛选条件 - query = _apply_filters() - - # 查询数据 - category_list = query.all() - - # 转换为字典列表 - records = models_to_list(category_list) - - return R.ok(data=records) - - except Exception as e: - logger.error(f"查询分类列表异常: {str(e)}") - return R.failed(msg="查询失败,请稍后重试") - - -def _apply_filters(): - """ - 应用查询筛选条件 - - 根据请求参数动态构建查询条件: - - 自动过滤已删除记录(is_delete == 0) - - 支持分类名称模糊查询 - - 支持分类类型精确查询 - - Returns: - Query: 应用筛选后的SQLAlchemy查询对象 - """ - # 基础查询:过滤已删除记录 - query = Category.query.filter(Category.is_delete == 0) - - # 分类名称模糊筛选 - category_name = request.args.get('category_name') - if category_name: - query = query.filter(Category.category_name.like(like_contains(category_name), escape='\\')) - - # 分类类型精确筛选 - type = request.args.get('type') - if type: - query = query.filter(Category.type == type) - - # 按排序值升序排列 - return query.order_by(Category.sort.asc()) - - -# ============================================================ -# 分类详情查询 -# ============================================================ - -def get_category_detail(category_id): - """ - 根据ID查询分类详情(返回字典格式) - - 此方法调用 get_info 获取模型对象,然后转换为字典格式返回。 - 适用于前端接口调用。 - - Args: - category_id: 分类ID - - Returns: - dict/None: 分类详情字典,不存在时返回 None - """ - try: - # 复用 get_info 方法查询分类 - category = get_info(category_id) - if not category: - return None - # 转换为字典格式 - return model_to_dict(category) - - except Exception as e: - logger.error(f"查询分类详情异常,ID: {category_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 分类信息查询(根据ID)- 核心查询方法 -# ============================================================ - -def get_info(category_id: int): - """ - 根据ID查询分类信息(返回模型对象) - - 这是一个通用的分类查询方法,返回分类模型对象而非字典。 - 与 get_category_detail 的区别: - - get_category_detail: 返回字典格式,用于前端接口 - - get_info: 返回模型对象,用于内部服务调用 - - 此方法被以下方法复用: - - get_category_detail: 获取详情字典 - - update_category: 验证分类是否存在 - - delete_categories: 验证分类是否存在 - - Args: - category_id: 分类ID - - Returns: - Category/None: 分类模型对象,不存在或已删除时返回 None - - Example: - category = get_info(category_id) - if category: - print(category.category_name) - """ - # 参数有效性验证 - if not category_id or category_id <= 0: - return None - - try: - # 查询未删除的分类 - category = Category.query.filter( - and_(Category.id == category_id, Category.is_delete == 0) - ).first() - return category - except Exception as e: - logger.error(f"查询分类信息异常,ID: {category_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 分类添加 -# ============================================================ - -def add_category(): - """ - 添加分类 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证(名称唯一性、字段格式等) - 3. 创建分类对象并保存到数据库 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = CategoryForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - # 创建分类 - category = Category(**form.data) - category.create_user = get_username() - category.save() - - return R.ok(msg="添加成功") - - except Exception as e: - logger.error(f"添加分类异常: {str(e)}") - return R.failed(msg="添加分类失败,请稍后重试") - - -# ============================================================ -# 分类更新 -# ============================================================ - -def update_category(): - """ - 更新分类 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证 - 3. 验证分类是否存在(复用 get_info) - 4. 更新分类信息 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = CategoryForm(data=json_data) - if not form.validate(): - return R.failed(msg=regular.get_err(form)) - - # 获取分类ID - category_id = form.data.get('id') - if not category_id or int(category_id) <= 0: - return R.failed("分类ID无效") - category_id = int(category_id) - - # 复用 get_info 方法验证分类是否存在 - category = get_info(category_id) - if not category: - return R.failed("分类不存在") - - # 更新分类信息(移除ID,避免更新主键) - form_data = convert_camel_to_snake(form.data.copy()) - form_data.pop('id', None) - - form_data['update_time'] = datetime.now(tz=timezone.utc) - form_data['update_user'] = get_username() - - result = Category.query.filter_by(id=category_id).update(form_data) - - if not result: - return R.failed("更新失败") - - return R.ok(msg="更新成功") - - except Exception as e: - logger.error(f"更新分类异常: {str(e)}") - return R.failed(msg="更新分类失败,请稍后重试") - - -# ============================================================ -# 分类删除 -# ============================================================ - -def delete_categories(category_ids): - """ - 删除分类(批量删除) - - 删除 - 支持逗号分隔的多个ID批量删除。 - - Args: - category_ids: 分类ID字符串,多个ID用逗号分隔,如 "1,2,3" - - Returns: - R: 操作结果 - """ - if not category_ids: - return R.failed("记录ID不存在") - - # 解析ID列表 - id_list = parse_id_list(category_ids) - if not id_list: - return R.failed("无效的记录ID") - - # 检查是否有子分类 - for category_id in id_list: - child_count = Category.query.filter( - and_(Category.parent_id == category_id, Category.is_delete == 0) - ).count() - if child_count > 0: - return R.failed("存在子分类,请先删除子分类") - - return Category.batch_delete_r(category_ids) - - -# ============================================================ -# 分类数据获取(用于下拉选择、树形展示) -# ============================================================ - -def get_category_data(): - """ - 获取分类数据列表(无分页) - - 用于分类下拉选择框、树形展示等需要全部数据的场景。 - 按排序值升序排列。 - - Returns: - list: 分类列表,每个元素为分类模型字典,无数据时返回空列表 - """ - try: - # 查询所有未删除的分类 - category_list = Category.query.filter( - Category.is_delete == 0 - ).order_by(Category.sort.asc()).all() - - # 转换为字典列表 - result = models_to_list(category_list) - - return result - - except Exception as e: - logger.error(f"获取分类数据异常: {str(e)}") - return [] +from src.modules.cms.category.service import * # noqa: F401,F403 diff --git a/application/services/city.py b/application/services/city.py index 33b17cd..77a0e62 100644 --- a/application/services/city.py +++ b/application/services/city.py @@ -1,482 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -城市管理服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了城市/行政区划管理相关的所有业务逻辑。 -包括城市的分页查询、详情查询、添加、更新、删除、子级城市查询等功能。 -城市数据采用树形结构,通过 pid/parent_code 字段建立层级关系, -支持省、市、区三级行政区划管理。 - -命名规范: -- 函数参数使用驼峰命名(如 cityId, cityCode) -- 表单字段使用下划线命名(如 name, code, level)- 与数据库字段保持一致 -- 返回数据使用驼峰命名(如 createUser, updateTime)- 与前端规范保持一致 -- 私有函数以下划线开头(如 _apply_filters, _build_tree) - -功能模块: -1. 列表查询:支持按父级ID、名称模糊搜索 -2. 详情查询:根据ID获取单个城市的详细信息 -3. 数据维护:城市的添加、更新、删除(支持批量) -4. 子级查询:根据城市编码获取子级城市列表 -5. 城市树形数据:获取完整的城市树形结构 +服务已迁移至 src.modules.city.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging - -from datetime import datetime, timezone -from flask import request -from sqlalchemy import and_ - -from application.constants.dict import CITY_LEVEL_LIST -from application.forms.city import CityForm -from application.models.city import City -from extensions import db -from utils import R, regular -from utils.case_converter import convert_camel_to_snake -from utils.request import parse_request_body, parse_id_list, like_contains -from utils.model_utils import models_to_list, model_to_dict -from utils.security import get_username - -# 获取日志记录器 -logger = logging.getLogger(__name__) - - -# ============================================================ -# 城市列表查询 -# ============================================================ - -def get_city_list(): - """ - 查询城市列表 - - 用于城市管理页面的列表展示,支持按父级ID和名称筛选。 - - Args: - request: Flask request对象,包含查询参数 - - pid: 父级ID(可选,默认为0,查询顶级城市) - - name: 城市名称(可选,模糊查询) - - Returns: - R: 包含城市列表的JSON响应 - """ - try: - # 应用筛选条件 - query = _apply_filters() - - # 查询数据 - city_list = query.all() - - # 转换为字典列表并补充关联数据 - records = _enrich_city_records(city_list) - - return R.ok(data=records) - - except Exception as e: - logger.error(f"查询城市列表异常: {str(e)}") - return R.failed(msg="查询失败,请稍后重试") - - -def _apply_filters(): - """ - 应用查询筛选条件 - - 根据请求参数动态构建查询条件: - - 自动过滤已删除记录(is_delete == 0) - - 支持父级ID精确查询 - - 支持城市名称模糊查询 - - Returns: - Query: 应用筛选后的SQLAlchemy查询对象 - """ - # 基础查询:过滤已删除记录 - query = City.query.filter(City.is_delete == 0) - - # 父级ID精确筛选(默认为0,查询顶级城市) - pid = request.args.get('pid', 0) - if pid is not None: - try: - query = query.filter(City.pid == int(pid)) - except ValueError: - pass - - # 城市名称模糊筛选 - name = request.args.get('name') - if name: - query = query.filter(City.name.like(like_contains(name), escape='\\')) - - # 按ID升序排列 - return query.order_by(City.id.asc()) - - -def _enrich_city_records(city_list): - """ - 为城市记录补充关联数据 - - Args: - city_list: 城市模型对象列表 - - Returns: - list: 补充关联数据后的城市字典列表 - """ - if not city_list: - return [] - - records = models_to_list(city_list) - - for data in records: - # 城市级别名称 - level = data.get('level') - data['level_name'] = CITY_LEVEL_LIST.get(level) if level else None - # 是否有子级(level < 3 表示还有下级) - data['have_child'] = data.get('level', 4) < 3 if data.get('level') else False - - return records - - -# ============================================================ -# 城市详情查询 -# ============================================================ - -def get_city_detail(city_id): - """ - 根据ID查询城市详情(返回字典格式) - - 此方法调用 get_info 获取模型对象,然后转换为字典格式返回。 - 适用于前端接口调用。 - - Args: - city_id: 城市ID - - Returns: - dict/None: 城市详情字典,不存在时返回 None - """ - try: - # 复用 get_info 方法查询城市 - city = get_info(city_id) - if not city: - return None - # 转换为字典格式 - return model_to_dict(city) - - except Exception as e: - logger.error(f"查询城市详情异常,ID: {city_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 城市信息查询(根据ID)- 核心查询方法 -# ============================================================ - -def get_info(city_id: int): - """ - 根据ID查询城市信息(返回模型对象) - - 这是一个通用的城市查询方法,返回城市模型对象而非字典。 - 与 get_city_detail 的区别: - - get_city_detail: 返回字典格式,用于前端接口 - - get_info: 返回模型对象,用于内部服务调用 - - 此方法被以下方法复用: - - get_city_detail: 获取详情字典 - - update_city: 验证城市是否存在 - - delete_cities: 验证城市是否存在 - - Args: - city_id: 城市ID - - Returns: - City/None: 城市模型对象,不存在或已删除时返回 None - - Example: - city = get_info(city_id) - if city: - print(city.name) - """ - # 参数有效性验证 - if not city_id or city_id <= 0: - return None - - try: - # 查询未删除的城市 - city = City.query.filter( - and_(City.id == city_id, City.is_delete == 0) - ).first() - return city - except Exception as e: - logger.error(f"查询城市信息异常,ID: {city_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 城市添加 -# ============================================================ - -def add_city(): - """ - 添加城市 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证(编码唯一性、字段格式等) - 3. 创建城市对象并保存到数据库 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = CityForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - # 创建城市 - city = City(**form.data) - city.create_user = get_username() - city.save() - - return R.ok(msg="添加成功") - - except Exception as e: - logger.error(f"添加城市异常: {str(e)}") - return R.failed(msg="添加城市失败,请稍后重试") - - -# ============================================================ -# 城市更新 -# ============================================================ - -def update_city(): - """ - 更新城市 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证 - 3. 验证城市是否存在(复用 get_info) - 4. 更新城市信息 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = CityForm(data=json_data) - if not form.validate(): - return R.failed(msg=regular.get_err(form)) - - # 获取城市ID - city_id = form.data.get('id') - if not city_id or int(city_id) <= 0: - return R.failed("城市ID无效") - city_id = int(city_id) - - # 复用 get_info 方法验证城市是否存在 - city = get_info(city_id) - if not city: - return R.failed("城市不存在") - - # 更新城市信息(移除ID,避免更新主键) - form_data = convert_camel_to_snake(form.data.copy()) - form_data.pop('id', None) - - form_data['update_time'] = datetime.now(tz=timezone.utc) - form_data['update_user'] = get_username() - - result = City.query.filter_by(id=city_id).update(form_data) - - if not result: - return R.failed("更新失败") - - return R.ok(msg="更新成功") - - except Exception as e: - logger.error(f"更新城市异常: {str(e)}") - return R.failed(msg="更新城市失败,请稍后重试") - - -# ============================================================ -# 城市删除 -# ============================================================ - -def delete_cities(city_ids): - """ - 删除城市(批量删除) - - 删除 - 支持逗号分隔的多个ID批量删除。 - - Args: - city_ids: 城市ID字符串,多个ID用逗号分隔,如 "1,2,3" - - Returns: - R: 操作结果 - """ - if not city_ids: - return R.failed("记录ID不存在") - - # 解析ID列表 - id_list = parse_id_list(city_ids) - if not id_list: - return R.failed("无效的记录ID") - - # 检查是否有子级城市 - for city_id in id_list: - child_count = City.query.filter( - and_(City.pid == city_id, City.is_delete == 0) - ).count() - if child_count > 0: - return R.failed(f"城市ID {city_id} 存在子级城市,请先删除子级城市") - - return City.batch_delete_r(city_ids) - - -# ============================================================ -# 子级城市查询 -# ============================================================ - -def get_child_cities(city_code): - """ - 根据城市编码获取子级城市列表 - - 用于级联选择器,根据父级编码查询下级城市。 - - Args: - city_code: 父级城市编码 - - Returns: - list: 子级城市列表,每个元素包含 area_code 和 name - - Example: - child_list = get_child_cities("440000") - # 返回:[{"area_code": "440100", "name": "广州市"}, ...] - """ - if not city_code: - return [] - - try: - # 查询子级城市 - child_list = City.query.filter( - and_(City.parent_code == city_code, City.is_delete == 0) - ).order_by(City.id.asc()).all() - - # 构建返回数据 - result = models_to_list(child_list) - return result - - except Exception as e: - logger.error(f"查询子级城市异常,城市编码: {city_code}, 错误: {str(e)}") - return [] - - -# ============================================================ -# 城市树形数据获取 -# ============================================================ - -def get_city_tree(): - """ - 获取城市树形数据 - - 用于级联选择器、行政区划选择等场景。 - 返回完整的省-市-区三级树形结构。 - - Returns: - list: 城市树形数据列表 - """ - try: - # 查询所有未删除的城市 - all_cities = City.query.filter(City.is_delete == 0).order_by(City.id.asc()).all() - - if not all_cities: - return [] - - # 转换为字典列表 - city_dicts = models_to_list(all_cities) - - # 构建树形结构 - tree = _build_city_tree(city_dicts) - - return tree - - except Exception as e: - logger.error(f"获取城市树形数据异常: {str(e)}") - return [] - - -def _build_city_tree(city_list, parent_id=0): - """ - 构建城市树形结构 - - Args: - city_list: 城市字典列表 - parent_id: 父级ID - - Returns: - list: 树形结构数据 - """ - tree = [] - for city in city_list: - if city.get('pid') == parent_id: - node = { - 'id': city.get('id'), - 'name': city.get('name'), - 'code': city.get('area_code'), - 'level': city.get('level'), - 'children': _build_city_tree(city_list, city.get('id')) - } - # 如果没有子节点,移除children字段 - if not node['children']: - del node['children'] - tree.append(node) - return tree +from src.modules.city.service import * # noqa: F401,F403 diff --git a/application/services/config.py b/application/services/config.py index 24e2864..7de9c7d 100644 --- a/application/services/config.py +++ b/application/services/config.py @@ -1,568 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -参数配置管理服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了参数配置管理相关的所有业务逻辑。 -包括参数配置的分页查询、详情查询、添加、更新、删除等功能。 -参数配置用于管理系统中的配置项,如:网站名称、ICP备案号、联系电话等, -每个配置包含名称、键名、键值、排序、状态等属性,通过sort字段控制显示顺序。 - -命名规范: -- 函数参数使用驼峰命名(如 configId, pageNo) -- 表单字段使用下划线命名(如 name, key, value)- 与数据库字段保持一致 -- 返回数据使用驼峰命名(如 createUser, updateTime)- 与前端规范保持一致 -- 私有函数以下划线开头(如 _apply_filters, _build_page_data) - -功能模块: -1. 分页查询:支持按名称模糊搜索、状态筛选,按sort字段排序 -2. 详情查询:根据ID获取单个参数配置的详细信息 -3. 数据维护:参数配置的添加、更新、删除(支持批量) +服务已迁移至 src.modules.configuration.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging -from collections import defaultdict -from datetime import datetime, timezone -from typing import List, Dict, Any - -from flask import request -from sqlalchemy import and_ - -from application.constants.common import PAGE_SIZE -from application.forms.config import ConfigForm -from application.models.config import Config -from application.models.config_item import ConfigItem -from extensions import db -from utils import R, regular -from utils.case_converter import convert_camel_to_snake -from utils.model_utils import models_to_list, model_to_dict -from utils.request import parse_request_body, parse_id_list, like_contains -from utils.rich_text import get_file_url, save_file -from utils.security import get_username - -# 获取日志记录器 -logger = logging.getLogger(__name__) - - -# ============================================================ -# 参数配置分页查询 -# ============================================================ - -def get_config_page(): - """ - 查询参数配置分页数据 - - Args: - request: Flask request对象,包含查询参数 - - pageNo: 当前页码(可选,默认为1) - - pageSize: 每页记录数(可选,默认为PAGE_SIZE) - - name: 配置名称(可选,模糊查询) - - status: 配置状态(可选,精确匹配) - - Returns: - R: 包含分页数据的JSON响应,格式为: - { - "records": 参数配置列表, - "total": 总记录数, - "size": 每页大小, - "current": 当前页码, - "pages": 总页数 - } - """ - try: - # 获取分页参数 - page_no = int(request.args.get('pageNo', 1)) - page_size = int(request.args.get('pageSize', PAGE_SIZE)) - - # 应用筛选条件 - query = _apply_filters() - - # 记录总数 - total = query.count() - # 计算总页数 - pages = (total + page_size - 1) // page_size if total > 0 else 0 - - # 分页查询 - config_list = query.limit(page_size).offset((page_no - 1) * page_size).all() - # 构建返回数据 - pageData = _build_page_data(config_list, total, page_size, page_no, pages) - return R.ok(data=pageData) - - except Exception as e: - logger.error(f"查询参数配置分页数据异常: {str(e)}") - return R.failed(msg="查询失败,请稍后重试") - - -def _apply_filters(): - """ - 应用查询筛选条件 - - 根据请求参数动态构建查询条件: - - 自动过滤已删除记录(isDelete == 0) - - 支持配置名称模糊查询 - - 支持配置状态精确查询 - - 默认按排序值升序排列 - - Returns: - Query: 应用筛选后的SQLAlchemy查询对象 - """ - # 基础查询:过滤已删除记录 - query = Config.query.filter(Config.is_delete == 0) - - # 配置名称模糊筛选 - name = request.args.get('name') - if name: - query = query.filter(Config.name.like(like_contains(name), escape='\\')) - - # 配置状态精确筛选 - status = request.args.get('status') - if status: - query = query.filter(Config.status == status) - - # 按排序值升序排列 - return query.order_by(Config.sort.asc()) - - -def _build_page_data(config_list, total, page_size, page_no, pages): - """ - 构建分页返回数据 - - Args: - config_list: 参数配置对象列表 - total: 总记录数 - page_size: 每页大小 - page_no: 当前页码 - pages: 总页数 - - Returns: - dict: 分页数据,包含 records、total、size、current、pages - """ - # 将模型对象列表转换为字典列表 - records = models_to_list(config_list) - - return { - 'records': records, - 'total': total, - 'size': page_size, - 'current': page_no, - 'pages': pages - } - - -# ============================================================ -# 参数配置详情查询 -# ============================================================ - -def get_config_detail(config_id): - """ - 根据ID查询参数配置详情(返回字典格式) - - 此方法调用 get_info 获取模型对象,然后转换为字典格式返回。 - 适用于前端接口调用。 - - Args: - config_id: 参数配置ID - - Returns: - dict/None: 参数配置详情字典,不存在时返回 None - """ - try: - # 复用 get_info 方法查询参数配置 - config = get_info(config_id) - if not config: - return None - # 转换为字典格式 - return model_to_dict(config) - - except Exception as e: - logger.error(f"查询参数配置详情异常,ID: {config_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 参数配置信息查询(根据ID)- 核心查询方法 -# ============================================================ - -def get_info(config_id: int): - """ - 根据ID查询参数配置信息(返回模型对象) - - 这是一个通用的参数配置查询方法,返回参数配置模型对象而非字典。 - 与 get_config_detail 的区别: - - get_config_detail: 返回字典格式,用于前端接口 - - get_info: 返回模型对象,用于内部服务调用 - - 此方法被以下方法复用: - - get_config_detail: 获取详情字典 - - update_config: 验证参数配置是否存在 - - Args: - config_id: 参数配置ID - - Returns: - Config/None: 参数配置模型对象,不存在或已删除时返回 None - - Example: - config = get_info(config_id) - if config: - print(config.name) - """ - # 参数有效性验证 - if not config_id or config_id <= 0: - return None - - try: - # 查询未删除的参数配置 - config = Config.query.filter( - and_(Config.id == config_id, Config.is_delete == 0) - ).first() - return config - except Exception as e: - logger.error(f"查询参数配置信息异常,ID: {config_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 参数配置添加 -# ============================================================ - -def add_config(): - """ - 添加参数配置 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证(名称唯一性、字段格式等) - 3. 创建参数配置对象并保存到数据库 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = ConfigForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - # 创建参数配置 - config = Config(**form.data) - config.create_user = get_username() - config.save() - - return R.ok(msg="添加成功") - - except Exception as e: - logger.error(f"添加参数配置异常: {str(e)}") - return R.failed(msg="添加失败,请稍后重试") - - -# ============================================================ -# 参数配置更新 -# ============================================================ - -def update_config(): - """ - 更新参数配置 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证 - 3. 验证参数配置是否存在(复用 get_info) - 4. 更新参数配置信息 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = ConfigForm(data=json_data) - if not form.validate(): - return R.failed(msg=regular.get_err(form)) - - # 获取参数配置ID - config_id = form.data.get('id') - if not config_id or int(config_id) <= 0: - return R.failed("参数配置ID无效") - config_id = int(config_id) - - # 复用 get_info 方法验证参数配置是否存在 - config = get_info(config_id) - if not config: - return R.failed("参数配置不存在") - - # 更新参数配置信息(移除ID,避免更新主键) - form_data = convert_camel_to_snake(form.data.copy()) - form_data.pop('id', None) - - form_data['update_time'] = datetime.now(tz=timezone.utc) - form_data['update_user'] = get_username() - - result = Config.query.filter_by(id=config_id).update(form_data) - - if not result: - return R.failed("更新失败") - - return R.ok(msg="更新成功") - - except Exception as e: - logger.error(f"更新参数配置异常: {str(e)}") - return R.failed(msg="更新参数配置失败,请稍后重试") - - -# ============================================================ -# 参数配置删除 -# ============================================================ - -def delete_configs(config_ids): - """ - 删除参数配置(批量删除) - - 删除 - 支持逗号分隔的多个ID批量删除。 - - Args: - config_ids: 参数配置ID字符串,多个ID用逗号分隔,如 "1,2,3" - - Returns: - R: 操作结果 - """ - return Config.batch_delete_r(config_ids) - - -# ============================================================ -# 网站配置管理 -# ============================================================ - -def get_config_data() -> List[Dict[str, Any]]: - """ - 获取系统配置(优化版,使用一次查询获取所有数据) - - Returns: - 返回格式化的配置数据列表 - """ - # 查询所有配置 - configs = Config.query.filter( - Config.is_delete == 0 - ).order_by(Config.sort.asc()).all() - - if not configs: - return [] - - # 获取所有配置ID - config_ids = [config.id for config in configs] - - # 一次性查询所有配置项 - all_items = ConfigItem.query.filter( - ConfigItem.is_delete == 0, - ConfigItem.status == 1, - ConfigItem.config_id.in_(config_ids) - ).order_by(ConfigItem.sort.asc()).all() - - # 按配置ID分组 - items_by_config = defaultdict(list) - for item in all_items: - items_by_config[item.config_id].append(item) - - # 构建返回数据 - config_list = [] - for config in configs: - config_data = { - "configId": config.id, - "configName": config.name, - "dataList": [] - } - - for item in items_by_config.get(config.id, []): - item_data = _process_config_item(item) - if item_data: - config_data["dataList"].append(item_data) - - config_list.append(config_data) - - return config_list - - -def _process_config_item(item) -> Dict[str, Any]: - """ - 处理单个配置项 - - Args: - item: 配置项模型对象 - - Returns: - 处理后的配置项数据 - """ - data = model_to_dict(item) - item_type = data.get('type') - item_value = data.get('value') - item_options = data.get('options') - - # 处理选项类型(array, radio, checkbox, select, selects) - option_types = ["array", "radio", "checkbox", "select", "selects"] - if item_type in option_types and item_options: - param = parse_options(item_options) - if param: - data["param"] = param - - # 根据不同类型处理value - if item_value: - if item_type in ["checkbox", "selects"]: - data["valueList"] = item_value.split(",") - - elif item_type == "image": - data["value"] = get_file_url(item_value) - - elif item_type == "images": - image_list = item_value.split(",") - data["valueList"] = [get_file_url(img) for img in image_list if img] - data["value"] = data["valueList"][0] if data["valueList"] else "" - - elif item_type in ["file", "files"]: - file_list = item_value.split(",") - processed_list = [] - - for file_item in file_list: - if not file_item: - continue - - parts = file_item.split("|", 1) - if len(parts) == 2: - file_name = parts[0] - file_path = parts[1] - full_url = f"{file_name}|{get_file_url(file_path)}" - processed_list.append(full_url) - else: - processed_list.append(file_item) - - data["valueList"] = processed_list - data["value"] = processed_list[0] if processed_list else "" - - elif item_type == "ueditor": - data["value"] = get_content_url(item_value) - - return data - - -def parse_options(options_str: str) -> Dict[int, str]: - """ - 解析选项字符串,格式如:'1=男,2=女,3=保密' - 返回字典格式:{1: '男', 2: '女', 3: '保密'} - """ - result = {} - if not options_str: - return result - - options_str = options_str.replace(",", ",") - options_list = options_str.split(",") - - for option in options_list: - if "=" in option: - parts = option.split("=") - if len(parts) == 2: - try: - key = int(parts[0].strip()) - value = parts[1].strip() - result[key] = value - except (ValueError, IndexError): - continue - - return result - - -def get_content_url(content: str) -> str: - """ - 处理富文本内容中的图片URL - """ - return content - - -def save_config_data(): - """ - 保存网站配置数据 - - 流程: - 1. 解析请求体JSON数据 - 2. 遍历处理表单数据(列表类型、图片URL等) - 3. 根据编码查询配置项并更新值 - """ - data, error = parse_request_body() - if error: - return R.failed(msg=error) - - if data: - for key in data: - val = data[key] - - if isinstance(val, list): - item = [] - for v in val: - if isinstance(v, str) and (v.startswith("http://") or v.startswith("https://")): - url = save_file(v, "config") - item.append(url) - else: - item.append(v) - val = ','.join(item) - else: - if isinstance(val, str) and (val.startswith("http://") or val.startswith("https://")): - val = save_file(val, "config") - - config_item = ConfigItem.query.filter(ConfigItem.is_delete == 0, ConfigItem.code == key).first() - - if not config_item: - continue - - config_item.value = val - config_item.save() - - return R.ok() +from src.modules.configuration.service import * # noqa: F401,F403 diff --git a/application/services/config_item.py b/application/services/config_item.py index 15fb280..7de9c7d 100644 --- a/application/services/config_item.py +++ b/application/services/config_item.py @@ -1,443 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -配置项管理服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了配置项管理相关的所有业务逻辑。 -包括配置项的分页查询、详情查询、添加、更新、删除、状态设置等功能。 -配置项用于管理具体配置的参数项,如:网站名称对应的值、ICP备案号对应的值等, -每个配置项包含标题、键名、键值、类型、排序、状态等属性,通过sort字段控制显示顺序。 - -命名规范: -- 函数参数使用驼峰命名(如 configItemId, pageNo) -- 表单字段使用下划线命名(如 title, key, value)- 与数据库字段保持一致 -- 返回数据使用驼峰命名(如 createUser, updateTime)- 与前端规范保持一致 -- 私有函数以下划线开头(如 _apply_filters, _build_page_data) - -功能模块: -1. 分页查询:支持按标题模糊搜索、配置ID筛选、状态筛选,按sort字段排序 -2. 详情查询:根据ID获取单个配置项的详细信息 -3. 数据维护:配置项的添加、更新、删除(支持批量) -4. 状态管理:单独更新配置项的启用/禁用状态 +服务已迁移至 src.modules.configuration.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging - -from datetime import datetime, timezone -from flask import request -from sqlalchemy import and_ - -from application.constants.common import PAGE_SIZE -from application.constants.dict import CONFIG_ITEM_TYPE_LIST -from application.forms.config_item import ConfigItemForm, ConfigItemStatusForm -from application.models.config import Config -from application.models.config_item import ConfigItem -from extensions import db -from utils import R, regular -from utils.case_converter import convert_camel_to_snake -from utils.model_utils import model_to_dict -from utils.request import parse_request_body, parse_id_list, like_contains -from utils.security import get_username - -# 获取日志记录器 -logger = logging.getLogger(__name__) - - -# ============================================================ -# 配置项分页查询 -# ============================================================ - -def get_config_item_page(): - """ - 查询配置项分页数据 - - Args: - request: Flask request对象,包含查询参数 - - pageNo: 当前页码(可选,默认为1) - - pageSize: 每页记录数(可选,默认为PAGE_SIZE) - - configId: 配置ID(必填,用于筛选属于哪个配置) - - title: 配置项名称(可选,模糊查询) - - status: 配置项状态(可选,精确匹配) - - Returns: - R: 包含分页数据的JSON响应,格式为: - { - "records": 配置项列表, - "total": 总记录数, - "size": 每页大小, - "current": 当前页码, - "pages": 总页数 - } - """ - try: - # 获取分页参数 - page_no = int(request.args.get('pageNo', 1)) - page_size = int(request.args.get('pageSize', PAGE_SIZE)) - - # 应用筛选条件 - query = _apply_filters() - - # 记录总数 - total = query.count() - # 计算总页数 - pages = (total + page_size - 1) // page_size if total > 0 else 0 - - # 分页查询 - config_item_list = query.limit(page_size).offset((page_no - 1) * page_size).all() - # 构建返回数据 - page_data = _build_page_data(config_item_list, total, page_size, page_no, pages) - return R.ok(data=page_data) - - except Exception as e: - logger.error(f"查询配置项分页数据异常: {str(e)}") - return R.failed(msg="查询失败,请稍后重试") - - -def _apply_filters(): - """ - 应用查询筛选条件 - - 根据请求参数动态构建查询条件: - - 自动过滤已删除记录(is_delete == 0) - - 必须按配置ID筛选(config_id) - - 支持配置项名称模糊查询 - - 支持配置项状态精确查询 - - 默认按排序值升序排列 - - Returns: - Query: 应用筛选后的SQLAlchemy查询对象 - """ - # 基础查询:过滤已删除记录 - query = ConfigItem.query.filter(ConfigItem.is_delete == 0) - - # 配置ID筛选(必填) - config_id = request.args.get('configId', 0) - if config_id: - query = query.filter(ConfigItem.config_id == config_id) - - # 配置项名称模糊筛选 - name = request.args.get('name') - if name: - query = query.filter(ConfigItem.name.like(like_contains(name), escape='\\')) - - # 配置项状态精确筛选 - status = request.args.get('status') - if status: - query = query.filter(ConfigItem.status == status) - - # 按排序值升序排列 - return query.order_by(ConfigItem.sort.asc()) - - -def _build_page_data(config_item_list, total, page_size, page_no, pages): - """ - 构建分页返回数据 - - Args: - config_item_list: 配置项对象列表 - total: 总记录数 - page_size: 每页大小 - page_no: 当前页码 - pages: 总页数 - - Returns: - dict: 分页数据,包含 records、total、size、current、pages - """ - # 将模型对象列表转换为字典列表,并添加额外字段 - records = [] - for item in config_item_list: - data = model_to_dict(item) - # 添加配置项类型描述 - data['typeName'] = CONFIG_ITEM_TYPE_LIST.get(item.type) - records.append(data) - - return { - 'records': records, - 'total': total, - 'size': page_size, - 'current': page_no, - 'pages': pages - } - - -# ============================================================ -# 配置项详情查询 -# ============================================================ - -def get_config_item_detail(config_item_id): - """ - 根据ID查询配置项详情(返回字典格式) - - 此方法调用 get_info 获取模型对象,然后转换为字典格式返回。 - 适用于前端接口调用。 - - Args: - config_item_id: 配置项ID - - Returns: - dict/None: 配置项详情字典,不存在时返回 None - """ - try: - # 复用 get_info 方法查询配置项 - config_item = get_info(config_item_id) - if not config_item: - return None - # 转换为字典格式 - return model_to_dict(config_item) - - except Exception as e: - logger.error(f"查询配置项详情异常,ID: {config_item_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 配置项信息查询(根据ID)- 核心查询方法 -# ============================================================ - -def get_info(config_item_id: int): - """ - 根据ID查询配置项信息(返回模型对象) - - 这是一个通用的配置项查询方法,返回配置项模型对象而非字典。 - 与 get_config_item_detail 的区别: - - get_config_item_detail: 返回字典格式,用于前端接口 - - get_info: 返回模型对象,用于内部服务调用 - - 此方法被以下方法复用: - - get_config_item_detail: 获取详情字典 - - update_config_item: 验证配置项是否存在 - - update_config_item_status: 验证配置项是否存在 - - Args: - config_item_id: 配置项ID - - Returns: - configItem/None: 配置项模型对象,不存在或已删除时返回 None - - Example: - config_item = get_info(config_item_id) - if config_item: - print(config_item.title) - """ - # 参数有效性验证 - if not config_item_id or config_item_id <= 0: - return None - - try: - # 查询未删除的配置项 - config_item = ConfigItem.query.filter( - and_(ConfigItem.id == config_item_id, ConfigItem.is_delete == 0) - ).first() - return config_item - except Exception as e: - logger.error(f"查询配置项信息异常,ID: {config_item_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 配置项添加 -# ============================================================ - -def add_config_item(): - """ - 添加配置项 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证(字段格式等) - 3. 创建配置项对象并保存到数据库 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = ConfigItemForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - # 查询配置(验证配置是否存在) - config_id = form.configId.data - config = Config.query.filter(Config.id == config_id).first() - if not config: - return R.failed(msg="配置不存在") - - # 创建配置项 - config_item = ConfigItem(**form.data) - config_item.config_code = config.code - config_item.create_user = get_username() - config_item.save() - - return R.ok(msg="添加成功") - - except Exception as e: - logger.error(f"添加配置项异常: {str(e)}") - return R.failed(msg="添加失败,请稍后重试") - - -# ============================================================ -# 配置项更新 -# ============================================================ - -def update_config_item(): - """ - 更新配置项 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证 - 3. 验证配置项是否存在(复用 get_info) - 4. 更新配置项信息 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = ConfigItemForm(data=json_data) - if not form.validate(): - return R.failed(msg=regular.get_err(form)) - - # 获取配置项ID - config_item_id = form.data.get('id') - if not config_item_id or int(config_item_id) <= 0: - return R.failed("配置项ID无效") - - # 复用 get_info 方法验证配置项是否存在 - config_item = get_info(config_item_id) - if not config_item: - return R.failed("配置项不存在") - - # 更新配置项信息(移除ID,避免更新主键) - form_data = convert_camel_to_snake(form.data.copy()) - form_data.pop('id', None) - form_data['update_time'] = datetime.now(tz=timezone.utc) - form_data['update_user'] = get_username() - result = ConfigItem.query.filter_by(id=config_item_id).update(form_data) - - if not result: - return R.failed("更新失败") - - return R.ok(msg="更新成功") - - except Exception as e: - logger.error(f"更新配置项异常: {str(e)}") - return R.failed(msg="更新配置项失败,请稍后重试") - - -# ============================================================ -# 配置项删除 -# ============================================================ - -def delete_config_items(config_item_ids): - """ - 删除配置项(批量删除) - - 删除 - 支持逗号分隔的多个ID批量删除。 - - Args: - config_item_ids: 配置项ID字符串,多个ID用逗号分隔,如 "1,2,3" - - Returns: - R: 操作结果 - """ - return ConfigItem.batch_delete_r(config_item_ids) - - -# ============================================================ -# 配置项状态设置 -# ============================================================ - -def update_config_item_status(): - """ - 设置配置项状态 - - 单独更新配置项的启用/禁用状态,不涉及其他字段。 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = ConfigItemStatusForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - config_item_id = int(form.data['id']) - status = int(form.data['status']) - - # 复用 get_info 方法验证配置项是否存在 - config_item = get_info(config_item_id) - if not config_item: - return R.failed("配置项不存在") - - # 更新状态 - update_data = {"status": status, "update_time": datetime.now(tz=timezone.utc), "update_user": get_username()} - result = ConfigItem.query.filter_by(id=config_item_id).update(update_data) - - if not result: - return R.failed("设置失败") - - return R.ok(msg="设置成功") - - except Exception as e: - logger.error(f"设置配置项状态异常: {str(e)}") - return R.failed(msg="设置配置项状态失败,请稍后重试") +from src.modules.configuration.service import * # noqa: F401,F403 diff --git a/application/services/dept.py b/application/services/dept.py index 426c973..58728e4 100644 --- a/application/services/dept.py +++ b/application/services/dept.py @@ -1,375 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -部门管理服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了部门管理相关的所有业务逻辑。 -包括部门的列表查询、详情查询、添加、更新、删除等功能。 -部门用于组织架构管理,如:技术部、市场部、销售部等, -每个部门包含名称、父级ID、排序、状态等字段,通过sort字段控制显示顺序。 - -命名规范: -- 函数参数使用驼峰命名(如 deptId, pageNo) -- 表单字段使用下划线命名(如 name, sort, status)- 与数据库字段保持一致 -- 返回数据使用驼峰命名(如 createUser, updateTime)- 与前端规范保持一致 -- 私有函数以下划线开头(如 _apply_filters) - -功能模块: -1. 列表查询:支持按名称模糊搜索,按sort字段排序 -2. 详情查询:根据ID获取单个部门的详细信息 -3. 数据维护:部门的添加、更新、删除(支持批量) -4. 部门列表:获取所有部门数据(用于下拉选择、树形展示等场景) +服务已迁移至 src.modules.system.dept.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging - -from datetime import datetime, timezone -from flask import request -from sqlalchemy import and_ - -from application.forms.dept import DeptForm -from application.models.dept import Dept -from extensions import db -from utils import R, regular -from utils.case_converter import convert_camel_to_snake -from utils.request import parse_request_body, parse_id_list, like_contains -from utils.model_utils import models_to_list, model_to_dict -from utils.security import get_username - -# 获取日志记录器 -logger = logging.getLogger(__name__) - - -# ============================================================ -# 部门列表查询 -# ============================================================ - -def get_dept_list(): - """ - 查询部门列表(无分页) - - 用于部门管理页面的列表展示,支持按名称模糊搜索。 - - Args: - request: Flask request对象,包含查询参数 - - name: 部门名称(可选,模糊查询) - - Returns: - R: 包含部门列表的JSON响应 - """ - try: - # 应用筛选条件 - query = _apply_filters() - - # 查询数据 - dept_list = query.all() - - # 转换为字典列表 - records = models_to_list(dept_list) - - return R.ok(data=records) - - except Exception as e: - logger.error(f"查询部门列表异常: {str(e)}") - return R.failed(msg="查询失败,请稍后重试") - - -def _apply_filters(): - """ - 应用查询筛选条件 - - 根据请求参数动态构建查询条件: - - 自动过滤已删除记录(is_delete == 0) - - 支持部门名称模糊查询 - - Returns: - Query: 应用筛选后的SQLAlchemy查询对象 - """ - # 基础查询:过滤已删除记录 - query = Dept.query.filter(Dept.is_delete == 0) - - # 部门名称模糊筛选 - name = request.args.get('name') - if name: - query = query.filter(Dept.name.like(like_contains(name), escape='\\')) - - # 按排序值升序排列 - return query.order_by(Dept.sort.asc()) - - -# ============================================================ -# 部门详情查询 -# ============================================================ - -def get_dept_detail(dept_id): - """ - 根据ID查询部门详情(返回字典格式) - - 此方法调用 get_info 获取模型对象,然后转换为字典格式返回。 - 适用于前端接口调用。 - - Args: - dept_id: 部门ID - - Returns: - dict/None: 部门详情字典,不存在时返回 None - """ - try: - # 复用 get_info 方法查询部门 - dept = get_info(dept_id) - if not dept: - return None - # 转换为字典格式 - return model_to_dict(dept) - - except Exception as e: - logger.error(f"查询部门详情异常,ID: {dept_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 部门信息查询(根据ID)- 核心查询方法 -# ============================================================ - -def get_info(dept_id: int): - """ - 根据ID查询部门信息(返回模型对象) - - 这是一个通用的部门查询方法,返回部门模型对象而非字典。 - 与 get_dept_detail 的区别: - - get_dept_detail: 返回字典格式,用于前端接口 - - get_info: 返回模型对象,用于内部服务调用 - - 此方法被以下方法复用: - - get_dept_detail: 获取详情字典 - - update_dept: 验证部门是否存在 - - delete_depts: 验证部门是否存在 - - Args: - dept_id: 部门ID - - Returns: - Dept/None: 部门模型对象,不存在或已删除时返回 None - - Example: - dept = get_info(dept_id) - if dept: - print(dept.name) - """ - # 参数有效性验证 - if not dept_id or dept_id <= 0: - return None - - try: - # 查询未删除的部门 - dept = Dept.query.filter( - and_(Dept.id == dept_id, Dept.is_delete == 0) - ).first() - return dept - except Exception as e: - logger.error(f"查询部门信息异常,ID: {dept_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 部门添加 -# ============================================================ - -def add_dept(): - """ - 添加部门 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证(名称唯一性、字段格式等) - 3. 创建部门对象并保存到数据库 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = DeptForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - # 创建部门 - dept = Dept(**form.data) - dept.create_user = get_username() - dept.save() - - return R.ok(msg="添加成功") - - except Exception as e: - logger.error(f"添加部门异常: {str(e)}") - return R.failed(msg="添加部门失败,请稍后重试") - - -# ============================================================ -# 部门更新 -# ============================================================ - -def update_dept(): - """ - 更新部门 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证 - 3. 验证部门是否存在(复用 get_info) - 4. 更新部门信息 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = DeptForm(data=json_data) - if not form.validate(): - return R.failed(msg=regular.get_err(form)) - - # 获取部门ID - dept_id = form.data.get('id') - if not dept_id or int(dept_id) <= 0: - return R.failed("部门ID无效") - dept_id = int(dept_id) - - # 复用 get_info 方法验证部门是否存在 - dept = get_info(dept_id) - if not dept: - return R.failed("部门不存在") - - # 更新部门信息(移除ID,避免更新主键) - form_data = convert_camel_to_snake(form.data.copy()) - form_data.pop('id', None) - - form_data['update_time'] = datetime.now(tz=timezone.utc) - form_data['update_user'] = get_username() - - result = Dept.query.filter_by(id=dept_id).update(form_data) - - if not result: - return R.failed("更新失败") - - return R.ok(msg="更新成功") - - except Exception as e: - logger.error(f"更新部门异常: {str(e)}") - return R.failed(msg="更新部门失败,请稍后重试") - - -# ============================================================ -# 部门删除 -# ============================================================ - -def delete_depts(dept_ids): - """ - 删除部门(批量删除) - - 采用软删除方式,将 is_delete 标识设置为 1。 - 支持逗号分隔的多个ID批量删除。 - - Args: - dept_ids: 部门ID字符串,多个ID用逗号分隔,如 "1,2,3" - - Returns: - R: 操作结果 - """ - if not dept_ids: - return R.failed("记录ID不存在") - - # 解析ID列表 - id_list = parse_id_list(dept_ids) - if not id_list: - return R.failed("无效的记录ID") - - # 检查是否有子部门 - for dept_id in id_list: - child_count = Dept.query.filter( - and_(Dept.parent_id == dept_id, Dept.is_delete == 0) - ).count() - if child_count > 0: - return R.failed("存在子部门,请先删除子部门") - - return Dept.batch_delete_r(dept_ids) - - -# ============================================================ -# 部门树形数据获取 -# ============================================================ - -def get_dept_data(): - """ - 获取部门树形数据(无分页) - - 用于部门下拉选择框、树形展示等需要全部数据的场景。 - 只返回状态为"正常"的部门,按排序值升序排列。 - - Returns: - list: 部门列表,每个元素为部门模型字典,无数据时返回空列表 - """ - try: - # 查询所有未删除的部门 - dept_list = Dept.query.filter( - and_(Dept.is_delete == 0, Dept.status == 1) - ).order_by(Dept.sort.asc()).all() - - # 转换为字典列表 - result = models_to_list(dept_list) - - return result - - except Exception as e: - logger.error(f"获取部门树形数据异常: {str(e)}") - return [] +from src.modules.system.dept.service import * # noqa: F401,F403 diff --git a/application/services/dict.py b/application/services/dict.py index b4c9876..2fafa44 100644 --- a/application/services/dict.py +++ b/application/services/dict.py @@ -1,376 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -字典类型管理服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了字典类型管理相关的所有业务逻辑。 -包括字典类型的分页查询、详情查询、添加、更新、删除等功能。 -字典类型用于管理系统中的字典分类,如:性别、状态、类型等, -每个字典类型包含名称、编码、排序、状态等属性,通过sort字段控制显示顺序。 - -命名规范: -- 函数参数使用驼峰命名(如 dictId, pageNo) -- 表单字段使用下划线命名(如 name, code, sort)- 与数据库字段保持一致 -- 返回数据使用驼峰命名(如 createUser, updateTime)- 与前端规范保持一致 -- 私有函数以下划线开头(如 _apply_filters, _build_page_data) - -功能模块: -1. 分页查询:支持按名称模糊搜索、状态筛选,按sort字段排序 -2. 详情查询:根据ID获取单个字典类型的详细信息 -3. 数据维护:字典类型的添加、更新、删除(支持批量) +服务已迁移至 src.modules.dictionary.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging - -from datetime import datetime, timezone -from flask import request -from sqlalchemy import and_ - -from application.constants.common import PAGE_SIZE -from application.forms.dict import DictForm -from application.models.dict import Dict -from extensions import db -from utils import R, regular -from utils.case_converter import convert_camel_to_snake -from utils.model_utils import models_to_list, model_to_dict -from utils.request import parse_request_body, parse_id_list, like_contains -from utils.security import get_username - -# 获取日志记录器 -logger = logging.getLogger(__name__) - - -# ============================================================ -# 字典类型分页查询 -# ============================================================ - -def get_dict_page(): - """ - 查询字典类型分页数据 - - Args: - request: Flask request对象,包含查询参数 - - pageNo: 当前页码(可选,默认为1) - - pageSize: 每页记录数(可选,默认为PAGE_SIZE) - - name: 字典名称(可选,模糊查询) - - status: 字典状态(可选,精确匹配) - - Returns: - R: 包含分页数据的JSON响应,格式为: - { - "records": 字典类型列表, - "total": 总记录数, - "size": 每页大小, - "current": 当前页码, - "pages": 总页数 - } - """ - try: - # 获取分页参数 - page_no = int(request.args.get('pageNo', 1)) - page_size = int(request.args.get('pageSize', PAGE_SIZE)) - - # 应用筛选条件 - query = _apply_filters() - - # 记录总数 - total = query.count() - # 计算总页数 - pages = (total + page_size - 1) // page_size if total > 0 else 0 - - # 分页查询 - dict_list = query.limit(page_size).offset((page_no - 1) * page_size).all() - # 构建返回数据 - page_data = _build_page_data(dict_list, total, page_size, page_no, pages) - return R.ok(data=page_data) - - except Exception as e: - logger.error(f"查询字典类型分页数据异常: {str(e)}") - return R.failed(msg="查询失败,请稍后重试") - - -def _apply_filters(): - """ - 应用查询筛选条件 - - 根据请求参数动态构建查询条件: - - 自动过滤已删除记录(is_delete == 0) - - 支持字典名称模糊查询 - - 支持字典状态精确查询 - - 默认按排序值升序排列 - - Returns: - Query: 应用筛选后的SQLAlchemy查询对象 - """ - # 基础查询:过滤已删除记录 - query = Dict.query.filter(Dict.is_delete == 0) - - # 字典名称模糊筛选 - name = request.args.get('name') - if name: - query = query.filter(Dict.name.like(like_contains(name), escape='\\')) - - # 字典状态精确筛选 - status = request.args.get('status') - if status: - query = query.filter(Dict.status == status) - - # 按排序值升序排列 - return query.order_by(Dict.sort.asc()) - - -def _build_page_data(dict_list, total, page_size, page_no, pages): - """ - 构建分页返回数据 - - Args: - dict_list: 字典类型对象列表 - total: 总记录数 - page_size: 每页大小 - page_no: 当前页码 - pages: 总页数 - - Returns: - dict: 分页数据,包含 records、total、size、current、pages - """ - # 将模型对象列表转换为字典列表 - records = models_to_list(dict_list) - - return { - 'records': records, - 'total': total, - 'size': page_size, - 'current': page_no, - 'pages': pages - } - - -# ============================================================ -# 字典类型详情查询 -# ============================================================ - -def get_dict_detail(dict_id): - """ - 根据ID查询字典类型详情(返回字典格式) - - 此方法调用 get_info 获取模型对象,然后转换为字典格式返回。 - 适用于前端接口调用。 - - Args: - dict_id: 字典类型ID - - Returns: - dict/None: 字典类型详情字典,不存在时返回 None - """ - try: - # 复用 get_info 方法查询字典类型 - dict_obj = get_info(dict_id) - if not dict_obj: - return None - # 转换为字典格式 - return model_to_dict(dict_obj) - - except Exception as e: - logger.error(f"查询字典类型详情异常,ID: {dict_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 字典类型信息查询(根据ID)- 核心查询方法 -# ============================================================ - -def get_info(dict_id: int): - """ - 根据ID查询字典类型信息(返回模型对象) - - 这是一个通用的字典类型查询方法,返回字典类型模型对象而非字典。 - 与 get_dict_detail 的区别: - - get_dict_detail: 返回字典格式,用于前端接口 - - get_info: 返回模型对象,用于内部服务调用 - - 此方法被以下方法复用: - - get_dict_detail: 获取详情字典 - - update_dict: 验证字典类型是否存在 - - Args: - dict_id: 字典类型ID - - Returns: - Dict/None: 字典类型模型对象,不存在或已删除时返回 None - - Example: - dict_obj = get_info(dict_id) - if dict_obj: - print(dict_obj.name) - """ - # 参数有效性验证 - if not dict_id or dict_id <= 0: - return None - - try: - # 查询未删除的字典类型 - dict_obj = Dict.query.filter( - and_(Dict.id == dict_id, Dict.is_delete == 0) - ).first() - return dict_obj - except Exception as e: - logger.error(f"查询字典类型信息异常,ID: {dict_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 字典类型添加 -# ============================================================ - -def add_dict(): - """ - 添加字典类型 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证(名称唯一性、字段格式等) - 3. 创建字典类型对象并保存到数据库 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = DictForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - # 创建字典类型 - dict_obj = Dict(**form.data) - dict_obj.create_user = get_username() - dict_obj.save() - - return R.ok(msg="添加成功") - - except Exception as e: - logger.error(f"添加字典类型异常: {str(e)}") - return R.failed(msg="添加失败,请稍后重试") - - -# ============================================================ -# 字典类型更新 -# ============================================================ - -def update_dict(): - """ - 更新字典类型 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证 - 3. 验证字典类型是否存在(复用 get_info) - 4. 更新字典类型信息 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = DictForm(data=json_data) - if not form.validate(): - return R.failed(msg=regular.get_err(form)) - - # 获取字典类型ID - dict_id = form.data.get('id') - if not dict_id or int(dict_id) <= 0: - return R.failed("字典类型ID无效") - dict_id = int(dict_id) - - # 复用 get_info 方法验证字典类型是否存在 - dict_obj = get_info(dict_id) - if not dict_obj: - return R.failed("字典类型不存在") - - # 更新字典类型信息(移除ID,避免更新主键) - form_data = convert_camel_to_snake(form.data.copy()) - form_data.pop('id', None) - - form_data['update_time'] = datetime.now(tz=timezone.utc) - form_data['update_user'] = get_username() - - result = Dict.query.filter_by(id=dict_id).update(form_data) - - if not result: - return R.failed("更新失败") - - return R.ok(msg="更新成功") - - except Exception as e: - logger.error(f"更新字典类型异常: {str(e)}") - return R.failed(msg="更新字典类型失败,请稍后重试") - - -# ============================================================ -# 字典类型删除 -# ============================================================ - -def delete_dicts(dict_ids): - """ - 删除字典类型(批量删除) - - 删除 - 支持逗号分隔的多个ID批量删除。 - - Args: - dict_ids: 字典类型ID字符串,多个ID用逗号分隔,如 "1,2,3" - - Returns: - R: 操作结果 - """ - return Dict.batch_delete_r(dict_ids) +from src.modules.dictionary.service import * # noqa: F401,F403 diff --git a/application/services/dict_item.py b/application/services/dict_item.py index 641cdd7..2fafa44 100644 --- a/application/services/dict_item.py +++ b/application/services/dict_item.py @@ -1,391 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -字典项管理服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了字典项管理相关的所有业务逻辑。 -包括字典项的分页查询、详情查询、添加、更新、删除等功能。 -字典项用于管理字典类型下的具体数据项,如:性别字典下的男、女等, -每个字典项包含名称、键值、排序、状态等属性,通过sort字段控制显示顺序。 - -命名规范: -- 函数参数使用驼峰命名(如 dictItemId, pageNo) -- 表单字段使用下划线命名(如 name, value, sort)- 与数据库字段保持一致 -- 返回数据使用驼峰命名(如 createUser, updateTime)- 与前端规范保持一致 -- 私有函数以下划线开头(如 _apply_filters, _build_page_data) - -功能模块: -1. 分页查询:支持按名称模糊搜索、字典ID筛选、状态筛选,按sort字段排序 -2. 详情查询:根据ID获取单个字典项的详细信息 -3. 数据维护:字典项的添加、更新、删除(支持批量) +服务已迁移至 src.modules.dictionary.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging - -from datetime import datetime, timezone -from flask import request -from sqlalchemy import and_ - -from application.constants.common import PAGE_SIZE -from application.forms.dict_item import DictItemForm -from application.models.dict import Dict -from application.models.dict_item import DictItem -from extensions import db -from utils import R, regular -from utils.case_converter import convert_camel_to_snake -from utils.model_utils import models_to_list, model_to_dict -from utils.request import parse_request_body, parse_id_list, like_contains -from utils.security import get_username - -# 获取日志记录器 -logger = logging.getLogger(__name__) - - -# ============================================================ -# 字典项分页查询 -# ============================================================ - -def get_dict_item_page(): - """ - 查询字典项分页数据 - - Args: - request: Flask request对象,包含查询参数 - - pageNo: 当前页码(可选,默认为1) - - pageSize: 每页记录数(可选,默认为PAGE_SIZE) - - dictId: 字典ID(必填,用于筛选属于哪个字典类型) - - name: 字典项名称(可选,模糊查询) - - status: 字典项状态(可选,精确匹配) - - Returns: - R: 包含分页数据的JSON响应,格式为: - { - "records": 字典项列表, - "total": 总记录数, - "size": 每页大小, - "current": 当前页码, - "pages": 总页数 - } - """ - try: - # 获取分页参数 - page_no = int(request.args.get('pageNo', 1)) - page_size = int(request.args.get('pageSize', PAGE_SIZE)) - - # 应用筛选条件 - query = _apply_filters() - - # 记录总数 - total = query.count() - # 计算总页数 - pages = (total + page_size - 1) // page_size if total > 0 else 0 - - # 分页查询 - dict_item_list = query.limit(page_size).offset((page_no - 1) * page_size).all() - # 构建返回数据 - page_data = _build_page_data(dict_item_list, total, page_size, page_no, pages) - return R.ok(data=page_data) - - except Exception as e: - logger.error(f"查询字典项分页数据异常: {str(e)}") - return R.failed(msg="查询失败,请稍后重试") - - -def _apply_filters(): - """ - 应用查询筛选条件 - - 根据请求参数动态构建查询条件: - - 自动过滤已删除记录(is_delete == 0) - - 必须按字典ID筛选(dict_id) - - 支持字典项名称模糊查询 - - 支持字典项状态精确查询 - - 默认按排序值升序排列 - - Returns: - Query: 应用筛选后的SQLAlchemy查询对象 - """ - # 基础查询:过滤已删除记录 - query = DictItem.query.filter(DictItem.is_delete == 0) - - # 字典ID筛选(必填) - dict_id = request.args.get('dictId', 0) - if dict_id: - query = query.filter(DictItem.dict_id == dict_id) - - # 字典项名称模糊筛选 - name = request.args.get('name') - if name: - query = query.filter(DictItem.name.like(like_contains(name), escape='\\')) - - # 字典项状态精确筛选 - status = request.args.get('status') - if status: - query = query.filter(DictItem.status == status) - - # 按排序值升序排列 - return query.order_by(DictItem.sort.asc()) - - -def _build_page_data(dict_item_list, total, page_size, page_no, pages): - """ - 构建分页返回数据 - - Args: - dict_item_list: 字典项对象列表 - total: 总记录数 - page_size: 每页大小 - page_no: 当前页码 - pages: 总页数 - - Returns: - dict: 分页数据,包含 records、total、size、current、pages - """ - # 将模型对象列表转换为字典列表 - records = models_to_list(dict_item_list) - - return { - 'records': records, - 'total': total, - 'size': page_size, - 'current': page_no, - 'pages': pages - } - - -# ============================================================ -# 字典项详情查询 -# ============================================================ - -def get_dict_item_detail(dict_item_id): - """ - 根据ID查询字典项详情(返回字典格式) - - 此方法调用 get_info 获取模型对象,然后转换为字典格式返回。 - 适用于前端接口调用。 - - Args: - dict_item_id: 字典项ID - - Returns: - dict/None: 字典项详情字典,不存在时返回 None - """ - try: - # 复用 get_info 方法查询字典项 - dict_item = get_info(dict_item_id) - if not dict_item: - return None - # 转换为字典格式 - return model_to_dict(dict_item) - - except Exception as e: - logger.error(f"查询字典项详情异常,ID: {dict_item_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 字典项信息查询(根据ID)- 核心查询方法 -# ============================================================ - -def get_info(dict_item_id: int): - """ - 根据ID查询字典项信息(返回模型对象) - - 这是一个通用的字典项查询方法,返回字典项模型对象而非字典。 - 与 get_dict_item_detail 的区别: - - get_dict_item_detail: 返回字典格式,用于前端接口 - - get_info: 返回模型对象,用于内部服务调用 - - 此方法被以下方法复用: - - get_dict_item_detail: 获取详情字典 - - update_dict_item: 验证字典项是否存在 - - Args: - dict_item_id: 字典项ID - - Returns: - DictItem/None: 字典项模型对象,不存在或已删除时返回 None - - Example: - dict_item = get_info(dict_item_id) - if dict_item: - print(dict_item.name) - """ - # 参数有效性验证 - if not dict_item_id or dict_item_id <= 0: - return None - - try: - # 查询未删除的字典项 - dict_item = DictItem.query.filter( - and_(DictItem.id == dict_item_id, DictItem.is_delete == 0) - ).first() - return dict_item - except Exception as e: - logger.error(f"查询字典项信息异常,ID: {dict_item_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 字典项添加 -# ============================================================ - -def add_dict_item(): - """ - 添加字典项 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证(字段格式等) - 3. 创建字典项对象并保存到数据库 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = DictItemForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - # 查询配置(验证配置是否存在) - dict_id = form.dictId.data - dict = Dict.query.filter(Dict.id == dict_id).first() - if not dict: - return R.failed(msg="字典不存在") - - # 创建字典项 - dict_item = DictItem(**form.data) - dict_item.dict_code = dict.code - dict_item.create_user = get_username() - dict_item.save() - - return R.ok(msg="添加成功") - - except Exception as e: - logger.error(f"添加字典项异常: {str(e)}") - return R.failed(msg="添加失败,请稍后重试") - - -# ============================================================ -# 字典项更新 -# ============================================================ - -def update_dict_item(): - """ - 更新字典项 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证 - 3. 验证字典项是否存在(复用 get_info) - 4. 更新字典项信息 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = DictItemForm(data=json_data) - if not form.validate(): - return R.failed(msg=regular.get_err(form)) - - # 获取字典项ID - dict_item_id = form.data.get('id') - if not dict_item_id or int(dict_item_id) <= 0: - return R.failed("字典项ID无效") - dict_item_id = int(dict_item_id) - - # 复用 get_info 方法验证字典项是否存在 - dict_item = get_info(dict_item_id) - if not dict_item: - return R.failed("字典项不存在") - - # 更新字典项信息(移除ID,避免更新主键) - form_data = convert_camel_to_snake(form.data.copy()) - form_data.pop('id', None) - - form_data['update_time'] = datetime.now(tz=timezone.utc) - form_data['update_user'] = get_username() - - result = DictItem.query.filter_by(id=dict_item_id).update(form_data) - - if not result: - return R.failed("更新失败") - - return R.ok(msg="更新成功") - - except Exception as e: - logger.error(f"更新字典项异常: {str(e)}") - return R.failed(msg="更新字典项失败,请稍后重试") - - -# ============================================================ -# 字典项删除 -# ============================================================ - -def delete_dict_items(dict_item_ids): - """ - 删除字典项(批量删除) - - 删除 - 支持逗号分隔的多个ID批量删除。 - - Args: - dict_item_ids: 字典项ID字符串,多个ID用逗号分隔,如 "1,2,3" - - Returns: - R: 操作结果 - """ - return DictItem.batch_delete_r(dict_item_ids) +from src.modules.dictionary.service import * # noqa: F401,F403 diff --git a/application/services/file_template.py b/application/services/file_template.py index d06c2cf..c9f4826 100644 --- a/application/services/file_template.py +++ b/application/services/file_template.py @@ -1,339 +1,8 @@ """ -文件模板管理服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了文件模板管理相关的所有业务逻辑。 -包括文件模板的分页查询、详情查询、添加、更新、删除、文件上传下载等功能。 - -命名规范: -- 函数参数使用驼峰命名(如 templateId, pageNo) -- 表单字段使用下划线命名(如 name, code, sort)- 与数据库字段保持一致 -- 返回数据使用驼峰命名(如 createUser, updateTime)- 与前端规范保持一致 -- 私有函数以下划线开头(如 _apply_filters, _build_page_data) - -功能模块: -1. 分页查询:支持按模板名称模糊搜索、编码模糊搜索,按sort字段排序 -2. 详情查询:根据ID获取单个文件模板的详细信息 -3. 数据维护:文件模板的添加、更新、删除(支持批量) -4. 文件上传:上传模板文件并保存文件信息 -5. 文件下载:下载指定模板的文件 -6. 数据列表:获取所有文件模板列表(用于下拉选择等场景) +服务已迁移至 src.modules.file_template.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging - -from datetime import datetime, timezone -from flask import request -from sqlalchemy import and_ - -from application.constants.common import PAGE_SIZE -from application.forms.file_template import FileTemplateForm -from application.models.file_template import FileTemplate -from extensions import db -from utils import R, regular -from utils.case_converter import convert_camel_to_snake -from utils.model_utils import models_to_list, model_to_dict -from utils.request import parse_request_body, parse_id_list, like_contains -from utils.rich_text import save_file, get_file_url -from utils.security import get_username - -# 获取日志记录器 -logger = logging.getLogger(__name__) - - -# ============================================================ -# 文件模板分页查询 -# ============================================================ - -def get_file_template_page(): - """ - 查询文件模板分页数据 - - Args: - request: Flask request对象,包含查询参数 - - pageNo: 当前页码(可选,默认为1) - - pageSize: 每页记录数(可选,默认为PAGE_SIZE) - - name: 模板名称(可选,模糊查询) - - code: 模板编码(可选,模糊查询) - - Returns: - R: 包含分页数据的JSON响应,格式为: - { - "records": 模板列表, - "total": 总记录数, - "size": 每页大小, - "current": 当前页码, - "pages": 总页数 - } - """ - try: - # 获取分页参数 - page_no = int(request.args.get('pageNo', 1)) - page_size = int(request.args.get('pageSize', PAGE_SIZE)) - - # 应用筛选条件 - query = _apply_filters() - - # 记录总数 - total = query.count() - # 计算总页数 - pages = (total + page_size - 1) // page_size if total > 0 else 0 - - # 分页查询 - template_list = query.limit(page_size).offset((page_no - 1) * page_size).all() - # 构建返回数据 - page_data = _build_page_data(template_list, total, page_size, page_no, pages) - return R.ok(data=page_data) - - except Exception as e: - logger.error(f"查询文件模板分页数据异常: {str(e)}") - return R.failed(msg="查询失败,请稍后重试") - - -def _apply_filters(): - """ - 应用查询筛选条件 - - 根据请求参数动态构建查询条件: - - 自动过滤已删除记录(is_delete == 0) - - 支持模板名称模糊查询 - - 支持模板编码模糊查询 - - 默认按排序值升序排列 - - Returns: - Query: 应用筛选后的SQLAlchemy查询对象 - """ - # 基础查询:过滤已删除记录 - query = FileTemplate.query.filter(FileTemplate.is_delete == 0) - - # 模板名称模糊筛选 - name = request.args.get('name') - if name: - query = query.filter(FileTemplate.name.like(like_contains(name), escape='\\')) - - # 模板编码模糊筛选 - code = request.args.get('code') - if code: - query = query.filter(FileTemplate.code.like(like_contains(code), escape='\\')) - - # 按排序值升序排列 - return query.order_by(FileTemplate.sort.asc()) - - -def _build_page_data(template_list, total, page_size, page_no, pages): - """ - 构建分页返回数据 - - Args: - template_list: 文件模板对象列表 - total: 总记录数 - page_size: 每页大小 - page_no: 当前页码 - pages: 总页数 - - Returns: - dict: 分页数据,包含 records、total、size、current、pages - """ - # 将模型对象列表转换为字典列表 - records = models_to_list(template_list) - - return { - 'records': records, - 'total': total, - 'size': page_size, - 'current': page_no, - 'pages': pages - } - - -# ============================================================ -# 文件模板详情查询 -# ============================================================ - -def get_file_template_detail(template_id): - """ - 根据ID查询文件模板详情(返回字典格式) - - 此方法调用 get_info 获取模型对象,然后转换为字典格式返回。 - 适用于前端接口调用。 - - Args: - template_id: 文件模板ID - - Returns: - dict/None: 文件模板详情字典,不存在时返回 None - """ - try: - # 复用 get_info 方法查询模板 - template = get_info(template_id) - if not template: - return None - # 转换为字典格式 - data = model_to_dict(template) - # 头像URL - data['filePath'] = get_file_url(data.get('filePath')) - # 返回结果 - return data - - except Exception as e: - logger.error(f"查询文件模板详情异常,ID: {template_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 文件模板信息查询(根据ID)- 核心查询方法 -# ============================================================ - -def get_info(template_id: int): - """ - 根据ID查询文件模板信息(返回模型对象) - - 这是一个通用的模板查询方法,返回模板模型对象而非字典。 - - Args: - template_id: 文件模板ID - - Returns: - FileTemplate/None: 文件模板模型对象,不存在或已删除时返回 None - """ - # 参数有效性验证 - if not template_id or template_id <= 0: - return None - - try: - # 查询未删除的模板 - template = FileTemplate.query.filter( - and_(FileTemplate.id == template_id, FileTemplate.is_delete == 0) - ).first() - return template - except Exception as e: - logger.error(f"查询文件模板信息异常,ID: {template_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 文件模板添加 -# ============================================================ - -def add_file_template(): - """ - 添加文件模板 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证(编码唯一性、字段格式等) - 3. 创建文件模板对象并保存到数据库 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = FileTemplateForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - # 文件处理 - file_path = form.data.get('filePath') - if file_path: - form.filePath.data = save_file(file_path, "file_template") - - # 创建文件模板 - template = FileTemplate(**form.data) - template.create_user = get_username() - template.save() - - return R.ok(msg="添加成功") - - except Exception as e: - logger.error(f"添加文件模板异常: {str(e)}") - return R.failed(msg="添加文件模板失败,请稍后重试") - - -# ============================================================ -# 文件模板更新 -# ============================================================ - -def update_file_template(): - """ - 更新文件模板 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证 - 3. 验证模板是否存在(复用 get_info) - 4. 更新模板信息 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = FileTemplateForm(data=json_data) - if not form.validate(): - return R.failed(msg=regular.get_err(form)) - - # 获取模板ID - template_id = form.data.get('id') - if not template_id or int(template_id) <= 0: - return R.failed("模板ID无效") - template_id = int(template_id) - - # 复用 get_info 方法验证模板是否存在 - template = get_info(template_id) - if not template: - return R.failed("模板不存在") - - # 文件处理 - file_path = form.data.get('filePath') - if file_path: - form.filePath.data = save_file(file_path, "file_template") - - # 更新模板信息(移除ID,避免更新主键) - form_data = convert_camel_to_snake(form.data.copy()) - form_data.pop('id', None) - - form_data['update_time'] = datetime.now(tz=timezone.utc) - form_data['update_user'] = get_username() - - result = FileTemplate.query.filter_by(id=template_id).update(form_data) - - if not result: - return R.failed("更新失败") - - return R.ok(msg="更新成功") - - except Exception as e: - logger.error(f"更新文件模板异常: {str(e)}") - return R.failed(msg="更新文件模板失败,请稍后重试") - - -# ============================================================ -# 文件模板删除 -# ============================================================ - -def delete_file_templates(template_ids): - """ - 删除文件模板(批量删除) - - 删除 - 支持逗号分隔的多个ID批量删除。 - - Args: - template_ids: 模板ID字符串,多个ID用逗号分隔,如 "1,2,3" - - Returns: - R: 操作结果 - """ - return FileTemplate.batch_delete_r(template_ids) +from src.modules.file_template.service import * # noqa: F401,F403 diff --git a/application/services/job.py b/application/services/job.py index 066f751..72a3dad 100644 --- a/application/services/job.py +++ b/application/services/job.py @@ -1,349 +1,8 @@ """ -定时任务管理服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了定时任务管理相关的所有业务逻辑。 +服务已迁移至 src.modules.job.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging - -from flask import request - -from application.constants.common import PAGE_SIZE -from application.forms.job import JobForm, JobStatusForm -from application.models.job import Job -from application.services.scheduler import ( - add_job_to_scheduler, - remove_job_from_scheduler, - pause_job_in_scheduler, - run_job_once as scheduler_run_job_once, - reload_scheduler -) -from utils import R, regular -from utils.model_utils import models_to_list, model_to_dict -from utils.request import parse_request_body, parse_id_list, like_contains -from utils.security import get_username - -logger = logging.getLogger(__name__) - - -# ============================================================ -# 任务分页查询 -# ============================================================ - -def get_job_page(): - """查询定时任务分页数据""" - try: - page_no = int(request.args.get('pageNo', 1)) - page_size = int(request.args.get('pageSize', PAGE_SIZE)) - - query = Job.query.filter(Job.is_delete == 0) - - # 筛选条件 - job_name = request.args.get('jobName') - if job_name: - query = query.filter(Job.job_name.like(like_contains(job_name), escape='\\')) - - status = request.args.get('status') - if status: - query = query.filter(Job.status == status) - - job_group = request.args.get('jobGroup') - if job_group: - query = query.filter(Job.job_group == job_group) - - total = query.count() - pages = (total + page_size - 1) // page_size if total > 0 else 0 - - job_list = query.order_by(Job.create_time.desc()) \ - .limit(page_size) \ - .offset((page_no - 1) * page_size) \ - .all() - - records = models_to_list(job_list) - - # 添加状态文本 - for record in records: - record['statusText'] = _get_status_text(record.get('status')) - record['executePolicyText'] = _get_execute_policy_text(record.get('execute_policy')) - - page_data = { - 'records': records, - 'total': total, - 'size': page_size, - 'current': page_no, - 'pages': pages - } - return R.ok(data=page_data) - - except Exception as e: - logger.error(f"查询任务分页异常: {str(e)}") - return R.failed(msg="查询失败") - - -def _get_status_text(status): - """获取状态文本""" - status_map = {0: '未发布', 1: '运行中', 2: '暂停', 3: '删除'} - return status_map.get(status, '') - - -def _get_execute_policy_text(policy): - """获取执行策略文本""" - policy_map = {1: '立即执行', 2: '执行一次', 3: '放弃执行'} - return policy_map.get(policy, '') - - -# ============================================================ -# 任务详情查询 -# ============================================================ - -def get_job_detail(job_id): - """查询定时任务详情""" - try: - job = Job.query.filter(Job.id == job_id, Job.is_delete == 0).first() - if not job: - return None - return model_to_dict(job) - except Exception as e: - logger.error(f"查询任务详情异常: {str(e)}") - return None - - -# ============================================================ -# 任务添加 -# ============================================================ - -def add_job(): - """添加定时任务""" - try: - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - form = JobForm(data=json_data) - if not form.validate(): - return R.failed(msg=regular.get_err(form)) - - job = Job(**form.data) - job.create_user = get_username() - job.save() - - # 添加到调度器 - if job.status == 1 and job.cron_expression: - add_job_to_scheduler(job) - - return R.ok(msg="添加成功") - - except Exception as e: - logger.error(f"添加任务异常: {str(e)}") - return R.failed(msg="添加任务失败,请稍后重试") - - -# ============================================================ -# 任务更新 -# ============================================================ - -def update_job(): - """更新定时任务""" - try: - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - form = JobForm(data=json_data) - if not form.validate(): - return R.failed(msg=regular.get_err(form)) - - job_id = form.data.get('id') - if not job_id: - return R.failed("任务ID无效") - - job = Job.query.filter(Job.id == job_id, Job.is_delete == 0).first() - if not job: - return R.failed("任务不存在") - - old_status = job.status - old_cron = job.cron_expression - - # 更新字段 - for key, value in form.data.items(): - if key != 'id' and hasattr(job, key): - setattr(job, key, value) - - job.update_user = get_username() - job.save() - - # 处理调度器 - _handle_scheduler_update(job, old_status, old_cron) - - return R.ok(msg="更新成功") - - except Exception as e: - logger.error(f"更新任务异常: {str(e)}") - return R.failed(msg="更新任务失败,请稍后重试") - - -def _handle_scheduler_update(job, old_status, old_cron): - """处理调度器更新""" - # 变为运行中 - if old_status != 1 and job.status == 1 and job.cron_expression: - add_job_to_scheduler(job) - # 从运行中变为其他状态 - elif old_status == 1 and job.status != 1: - remove_job_from_scheduler(job.id) - # 运行中但Cron变化 - elif job.status == 1 and old_cron != job.cron_expression: - if job.cron_expression: - add_job_to_scheduler(job) - else: - remove_job_from_scheduler(job.id) - - -# ============================================================ -# 任务删除 -# ============================================================ - -def delete_jobs(job_ids): - """删除定时任务(批量)""" - if not job_ids: - return R.failed("记录ID不存在") - - id_list = parse_id_list(job_ids) - if not id_list: - return R.failed("无效的记录ID") - - for job_id in id_list: - remove_job_from_scheduler(job_id) - - return Job.batch_delete_r(job_ids) - - -# ============================================================ -# 任务状态设置 -# ============================================================ - -def update_job_status(): - """设置任务状态""" - try: - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - form = JobStatusForm(data=json_data) - if not form.validate(): - return R.failed(msg=regular.get_err(form)) - - job_id = int(form.data['id']) - target_status = int(form.data['status']) - - job = Job.query.filter(Job.id == job_id, Job.is_delete == 0).first() - if not job: - return R.failed("任务不存在") - - old_status = job.status - job.status = target_status - job.update_user = get_username() - job.save() - - # 处理调度器 - if target_status == 1 and job.cron_expression: - add_job_to_scheduler(job) - elif target_status == 2: - pause_job_in_scheduler(job_id) - elif old_status == 1 and target_status != 1: - remove_job_from_scheduler(job_id) - - return R.ok(msg="设置成功") - - except Exception as e: - logger.error(f"设置任务状态异常: {str(e)}") - return R.failed(msg="设置任务状态失败,请稍后重试") - - -# ============================================================ -# 暂停任务 -# ============================================================ - -def pause_job(job_id): - """暂停任务""" - try: - job = Job.query.filter(Job.id == job_id, Job.is_delete == 0).first() - if not job: - return R.failed("任务不存在") - - if job.status == 2: - return R.ok(msg="任务已处于暂停状态") - - job.status = 2 - job.update_user = get_username() - job.save() - - pause_job_in_scheduler(job_id) - - return R.ok(msg="暂停成功") - - except Exception as e: - logger.error(f"暂停任务异常: {str(e)}") - return R.failed(msg="暂停任务失败,请稍后重试") - - -# ============================================================ -# 恢复任务 -# ============================================================ - -def resume_job(job_id): - """恢复任务""" - try: - job = Job.query.filter(Job.id == job_id, Job.is_delete == 0).first() - if not job: - return R.failed("任务不存在") - - if job.status == 1: - return R.ok(msg="任务已处于运行中状态") - - if not job.cron_expression: - return R.failed("Cron表达式为空,无法恢复") - - job.status = 1 - job.update_user = get_username() - job.save() - - # 直接传入模型对象,与其它方法保持一致 - add_job_to_scheduler(job) - - return R.ok(msg="恢复成功") - - except Exception as e: - logger.error(f"恢复任务异常: {str(e)}") - return R.failed(msg="恢复任务失败,请稍后重试") - - -# ============================================================ -# 立即执行 -# ============================================================ - -def run_job_once(job_id): - """立即执行一次任务""" - try: - result = scheduler_run_job_once(job_id) - if result.get('success'): - return R.ok(data=result, msg="执行成功") - else: - return R.failed(msg=result.get('error', '执行失败')) - except Exception as e: - logger.error(f"立即执行异常: {str(e)}") - return R.failed(msg="执行失败") - - -# ============================================================ -# 重新加载调度器 -# ============================================================ - -def reload_all_jobs(): - """重新加载所有任务""" - try: - count = reload_scheduler() - return R.ok(data=count, msg=f"已重新加载 {count} 个任务") - except Exception as e: - logger.error(f"重新加载异常: {str(e)}") - return R.failed(msg="重新加载失败") +from src.modules.job.service import * # noqa: F401,F403 diff --git a/application/services/job_executor.py b/application/services/job_executor.py index efd2570..55d45dd 100644 --- a/application/services/job_executor.py +++ b/application/services/job_executor.py @@ -1,165 +1,8 @@ """ -任务执行器模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了根据触发器标识执行对应任务的功能。 +服务已迁移至 src.modules.job.executor。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging -from typing import Dict, Any, Optional - -logger = logging.getLogger(__name__) - - -# ============================================================ -# 任务执行器 -# ============================================================ - -class JobExecutor: - """ - 任务执行器 - - 根据触发器标识调用对应的内部方法执行任务。 - """ - - def __init__(self): - """初始化执行器,注册默认任务""" - self._handlers = {} - self._register_default_handlers() - - def _register_default_handlers(self): - """注册默认的任务处理器""" - # 示例:短信发送任务 - self.register('send_sms', self._send_sms) - # 示例:邮件发送任务 - self.register('send_email', self._send_email) - # 示例:数据同步任务 - self.register('sync_data', self._sync_data) - - def register(self, trigger: str, handler): - """ - 注册任务处理器 - - Args: - trigger: 触发器标识 - handler: 处理函数 - """ - self._handlers[trigger] = handler - logger.info(f"注册任务处理器: {trigger}") - - def execute(self, trigger: str, **kwargs) -> Any: - """ - 执行任务 - - Args: - trigger: 触发器标识 - **kwargs: 执行参数 - - Returns: - Any: 执行结果 - """ - if trigger not in self._handlers: - logger.error(f"未找到触发器对应的方法: {trigger}") - raise KeyError(f"未找到触发器对应的方法: {trigger}") - - handler = self._handlers[trigger] - logger.info(f"执行任务: {trigger}, 参数: {kwargs}") - return handler(**kwargs) - - # ============================================================ - # 默认任务处理器 - # ============================================================ - - def _send_sms(self, **kwargs) -> Dict: - """ - 发送短信任务 - - Args: - **kwargs: 包含 phone, template_code, template_params, sign_name 等参数 - - Returns: - Dict: 执行结果 - """ - from application.services import sms_svc - - phone = kwargs.get('phone') - template_code = kwargs.get('template_code') - template_params = kwargs.get('template_params') - sign_name = kwargs.get('sign_name') - job_id = kwargs.get('job_id') - - logger.info(f"发送短信 [任务ID: {job_id}, 手机号: {phone}, 模板: {template_code}]") - - success = sms_svc.send_sms( - phone=phone, - template_code=template_code, - template_params=template_params, - sign_name=sign_name, - ) - - return { - 'success': success, - 'message': '短信发送成功' if success else '短信发送失败', - 'phone': phone, - 'template_code': template_code - } - - def _send_email(self, **kwargs) -> Dict: - """ - 发送邮件任务 - - Args: - **kwargs: 包含 to, subject, body, html 等参数 - - Returns: - Dict: 执行结果 - """ - from application.services import mail_svc - - to = kwargs.get('to') - subject = kwargs.get('subject') - body = kwargs.get('body') - html = kwargs.get('html') - cc = kwargs.get('cc') - job_id = kwargs.get('job_id') - - logger.info(f"发送邮件 [任务ID: {job_id}, 收件人: {to}, 主题: {subject}]") - - success = mail_svc.send_mail(to=to, subject=subject, body=body, html=html, cc=cc) - - return { - 'success': success, - 'message': '邮件发送成功' if success else '邮件发送失败', - 'to': to, - 'subject': subject - } - - def _sync_data(self, **kwargs) -> Dict: - """ - 数据同步任务 - - Args: - **kwargs: 包含 source, target, sync_type 等参数 - - Returns: - Dict: 执行结果 - """ - source = kwargs.get('source') - target = kwargs.get('target') - sync_type = kwargs.get('sync_type') - job_id = kwargs.get('job_id') - - logger.info(f"数据同步 [任务ID: {job_id}, 源: {source}, 目标: {target}, 类型: {sync_type}]") - - # TODO: 调用实际的数据同步服务 - - return { - 'success': True, - 'message': '数据同步成功', - 'source': source, - 'target': target - } - - -# 创建全局执行器实例 -job_executor = JobExecutor() +from src.modules.job.executor import * # noqa: F401,F403 diff --git a/application/services/level.py b/application/services/level.py index 7b6a07a..bd4eb31 100644 --- a/application/services/level.py +++ b/application/services/level.py @@ -1,589 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - - """ -职级管理服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了职级管理相关的所有业务逻辑。 -包括职级的分页查询、详情查询、添加、更新、删除、状态设置、导入导出等功能。 -职级用于标识用户的职位等级,如:初级、中级、高级等, -每个职级包含名称、排序、状态等属性,通过sort字段控制显示顺序。 - -命名规范: -- 函数参数使用驼峰命名(如 levelId, pageNo) -- 表单字段使用下划线命名(如 name, sort, status)- 与数据库字段保持一致 -- 返回数据使用驼峰命名(如 createUser, updateTime)- 与前端规范保持一致 -- 私有函数以下划线开头(如 _apply_filters, _build_page_data) - -功能模块: -1. 分页查询:支持按名称模糊搜索、状态筛选,按sort字段排序 -2. 详情查询:根据ID获取单个职级的详细信息 -3. 数据维护:职级的添加、更新、删除(支持批量) -4. 状态管理:单独更新职级的启用/禁用状态 -5. Excel导入:批量导入职级数据 -6. Excel导出:导出职级数据到Excel文件 -7. 数据列表:获取所有启用的职级列表(用于下拉选择等场景) +服务已迁移至 src.modules.system.level.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import os -import random -import time -import logging - -from datetime import datetime, timezone -from flask import request -from openpyxl.reader.excel import load_workbook -from openpyxl.workbook import Workbook -from sqlalchemy import and_ - -import utils - -from application.constants.common import PAGE_SIZE -from application.forms.level import LevelForm, LevelStatusForm -from application.models.level import Level -from application.services import upload -from config import FLASK_TEMP_PATH, FLASK_UPLOAD_DIR -from extensions import db -from utils import R, regular -from utils.case_converter import convert_camel_to_snake -from utils.model_utils import models_to_list, model_to_dict -from utils.request import parse_request_body, parse_id_list, like_contains -from utils.rich_text import get_file_url -from utils.file import mkdir -from utils.security import get_username - -# 获取日志记录器 -logger = logging.getLogger(__name__) - - -# ============================================================ -# 职级分页查询 -# ============================================================ - -def get_level_page(): - """ - 查询职级分页数据 - - Args: - request: Flask request对象,包含查询参数 - - pageNo: 当前页码(可选,默认为1) - - pageSize: 每页记录数(可选,默认为PAGE_SIZE) - - name: 职级名称(可选,模糊查询) - - status: 职级状态(可选,精确匹配) - - Returns: - R: 包含分页数据的JSON响应,格式为: - { - "records": 职级列表, - "total": 总记录数, - "size": 每页大小, - "current": 当前页码, - "pages": 总页数 - } - """ - try: - # 获取分页参数 - page_no = int(request.args.get('pageNo', 1)) - page_size = int(request.args.get('pageSize', PAGE_SIZE)) - - # 应用筛选条件 - query = _apply_filters() - - # 记录总数 - total = query.count() - # 计算总页数 - pages = (total + page_size - 1) // page_size if total > 0 else 0 - - # 分页查询 - level_list = query.limit(page_size).offset((page_no - 1) * page_size).all() - # 构建返回数据 - page_data = _build_page_data(level_list, total, page_size, page_no, pages) - return R.ok(data=page_data) - - except Exception as e: - logger.error(f"查询职级分页数据异常: {str(e)}") - return R.failed(msg="查询失败,请稍后重试") - - -def _apply_filters(): - """ - 应用查询筛选条件 - - 根据请求参数动态构建查询条件: - - 自动过滤已删除记录(is_delete == 0) - - 支持职级名称模糊查询 - - 支持职级状态精确查询 - - 默认按排序值升序排列 - - Returns: - Query: 应用筛选后的SQLAlchemy查询对象 - """ - # 基础查询:过滤已删除记录 - query = Level.query.filter(Level.is_delete == 0) - - # 职级名称模糊筛选 - name = request.args.get('name') - if name: - query = query.filter(Level.name.like(like_contains(name), escape='\\')) - - # 职级状态精确筛选 - status = request.args.get('status') - if status: - query = query.filter(Level.status == status) - - # 按排序值升序排列 - return query.order_by(Level.sort.asc()) - - -def _build_page_data(level_list, total, page_size, page_no, pages): - """ - 构建分页返回数据 - - Args: - level_list: 职级对象列表 - total: 总记录数 - page_size: 每页大小 - page_no: 当前页码 - pages: 总页数 - - Returns: - dict: 分页数据,包含 records、total、size、current、pages - """ - # 将模型对象列表转换为字典列表 - records = models_to_list(level_list) - - return { - 'records': records, - 'total': total, - 'size': page_size, - 'current': page_no, - 'pages': pages - } - - -# ============================================================ -# 职级详情查询 -# ============================================================ - -def get_level_detail(level_id): - """ - 根据ID查询职级详情(返回字典格式) - - 此方法调用 get_info 获取模型对象,然后转换为字典格式返回。 - 适用于前端接口调用。 - - Args: - level_id: 职级ID - - Returns: - dict/None: 职级详情字典,不存在时返回 None - """ - try: - # 复用 get_info 方法查询职级 - level = get_info(level_id) - if not level: - return None - # 转换为字典格式 - return model_to_dict(level) - - except Exception as e: - logger.error(f"查询职级详情异常,ID: {level_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 职级信息查询(根据ID)- 核心查询方法 -# ============================================================ - -def get_info(level_id: int): - """ - 根据ID查询职级信息(返回模型对象) - - 这是一个通用的职级查询方法,返回职级模型对象而非字典。 - 与 get_level_detail 的区别: - - get_level_detail: 返回字典格式,用于前端接口 - - get_info: 返回模型对象,用于内部服务调用 - - 此方法被以下方法复用: - - get_level_detail: 获取详情字典 - - update_level: 验证职级是否存在 - - update_level_status: 验证职级是否存在 - - Args: - level_id: 职级ID - - Returns: - Level/None: 职级模型对象,不存在或已删除时返回 None - - Example: - level = get_info(level_id) - if level: - print(level.name) - """ - # 参数有效性验证 - if not level_id or level_id <= 0: - return None - - try: - # 查询未删除的职级 - level = Level.query.filter( - and_(Level.id == level_id, Level.is_delete == 0) - ).first() - return level - except Exception as e: - logger.error(f"查询职级信息异常,ID: {level_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 职级添加 -# ============================================================ - -def add_level(): - """ - 添加职级 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证(名称唯一性、字段格式等) - 3. 创建职级对象并保存到数据库 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = LevelForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - # 创建职级 - level = Level(**form.data) - level.create_user = get_username() - level.save() - - return R.ok(msg="添加成功") - - except Exception as e: - logger.error(f"添加职级异常: {str(e)}") - return R.failed(msg="添加失败,请稍后重试") - - -# ============================================================ -# 职级更新 -# ============================================================ - -def update_level(): - """ - 更新职级 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证 - 3. 验证职级是否存在(复用 get_info) - 4. 更新职级信息 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = LevelForm(data=json_data) - if not form.validate(): - return R.failed(msg=regular.get_err(form)) - - # 获取职级ID - level_id = form.data.get('id') - if not level_id or int(level_id) <= 0: - return R.failed("职级ID无效") - level_id = int(level_id) - - # 复用 get_info 方法验证职级是否存在 - level = get_info(level_id) - if not level: - return R.failed("职级不存在") - - # 更新职级信息(移除ID,避免更新主键) - form_data = convert_camel_to_snake(form.data.copy()) - form_data.pop('id', None) - - form_data['update_time'] = datetime.now(tz=timezone.utc) - form_data['update_user'] = get_username() - - result = Level.query.filter_by(id=level_id).update(form_data) - - if not result: - return R.failed("更新失败") - - return R.ok(msg="更新成功") - - except Exception as e: - logger.error(f"更新职级异常: {str(e)}") - return R.failed(msg="更新职级失败,请稍后重试") - - -# ============================================================ -# 职级删除 -# ============================================================ - -def delete_levels(level_ids): - """ - 删除职级(批量删除) - - 删除 - 支持逗号分隔的多个ID批量删除。 - - Args: - level_ids: 职级ID字符串,多个ID用逗号分隔,如 "1,2,3" - - Returns: - R: 操作结果 - """ - return Level.batch_delete_r(level_ids) - - -# ============================================================ -# 职级状态设置 -# ============================================================ - -def update_level_status(): - """ - 设置职级状态 - - 单独更新职级的启用/禁用状态,不涉及其他字段。 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = LevelStatusForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - level_id = int(form.data['id']) - status = int(form.data['status']) - - # 复用 get_info 方法验证职级是否存在 - level = get_info(level_id) - if not level: - return R.failed("职级不存在") - - # 更新状态 - update_data = {"status": status, "update_time": datetime.now(tz=timezone.utc), "update_user": get_username()} - result = Level.query.filter_by(id=level_id).update(update_data) - - if not result: - return R.failed("设置失败") - - return R.ok(msg="设置成功") - - except Exception as e: - logger.error(f"设置职级状态异常: {str(e)}") - return R.failed(msg="设置职级状态失败,请稍后重试") - - -# ============================================================ -# Excel导入 -# ============================================================ - -def import_levels(): - """ - 导入Excel文件批量导入职级 - - 功能说明: - 通过上传Excel文件批量导入职级数据。 - Excel格式要求: - - 第2列:职级名称 - - 第3列:职级排序 - - 第4列:职级状态("正常" 或 "禁用") - - Returns: - R: 导入结果,包含成功数量 - """ - try: - # 上传并读取Excel文件 - file = upload.upload_file(".xls|.xlsx") - path = file['filePath'] - - wb = load_workbook(path) - ws = wb.worksheets[0] - - max_row = ws.max_row - level_list = [] - current_user = get_username() - - # 遍历读取数据(从第2行开始,跳过表头) - for item in range(2, max_row + 1): - name = ws.cell(item, 2).value # 职级名称 - sort = ws.cell(item, 3).value # 职级排序 - status_value = ws.cell(item, 4).value # 职级状态 - - # 跳过空行 - if not name: - continue - - # 创建职级对象 - level = Level( - id=None, - name=name, - status=1 if status_value == "正常" else 2, - sort=sort, - ) - level.create_user = current_user - level_list.append(level) - - # 批量添加 - db.session.add_all(level_list) - - return R.ok(msg="本次共导入【{}】条数据".format(len(level_list))) - - except Exception as e: - logger.error(f"导入职级异常: {str(e)}") - return R.failed(msg="导入职级失败,请稍后重试") - finally: - # 清理导入产生的临时Excel文件 - try: - if 'path' in dir() and path: - abs_path = os.path.join(FLASK_UPLOAD_DIR, path.lstrip('/')) - if os.path.isfile(abs_path): - os.remove(abs_path) - except Exception: - pass - - -# ============================================================ -# Excel导出 -# ============================================================ - -def export_levels(): - """ - 导出职级数据到Excel文件 - - 导出当前筛选条件下的所有职级数据,生成Excel文件并返回下载URL。 - - Returns: - R: 导出结果,包含文件访问URL - """ - try: - # 获取筛选后的数据列表 - query_list = _apply_filters().all() - # 转换为字典列表 - level_list = models_to_list(query_list) - - # 创建Excel工作簿 - wb = Workbook() - ws = wb.worksheets[0] - # 写入表头 - ws.append(['职级ID', '职级名称', '职级排序', '职级状态', '创建时间', '更新时间']) - - # 写入数据 - for data in level_list: - ws.append(list(data.values())) - - # 保存文件 - save_path = FLASK_TEMP_PATH + "/" + time.strftime('%Y%m%d') - mkdir(save_path) - - file_name = time.strftime('%Y%m%d%H%M%S') + '%05d' % random.randint(0, 100) + ".xlsx" - file_path = save_path + "/" + file_name - - wb.save(file_path) - - # 生成文件访问地址 - file_url = get_file_url(file_path.replace(FLASK_UPLOAD_DIR, "")) - - return R.ok(data=file_url) - - except Exception as e: - logger.error(f"导出职级异常: {str(e)}") - return R.failed(msg="导出失败,请稍后重试") - - -# ============================================================ -# 职级列表获取(无分页) -# ============================================================ - -def get_level_list(): - """ - 获取所有启用的职级列表(无分页) - - 用于下拉选择框等需要全部数据的场景。 - 只返回状态为"正常"的职级,按排序值升序排列。 - - Returns: - list: 职级列表,每个元素为职级模型字典,无数据时返回空列表 - """ - try: - # 查询所有启用的职级 - level_list = Level.query.filter( - and_(Level.is_delete == 0, Level.status == 1) - ).order_by(Level.sort.asc()).all() - - # 转换为字典列表 - result = [] - for level in level_list: - result.append(utils.load2dict(level)) - - return result - - except Exception as e: - logger.error(f"获取职级列表异常: {str(e)}") - return [] +from src.modules.system.level.service import * # noqa: F401,F403 diff --git a/application/services/link.py b/application/services/link.py index 3d3c98e..a6e902a 100644 --- a/application/services/link.py +++ b/application/services/link.py @@ -1,469 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - - """ -友链管理服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了友链管理相关的所有业务逻辑。 -包括友链的分页查询、详情查询、添加、更新、删除、状态设置等功能。 -友链用于管理友情链接和合作伙伴,支持文字链接和图片链接两种形式, -每个友链包含名称、链接、类型、平台、形式、排序、状态等属性。 - -命名规范: -- 函数参数使用驼峰命名(如 linkId, pageNo) -- 表单字段使用下划线命名(如 name, url, sort)- 与数据库字段保持一致 -- 返回数据使用驼峰命名(如 createUser, updateTime)- 与前端规范保持一致 -- 私有函数以下划线开头(如 _apply_filters, _build_page_data) - -功能模块: -1. 分页查询:支持按名称模糊搜索、类型/平台/形式/状态筛选,按sort字段倒序排列 -2. 详情查询:根据ID获取单个友链的详细信息 -3. 数据维护:友链的添加、更新、删除(支持批量) -4. 状态管理:单独更新友链的启用/禁用状态 +服务已迁移至 src.modules.cms.link.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging - -from datetime import datetime, timezone -from flask import request -from sqlalchemy import and_ - -from application.constants.common import PAGE_SIZE -from application.constants.dict import LINK_TYPE_LIST, LINK_FORM_LIST -from application.forms.link import LinkForm, LinkStatusForm -from application.models.link import Link -from extensions import db -from utils import R, regular -from utils.case_converter import convert_camel_to_snake -from utils.model_utils import models_to_list, model_to_dict -from utils.request import parse_request_body, parse_id_list, like_contains -from utils.rich_text import get_file_url, save_file -from utils.security import get_username - -# 获取日志记录器 -logger = logging.getLogger(__name__) - - -# ============================================================ -# 友链分页查询 -# ============================================================ - -def get_link_page(): - """ - 查询友链分页数据 - - Args: - request: Flask request对象,包含查询参数 - - pageNo: 当前页码(可选,默认为1) - - pageSize: 每页记录数(可选,默认为PAGE_SIZE) - - name: 友链名称(可选,模糊查询) - - type: 友链类型(可选,精确匹配) - - form: 友链形式(可选,精确匹配) - - status: 友链状态(可选,精确匹配) - - Returns: - R: 包含分页数据的JSON响应,格式为: - { - "records": 友链列表, - "total": 总记录数, - "size": 每页大小, - "current": 当前页码, - "pages": 总页数 - } - """ - try: - # 获取分页参数 - page_no = int(request.args.get('pageNo', 1)) - page_size = int(request.args.get('pageSize', PAGE_SIZE)) - - # 应用筛选条件 - query = _apply_filters() - - # 记录总数 - total = query.count() - # 计算总页数 - pages = (total + page_size - 1) // page_size if total > 0 else 0 - - # 分页查询 - link_list = query.limit(page_size).offset((page_no - 1) * page_size).all() - # 构建返回数据 - page_data = _build_page_data(link_list, total, page_size, page_no, pages) - return R.ok(data=page_data) - - except Exception as e: - logger.error(f"查询友链分页数据异常: {str(e)}") - return R.failed(msg="查询失败,请稍后重试") - - -def _apply_filters(): - """ - 应用查询筛选条件 - - 根据请求参数动态构建查询条件: - - 自动过滤已删除记录(is_delete == 0) - - 支持友链名称模糊查询 - - 支持友链类型精确查询 - - 支持投放平台精确查询 - - 支持友链形式精确查询 - - 支持友链状态精确查询 - - 默认按排序值倒序排列 - - Returns: - Query: 应用筛选后的SQLAlchemy查询对象 - """ - # 基础查询:过滤已删除记录 - query = Link.query.filter(Link.is_delete == 0) - - # 友链名称模糊筛选 - name = request.args.get('name') - if name: - query = query.filter(Link.name.like(like_contains(name), escape='\\')) - - # 友链类型精确筛选 - link_type = request.args.get('type') - if link_type: - query = query.filter(Link.type == link_type) - - # 友链形式精确筛选 - form = request.args.get('form') - if form: - query = query.filter(Link.form == form) - - # 友链状态精确筛选 - status = request.args.get('status') - if status: - query = query.filter(Link.status == status) - - # 按排序值倒序排列 - return query.order_by(Link.sort.desc()) - - -def _build_page_data(link_list, total, page_size, page_no, pages): - """ - 构建分页返回数据 - - Args: - link_list: 友链对象列表 - total: 总记录数 - page_size: 每页大小 - page_no: 当前页码 - pages: 总页数 - - Returns: - dict: 分页数据,包含 records、total、size、current、pages - """ - # 将模型对象列表转换为字典列表,并添加额外字段 - records = [] - for item in link_list: - data = model_to_dict(item) - # 添加友链类型描述 - data['typeName'] = LINK_TYPE_LIST.get(item.type) - # 添加友链形式描述 - data['formName'] = LINK_FORM_LIST.get(item.form) - # 友链图片URL - if item.image: - data['image'] = get_file_url(item.image) - records.append(data) - - return { - 'records': records, - 'total': total, - 'size': page_size, - 'current': page_no, - 'pages': pages - } - - -# ============================================================ -# 友链详情查询 -# ============================================================ - -def get_link_detail(link_id): - """ - 根据ID查询友链详情(返回字典格式) - - 此方法调用 get_info 获取模型对象,然后转换为字典格式返回。 - 适用于前端接口调用。 - - Args: - link_id: 友链ID - - Returns: - dict/None: 友链详情字典,不存在时返回 None - """ - try: - # 复用 get_info 方法查询友链 - link = get_info(link_id) - if not link: - return None - # 转换为字典格式 - data = model_to_dict(link) - # 友链图片URL - if link.image: - data['image'] = get_file_url(link.image) - return data - - except Exception as e: - logger.error(f"查询友链详情异常,ID: {link_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 友链信息查询(根据ID)- 核心查询方法 -# ============================================================ - -def get_info(link_id: int): - """ - 根据ID查询友链信息(返回模型对象) - - 这是一个通用的友链查询方法,返回友链模型对象而非字典。 - 与 get_link_detail 的区别: - - get_link_detail: 返回字典格式,用于前端接口 - - get_info: 返回模型对象,用于内部服务调用 - - 此方法被以下方法复用: - - get_link_detail: 获取详情字典 - - update_link: 验证友链是否存在 - - update_link_status: 验证友链是否存在 - - Args: - link_id: 友链ID - - Returns: - Link/None: 友链模型对象,不存在或已删除时返回 None - - Example: - link = get_info(link_id) - if link: - print(link.name) - """ - # 参数有效性验证 - if not link_id or link_id <= 0: - return None - - try: - # 查询未删除的友链 - link = Link.query.filter( - and_(Link.id == link_id, Link.is_delete == 0) - ).first() - return link - except Exception as e: - logger.error(f"查询友链信息异常,ID: {link_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 友链添加 -# ============================================================ - -def add_link(): - """ - 添加友链 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证(字段格式等) - 3. 处理友链图片上传 - 4. 创建友链对象并保存到数据库 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = LinkForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - # 处理友链图片 - image = form.data.get('image') - if image: - form.image.data = save_file(image, "link") - - # 创建友链 - link = Link(**form.data) - link.create_user = get_username() - link.save() - - return R.ok(msg="添加成功") - - except Exception as e: - logger.error(f"添加友链异常: {str(e)}") - return R.failed(msg="添加失败,请稍后重试") - - -# ============================================================ -# 友链更新 -# ============================================================ - -def update_link(): - """ - 更新友链 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证 - 3. 验证友链是否存在(复用 get_info) - 4. 处理友链图片上传 - 5. 更新友链信息 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = LinkForm(data=json_data) - if not form.validate(): - return R.failed(msg=regular.get_err(form)) - - # 获取友链ID - link_id = form.data.get('id') - if not link_id or int(link_id) <= 0: - return R.failed("友链ID无效") - link_id = int(link_id) - - # 复用 get_info 方法验证友链是否存在 - link = get_info(link_id) - if not link: - return R.failed("友链不存在") - - # 处理友链图片 - image = form.data.get('image') - if image: - form.image.data = save_file(image, "link") - - # 更新友链信息(移除ID,避免更新主键) - form_data = convert_camel_to_snake(form.data.copy()) - form_data.pop('id', None) - - form_data['update_time'] = datetime.now(tz=timezone.utc) - form_data['update_user'] = get_username() - - result = Link.query.filter_by(id=link_id).update(form_data) - - if not result: - return R.failed("更新失败") - - return R.ok(msg="更新成功") - - except Exception as e: - logger.error(f"更新友链异常: {str(e)}") - return R.failed(msg="更新友链失败,请稍后重试") - - -# ============================================================ -# 友链删除 -# ============================================================ - -def delete_links(link_ids): - """ - 删除友链(批量删除) - - 删除 - 支持逗号分隔的多个ID批量删除。 - - Args: - link_ids: 友链ID字符串,多个ID用逗号分隔,如 "1,2,3" - - Returns: - R: 操作结果 - """ - return Link.batch_delete_r(link_ids) - - -# ============================================================ -# 友链状态设置 -# ============================================================ - -def update_link_status(): - """ - 设置友链状态 - - 单独更新友链的启用/禁用状态,不涉及其他字段。 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = LinkStatusForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - link_id = int(form.data['id']) - status = int(form.data['status']) - - # 复用 get_info 方法验证友链是否存在 - link = get_info(link_id) - if not link: - return R.failed("友链不存在") - - # 更新状态 - update_data = {"status": status, "update_time": datetime.now(tz=timezone.utc), "update_user": get_username()} - result = Link.query.filter_by(id=link_id).update(update_data) - - if not result: - return R.failed("设置失败") - - return R.ok(msg="设置成功") - - except Exception as e: - logger.error(f"设置友链状态异常: {str(e)}") - return R.failed(msg="设置友链状态失败,请稍后重试") +from src.modules.cms.link.service import * # noqa: F401,F403 diff --git a/application/services/login.py b/application/services/login.py index 2d67fba..2eb1322 100644 --- a/application/services/login.py +++ b/application/services/login.py @@ -1,233 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== +""" +兼容 shim(绞杀者迁移期间保留) +============================================================= -from flask import request, g -from sqlalchemy import and_ - -from application.forms.login import LoginForm -from application.models.login_log import LoginLog -from application.models.user import User -from application.services import login_log -from config import CAPTCHA_BYPASS_CODE, FLASK_DEMO, LOGIN_FAIL_MAX, LOGIN_LOCK_SECONDS -from utils import regular, R -from utils.password import verify_password -from utils.request import parse_request_body -from utils.jwt import create_token -from utils.redis_client import redis -from utils.ip import get_client_ip -from utils.ip2region import get_ip_location -from utils.user_agent import parse_user_agent - - -# 系统登录 -def login(): - """ - 用户登录接口 - - 处理用户登录请求,验证用户名、密码和验证码,登录成功后返回访问令牌 - - Returns: - dict: 包含状态码、消息和数据的响应字典 - - 成功: {"code": 200, "msg": "登录成功", "data": {"access_token": "token值"}} - - 失败: {"code": 非200, "msg": "错误信息"} - """ - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - # 表单验证 - form = LoginForm(data=json_data) - if not form.validate(): - # 获取表单验证失败的错误信息 - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - # 提取表单数据 - username = form.username.data # 登录账号 - password = form.password.data # 登录密码 - code = form.code.data # 验证码 - key = form.key.data # 验证码KEY - - # 验证码校验 - image_code = redis.get(key) - if FLASK_DEMO and CAPTCHA_BYPASS_CODE and code == CAPTCHA_BYPASS_CODE: - pass # 演示模式万能验证码,仅FLASK_DEMO=True时生效 - else: - # 验证码不区分大小写比对 - if not image_code or image_code.lower() != code.lower(): - return R.failed("验证码不正确") - # 验证码使用后立即删除,防止重复使用 - redis.delete(key) - - # 查询用户信息(只查询未删除的用户) - user = User.query.filter( - and_( - User.username == username, - User.is_delete == 0 - ) - ).first() - - # 校验用户是否存在 - if not user: - _record_login(username, LoginLog.Status.ERROR, error="用户不存在") - return R.failed("用户不存在") - - # 校验用户状态(可选,根据业务需求) - if hasattr(user, 'status') and user.status != 1: - _record_login(username, LoginLog.Status.ERROR, error="账号已被禁用") - return R.failed("账号已被禁用,请联系管理员") - - # 检查登录失败次数(在密码验证之前,避免锁定后仍执行高成本的bcrypt计算) - client_ip = request.remote_addr - login_attempts_key = f"login_attempts:{client_ip}:{username}" - attempts = redis.get(login_attempts_key) or 0 - if int(attempts) >= LOGIN_FAIL_MAX: - _record_login(username, LoginLog.Status.ERROR, error="登录失败次数过多,账号被锁定") - lock_minutes = LOGIN_LOCK_SECONDS // 60 - return R.failed(msg=f"登录失败次数过多,请{lock_minutes}分钟后再试") - - # 密码验证 - if not verify_password(password, user.salt, user.password): - # 递增失败计数,首次失败时原子设置初始值和过期时间 - if redis.ttl(login_attempts_key) == -2: - redis.client.setex(login_attempts_key, LOGIN_LOCK_SECONDS, 1) - else: - redis.incr(login_attempts_key) - _record_login(username, LoginLog.Status.ERROR, error="密码不正确") - return R.failed(msg="密码不正确") - - # 登录成功后清除失败计数 - redis.delete(login_attempts_key) - - # 生成访问令牌 - access_token = create_token({ - "userId": user.id, - "username": user.username, - "realname": user.realname - }) - - # 登录成功,返回令牌 - result = R.ok( - msg="登录成功", - data={ - "access_token": access_token - } - ) - - # 记录登录成功日志(传入result用于记录返回结果) - _record_login(username, LoginLog.Status.NORMAL, result_data=result) - - return result - - -def _record_login(username, status, error="", result_data=None): - """ - 记录登录日志(内部辅助函数) - - 自动收集请求信息(IP、UA、URL、参数等),调用 login_log.record_login_log。 - 异常被吞掉,不影响登录主流程。 - - Args: - username: 登录账号 - status: 操作状态(LoginLog.Status.NORMAL / ERROR) - error: 错误信息(登录失败时) - result_data: 返回结果字典(登录成功时传入 R.ok() 的返回值) - """ - try: - import json - import time - - client_ip = get_client_ip() - ua_info = parse_user_agent() - - # 请求参数(密码脱敏) - param = '' - try: - json_data = request.get_json(silent=True) - if json_data: - safe_data = dict(json_data) - if 'password' in safe_data: - safe_data['password'] = '******' - param = json.dumps(safe_data, ensure_ascii=False) - except Exception: - pass - - # 返回结果(只取响应JSON中的data字段) - result = '' - if result_data is not None: - try: - data = result_data.get_json(silent=True) - if data and isinstance(data, dict): - result_data_value = data.get('data') - if result_data_value is not None: - result = json.dumps(result_data_value, ensure_ascii=False) - else: - result = data.get('msg', '') - except Exception: - result = str(result_data) - elif error: - result = error - - # 请求耗时 - start_time = getattr(g, 'request_start_time', None) - consume_time = int((time.time() - start_time) * 1000) if start_time else 0 - - login_log.record_login_log( - username=username, - source=LoginLog.Source.ADMIN, - status=status, - log_type=LoginLog.LogType.LOGIN, - method=request.method, - request_method=request.endpoint or '', - url=request.path, - param=param, - result=result, - ip=client_ip, - location=get_ip_location(client_ip), - os=ua_info.get('os', '未知'), - browser=ua_info.get('browser', '未知'), - consume_time=consume_time, - error=error, - ) - except Exception as e: - # 日志记录失败不影响主流程 - import logging - logging.getLogger(__name__).warning(f"记录登录日志失败: {str(e)}") +服务已迁移至 src.modules.auth.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 +""" +from src.modules.auth.service import * # noqa: F401,F403 diff --git a/application/services/login_log.py b/application/services/login_log.py index 2e05be8..1e92fae 100644 --- a/application/services/login_log.py +++ b/application/services/login_log.py @@ -1,317 +1,8 @@ """ -登录日志管理服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了登录日志管理相关的所有业务逻辑。 -包括登录日志的分页查询、详情查询、添加、删除、清空等功能。 - -命名规范: -- 函数参数使用驼峰命名(如 logId, pageNo) -- 表单字段使用下划线命名(如 username, log_type)- 与数据库字段保持一致 -- 返回数据使用驼峰命名(如 createUser, updateTime)- 与前端规范保持一致 -- 私有函数以下划线开头(如 _apply_filters, _build_page_data) - -功能模块: -1. 分页查询:支持按账号模糊搜索、日志类型筛选、来源筛选、状态筛选、时间范围筛选 -2. 详情查询:根据ID获取单个登录日志的详细信息 -3. 日志添加:记录用户登录/退出操作日志 -4. 日志删除:删除指定日志(支持批量) -5. 日志清空:清空所有登录日志 -6. 统计查询:获取登录日志统计信息 +服务已迁移至 src.modules.log.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging -from datetime import datetime - -from flask import request -from sqlalchemy import and_ - -from application.constants.common import PAGE_SIZE -from application.constants.dict import OPERATION_LOG_SOURCE_LIST, LOGIN_LOG_TYPE_LIST -from application.models.login_log import LoginLog -from extensions import db -from utils import R -from utils.model_utils import models_to_list, model_to_dict -from utils.request import parse_id_list, like_contains - -# 获取日志记录器 -logger = logging.getLogger(__name__) - - -# ============================================================ -# 登录日志分页查询 -# ============================================================ - -def get_login_log_page(): - """ - 查询登录日志分页数据 - - Args: - request: Flask request对象,包含查询参数 - - pageNo: 当前页码(可选,默认为1) - - pageSize: 每页记录数(可选,默认为PAGE_SIZE) - - username: 登录账号(可选,模糊查询) - - logType: 日志类型(可选,1-登录,2-退出) - - source: 登录来源(可选,0-后台,1-前台) - - status: 操作状态(可选,0-正常,1-异常) - - startTime: 开始时间(可选) - - endTime: 结束时间(可选) - - Returns: - R: 包含分页数据的JSON响应,格式为: - { - "records": 日志列表, - "total": 总记录数, - "size": 每页大小, - "current": 当前页码, - "pages": 总页数 - } - """ - try: - # 获取分页参数 - page_no = int(request.args.get('pageNo', 1)) - page_size = int(request.args.get('pageSize', PAGE_SIZE)) - - # 应用筛选条件 - query = _apply_filters() - - # 记录总数 - total = query.count() - # 计算总页数 - pages = (total + page_size - 1) // page_size if total > 0 else 0 - - # 分页查询(按创建时间倒序) - log_list = query.order_by(LoginLog.create_time.desc()) \ - .limit(page_size) \ - .offset((page_no - 1) * page_size) \ - .all() - - # 构建返回数据 - page_data = _build_page_data(log_list, total, page_size, page_no, pages) - return R.ok(data=page_data) - - except Exception as e: - logger.error(f"查询登录日志分页数据异常: {str(e)}") - return R.failed(msg="查询失败,请稍后重试") - - -def _apply_filters(): - """ - 应用查询筛选条件 - - 根据请求参数动态构建查询条件: - - 自动过滤已删除记录(is_delete == 0) - - 支持登录账号模糊查询 - - 支持日志类型精确查询 - - 支持登录来源精确查询 - - 支持操作状态精确查询 - - 支持时间范围查询 - - Returns: - Query: 应用筛选后的SQLAlchemy查询对象 - """ - # 基础查询:过滤已删除记录 - query = LoginLog.query.filter(LoginLog.is_delete == 0) - - # 登录账号模糊筛选 - username = request.args.get('username') - if username: - query = query.filter(LoginLog.username.like(like_contains(username), escape='\\')) - - # 日志类型精确筛选 - log_type = request.args.get('logType') - if log_type: - query = query.filter(LoginLog.log_type == log_type) - - # 登录来源精确筛选 - source = request.args.get('source') - if source: - query = query.filter(LoginLog.source == source) - - # 操作状态精确筛选 - status = request.args.get('status') - if status: - query = query.filter(LoginLog.status == status) - - # 时间范围筛选 - start_time = request.args.get('startTime') - end_time = request.args.get('endTime') - - if start_time: - try: - start_date = datetime.strptime(start_time, '%Y-%m-%d %H:%M:%S') - query = query.filter(LoginLog.create_time >= start_date) - except ValueError: - pass - - if end_time: - try: - end_date = datetime.strptime(end_time, '%Y-%m-%d %H:%M:%S') - query = query.filter(LoginLog.create_time <= end_date) - except ValueError: - pass - - return query - - -def _build_page_data(log_list, total, page_size, page_no, pages): - """ - 构建分页返回数据 - - Args: - log_list: 登录日志对象列表 - total: 总记录数 - page_size: 每页大小 - page_no: 当前页码 - pages: 总页数 - - Returns: - dict: 分页数据,包含 records、total、size、current、pages - """ - # 将模型对象列表转换为字典列表 - records = models_to_list(log_list) - - # 遍历数据源 - for record in records: - # 日志来源 - record['sourceText'] = OPERATION_LOG_SOURCE_LIST.get(record.get('source'), '') - # 日志类型 - record['logTypeText'] = LOGIN_LOG_TYPE_LIST.get(record.get('logType'), '') - - return { - 'records': records, - 'total': total, - 'size': page_size, - 'current': page_no, - 'pages': pages - } - - -# ============================================================ -# 登录日志详情查询 -# ============================================================ - -def get_login_log_detail(log_id): - """ - 根据ID查询登录日志详情(返回字典格式) - - Args: - log_id: 登录日志ID - - Returns: - dict/None: 登录日志详情字典,不存在时返回 None - """ - try: - log = get_info(log_id) - if not log: - return None - return model_to_dict(log) - - except Exception as e: - logger.error(f"查询登录日志详情异常,ID: {log_id}, 错误: {str(e)}") - return None - - -def get_info(log_id: int): - """ - 根据ID查询登录日志信息(返回模型对象) - - Args: - log_id: 登录日志ID - - Returns: - LoginLog/None: 登录日志模型对象,不存在或已删除时返回 None - """ - if not log_id or log_id <= 0: - return None - - try: - log = LoginLog.query.filter( - and_(LoginLog.id == log_id, LoginLog.is_delete == 0) - ).first() - return log - except Exception as e: - logger.error(f"查询登录日志信息异常,ID: {log_id}, 错误: {str(e)}") - return None - - -def record_login_log(username, source, status, **kwargs): - """ - 记录登录日志(便捷方法) - - 供登录/退出接口直接调用,无需通过HTTP请求。 - 使用独立session写入,不受请求事务commit/rollback影响。 - - Args: - username: 登录账号 - source: 登录来源(0-后台,1-前台) - status: 操作状态(0-正常,1-异常) - **kwargs: 其他可选参数 - - log_type: 日志类型(默认1-登录) - - method: 请求方法 - - request_method: 请求方式 - - url: 请求URL - - param: 请求参数 - - result: 返回结果 - - ip: IP地址 - - location: IP归属地 - - os: 操作系统 - - browser: 浏览器 - - consume_time: 请求耗时 - - error: 错误信息 - - Returns: - LoginLog/None: 创建的日志对象,失败返回None - """ - try: - log_data = { - 'username': username, - 'log_type': kwargs.get('log_type', LoginLog.LogType.LOGIN), - 'source': source, - 'method': kwargs.get('method', ''), - 'request_method': kwargs.get('request_method', ''), - 'url': kwargs.get('url', ''), - 'param': kwargs.get('param', ''), - 'result': kwargs.get('result', ''), - 'ip': kwargs.get('ip', ''), - 'location': kwargs.get('location', ''), - 'os': kwargs.get('os', ''), - 'browser': kwargs.get('browser', ''), - 'consume_time': kwargs.get('consume_time', 0), - 'status': status, - 'error': kwargs.get('error', ''), - } - - log = LoginLog(**log_data) - log.create_user = username - log.update_user = username - - # 使用独立session写入,不受请求事务影响 - # 日志是审计数据,无论业务成功或失败都应持久化 - from extensions.sqlalchemy import commit_independently - commit_independently(log) - - return log - - except Exception as e: - logger.error(f"记录登录日志异常: {str(e)}") - return None - - -# ============================================================ -# 登录日志删除 -# ============================================================ - -def delete_login_logs(log_ids): - """ - 删除登录日志(批量删除) - - 删除 - - Args: - log_ids: 日志ID字符串,多个ID用逗号分隔,如 "1,2,3" - - Returns: - R: 操作结果 - """ - return LoginLog.batch_delete_r(log_ids) +from src.modules.log.service import * # noqa: F401,F403 diff --git a/application/services/mail_svc.py b/application/services/mail_svc.py index 3e4e0fb..2889cc3 100644 --- a/application/services/mail_svc.py +++ b/application/services/mail_svc.py @@ -1,96 +1,8 @@ """ -邮件发送服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -封装 Flask-Mail 提供邮件发送能力,供业务模块和定时任务调用。 - -使用方式: - from application.services import mail_svc - - # 简单发送 - mail_svc.send_mail(to='user@example.com', subject='标题', body='内容') - - # HTML 邮件 - mail_svc.send_mail(to='user@example.com', subject='标题', html='

Hello

') - - # 多收件人 / 抄送 - mail_svc.send_mail( - to=['a@example.com', 'b@example.com'], - subject='标题', - body='内容', - cc=['cc@example.com'], - ) +服务已迁移至 src.modules.job.mail_service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging -from typing import List, Optional, Union - -from extensions.mail import mail -from flask import current_app -from flask_mail import Message - -logger = logging.getLogger(__name__) - - -def send_mail( - to: Union[str, List[str]], - subject: str, - body: Optional[str] = None, - html: Optional[str] = None, - cc: Optional[Union[str, List[str]]] = None, - bcc: Optional[Union[str, List[str]]] = None, - reply_to: Optional[str] = None, -) -> bool: - """ - 发送邮件 - - Args: - to: 收件人地址,字符串或列表 - subject: 邮件主题 - body: 纯文本正文(与 html 二选一,都提供时邮件客户端自行选择显示) - html: HTML 正文 - cc: 抄送地址 - bcc: 密送地址 - reply_to: 回复地址 - - Returns: - bool: 发送成功返回 True,失败返回 False - """ - if not to: - logger.warning("发送邮件失败:收件人不能为空") - return False - - if not subject: - logger.warning("发送邮件失败:主题不能为空") - return False - - if not body and not html: - logger.warning("发送邮件失败:正文不能为空") - return False - - # 统一转为列表 - if isinstance(to, str): - to = [to] - if cc and isinstance(cc, str): - cc = [cc] - if bcc and isinstance(bcc, str): - bcc = [bcc] - - try: - msg = Message( - subject=subject, - recipients=to, - cc=cc, - bcc=bcc, - reply_to=reply_to, - body=body, - html=html, - ) - mail.send(msg) - - logger.info(f"邮件发送成功: to={to}, subject={subject}") - return True - - except Exception as e: - logger.error(f"邮件发送失败: to={to}, subject={subject}, error={str(e)}") - return False +from src.modules.job.mail_service import * # noqa: F401,F403 diff --git a/application/services/menu.py b/application/services/menu.py index 5d666fc..8630ce4 100644 --- a/application/services/menu.py +++ b/application/services/menu.py @@ -1,744 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -菜单管理服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了菜单管理相关的所有业务逻辑。 -包括菜单的列表查询、详情查询、添加、更新、删除、权限节点管理等功能。 -菜单用于构建系统导航栏和权限控制,支持目录、菜单、按钮三种类型, -通过parent_id字段建立层级关系,sort字段控制显示顺序。 - -命名规范: -- 函数参数使用驼峰命名(如 menuId, userId) -- 表单字段使用下划线命名(如 name, path, permission)- 与数据库字段保持一致 -- 返回数据使用驼峰命名(如 createUser, updateTime)- 与前端规范保持一致 -- 私有函数以下划线开头(如 _apply_filters, _build_tree) - -功能模块: -1. 列表查询:支持按名称模糊搜索,按sort字段排序 -2. 详情查询:根据ID获取单个菜单的详细信息(含权限节点) -3. 数据维护:菜单的添加、更新、删除(支持批量) -4. 权限节点:自动生成菜单对应的权限节点(增删改查等) -5. 用户权限:根据用户ID获取权限节点列表 -6. 菜单树:根据用户ID获取权限菜单树结构 +服务已迁移至 src.modules.system.menu.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging - -from datetime import datetime, timezone -from flask import request -from sqlalchemy import and_ - -from application.forms.menu import MenuForm -from application.models.menu import Menu -from application.models.role_menu import RoleMenu -from application.models.user_role import UserRole -from extensions import db -from utils import R, regular -from utils.case_converter import convert_camel_to_snake -from utils.redis_client import redis -from utils.request import parse_request_body, parse_id_list, like_contains -from utils.model_utils import models_to_list, model_to_dict -from utils.security import get_username - -# 获取日志记录器 -logger = logging.getLogger(__name__) - -# ============================================================ -# 权限缓存配置 -# ============================================================ - -# 缓存Key前缀 -_CACHE_PREFIX_PERM = "perm:user:" -_CACHE_PREFIX_MENU = "menu:user:" -_CACHE_KEY_VERSION = "perm:version" -# 缓存过期时间(秒),2小时 -_CACHE_EXPIRE = 7200 - - -# ============================================================ -# 权限节点常量映射 -# ============================================================ - -class PermissionNode: - """权限节点常量定义""" - QUERY = 1 # 查询 - ADD = 5 # 添加 - UPDATE = 10 # 修改 - DELETE = 15 # 删除 - DETAIL = 20 # 详情 - STATUS = 25 # 设置状态 - BATCH_DELETE = 30 # 批量删除 - ADD_CHILD = 35 # 添加子级 - EXPAND = 40 # 全部展开 - COLLAPSE = 45 # 全部折叠 - EXPORT = 50 # 导出 - IMPORT = 55 # 导入 - PERMISSION = 60 # 分配权限 - RESET_PWD = 65 # 重置密码 - - -# 权限节点配置映射 -PERMISSION_CONFIG = { - PermissionNode.QUERY: {'suffix': '查询', 'action': 'list', 'perm_suffix': 'list'}, - PermissionNode.ADD: {'suffix': '添加', 'action': 'add', 'perm_suffix': 'add'}, - PermissionNode.UPDATE: {'suffix': '修改', 'action': 'update', 'perm_suffix': 'update'}, - PermissionNode.DELETE: {'suffix': '删除', 'action': 'delete', 'perm_suffix': 'delete'}, - PermissionNode.DETAIL: {'suffix': '详情', 'action': 'detail', 'perm_suffix': 'detail'}, - PermissionNode.STATUS: {'suffix': '设置状态', 'action': 'status', 'perm_suffix': 'status'}, - PermissionNode.BATCH_DELETE: {'suffix': '批量删除', 'action': 'dall', 'perm_suffix': 'dall'}, - PermissionNode.ADD_CHILD: {'suffix': '添加子级', 'action': 'addz', 'perm_suffix': 'addz'}, - PermissionNode.EXPAND: {'suffix': '全部展开', 'action': 'expand', 'perm_suffix': 'expand'}, - PermissionNode.COLLAPSE: {'suffix': '全部折叠', 'action': 'collapse', 'perm_suffix': 'collapse'}, - PermissionNode.EXPORT: {'suffix': '导出', 'action': 'export', 'perm_suffix': 'export'}, - PermissionNode.IMPORT: {'suffix': '导入', 'action': 'import', 'perm_suffix': 'import'}, - PermissionNode.PERMISSION: {'suffix': '分配权限', 'action': 'permission', 'perm_suffix': 'permission'}, - PermissionNode.RESET_PWD: {'suffix': '重置密码', 'action': 'resetPwd', 'perm_suffix': 'resetPwd'}, -} - - -# ============================================================ -# 菜单列表查询 -# ============================================================ - -def get_menu_list(): - """ - 查询菜单列表 - - 用于菜单管理页面的列表展示,支持按名称模糊搜索。 - - Args: - request: Flask request对象,包含查询参数 - - name: 菜单名称(可选,模糊查询) - - Returns: - R: 包含菜单列表的JSON响应 - """ - try: - # 应用筛选条件 - query = _apply_filters() - - # 查询数据(按排序升序) - menu_list = query.order_by(Menu.sort.asc()).all() - - # 转换为字典列表 - records = models_to_list(menu_list) - - return R.ok(data=records) - - except Exception as e: - logger.error(f"查询菜单列表异常: {str(e)}") - return R.failed(msg="查询失败,请稍后重试") - - -def _apply_filters(): - """ - 应用查询筛选条件 - - 根据请求参数动态构建查询条件: - - 自动过滤已删除记录(is_delete == 0) - - 支持菜单名称模糊查询 - - Returns: - Query: 应用筛选后的SQLAlchemy查询对象 - """ - # 基础查询:过滤已删除记录 - query = Menu.query.filter(Menu.is_delete == 0) - - # 菜单名称模糊筛选 - name = request.args.get('name') - if name: - query = query.filter(Menu.name.like(like_contains(name), escape='\\')) - - return query - - -# ============================================================ -# 菜单详情查询 -# ============================================================ - -def get_menu_detail(menu_id): - """ - 根据ID查询菜单详情(返回字典格式) - - 此方法调用 get_info 获取模型对象,然后转换为字典格式返回。 - 同时查询该菜单下的权限节点列表。 - - Args: - menu_id: 菜单ID - - Returns: - dict/None: 菜单详情字典,包含 checked_list 权限节点列表,不存在时返回 None - """ - try: - # 复用 get_info 方法查询菜单 - menu = get_info(menu_id) - if not menu: - return None - - # 转换为字典格式 - data = model_to_dict(menu) - - # 查询菜单权限节点(type=1 表示权限节点) - permission_list = Menu.query.filter( - and_(Menu.parent_id == menu_id, Menu.type == 1, Menu.is_delete == 0) - ).all() - - # 选中的权限节点(按sort值收集) - checked_list = [p.sort for p in permission_list] if permission_list else [] - - # 权限节点 - data['checked_list'] = checked_list - - return data - - except Exception as e: - logger.error(f"查询菜单详情异常,ID: {menu_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 菜单信息查询(根据ID)- 核心查询方法 -# ============================================================ - -def get_info(menu_id: int): - """ - 根据ID查询菜单信息(返回模型对象) - - 这是一个通用的菜单查询方法,返回菜单模型对象而非字典。 - 与 get_menu_detail 的区别: - - get_menu_detail: 返回字典格式,用于前端接口 - - get_info: 返回模型对象,用于内部服务调用 - - 此方法被以下方法复用: - - get_menu_detail: 获取详情字典 - - update_menu: 验证菜单是否存在 - - delete_menus: 验证菜单是否存在 - - Args: - menu_id: 菜单ID - - Returns: - Menu/None: 菜单模型对象,不存在或已删除时返回 None - - Example: - menu = get_info(menu_id) - if menu: - print(menu.title) - """ - # 参数有效性验证 - if not menu_id or menu_id <= 0: - return None - - try: - # 查询未删除的菜单 - menu = Menu.query.filter( - and_(Menu.id == menu_id, Menu.is_delete == 0) - ).first() - return menu - except Exception as e: - logger.error(f"查询菜单信息异常,ID: {menu_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 菜单添加 -# ============================================================ - -def add_menu(): - """ - 添加菜单 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证 - 3. 创建菜单对象并保存到数据库 - 4. 如果是目录类型,自动生成权限节点 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = MenuForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - # 权限节点 - checked_list = form.data.get('checked_list') - menu_type = form.data.get('type') - title = form.data.get('title') - path = form.data.get('path', '') - - # 移除不需要直接保存到Menu表的字段 - form_data = form.data.copy() - form_data.pop('checked_list', None) - - # 创建菜单 - menu = Menu(**form_data) - menu.create_user = get_username() - db.session.add(menu) - db.session.flush() - - # 保存权限节点数据 - _create_permission_nodes(menu_type, checked_list, title, path, menu.id) - - # 菜单变更,递增全局版本号使所有用户缓存失效 - invalidate_all_cache() - - return R.ok(msg="添加成功") - - except Exception as e: - logger.error(f"添加菜单异常: {str(e)}") - return R.failed(msg="添加菜单失败,请稍后重试") - - -# ============================================================ -# 菜单更新 -# ============================================================ - -def update_menu(): - """ - 更新菜单 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证 - 3. 验证菜单是否存在(复用 get_info) - 4. 更新菜单信息 - 5. 更新权限节点数据 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = MenuForm(data=json_data) - if not form.validate(): - return R.failed(msg=regular.get_err(form)) - - # 获取菜单ID - menu_id = form.data.get('id') - if not menu_id or int(menu_id) <= 0: - return R.failed("菜单ID无效") - menu_id = int(menu_id) - - # 复用 get_info 方法验证菜单是否存在 - menu = get_info(menu_id) - if not menu: - return R.failed("菜单不存在") - - # 权限节点 - checked_list = form.data.get('checked_list') - menu_type = form.data.get('type') - title = form.data.get('title') - path = form.data.get('path', '') - - # 移除不需要直接更新到Menu表的字段 - form_data = convert_camel_to_snake(form.data.copy()) - form_data.pop('id', None) - form_data.pop('checked_list', None) - - form_data['update_time'] = datetime.now(tz=timezone.utc) - form_data['update_user'] = get_username() - - # 更新菜单信息 - result = Menu.query.filter_by(id=menu_id).update(form_data) - - if not result: - return R.failed("更新失败") - - # 保存权限节点数据 - _create_permission_nodes(menu_type, checked_list, title, path, menu_id) - - # 菜单变更,递增全局版本号使所有用户缓存失效 - invalidate_all_cache() - - return R.ok(msg="更新成功") - - except Exception as e: - logger.error(f"更新菜单异常: {str(e)}") - return R.failed(msg="更新菜单失败,请稍后重试") - - -# ============================================================ -# 菜单删除 -# ============================================================ - -def delete_menus(menu_ids): - """ - 删除菜单(批量删除) - - 采用软删除方式,将 is_delete 标识设置为 1。 - 支持逗号分隔的多个ID批量删除。 - - Args: - menu_ids: 菜单ID字符串,多个ID用逗号分隔,如 "1,2,3" - - Returns: - R: 操作结果 - """ - if not menu_ids: - return R.failed("记录ID不存在") - - # 解析ID列表 - id_list = parse_id_list(menu_ids) - if not id_list: - return R.failed("无效的记录ID") - - # 检查是否有子菜单 - for menu_id in id_list: - child_count = Menu.query.filter( - and_(Menu.parent_id == menu_id, Menu.is_delete == 0) - ).count() - if child_count > 0: - return R.failed(f"菜单ID {menu_id} 存在子菜单,请先删除子菜单") - - result = Menu.batch_delete_r(menu_ids) - - # 菜单变更,递增全局版本号使所有用户缓存失效 - invalidate_all_cache() - - return result - - -# ============================================================ -# 权限节点管理 -# ============================================================ - -def _create_permission_nodes(menu_type, checked_list, title, path, parent_id): - """ - 创建权限节点 - - 根据菜单类型和选中的权限节点,自动生成对应的权限菜单。 - - Args: - menu_type: 菜单类型(0-目录/菜单,1-权限节点) - checked_list: 选中的权限节点列表(逗号分隔的字符串) - title: 菜单标题 - path: 菜单路径 - parent_id: 父级菜单ID - """ - # 非目录/菜单类型,或没有选中节点,或路径为空时直接返回 - if menu_type != 0 or not checked_list or not path: - return - - try: - # 删除现有权限节点 - Menu.query.filter( - and_(Menu.parent_id == parent_id, Menu.type == 1, Menu.is_delete == 0) - ).delete() - - # 模块名称(去掉"管理"后缀) - module_title = title.replace("管理", "") if title else "" - - # 请求path处理,获取模块名 - path_arr = path.split('/') - if len(path_arr) < 3: - return - module_name = path_arr[-1] - - # 权限节点处理 - permission_list = parse_id_list(checked_list) - - for value in permission_list: - config = PERMISSION_CONFIG.get(value) - if not config: - continue - - # 构建节点标题 - if value in [PermissionNode.DETAIL]: - node_title = f"{module_title}{config['suffix']}" - elif value in [PermissionNode.QUERY, PermissionNode.ADD, PermissionNode.UPDATE, - PermissionNode.DELETE, PermissionNode.EXPORT, PermissionNode.IMPORT]: - node_title = f"{config['suffix']}{module_title}" - else: - node_title = config['suffix'] - - # 构建路径 - node_path = f"/{module_name}/{config['action']}" - - # 构建权限标识 - permission = f"sys:{module_name}:{config['perm_suffix']}" - - # 创建权限节点 - permission_menu = Menu( - parent_id=parent_id, - title=node_title, - path=node_path, - permission=permission, - type=1, # 权限节点类型 - status=1, - target="_self", - sort=value, - create_user=get_username() - ) - permission_menu.save() - - except Exception as e: - logger.error(f"创建权限节点异常: {str(e)}") - raise - - -# ============================================================ -# 用户权限节点获取 -# ============================================================ - -def get_user_permissions(user_id): - """ - 获取用户权限节点列表(带Redis缓存) - - 缓存策略:用户级缓存 + 全局版本号 - - Key: perm:user:{user_id}:{version} - - 菜单/角色变更时递增全局版本号,旧缓存自然失效 - - 用户角色变更时直接删除该用户缓存 - - Args: - user_id: 用户ID - - Returns: - list: 权限标识列表,如 ['sys:user:list', 'sys:user:add', ...] - """ - if not user_id or user_id <= 0: - return [] - - try: - # 查询全局版本号 - version = redis.get(_CACHE_KEY_VERSION, deserialize=False) or "1" - cache_key = f"{_CACHE_PREFIX_PERM}{user_id}:{version}" - - # 尝试从缓存获取 - cached = redis.get(cache_key) - if cached is not None: - return cached - - # 缓存未命中,查询数据库 - if user_id == 1: - permission_list = Menu.query.filter( - and_(Menu.is_delete == 0, Menu.type == 1, Menu.status == 0) - ).all() - result = [item.permission for item in permission_list if item.permission] - else: - rows = ( - db.session.query(Menu.permission) - .join(RoleMenu, Menu.id == RoleMenu.menu_id) - .join(UserRole, UserRole.role_id == RoleMenu.role_id) - .filter( - UserRole.user_id == user_id, - Menu.type == 1, - Menu.status == 0, - Menu.is_delete == 0, - Menu.permission != '' - ) - .all() - ) - result = [row.permission for row in rows if row.permission] - - # 写入缓存 - redis.set(cache_key, result, expire=_CACHE_EXPIRE) - return result - - except Exception as e: - logger.error(f"获取用户权限节点异常,用户ID: {user_id}, 错误: {str(e)}") - return [] - - -# ============================================================ -# 用户菜单树获取 -# ============================================================ - -def get_user_permission_menus(user_id): - """ - 根据用户ID获取权限菜单树(带Redis缓存) - - 缓存策略:用户级缓存 + 全局版本号 - - Key: menu:user:{user_id}:{version} - - 菜单变更时递增全局版本号,旧缓存自然失效 - - 用户角色变更时直接删除该用户缓存 - - Args: - user_id: 用户ID - - Returns: - list: 菜单树结构数据 - """ - if not user_id or user_id <= 0: - return [] - - try: - # 查询全局版本号 - version = redis.get(_CACHE_KEY_VERSION, deserialize=False) or "1" - cache_key = f"{_CACHE_PREFIX_MENU}{user_id}:{version}" - - # 尝试从缓存获取 - cached = redis.get(cache_key) - if cached is not None: - return cached - - # 缓存未命中,查询数据库 - if user_id == 1: - menu_list = Menu.query.filter( - and_(Menu.is_delete == 0, Menu.status == 0, Menu.type == 0) - ).order_by(Menu.sort.asc()).all() - else: - menu_list = ( - db.session.query(Menu) - .join(RoleMenu, Menu.id == RoleMenu.menu_id) - .join(UserRole, UserRole.role_id == RoleMenu.role_id) - .filter( - UserRole.user_id == user_id, - Menu.type == 0, - Menu.status == 0, - Menu.is_delete == 0 - ) - .order_by(Menu.sort.asc()) - .all() - ) - - menu_data = models_to_list(menu_list) - menu_tree = _build_menu_tree(menu_data, 0) - - # 写入缓存 - redis.set(cache_key, menu_tree, expire=_CACHE_EXPIRE) - return menu_tree - - except Exception as e: - logger.error(f"获取用户菜单树异常,用户ID: {user_id}, 错误: {str(e)}") - return [] - - -# 获取树状结构 -def _build_menu_tree(menu_data, parent_id): - """ - 递归构建菜单树形结构 - - 使用字典按 parentId 分组,避免每次递归遍历全量数据, - 时间复杂度从 O(n²) 降为 O(n)。 - - Args: - menu_data: 菜单字典列表 - parent_id: 父级菜单ID - - Returns: - list: 树形结构数据 - """ - # 按 parentId 分组,O(n) 构建 - children_map = {} - for item in menu_data: - pid = item["parentId"] - children_map.setdefault(pid, []).append(item) - - def _build(pid): - children = children_map.get(pid, []) - for item in children: - item['children'] = _build(item["id"]) - children.sort(key=lambda x: x.get('sort', 0)) - return children - - return _build(parent_id) - - -# ============================================================ -# 权限缓存失效 -# ============================================================ - -def invalidate_user_cache(user_id): - """ - 清除指定用户的权限和菜单缓存 - - 用于用户角色变更场景,直接删除该用户的缓存Key。 - 由于缓存Key带版本号后缀,需要遍历所有版本删除。 - - Args: - user_id: 用户ID - """ - try: - for prefix in (_CACHE_PREFIX_PERM, _CACHE_PREFIX_MENU): - pattern = f"{prefix}{user_id}:*" - keys = redis.keys(pattern) - if keys: - redis.delete(*keys) - except Exception as e: - logger.warning(f"清除用户缓存异常,user_id={user_id}: {e}") - - -def invalidate_role_users_cache(role_id): - """ - 清除指定角色下所有用户的权限和菜单缓存 - - 用于角色菜单变更/角色删除场景,查询该角色关联的所有用户ID, - 逐个清除缓存。 - - Args: - role_id: 角色ID - """ - try: - user_ids = [row.user_id for row in UserRole.query.filter(UserRole.role_id == role_id).all()] - for uid in user_ids: - invalidate_user_cache(uid) - except Exception as e: - logger.warning(f"清除角色用户缓存异常,role_id={role_id}: {e}") - - -def invalidate_all_cache(): - """ - 清除所有用户的权限和菜单缓存 - - 用于菜单增删改场景,递增全局版本号使所有旧缓存自然失效。 - 下次读取时因版本号不匹配,会重新查询数据库并写入新版本缓存。 - """ - try: - redis.incr(_CACHE_KEY_VERSION) - except Exception as e: - logger.warning(f"递增权限缓存版本号异常: {e}") +from src.modules.system.menu.service import * # noqa: F401,F403 diff --git a/application/services/notice.py b/application/services/notice.py index a68d3a8..40c8901 100644 --- a/application/services/notice.py +++ b/application/services/notice.py @@ -1,421 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -通知公告管理服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了通知公告管理相关的所有业务逻辑。 -包括通知公告的分页查询、详情查询、添加、更新、删除等功能。 -通知公告用于发布系统通知、平台动态等信息, -包含标题、内容、来源、状态等属性。 - -命名规范: -- 函数参数使用驼峰命名(如 noticeId, pageNo) -- 表单字段使用下划线命名(如 title, content)- 与数据库字段保持一致 -- 返回数据使用驼峰命名(如 createUser, updateTime)- 与前端规范保持一致 -- 私有函数以下划线开头(如 _apply_filters, _build_page_data) - -功能模块: -1. 分页查询:支持按标题模糊搜索、来源筛选、状态筛选,按ID倒序排列 -2. 详情查询:根据ID获取单个通知公告的详细信息(包含富文本内容处理) -3. 数据维护:通知公告的添加、更新、删除(支持批量) +服务已迁移至 src.modules.notice.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging - -from datetime import datetime, timezone -from flask import request -from sqlalchemy import and_ - -from application.constants.common import PAGE_SIZE -from application.constants.dict import NOTICE_TYPE_LIST -from application.forms.notice import NoticeForm -from application.models.notice import Notice -from config import FLASK_FILE_URL -from extensions import db -from utils import R, regular -from utils.case_converter import convert_camel_to_snake -from utils.model_utils import model_to_dict -from utils.request import parse_request_body, parse_id_list, like_contains -from utils.rich_text import save_content, save_file -from utils.security import get_username - -# 获取日志记录器 -logger = logging.getLogger(__name__) - - -# ============================================================ -# 通知公告分页查询 -# ============================================================ - -def get_notice_page(): - """ - 查询通知公告分页数据 - - Args: - request: Flask request对象,包含查询参数 - - pageNo: 当前页码(可选,默认为1) - - pageSize: 每页记录数(可选,默认为PAGE_SIZE) - - title: 通知公告标题(可选,模糊查询) - - type: 通知类型(可选,精确匹配) - - status: 通知状态(可选,精确匹配) - - Returns: - R: 包含分页数据的JSON响应,格式为: - { - "records": 通知公告列表, - "total": 总记录数, - "size": 每页大小, - "current": 当前页码, - "pages": 总页数 - } - """ - try: - # 获取分页参数 - page_no = int(request.args.get('pageNo', 1)) - page_size = int(request.args.get('pageSize', PAGE_SIZE)) - - # 应用筛选条件 - query = _apply_filters() - - # 记录总数 - total = query.count() - # 计算总页数 - pages = (total + page_size - 1) // page_size if total > 0 else 0 - - # 分页查询 - notice_list = query.limit(page_size).offset((page_no - 1) * page_size).all() - # 构建返回数据 - page_data = _build_page_data(notice_list, total, page_size, page_no, pages) - return R.ok(data=page_data) - - except Exception as e: - logger.error(f"查询通知公告分页数据异常: {str(e)}") - return R.failed(msg="查询失败,请稍后重试") - - -def _apply_filters(): - """ - 应用查询筛选条件 - - 根据请求参数动态构建查询条件: - - 自动过滤已删除记录(is_delete == 0) - - 支持通知公告标题模糊查询 - - 支持通知来源精确查询 - - 支持通知状态精确查询 - - 默认按ID倒序排列 - - Returns: - Query: 应用筛选后的SQLAlchemy查询对象 - """ - # 基础查询:过滤已删除记录 - query = Notice.query.filter(Notice.is_delete == 0) - - # 通知公告标题模糊筛选 - title = request.args.get('title') - if title: - query = query.filter(Notice.title.like(like_contains(title), escape='\\')) - - # 通知类型精确筛选 - type = request.args.get('type') - if type: - query = query.filter(Notice.type == type) - - # 通知状态精确筛选 - status = request.args.get('status') - if status: - query = query.filter(Notice.status == status) - - # 按ID倒序排列 - return query.order_by(Notice.id.desc()) - - -def _build_page_data(notice_list, total, page_size, page_no, pages): - """ - 构建分页返回数据 - - Args: - notice_list: 通知公告对象列表 - total: 总记录数 - page_size: 每页大小 - page_no: 当前页码 - pages: 总页数 - - Returns: - dict: 分页数据,包含 records、total、size、current、pages - """ - # 将模型对象列表转换为字典列表,并添加额外字段 - records = [] - for notice in notice_list: - data = model_to_dict(notice) - # 添加通知来源描述 - data['typeText'] = NOTICE_TYPE_LIST.get(notice.type) - records.append(data) - - return { - 'records': records, - 'total': total, - 'size': page_size, - 'current': page_no, - 'pages': pages - } - - -# ============================================================ -# 通知公告详情查询 -# ============================================================ - -def get_notice_detail(notice_id): - """ - 根据ID查询通知公告详情(返回字典格式) - - 此方法调用 get_info 获取模型对象,然后转换为字典格式返回。 - 适用于前端接口调用。 - - Args: - notice_id: 通知公告ID - - Returns: - dict/None: 通知公告详情字典,不存在时返回 None - """ - try: - # 复用 get_info 方法查询通知公告 - notice = get_info(notice_id) - if not notice: - return None - # 转换为字典格式 - data = model_to_dict(notice) - # 富文本内容处理:替换图片URL占位符 - content = notice.content.replace("[IMG_URL]", FLASK_FILE_URL) if notice.content else "" - data['content'] = content - # 通知来源描述 - data['typeText'] = NOTICE_TYPE_LIST.get(notice.type) - return data - - except Exception as e: - logger.error(f"查询通知公告详情异常,ID: {notice_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 通知公告信息查询(根据ID)- 核心查询方法 -# ============================================================ - -def get_info(notice_id: int): - """ - 根据ID查询通知公告信息(返回模型对象) - - 这是一个通用的通知公告查询方法,返回通知公告模型对象而非字典。 - 与 get_notice_detail 的区别: - - get_notice_detail: 返回字典格式,用于前端接口 - - get_info: 返回模型对象,用于内部服务调用 - - 此方法被以下方法复用: - - get_notice_detail: 获取详情字典 - - update_notice: 验证通知公告是否存在 - - Args: - notice_id: 通知公告ID - - Returns: - Notice/None: 通知公告模型对象,不存在或已删除时返回 None - - Example: - notice = get_info(notice_id) - if notice: - print(notice.title) - """ - # 参数有效性验证 - if not notice_id or notice_id <= 0: - return None - - try: - # 查询未删除的通知公告 - notice = Notice.query.filter( - and_(Notice.id == notice_id, Notice.is_delete == 0) - ).first() - return notice - except Exception as e: - logger.error(f"查询通知公告信息异常,ID: {notice_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 通知公告添加 -# ============================================================ - -def add_notice(): - """ - 添加通知公告 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证(字段格式等) - 3. 处理富文本内容(保存图片) - 4. 创建通知公告对象并保存到数据库 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = NoticeForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - # 通知封面处理 - cover = form.data.get('cover') - if cover: - cover = save_file(cover, "notice") - form.cover.data = cover - # 处理富文本内容 - title = form.data.get('title', '') - content = form.data.get('content', '') - if content: - form.content.data = save_content(content, title, "notice") - - # 创建通知公告 - notice = Notice(**form.data) - notice.create_user = get_username() - notice.save() - - return R.ok(msg="添加成功") - - except Exception as e: - logger.error(f"添加通知公告异常: {str(e)}") - return R.failed(msg="添加失败,请稍后重试") - - -# ============================================================ -# 通知公告更新 -# ============================================================ - -def update_notice(): - """ - 更新通知公告 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证 - 3. 验证通知公告是否存在(复用 get_info) - 4. 处理富文本内容(保存图片) - 5. 更新通知公告信息 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = NoticeForm(data=json_data) - if not form.validate(): - return R.failed(msg=regular.get_err(form)) - - # 获取通知公告ID - notice_id = form.data.get('id') - if not notice_id or int(notice_id) <= 0: - return R.failed("通知公告ID无效") - notice_id = int(notice_id) - - # 复用 get_info 方法验证通知公告是否存在 - notice = get_info(notice_id) - if not notice: - return R.failed("通知公告不存在") - - # 通知封面处理 - cover = form.data.get('cover') - if cover: - cover = save_file(cover, "notice") - form.cover.data = cover - # 处理富文本内容 - title = form.data.get('title', '') - content = form.data.get('content', '') - if content: - form.content.data = save_content(content, title, "notice") - - # 更新通知公告信息(移除ID,避免更新主键) - form_data = convert_camel_to_snake(form.data.copy()) - form_data.pop('id', None) - - form_data['update_time'] = datetime.now(tz=timezone.utc) - form_data['update_user'] = get_username() - - result = Notice.query.filter_by(id=notice_id).update(form_data) - - if not result: - return R.failed("更新失败") - - return R.ok(msg="更新成功") - - except Exception as e: - logger.error(f"更新通知公告异常: {str(e)}") - return R.failed(msg="更新通知公告失败,请稍后重试") - - -# ============================================================ -# 通知公告删除 -# ============================================================ - -def delete_notices(notice_ids): - """ - 删除通知公告(批量删除) - - 删除 - 支持逗号分隔的多个ID批量删除。 - - Args: - notice_ids: 通知公告ID字符串,多个ID用逗号分隔,如 "1,2,3" - - Returns: - R: 操作结果 - """ - return Notice.batch_delete_r(notice_ids) +from src.modules.notice.service import * # noqa: F401,F403 diff --git a/application/services/operation_log.py b/application/services/operation_log.py index 668d624..1e92fae 100644 --- a/application/services/operation_log.py +++ b/application/services/operation_log.py @@ -1,322 +1,8 @@ """ -操作日志管理服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了操作日志管理相关的所有业务逻辑。 -包括操作日志的分页查询、详情查询、添加、删除、清空、统计等功能。 - -命名规范: -- 函数参数使用驼峰命名(如 logId, pageNo) -- 表单字段使用下划线命名(如 title, log_type)- 与数据库字段保持一致 -- 返回数据使用驼峰命名(如 createUser, updateTime)- 与前端规范保持一致 -- 私有函数以下划线开头(如 _apply_filters, _build_page_data) - -功能模块: -1. 分页查询:支持按标题模糊搜索、操作类型筛选、来源筛选、状态筛选、时间范围筛选 -2. 详情查询:根据ID获取单个操作日志的详细信息 -3. 日志添加:记录用户操作日志 -4. 日志删除:删除指定日志(支持批量) -5. 日志清空:清空所有操作日志 -6. 统计查询:获取操作日志统计信息 -7. 类型查询:获取操作日志类型列表 +服务已迁移至 src.modules.log.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging -from datetime import datetime - -from flask import request -from sqlalchemy import and_ - -from application.constants.common import PAGE_SIZE -from application.constants.dict import OPERATION_LOG_SOURCE_LIST, OPERATION_LOG_TYPE_LIST -from application.models.operation_log import OperationLog -from extensions import db -from utils import R -from utils.model_utils import models_to_list, model_to_dict -from utils.request import parse_id_list, like_contains - -# 获取日志记录器 -logger = logging.getLogger(__name__) - - -# ============================================================ -# 操作日志分页查询 -# ============================================================ - -def get_operation_log_page(): - """ - 查询操作日志分页数据 - - Args: - request: Flask request对象,包含查询参数 - - pageNo: 当前页码(可选,默认为1) - - pageSize: 每页记录数(可选,默认为PAGE_SIZE) - - title: 请求标题(可选,模糊查询) - - type: 请求类型(可选,精确查询) - - source: 请求来源(可选,0-后台,1-前台) - - status: 请求状态(可选,0-正常,1-异常) - - startTime: 开始时间(可选) - - endTime: 结束时间(可选) - - Returns: - R: 包含分页数据的JSON响应,格式为: - { - "records": 日志列表, - "total": 总记录数, - "size": 每页大小, - "current": 当前页码, - "pages": 总页数 - } - """ - try: - # 获取分页参数 - page_no = int(request.args.get('pageNo', 1)) - page_size = int(request.args.get('pageSize', PAGE_SIZE)) - - # 应用筛选条件 - query = _apply_filters() - - # 记录总数 - total = query.count() - # 计算总页数 - pages = (total + page_size - 1) // page_size if total > 0 else 0 - - # 分页查询(按创建时间倒序) - log_list = query.order_by(OperationLog.create_time.desc()) \ - .limit(page_size) \ - .offset((page_no - 1) * page_size) \ - .all() - - # 构建返回数据 - page_data = _build_page_data(log_list, total, page_size, page_no, pages) - return R.ok(data=page_data) - - except Exception as e: - logger.error(f"查询操作日志分页数据异常: {str(e)}") - return R.failed(msg="查询失败,请稍后重试") - - -def _apply_filters(): - """ - 应用查询筛选条件 - - 根据请求参数动态构建查询条件: - - 自动过滤已删除记录(is_delete == 0) - - 支持请求标题模糊查询 - - 支持操作类型精确查询 - - 支持请求来源精确查询 - - 支持请求状态精确查询 - - 支持时间范围查询 - - Returns: - Query: 应用筛选后的SQLAlchemy查询对象 - """ - # 基础查询:过滤已删除记录 - query = OperationLog.query.filter(OperationLog.is_delete == 0) - - # 请求标题模糊筛选 - title = request.args.get('title') - if title: - query = query.filter(OperationLog.title.like(like_contains(title), escape='\\')) - - # 请求类型精确筛选 - log_type = request.args.get('type') - if log_type: - query = query.filter(OperationLog.type == log_type) - - # 请求来源精确筛选 - source = request.args.get('source') - if source: - query = query.filter(OperationLog.source == source) - - # 请求状态精确筛选 - status = request.args.get('status') - if status: - query = query.filter(OperationLog.status == status) - - # 时间范围筛选 - start_time = request.args.get('startTime') - end_time = request.args.get('endTime') - - if start_time: - try: - start_date = datetime.strptime(start_time, '%Y-%m-%d %H:%M:%S') - query = query.filter(OperationLog.create_time >= start_date) - except ValueError: - pass - - if end_time: - try: - end_date = datetime.strptime(end_time, '%Y-%m-%d %H:%M:%S') - query = query.filter(OperationLog.create_time <= end_date) - except ValueError: - pass - - return query - - -def _build_page_data(log_list, total, page_size, page_no, pages): - """ - 构建分页返回数据 - - Args: - log_list: 操作日志对象列表 - total: 总记录数 - page_size: 每页大小 - page_no: 当前页码 - pages: 总页数 - - Returns: - dict: 分页数据,包含 records、total、size、current、pages - """ - # 将模型对象列表转换为字典列表 - records = models_to_list(log_list) - - # 遍历数据源 - for record in records: - # 日志来源 - record['sourceText'] = OPERATION_LOG_SOURCE_LIST.get(record.get('source'), '') - # 日志类型 - record['typeText'] = OPERATION_LOG_TYPE_LIST.get(record.get('type'), '') - - return { - 'records': records, - 'total': total, - 'size': page_size, - 'current': page_no, - 'pages': pages - } - - -# ============================================================ -# 操作日志详情查询 -# ============================================================ - -def get_operation_log_detail(log_id): - """ - 根据ID查询操作日志详情(返回字典格式) - - Args: - log_id: 操作日志ID - - Returns: - dict/None: 操作日志详情字典,不存在时返回 None - """ - try: - log = get_info(log_id) - if not log: - return None - data = model_to_dict(log) - # 日志来源 - data['sourceText'] = OPERATION_LOG_SOURCE_LIST.get(data.get('source'), '') - return data - - except Exception as e: - logger.error(f"查询操作日志详情异常,ID: {log_id}, 错误: {str(e)}") - return None - - -def get_info(log_id: int): - """ - 根据ID查询操作日志信息(返回模型对象) - - Args: - log_id: 操作日志ID - - Returns: - OperationLog/None: 操作日志模型对象,不存在或已删除时返回 None - """ - if not log_id or log_id <= 0: - return None - - try: - log = OperationLog.query.filter( - and_(OperationLog.id == log_id, OperationLog.is_delete == 0) - ).first() - return log - except Exception as e: - logger.error(f"查询操作日志信息异常,ID: {log_id}, 错误: {str(e)}") - return None - - -def record_operation_log(title, log_type, source, status, **kwargs): - """ - 记录操作日志(便捷方法) - - 供业务接口直接调用,无需通过HTTP请求。 - 使用独立session写入,不受请求事务commit/rollback影响。 - - Args: - title: 请求标题 - log_type: 请求类型 - source: 请求来源(0-后台,1-前台) - status: 操作状态(0-正常,1-异常) - **kwargs: 其他可选参数 - - method: 请求方法 - - request_method: 请求方式 - - url: 请求URL - - param: 请求参数 - - result: 返回结果 - - ip: IP地址 - - location: IP归属地 - - os: 操作系统 - - browser: 浏览器 - - consume_time: 请求耗时 - - error: 错误信息 - - username: 操作用户 - - Returns: - OperationLog/None: 创建的日志对象,失败返回None - """ - try: - log_data = { - 'title': title, - 'type': log_type, - 'source': source, - 'method': kwargs.get('method', ''), - 'request_method': kwargs.get('request_method', ''), - 'url': kwargs.get('url', ''), - 'param': kwargs.get('param', ''), - 'result': kwargs.get('result', ''), - 'ip': kwargs.get('ip', ''), - 'location': kwargs.get('location', ''), - 'os': kwargs.get('os', ''), - 'browser': kwargs.get('browser', ''), - 'consume_time': kwargs.get('consume_time', 0), - 'status': status, - 'error': kwargs.get('error', ''), - } - - log = OperationLog(**log_data) - log.create_user = kwargs.get('username', 'system') - log.update_user = kwargs.get('username', 'system') - - # 使用独立session写入,不受请求事务影响 - # 日志是审计数据,无论业务成功或失败都应持久化 - from extensions.sqlalchemy import commit_independently - commit_independently(log) - - return log - - except Exception as e: - logger.error(f"记录操作日志异常: {str(e)}") - return None - - -# ============================================================ -# 操作日志删除 -# ============================================================ - -def delete_operation_logs(log_ids): - """ - 删除操作日志(批量删除) - - 删除 - - Args: - log_ids: 日志ID字符串,多个ID用逗号分隔,如 "1,2,3" - - Returns: - R: 操作结果 - """ - return OperationLog.batch_delete_r(log_ids) +from src.modules.log.service import * # noqa: F401,F403 diff --git a/application/services/param.py b/application/services/param.py index d91ecba..c7286cb 100644 --- a/application/services/param.py +++ b/application/services/param.py @@ -1,400 +1,8 @@ """ -参数管理服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了参数管理相关的所有业务逻辑。 -包括参数的分页查询、详情查询、添加、更新、删除、状态设置、导入导出等功能。 -参数用于管理系统配置和业务配置,每个参数包含名称、编码、值、类型、排序、状态等属性。 - -命名规范: -- 函数参数使用驼峰命名(如 paramId, pageNo) -- 表单字段使用下划线命名(如 name, code, sort)- 与数据库字段保持一致 -- 返回数据使用驼峰命名(如 createUser, updateTime)- 与前端规范保持一致 -- 私有函数以下划线开头(如 _apply_filters, _build_page_data) - -功能模块: -1. 分页查询:支持按名称模糊搜索、编码模糊搜索、类型筛选、状态筛选,按sort字段排序 -2. 详情查询:根据ID获取单个参数的详细信息 -3. 数据维护:参数的添加、更新、删除(支持批量) -4. 状态管理:单独更新参数的启用/禁用状态 -5. Excel导入:批量导入参数数据 -6. Excel导出:导出参数数据到Excel文件 -7. 数据列表:获取所有启用的参数列表(用于下拉选择等场景) -8. 根据编码获取参数值:通过参数编码获取对应的参数值 +服务已迁移至 src.modules.param.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging - -from datetime import datetime, timezone -from flask import request -from sqlalchemy import and_ - -from application.constants.common import PAGE_SIZE -from application.forms.param import ParamForm, ParamStatusForm -from application.models.param import Param -from extensions import db -from utils import R, regular -from utils.model_utils import models_to_list, model_to_dict -from utils.request import parse_request_body, parse_id_list, like_contains -from utils.security import get_username - -# 获取日志记录器 -logger = logging.getLogger(__name__) - - -# ============================================================ -# 参数分页查询 -# ============================================================ - -def get_param_page(): - """ - 查询参数分页数据 - - Args: - request: Flask request对象,包含查询参数 - - pageNo: 当前页码(可选,默认为1) - - pageSize: 每页记录数(可选,默认为PAGE_SIZE) - - name: 参数名称(可选,模糊查询) - - code: 参数编码(可选,模糊查询) - - type: 参数类型(可选,精确匹配) - - status: 参数状态(可选,精确匹配) - - Returns: - R: 包含分页数据的JSON响应,格式为: - { - "records": 参数列表, - "total": 总记录数, - "size": 每页大小, - "current": 当前页码, - "pages": 总页数 - } - """ - try: - # 获取分页参数 - page_no = int(request.args.get('pageNo', 1)) - page_size = int(request.args.get('pageSize', PAGE_SIZE)) - - # 应用筛选条件 - query = _apply_filters() - - # 记录总数 - total = query.count() - # 计算总页数 - pages = (total + page_size - 1) // page_size if total > 0 else 0 - - # 分页查询 - param_list = query.limit(page_size).offset((page_no - 1) * page_size).all() - # 构建返回数据 - page_data = _build_page_data(param_list, total, page_size, page_no, pages) - return R.ok(data=page_data) - - except Exception as e: - logger.error(f"查询参数分页数据异常: {str(e)}") - return R.failed(msg="查询失败,请稍后重试") - - -def _apply_filters(): - """ - 应用查询筛选条件 - - 根据请求参数动态构建查询条件: - - 自动过滤已删除记录(is_delete == 0) - - 支持参数名称模糊查询 - - 支持参数编码模糊查询 - - 支持参数类型精确查询 - - 支持参数状态精确查询 - - 默认按排序值升序排列 - - Returns: - Query: 应用筛选后的SQLAlchemy查询对象 - """ - # 基础查询:过滤已删除记录 - query = Param.query.filter(Param.is_delete == 0) - - # 参数名称模糊筛选 - name = request.args.get('name') - if name: - query = query.filter(Param.name.like(like_contains(name), escape='\\')) - - # 参数编码模糊筛选 - code = request.args.get('code') - if code: - query = query.filter(Param.code.like(like_contains(code), escape='\\')) - - # 参数类型精确筛选 - param_type = request.args.get('type') - if param_type: - query = query.filter(Param.type == param_type) - - # 参数状态精确筛选 - status = request.args.get('status') - if status: - query = query.filter(Param.status == status) - - # 按排序值升序排列 - return query.order_by(Param.sort.asc()) - - -def _build_page_data(param_list, total, page_size, page_no, pages): - """ - 构建分页返回数据 - - Args: - param_list: 参数对象列表 - total: 总记录数 - page_size: 每页大小 - page_no: 当前页码 - pages: 总页数 - - Returns: - dict: 分页数据,包含 records、total、size、current、pages - """ - # 将模型对象列表转换为字典列表 - records = models_to_list(param_list) - - return { - 'records': records, - 'total': total, - 'size': page_size, - 'current': page_no, - 'pages': pages - } - - -# ============================================================ -# 参数详情查询 -# ============================================================ - -def get_param_detail(param_id): - """ - 根据ID查询参数详情(返回字典格式) - - 此方法调用 get_info 获取模型对象,然后转换为字典格式返回。 - 适用于前端接口调用。 - - Args: - param_id: 参数ID - - Returns: - dict/None: 参数详情字典,不存在时返回 None - """ - try: - # 复用 get_info 方法查询参数 - param = get_info(param_id) - if not param: - return None - # 转换为字典格式 - return model_to_dict(param) - - except Exception as e: - logger.error(f"查询参数详情异常,ID: {param_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 参数信息查询(根据ID)- 核心查询方法 -# ============================================================ - -def get_info(param_id: int): - """ - 根据ID查询参数信息(返回模型对象) - - 这是一个通用的参数查询方法,返回参数模型对象而非字典。 - 与 get_param_detail 的区别: - - get_param_detail: 返回字典格式,用于前端接口 - - get_info: 返回模型对象,用于内部服务调用 - - 此方法被以下方法复用: - - get_param_detail: 获取详情字典 - - update_param: 验证参数是否存在 - - update_param_status: 验证参数是否存在 - - Args: - param_id: 参数ID - - Returns: - Param/None: 参数模型对象,不存在或已删除时返回 None - - Example: - param = get_info(param_id) - if param: - print(param.name) - """ - # 参数有效性验证 - if not param_id or param_id <= 0: - return None - - try: - # 查询未删除的参数 - param = Param.query.filter( - and_(Param.id == param_id, Param.is_delete == 0) - ).first() - return param - except Exception as e: - logger.error(f"查询参数信息异常,ID: {param_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 参数添加 -# ============================================================ - -def add_param(): - """ - 添加参数 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证(名称唯一性、编码唯一性、字段格式等) - 3. 创建参数对象并保存到数据库 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = ParamForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - # 创建参数 - param = Param(**form.data) - param.create_user = get_username() - param.save() - - return R.ok(msg="添加成功") - - except Exception as e: - logger.error(f"添加参数异常: {str(e)}") - return R.failed(msg="添加参数失败,请稍后重试") - - -# ============================================================ -# 参数更新 -# ============================================================ - -def update_param(): - """ - 更新参数 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证 - 3. 验证参数是否存在(复用 get_info) - 4. 更新参数信息 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = ParamForm(data=json_data) - if not form.validate(): - return R.failed(msg=regular.get_err(form)) - - # 获取参数ID - param_id = form.data.get('id') - if not param_id or int(param_id) <= 0: - return R.failed("参数ID无效") - param_id = int(param_id) - - # 复用 get_info 方法验证参数是否存在 - param = get_info(param_id) - if not param: - return R.failed("参数不存在") - - # 更新参数信息(移除ID,避免更新主键) - form_data = form.data.copy() - form_data.pop('id', None) - - form_data['update_time'] = datetime.now(tz=timezone.utc) - form_data['update_user'] = get_username() - - result = Param.query.filter_by(id=param_id).update(form_data) - - if not result: - return R.failed("更新失败") - - return R.ok(msg="更新成功") - - except Exception as e: - logger.error(f"更新参数异常: {str(e)}") - return R.failed(msg="更新参数失败,请稍后重试") - - -# ============================================================ -# 参数删除 -# ============================================================ - -def delete_params(param_ids): - """ - 删除参数(批量删除) - - 删除 - 支持逗号分隔的多个ID批量删除。 - - Args: - param_ids: 参数ID字符串,多个ID用逗号分隔,如 "1,2,3" - - Returns: - R: 操作结果 - """ - return Param.batch_delete_r(param_ids) - - -# ============================================================ -# 参数状态设置 -# ============================================================ - -def update_param_status(): - """ - 设置参数状态 - - 单独更新参数的启用/禁用状态,不涉及其他字段。 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = ParamStatusForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - param_id = int(form.data['id']) - status = int(form.data['status']) - - # 复用 get_info 方法验证参数是否存在 - param = get_info(param_id) - if not param: - return R.failed("参数不存在") - - # 更新状态 - update_data = {"status": status, "update_time": datetime.now(tz=timezone.utc), "update_user": get_username()} - result = Param.query.filter_by(id=param_id).update(update_data) - - if not result: - return R.failed("设置失败") - - return R.ok(msg="设置成功") - - except Exception as e: - logger.error(f"设置参数状态异常: {str(e)}") - return R.failed(msg="设置参数状态失败,请稍后重试") +from src.modules.param.service import * # noqa: F401,F403 diff --git a/application/services/position.py b/application/services/position.py index 9aa8990..3ec15bf 100644 --- a/application/services/position.py +++ b/application/services/position.py @@ -1,444 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -岗位管理服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了岗位管理相关的所有业务逻辑。 -包括岗位的分页查询、详情查询、添加、更新、删除、状态设置等功能。 -岗位用于标识用户的职位岗位,如:项目经理、开发工程师、测试工程师等, -每个岗位包含名称、排序、状态等属性,通过sort字段控制显示顺序。 - -命名规范: -- 函数参数使用驼峰命名(如 positionId, pageNo) -- 表单字段使用下划线命名(如 name, sort, status)- 与数据库字段保持一致 -- 返回数据使用驼峰命名(如 createUser, updateTime)- 与前端规范保持一致 -- 私有函数以下划线开头(如 _apply_filters, _build_page_data) - -功能模块: -1. 分页查询:支持按名称模糊搜索、状态筛选,按sort字段排序 -2. 详情查询:根据ID获取单个岗位的详细信息 -3. 数据维护:岗位的添加、更新、删除(支持批量) -4. 状态管理:单独更新岗位的启用/禁用状态 -5. 数据列表:获取所有启用的岗位列表(用于下拉选择等场景) +服务已迁移至 src.modules.system.position.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging - -from datetime import datetime, timezone -from flask import request -from sqlalchemy import and_ - -from application.constants.common import PAGE_SIZE -from application.forms.position import PositionForm, PositionStatusForm -from application.models.position import Position -from extensions import db -from utils import R, regular -from utils.case_converter import convert_camel_to_snake -from utils.model_utils import models_to_list, model_to_dict -from utils.request import parse_request_body, parse_id_list, like_contains -from utils.security import get_username - -# 获取日志记录器 -logger = logging.getLogger(__name__) - - -# ============================================================ -# 岗位分页查询 -# ============================================================ - -def get_position_page(): - """ - 查询岗位分页数据 - - Args: - request: Flask request对象,包含查询参数 - - pageNo: 当前页码(可选,默认为1) - - pageSize: 每页记录数(可选,默认为PAGE_SIZE) - - name: 岗位名称(可选,模糊查询) - - status: 岗位状态(可选,精确匹配) - - Returns: - R: 包含分页数据的JSON响应,格式为: - { - "records": 岗位列表, - "total": 总记录数, - "size": 每页大小, - "current": 当前页码, - "pages": 总页数 - } - """ - try: - # 获取分页参数 - page_no = int(request.args.get('pageNo', 1)) - page_size = int(request.args.get('pageSize', PAGE_SIZE)) - - # 应用筛选条件 - query = _apply_filters() - - # 记录总数 - total = query.count() - # 计算总页数 - pages = (total + page_size - 1) // page_size if total > 0 else 0 - - # 分页查询 - position_list = query.limit(page_size).offset((page_no - 1) * page_size).all() - # 构建返回数据 - page_data = _build_page_data(position_list, total, page_size, page_no, pages) - return R.ok(data=page_data) - - except Exception as e: - logger.error(f"查询岗位分页数据异常: {str(e)}") - return R.failed(msg="查询失败,请稍后重试") - - -def _apply_filters(): - """ - 应用查询筛选条件 - - 根据请求参数动态构建查询条件: - - 自动过滤已删除记录(is_delete == 0) - - 支持岗位名称模糊查询 - - 支持岗位状态精确查询 - - 默认按排序值升序排列 - - Returns: - Query: 应用筛选后的SQLAlchemy查询对象 - """ - # 基础查询:过滤已删除记录 - query = Position.query.filter(Position.is_delete == 0) - - # 岗位名称模糊筛选 - name = request.args.get('name') - if name: - query = query.filter(Position.name.like(like_contains(name), escape='\\')) - - # 岗位状态精确筛选 - status = request.args.get('status') - if status: - query = query.filter(Position.status == status) - - # 按排序值升序排列 - return query.order_by(Position.sort.asc()) - - -def _build_page_data(position_list, total, page_size, page_no, pages): - """ - 构建分页返回数据 - - Args: - position_list: 岗位对象列表 - total: 总记录数 - page_size: 每页大小 - page_no: 当前页码 - pages: 总页数 - - Returns: - dict: 分页数据,包含 records、total、size、current、pages - """ - # 将模型对象列表转换为字典列表 - records = models_to_list(position_list) - - return { - 'records': records, - 'total': total, - 'size': page_size, - 'current': page_no, - 'pages': pages - } - - -# ============================================================ -# 岗位详情查询 -# ============================================================ - -def get_position_detail(position_id): - """ - 根据ID查询岗位详情(返回字典格式) - - 此方法调用 get_info 获取模型对象,然后转换为字典格式返回。 - 适用于前端接口调用。 - - Args: - position_id: 岗位ID - - Returns: - dict/None: 岗位详情字典,不存在时返回 None - """ - try: - # 复用 get_info 方法查询岗位 - position = get_info(position_id) - if not position: - return None - # 转换为字典格式 - return model_to_dict(position) - - except Exception as e: - logger.error(f"查询岗位详情异常,ID: {position_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 岗位信息查询(根据ID)- 核心查询方法 -# ============================================================ - -def get_info(position_id: int): - """ - 根据ID查询岗位信息(返回模型对象) - - 这是一个通用的岗位查询方法,返回岗位模型对象而非字典。 - 与 get_position_detail 的区别: - - get_position_detail: 返回字典格式,用于前端接口 - - get_info: 返回模型对象,用于内部服务调用 - - 此方法被以下方法复用: - - get_position_detail: 获取详情字典 - - update_position: 验证岗位是否存在 - - update_position_status: 验证岗位是否存在 - - Args: - position_id: 岗位ID - - Returns: - Position/None: 岗位模型对象,不存在或已删除时返回 None - - Example: - position = get_info(position_id) - if position: - print(position.name) - """ - # 参数有效性验证 - if not position_id or position_id <= 0: - return None - - try: - # 查询未删除的岗位 - position = Position.query.filter( - and_(Position.id == position_id, Position.is_delete == 0) - ).first() - return position - except Exception as e: - logger.error(f"查询岗位信息异常,ID: {position_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 岗位添加 -# ============================================================ - -def add_position(): - """ - 添加岗位 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证(名称唯一性、字段格式等) - 3. 创建岗位对象并保存到数据库 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = PositionForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - # 创建岗位 - position = Position(**form.data) - position.create_user = get_username() - position.save() - - return R.ok(msg="添加成功") - - except Exception as e: - logger.error(f"添加岗位异常: {str(e)}") - return R.failed(msg="添加失败,请稍后重试") - - -# ============================================================ -# 岗位更新 -# ============================================================ - -def update_position(): - """ - 更新岗位 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证 - 3. 验证岗位是否存在(复用 get_info) - 4. 更新岗位信息 - - Returns: - R: 操作结果 - """ - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = PositionForm(data=json_data) - if not form.validate(): - return R.failed(msg=regular.get_err(form)) - - # 获取岗位ID - position_id = form.data.get('id') - if not position_id or int(position_id) <= 0: - return R.failed("岗位ID无效") - position_id = int(position_id) - - # 复用 get_info 方法验证岗位是否存在 - position = get_info(position_id) - if not position: - return R.failed("岗位不存在") - - # 更新岗位信息(移除ID,避免更新主键) - form_data = convert_camel_to_snake(form.data.copy()) - form_data.pop('id', None) - - form_data['update_time'] = datetime.now(tz=timezone.utc) - form_data['update_user'] = get_username() - - result = Position.query.filter_by(id=position_id).update(form_data) - - if not result: - return R.failed("更新失败") - - return R.ok(msg="更新成功") - - -# ============================================================ -# 岗位删除 -# ============================================================ - -def delete_positions(position_ids): - """ - 删除岗位(批量删除) - - 删除 - 支持逗号分隔的多个ID批量删除。 - - Args: - position_ids: 岗位ID字符串,多个ID用逗号分隔,如 "1,2,3" - - Returns: - R: 操作结果 - """ - return Position.batch_delete_r(position_ids) - - -# ============================================================ -# 岗位状态设置 -# ============================================================ - -def update_position_status(): - """ - 设置岗位状态 - - 单独更新岗位的启用/禁用状态,不涉及其他字段。 - - Returns: - R: 操作结果 - """ - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = PositionStatusForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - position_id = int(form.data['id']) - status = int(form.data['status']) - - # 复用 get_info 方法验证岗位是否存在 - position = get_info(position_id) - if not position: - return R.failed("岗位不存在") - - # 更新状态 - update_data = {"status": status, "update_time": datetime.now(tz=timezone.utc), "update_user": get_username()} - result = Position.query.filter_by(id=position_id).update(update_data) - - if not result: - return R.failed("设置失败") - - return R.ok(msg="设置成功") - - -# ============================================================ -# 岗位列表获取(无分页) -# ============================================================ - -def get_position_list(): - """ - 获取所有启用的岗位列表(无分页) - - 用于下拉选择框等需要全部数据的场景。 - 只返回状态为"正常"的岗位,按排序值升序排列。 - - Returns: - list: 岗位列表,每个元素为岗位模型字典,无数据时返回空列表 - """ - try: - # 查询所有启用的岗位 - position_list = Position.query.filter( - and_(Position.is_delete == 0, Position.status == 1) - ).order_by(Position.sort.asc()).all() - - # 转换为字典列表 - return models_to_list(position_list) - - except Exception as e: - logger.error(f"获取岗位列表异常: {str(e)}") - return [] +from src.modules.system.position.service import * # noqa: F401,F403 diff --git a/application/services/role.py b/application/services/role.py index f8120aa..f6c7a40 100644 --- a/application/services/role.py +++ b/application/services/role.py @@ -1,409 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -角色管理服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了角色管理相关的所有业务逻辑。 -包括角色的分页查询、详情查询、添加、更新、删除、状态设置等功能。 -角色用于管理系统中的用户角色,如:超级管理员、普通管理员等, -每个角色包含名称、编码、排序、状态等属性,通过sort字段控制显示顺序。 - -命名规范: -- 函数参数使用驼峰命名(如 roleId, pageNo) -- 表单字段使用下划线命名(如 name, code, sort)- 与数据库字段保持一致 -- 返回数据使用驼峰命名(如 createUser, updateTime)- 与前端规范保持一致 -- 私有函数以下划线开头(如 _apply_filters, _build_page_data) - -功能模块: -1. 分页查询:支持按名称模糊搜索、状态筛选,按sort字段排序 -2. 详情查询:根据ID获取单个角色的详细信息 -3. 数据维护:角色的添加、更新、删除(支持批量) -4. 状态管理:单独更新角色的启用/禁用状态 -5. 数据列表:获取所有启用的角色列表(用于下拉选择等场景) +服务已迁移至 src.modules.system.role.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging - -from datetime import datetime, timezone -from flask import request -from sqlalchemy import and_ - -from application.constants.common import PAGE_SIZE -from application.forms.role import RoleForm -from application.models.role import Role -from application.models.user_role import UserRole -from application.services.menu import invalidate_user_cache -from extensions import db -from utils import R, regular -from utils.case_converter import convert_camel_to_snake -from utils.model_utils import models_to_list, model_to_dict -from utils.request import parse_request_body, parse_id_list, like_contains -from utils.security import get_username - -# 获取日志记录器 -logger = logging.getLogger(__name__) - - -# ============================================================ -# 角色分页查询 -# ============================================================ - -def get_role_page(): - """ - 查询角色分页数据 - - Args: - request: Flask request对象,包含查询参数 - - pageNo: 当前页码(可选,默认为1) - - pageSize: 每页记录数(可选,默认为PAGE_SIZE) - - name: 角色名称(可选,模糊查询) - - Returns: - R: 包含分页数据的JSON响应,格式为: - { - "records": 角色列表, - "total": 总记录数, - "size": 每页大小, - "current": 当前页码, - "pages": 总页数 - } - """ - try: - # 获取分页参数 - page_no = int(request.args.get('pageNo', 1)) - page_size = int(request.args.get('pageSize', PAGE_SIZE)) - - # 应用筛选条件 - query = _apply_filters() - - # 记录总数 - total = query.count() - # 计算总页数 - pages = (total + page_size - 1) // page_size if total > 0 else 0 - - # 分页查询 - role_list = query.limit(page_size).offset((page_no - 1) * page_size).all() - # 构建返回数据 - page_data = _build_page_data(role_list, total, page_size, page_no, pages) - return R.ok(data=page_data) - - except Exception as e: - logger.error(f"查询角色分页数据异常: {str(e)}") - return R.failed(msg="查询失败,请稍后重试") - - -def _apply_filters(): - """ - 应用查询筛选条件 - - 根据请求参数动态构建查询条件: - - 自动过滤已删除记录(is_delete == 0) - - 支持角色名称模糊查询 - - 支持角色状态精确查询 - - 默认按排序值升序排列 - - Returns: - Query: 应用筛选后的SQLAlchemy查询对象 - """ - # 基础查询:过滤已删除记录 - query = Role.query.filter(Role.is_delete == 0) - - # 角色名称模糊筛选 - name = request.args.get('name') - if name: - query = query.filter(Role.name.like(like_contains(name), escape='\\')) - - # 按排序值升序排列 - return query.order_by(Role.sort.asc()) - - -def _build_page_data(role_list, total, page_size, page_no, pages): - """ - 构建分页返回数据 - - Args: - role_list: 角色对象列表 - total: 总记录数 - page_size: 每页大小 - page_no: 当前页码 - pages: 总页数 - - Returns: - dict: 分页数据,包含 records、total、size、current、pages - """ - # 将模型对象列表转换为字典列表 - records = models_to_list(role_list) - - return { - 'records': records, - 'total': total, - 'size': page_size, - 'current': page_no, - 'pages': pages - } - - -# ============================================================ -# 角色详情查询 -# ============================================================ - -def get_role_detail(role_id): - """ - 根据ID查询角色详情(返回字典格式) - - 此方法调用 get_info 获取模型对象,然后转换为字典格式返回。 - 适用于前端接口调用。 - - Args: - role_id: 角色ID - - Returns: - dict/None: 角色详情字典,不存在时返回 None - """ - try: - # 复用 get_info 方法查询角色 - role = get_info(role_id) - if not role: - return None - # 转换为字典格式 - return model_to_dict(role) - - except Exception as e: - logger.error(f"查询角色详情异常,ID: {role_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 角色信息查询(根据ID)- 核心查询方法 -# ============================================================ - -def get_info(role_id: int): - """ - 根据ID查询角色信息(返回模型对象) - - 这是一个通用的角色查询方法,返回角色模型对象而非字典。 - 与 get_role_detail 的区别: - - get_role_detail: 返回字典格式,用于前端接口 - - get_info: 返回模型对象,用于内部服务调用 - - 此方法被以下方法复用: - - get_role_detail: 获取详情字典 - - update_role: 验证角色是否存在 - - Args: - role_id: 角色ID - - Returns: - Role/None: 角色模型对象,不存在或已删除时返回 None - - Example: - role = get_info(role_id) - if role: - print(role.name) - """ - # 参数有效性验证 - if not role_id or role_id <= 0: - return None - - try: - # 查询未删除的角色 - role = Role.query.filter( - and_(Role.id == role_id, Role.is_delete == 0) - ).first() - return role - except Exception as e: - logger.error(f"查询角色信息异常,ID: {role_id}, 错误: {str(e)}") - return None - - -# ============================================================ -# 角色添加 -# ============================================================ - -def add_role(): - """ - 添加角色 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证(名称唯一性、字段格式等) - 3. 创建角色对象并保存到数据库 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = RoleForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - # 创建角色 - role = Role(**form.data) - role.create_user = get_username() - role.save() - - return R.ok(msg="添加成功") - - except Exception as e: - logger.error(f"添加角色异常: {str(e)}") - return R.failed(msg="添加失败,请稍后重试") - - -# ============================================================ -# 角色更新 -# ============================================================ - -def update_role(): - """ - 更新角色 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证 - 3. 验证角色是否存在(复用 get_info) - 4. 更新角色信息 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = RoleForm(data=json_data) - if not form.validate(): - return R.failed(msg=regular.get_err(form)) - - # 获取角色ID - role_id = form.data.get('id') - if not role_id or int(role_id) <= 0: - return R.failed("角色ID无效") - role_id = int(role_id) - - # 复用 get_info 方法验证角色是否存在 - role = get_info(role_id) - if not role: - return R.failed("角色不存在") - - # 更新角色信息(移除ID,避免更新主键) - form_data = convert_camel_to_snake(form.data.copy()) - form_data.pop('id', None) - - form_data['update_time'] = datetime.now(tz=timezone.utc) - form_data['update_user'] = get_username() - - result = Role.query.filter_by(id=role_id).update(form_data) - - if not result: - return R.failed("更新失败") - - return R.ok(msg="更新成功") - - except Exception as e: - logger.error(f"更新角色异常: {str(e)}") - return R.failed(msg="更新角色失败,请稍后重试") - - -# ============================================================ -# 角色删除 -# ============================================================ - -def delete_roles(role_ids): - """ - 删除角色(批量删除) - - 支持逗号分隔的多个ID批量删除。 - 删除前清除该角色下所有用户的权限缓存。 - - Args: - role_ids: 角色ID字符串,多个ID用逗号分隔,如 "1,2,3" - - Returns: - R: 操作结果 - """ - # 删除前清除该角色关联用户的缓存 - id_list = parse_id_list(role_ids) - for rid in id_list: - user_ids = [row.user_id for row in UserRole.query.filter(UserRole.role_id == rid).all()] - for uid in user_ids: - invalidate_user_cache(uid) - - return Role.batch_delete_r(role_ids) - - -# ============================================================ -# 角色列表获取(无分页) -# ============================================================ - -def get_role_list(): - """ - 获取所有启用的角色列表(无分页) - - 用于下拉选择框等需要全部数据的场景。 - 只返回状态为"正常"的角色,按排序值升序排列。 - - Returns: - list: 角色列表,每个元素为角色模型字典,无数据时返回空列表 - """ - try: - # 查询所有启用的角色 - role_list = Role.query.filter( - Role.is_delete == 0 - ).order_by(Role.sort.asc()).all() - - # 转换为字典列表 - return models_to_list(role_list) - - except Exception as e: - logger.error(f"获取角色列表异常: {str(e)}") - return [] +from src.modules.system.role.service import * # noqa: F401,F403 diff --git a/application/services/role_menu.py b/application/services/role_menu.py index d57bef6..1f500cd 100644 --- a/application/services/role_menu.py +++ b/application/services/role_menu.py @@ -1,192 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -角色菜单关联服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了角色与菜单之间的关联管理功能。 -包括根据角色ID查询菜单树、保存角色菜单权限分配等操作。 - -命名规范: -- 函数参数使用驼峰命名(如 roleId, menuIds) -- 返回数据使用驼峰命名(如 menuList, roleMenuList)- 与前端规范保持一致 -- 私有函数以下划线开头(如 _build_menu_tree) - -功能模块: -1. 菜单树查询:根据角色ID获取该角色已分配的菜单权限树 -2. 权限保存:保存角色的菜单权限分配(先删后增) +服务已迁移至 src.modules.association.role_menu.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging - -from sqlalchemy import and_ - -from application.forms.role_menu import RoleMenuForm -from application.models.menu import Menu -from application.models.role_menu import RoleMenu -from application.services.menu import invalidate_role_users_cache -from extensions import db -from utils import regular, R -from utils.request import parse_request_body - -# 获取日志记录器 -logger = logging.getLogger(__name__) - - -# ============================================================ -# 角色菜单树查询 -# ============================================================ - -def get_role_menus(role_id): - """ - 根据角色ID查询菜单权限树 - - 获取所有启用的菜单列表,并标记该角色已分配的菜单项。 - 用于角色权限分配页面的菜单树展示。 - - Args: - role_id: 角色ID - - Returns: - list: 菜单树列表,每个元素包含: - - id: 菜单ID - - title: 菜单标题 - - open: 是否展开(默认True) - - parentId: 父级菜单ID - - checked: 是否已分配(可选,已分配时为True) - - Example: - menu_tree = get_role_menu_tree(1) - # 返回格式:[{"id": 1, "title": "系统管理", "open": True, "parentId": 0, "checked": True}, ...] - """ - # 参数有效性验证 - if not role_id or role_id <= 0: - logger.warning(f"查询角色菜单树失败:角色ID无效 {role_id}") - return [] - - try: - # 获取全部启用的菜单列表(按排序升序) - menu_list = Menu.query.filter( - and_(Menu.is_delete == 0, Menu.status == 0) - ).order_by(Menu.sort.asc()).all() - - if not menu_list: - return [] - - # 根据角色ID查询已分配的角色菜单关系数据 - role_menus = RoleMenu.query.filter(RoleMenu.role_id == role_id).all() - assigned_menu_ids = {rm.menu_id for rm in role_menus} if role_menus else set() - - # 构建菜单树数据 - result = [] - for menu in menu_list: - data = { - 'id': menu.id, - 'name': menu.name, - 'parentId': menu.parent_id, - } - if menu.id in assigned_menu_ids: - data['checked'] = True - result.append(data) - - return result - - except Exception as e: - logger.error(f"查询角色菜单树异常,角色ID: {role_id}, 错误: {str(e)}") - return [] - - -# ============================================================ -# 角色菜单权限保存 -# ============================================================ - -def save_role_menu(): - """ - 保存角色菜单权限分配 - - 流程: - 1. 解析请求体JSON数据 - 2. 表单验证 - 3. 删除该角色原有的所有菜单权限 - 4. 重新创建新的菜单权限关联 - - Returns: - R: 操作结果 - """ - try: - # 获取参数 - json_data, error = parse_request_body() - if error: - return R.failed(msg=error) - - # 表单验证 - form = RoleMenuForm(data=json_data) - if not form.validate(): - err_msg = regular.get_err(form) - return R.failed(msg=err_msg) - - role_id = form.roleId.data - menu_ids = form.menuIds.data - - # 删除当前角色ID相关的所有菜单权限 - RoleMenu.query.filter(RoleMenu.role_id == role_id).delete() - - # 处理菜单数据 - if menu_ids and menu_ids != "": - for menu_id in menu_ids: - if not menu_id or menu_id <= 0: - continue - # 创建角色菜单关联数据 - role_menu = RoleMenu( - role_id=role_id, - menu_id=menu_id - ) - role_menu.save() - - # 清除该角色下所有用户的权限缓存 - invalidate_role_users_cache(role_id) - - return R.ok(msg="保存成功") - - except Exception as e: - logger.error(f"保存角色菜单权限异常: {str(e)}") - return R.failed(msg="保存角色菜单权限失败,请稍后重试") +from src.modules.association.role_menu.service import * # noqa: F401,F403 diff --git a/application/services/scheduler.py b/application/services/scheduler.py index 7b0561b..55eda8f 100644 --- a/application/services/scheduler.py +++ b/application/services/scheduler.py @@ -1,583 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -定时任务调度核心模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了定时任务的调度核心功能。 -使用APScheduler库来管理和执行定时任务。 +服务已迁移至 src.modules.job.scheduler_service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging -import sys -import threading -from datetime import datetime -from typing import Dict, Any, Optional - -import requests -from apscheduler.schedulers.background import BackgroundScheduler -from apscheduler.triggers.cron import CronTrigger -from apscheduler.jobstores.base import JobLookupError - -from config import INTERNAL_API_KEY, FLASK_SITE_DOMAIN -from utils.model_utils import model_to_dict - -# ============================================================ -# 全局变量和日志配置 -# ============================================================ - -# 线程局部存储(用于线程间数据隔离) -_thread_local = threading.local() -logger = logging.getLogger(__name__) - -# 调度器实例(延迟初始化) -_scheduler = None - - -# ============================================================ -# 调度器实例获取函数 -# ============================================================ - -def get_scheduler(): - """ - 获取调度器实例(延迟初始化) - - 使用延迟初始化模式,确保调度器只被创建一次。 - 避免在模块导入时立即创建,提高应用启动速度。 - - Returns: - BackgroundScheduler: APScheduler 后台调度器实例 - """ - global _scheduler - if _scheduler is None: - # 创建后台调度器,设置时区为北京时间 - _scheduler = BackgroundScheduler(timezone='Asia/Shanghai') - return _scheduler - - -# 创建全局调度器实例 -scheduler = get_scheduler() - - -# ============================================================ -# URL 构建工具函数 -# ============================================================ - -def build_full_url(url: str) -> str: - """ - 构建完整的URL - - 将相对路径拼接为完整的带域名的URL, - 如果已经是完整URL则直接返回。 - - Args: - url: 原始URL(相对路径或完整URL) - - Returns: - str: 完整URL - - Examples: - >>> build_full_url('/job/sendSms') - 'http://127.0.0.1:8021/job/sendSms' - >>> build_full_url('https://api.example.com/test') - 'https://api.example.com/test' - """ - if not url: - return url - - # 已经是完整URL,直接返回 - if url.startswith(('http://', 'https://')): - return url - - # 相对路径,拼接站点域名 - if url.startswith('/'): - url = f"{FLASK_SITE_DOMAIN}{url}" - - return url - - -# ============================================================ -# 任务执行核心函数 -# ============================================================ - -def execute_job( - job_id: int, - url: Optional[str] = None, - is_sync: int = 0, - job_trigger: Optional[str] = None, - params: Optional[Dict[str, Any]] = None -) -> Dict[str, Any]: - """ - 执行任务(调度器调用的入口函数) - - 这是 APScheduler 调用的核心函数,根据任务配置分发到不同的执行方式。 - - Args: - job_id: 任务ID - url: 任务URL(HTTP请求方式) - is_sync: 是否同步执行(1-同步,0-异步) - job_trigger: 任务触发器(内部调用方式) - params: 额外参数 - - Returns: - Dict: 执行结果,包含 success 和 data/error 字段 - """ - try: - print(f"[scheduler] execute_job id={job_id} url={url} sync={is_sync} trigger={job_trigger} params={params}") - sys.stdout.flush() - - logger.info(f"执行定时任务 [ID: {job_id}, URL: {url}, Trigger: {job_trigger}]") - - # 根据任务类型分发执行 - if url: - # URL方式:发送HTTP请求 - result = _execute_by_url(job_id, url, is_sync, params) - elif job_trigger: - # 触发器方式:调用内部执行器 - result = _execute_by_trigger(job_trigger, job_id, params) - else: - # 无效的任务配置 - result = {'success': False, 'error': '任务既没有URL也没有触发器'} - - return result - - except Exception as e: - logger.error(f"执行任务异常 [ID: {job_id}]: {str(e)}") - return {'success': False, 'error': str(e)} - - -# ============================================================ -# URL 方式任务执行 -# ============================================================ - -def _execute_by_url(job_id: int, url: str, is_sync: int, params: Optional[Dict] = None) -> Dict: - """ - 通过URL方式执行任务 - - 构建请求数据并发送HTTP请求。 - - Args: - job_id: 任务ID - url: 目标URL - is_sync: 1-同步等待响应,0-异步不等待 - params: 额外参数 - - Returns: - Dict: 执行结果 - """ - # 构建完整URL - full_url = build_full_url(url) - - # 构建请求数据 - request_data = { - 'job_id': job_id, - 'timestamp': datetime.now().isoformat(), # 请求时间戳 - 'params': params or {} - } - - # 根据同步/异步选择发送方式 - if is_sync == 1: - return _send_sync_request(full_url, request_data, job_id) - else: - return _send_async_request(full_url, request_data, job_id) - - -def _send_sync_request(url: str, data: Dict, job_id: int) -> Dict: - """ - 发送同步HTTP请求(等待响应) - - 同步发送POST请求,等待目标服务返回响应后继续执行。 - - Args: - url: 目标URL - data: 请求数据(JSON格式) - job_id: 任务ID(用于日志) - - Returns: - Dict: 包含响应状态码和数据的执行结果 - """ - try: - logger.info(f"发送同步请求 [ID: {job_id}, URL: {url}]") - - # 准备请求头 - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'Scheduler/1.0', - # 添加定时任务专用密钥,用于跳过权限验证 - 'X-Scheduler-Key': INTERNAL_API_KEY, - } - - # 发送POST请求 - response = requests.post( - url, - json=data, - timeout=30, # 30秒超时 - headers=headers - ) - response.raise_for_status() # 非200状态码抛出异常 - - # 解析响应数据 - try: - result_data = response.json() - except Exception: - # JSON解析失败时返回原始文本(限制长度) - result_data = {'text': response.text[:200]} - - return { - 'success': True, - 'status_code': response.status_code, - 'data': result_data - } - - except requests.exceptions.RequestException as e: - return {'success': False, 'error': str(e)} - - -def _send_async_request(url: str, data: Dict, job_id: int) -> Dict: - """ - 发送异步HTTP请求(不等待响应) - - 异步发送POST请求,在独立线程中执行,不等待响应结果。 - - Args: - url: 目标URL - data: 请求数据(JSON格式) - job_id: 任务ID(用于日志) - - Returns: - Dict: 立即返回触发状态 - """ - import threading - - def async_request(): - """异步请求执行函数(在独立线程中运行)""" - try: - headers = { - 'Content-Type': 'application/json', - 'X-Scheduler-Key': INTERNAL_API_KEY, - } - resp = requests.post(url, json=data, timeout=30, headers=headers) - logger.info(f"异步任务完成 [ID: {job_id}, 状态码: {resp.status_code}]") - except Exception as e: - logger.error(f"异步任务异常 [ID: {job_id}]: {str(e)}") - - # 创建并启动独立线程执行异步请求 - thread = threading.Thread(target=async_request) - thread.start() - - return {'success': True, 'status': 'triggered'} - - -def _execute_by_trigger(trigger: str, job_id: int, params: Optional[Dict] = None) -> Dict: - """ - 通过触发器方式执行任务 - - 调用内部任务执行器执行业务逻辑。 - - Args: - trigger: 触发器标识(任务类型) - job_id: 任务ID - params: 额外参数 - - Returns: - Dict: 执行结果 - """ - try: - from application.services.job_executor import job_executor - result = job_executor.execute(trigger, job_id=job_id, **(params or {})) - return {'success': True, 'trigger': trigger, 'data': result} - except Exception as e: - return {'success': False, 'error': str(e)} - - -# ============================================================ -# 调度器任务管理函数 -# ============================================================ - -def add_job_to_scheduler(job_dict:dict ) -> bool: - """ - 添加任务到调度器 - - 将任务配置添加到APScheduler中,设置Cron触发规则。 - - Args: - job_dict: 任务参数字典,包含以下字段: - - id: 任务ID - - status: 任务状态(1-启用) - - cronExpression: Cron表达式 - - url: 请求URL - - isSync: 是否同步 - - jobTrigger: 触发器标识 - - jobName: 任务名称 - - Returns: - bool: 添加成功返回True,否则返回False - """ - try: - # ============================================================ - # 1. 从字典中提取任务参数 - # ============================================================ - job_id = job_dict.get('id') - job_status = job_dict.get('status') - job_cron = job_dict.get('cronExpression') - job_url = job_dict.get('url') - job_sync = job_dict.get('isSync') - job_trigger = job_dict.get('jobTrigger') - job_name = job_dict.get('jobName') - - print(f"[scheduler] add_job id={job_id} status={job_status} cron='{job_cron}'") - - # ============================================================ - # 2. 参数校验 - # ============================================================ - # 检查任务状态是否启用 - if job_status != 1: - logger.debug(f"跳过添加任务: 状态不是启用 (id={job_id}, status={job_status})") - return False - - # 检查Cron表达式是否为空 - if not job_cron: - logger.warning(f"跳过添加任务: Cron表达式为空 (id={job_id})") - return False - - # ============================================================ - # 3. 验证Cron表达式并计算下次执行时间 - # ============================================================ - try: - trigger = CronTrigger.from_crontab(job_cron) - from datetime import datetime - next_run = trigger.get_next_fire_time(None, datetime.now()) - logger.debug(f"Cron表达式有效 (id={job_id}, 下次执行: {next_run})") - - if next_run is None: - logger.warning(f"无法计算下次执行时间,Cron表达式可能永远不会触发 (id={job_id})") - return False - - except Exception as e: - logger.error(f"Cron表达式无效 (id={job_id}): {str(e)}") - return False - - # ============================================================ - # 4. 添加任务到调度器 - # ============================================================ - scheduler_job_id = f"job_{job_id}" - - # 移除已存在的同名任务(避免重复) - try: - scheduler.remove_job(scheduler_job_id) - except JobLookupError: - pass - - # 添加新任务 - scheduler.add_job( - func=execute_job, # 执行函数 - trigger=trigger, # Cron触发器 - args=[job_id, job_url, job_sync, job_trigger, {}], # 函数参数 - id=scheduler_job_id, # 任务唯一标识 - name=job_name, # 任务显示名称 - replace_existing=True, # 替换已存在的任务 - misfire_grace_time=30, # 错过执行的宽限时间(秒) - coalesce=True # 合并错过的执行 - ) - - logger.info(f"任务已添加到调度器 [ID: {job_id}, cron: {job_cron}]") - return True - - except Exception as e: - logger.error(f"添加任务到调度器异常: {str(e)}") - return False - - -def remove_job_from_scheduler(job_id: int) -> bool: - """ - 从调度器移除任务 - - Args: - job_id: 任务ID - - Returns: - bool: 移除成功返回True,否则返回False - """ - try: - scheduler.remove_job(f"job_{job_id}") - return True - except JobLookupError: - # 任务不存在,视为成功 - return False - except Exception as e: - logger.error(f"移除任务异常: {str(e)}") - return False - - -def pause_job_in_scheduler(job_id: int) -> bool: - """ - 暂停调度器中的任务 - - Args: - job_id: 任务ID - - Returns: - bool: 暂停成功返回True,否则返回False - """ - try: - scheduler.pause_job(f"job_{job_id}") - return True - except JobLookupError: - return False - except Exception as e: - logger.error(f"暂停任务异常: {str(e)}") - return False - - -def resume_job_in_scheduler(job_id: int) -> bool: - """ - 恢复调度器中的任务 - - Args: - job_id: 任务ID - - Returns: - bool: 恢复成功返回True,否则返回False - """ - try: - scheduler.resume_job(f"job_{job_id}") - return True - except JobLookupError: - return False - except Exception as e: - logger.error(f"恢复任务异常: {str(e)}") - return False - - -def run_job_once(job_id: int, params: Optional[Dict] = None) -> Dict: - """ - 立即执行一次任务(不依赖调度器) - - 忽略Cron表达式,立即执行指定任务一次。 - - Args: - job_id: 任务ID - params: 执行参数 - - Returns: - Dict: 执行结果 - """ - try: - from application.models.job import Job - job = Job.query.filter(Job.id == job_id, Job.is_delete == 0).first() - - if not job: - return {'success': False, 'error': '任务不存在'} - - # 直接执行任务(同步方式) - return execute_job(job.id, job.url, 1, job.job_trigger, params or {}) - - except Exception as e: - return {'success': False, 'error': str(e)} - - -# ============================================================ -# 调度器初始化和重载函数 -# ============================================================ - -def init_scheduler() -> int: - """ - 初始化调度器 - - 启动调度器并从数据库加载所有启用的任务。 - - Returns: - int: 成功加载的任务数量 - """ - try: - from application.models.job import Job - - # 启动调度器(如果尚未启动) - if not scheduler.running: - scheduler.start() - logger.info("调度器已启动") - - # 查询所有启用的任务 - jobs = Job.query.filter( - Job.status == 1, - Job.is_delete == 0 - ).all() - - # 使用 model_to_dict 将每个 job 对象转换为字典 - # 避免 SQLAlchemy Session 分离问题 - job_dicts = [] - for job in jobs: - job_dict = dict(model_to_dict(job)) - job_dicts.append(job_dict) - - # 使用字典数据添加任务到调度器 - success_count = 0 - for job_dict in job_dicts: - if add_job_to_scheduler(job_dict): - success_count += 1 - - logger.info(f"调度器初始化完成,加载 {success_count}/{len(job_dicts)} 个任务") - return success_count - - except Exception as e: - logger.error(f"初始化调度器异常: {str(e)}") - return 0 - - -def reload_scheduler() -> int: - """ - 重新加载调度器 - - 清空所有现有任务,然后重新从数据库加载。 - - Returns: - int: 成功加载的任务数量 - """ - try: - # 清空所有现有任务 - scheduler.remove_all_jobs() - # 重新初始化 - return init_scheduler() - except Exception as e: - logger.error(f"重新加载调度器异常: {str(e)}") - return 0 +from src.modules.job.scheduler_service import * # noqa: F401,F403 diff --git a/application/services/sms_svc.py b/application/services/sms_svc.py index e3b14c8..752b4b6 100644 --- a/application/services/sms_svc.py +++ b/application/services/sms_svc.py @@ -1,193 +1,8 @@ """ -短信发送服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -封装阿里云短信 SDK 提供短信发送能力,供业务模块和定时任务调用。 - -使用方式: - from application.services import sms_svc - - # 模板短信(推荐) - sms_svc.send_sms( - phone='13800138000', - template_code='SMS_123456', - template_params={'code': '123456'} - ) - - # 指定签名(覆盖默认签名) - sms_svc.send_sms( - phone='13800138000', - template_code='SMS_123456', - template_params={'code': '123456'}, - sign_name='我的应用' - ) - - # 群发(多号码) - sms_svc.send_sms( - phone=['13800138000', '13900139000'], - template_code='SMS_123456', - template_params={'name': '张三'} - ) +服务已迁移至 src.modules.job.sms_service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import json -import logging -from typing import Dict, List, Optional, Union - -from config import ( - SMS_ALIYUN_ACCESS_KEY_ID, - SMS_ALIYUN_ACCESS_KEY_SECRET, - SMS_ALIYUN_CONNECT_TIMEOUT, - SMS_ALIYUN_ENDPOINT, - SMS_ALIYUN_READ_TIMEOUT, - SMS_ALIYUN_SIGN_NAME, - SMS_PROVIDER, -) - -logger = logging.getLogger(__name__) - - -# ============================================================ -# 阿里云短信客户端(延迟初始化) -# ============================================================ - -_client = None - - -def _get_client(): - """ - 获取阿里云短信客户端(单例,延迟初始化) - - Returns: - dysmsapi.Client: 阿里云短信客户端实例 - - Raises: - ImportError: 阿里云 SDK 未安装 - Exception: 客户端创建失败 - """ - global _client - if _client is not None: - return _client - - try: - from alibabacloud_dysmsapi20170525.client import Client - from alibabacloud_tea_openapi.models import Config - except ImportError: - raise ImportError( - "阿里云短信 SDK 未安装,请执行: " - "pip install alibabacloud-dysmsapi20170525 alibabacloud-tea-openapi" - ) - - config = Config( - access_key_id=SMS_ALIYUN_ACCESS_KEY_ID, - access_key_secret=SMS_ALIYUN_ACCESS_KEY_SECRET, - endpoint=SMS_ALIYUN_ENDPOINT, - connect_timeout=SMS_ALIYUN_CONNECT_TIMEOUT, - read_timeout=SMS_ALIYUN_READ_TIMEOUT, - ) - _client = Client(config) - return _client - - -# ============================================================ -# 短信发送 -# ============================================================ - -def send_sms( - phone: Union[str, List[str]], - template_code: str, - template_params: Optional[Dict] = None, - sign_name: Optional[str] = None, -) -> bool: - """ - 发送短信(阿里云模板短信) - - Args: - phone: 手机号,字符串或列表(群发时传入多个号码) - template_code: 短信模板CODE(在阿里云控制台获取,如 SMS_123456) - template_params: 模板变量,如 {'code': '123456'} 对应模板中的 ${code} - sign_name: 短信签名(可选,默认使用配置中的 SMS_ALIYUN_SIGN_NAME) - - Returns: - bool: 全部发送成功返回 True,任一失败返回 False - """ - if not phone: - logger.warning("发送短信失败:手机号不能为空") - return False - - if not template_code: - logger.warning("发送短信失败:模板CODE不能为空") - return False - - if not sign_name: - sign_name = SMS_ALIYUN_SIGN_NAME - - if not sign_name: - logger.warning("发送短信失败:短信签名未配置") - return False - - # 统一转为列表 - phones = [phone] if isinstance(phone, str) else phone - - # 检查 AccessKey 配置 - if not SMS_ALIYUN_ACCESS_KEY_ID or not SMS_ALIYUN_ACCESS_KEY_SECRET: - logger.warning("发送短信失败:阿里云 AccessKey 未配置") - return False - - # 模板变量序列化 - params_str = json.dumps(template_params, ensure_ascii=False) if template_params else '' - - # 逐条发送(阿里云短信 API 不支持批量同发) - all_success = True - for p in phones: - if not _send_one(p, sign_name, template_code, params_str): - all_success = False - - return all_success - - -def _send_one(phone: str, sign_name: str, template_code: str, template_params: str) -> bool: - """ - 发送单条短信 - - Args: - phone: 手机号 - sign_name: 短信签名 - template_code: 模板CODE - template_params: 模板变量 JSON 字符串 - - Returns: - bool: 发送成功返回 True - """ - try: - from alibabacloud_dysmsapi20170525 import models as dysms_models - - client = _get_client() - - request = dysms_models.SendSmsRequest( - phone_numbers=phone, - sign_name=sign_name, - template_code=template_code, - template_param=template_params, - ) - - response = client.send_sms(request) - body = response.body - - if body.code == 'OK': - logger.info(f"短信发送成功: phone={phone}, template={template_code}, biz_id={body.biz_id}") - return True - else: - logger.warning( - f"短信发送失败: phone={phone}, template={template_code}, " - f"code={body.code}, message={body.message}" - ) - return False - - except ImportError as e: - logger.error(f"阿里云短信 SDK 未安装: {str(e)}") - return False - - except Exception as e: - logger.error(f"短信发送异常: phone={phone}, template={template_code}, error={str(e)}") - return False +from src.modules.job.sms_service import * # noqa: F401,F403 diff --git a/application/services/upload.py b/application/services/upload.py index 98381c8..24d31cc 100644 --- a/application/services/upload.py +++ b/application/services/upload.py @@ -1,396 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -文件上传服务模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块实现了文件上传相关的业务逻辑。 -支持图片和各类文件的上传,自动进行文件类型验证、存储路径生成、 -文件重命名等操作,并返回标准化的文件信息。 +服务已迁移至 src.modules.upload.service。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging -import os -import re -import random -import time -import mimetypes - -from flask import request - -from config import FLASK_TEMP_PATH, FLASK_UPLOAD_DIR, UPLOAD_ALLOWED_EXTS, UPLOAD_MAX_SIZE_MB -from utils import R -from utils.file import mkdir, verify_file_magic -from utils.rich_text import get_file_url - -# 获取日志记录器 -logger = logging.getLogger(__name__) - - -# ============================================================ -# 文件上传 -# ============================================================ - -def upload_file(file_exts=UPLOAD_ALLOWED_EXTS): - """ - 上传文件(兼容图片和各类文件) - - Args: - file_exts: 允许的文件扩展名列表或字符串,如 ['.jpg', '.png'] 或 ".jpg|.png|.gif" - 默认使用 UPLOAD_ALLOWED_EXTS 全局配置 - - Returns: - dict: 包含文件信息的字典,格式为: - { - 'originalName': 原始文件名, - 'fileExtension': 文件扩展名, - 'fileType': 文件MIME类型, - 'fileSize': 文件大小, - 'fileName': 生成的文件名, - 'filePath': 文件相对路径, - 'fileUrl': 文件访问URL - } - """ - try: - # 步骤1:获取上传的文件对象 - file = request.files.get('file') - if not file: - logger.warning("未选择上传文件") - return R.failed(msg="请选择要上传的文件") - - # 步骤2:获取文件基本信息 - original_name = file.filename - file_size = _get_file_size(file) - - # 步骤2.5:校验文件大小 - max_size_bytes = UPLOAD_MAX_SIZE_MB * 1024 * 1024 - if file_size and file_size > max_size_bytes: - logger.warning(f"文件大小超限: {file_size} bytes, 上限: {max_size_bytes} bytes") - return R.failed(msg=f"文件大小超过限制,最大允许 {UPLOAD_MAX_SIZE_MB}MB") - - # 步骤3:获取并处理文件扩展名 - file_ext_with_dot = os.path.splitext(original_name)[1].lower() - file_ext = file_ext_with_dot[1:] if file_ext_with_dot.startswith('.') else '' - - # 步骤4:验证文件扩展名是否在允许的范围内 - if file_exts: - allowed_exts = _parse_allowed_extensions(file_exts) - if allowed_exts and file_ext_with_dot not in allowed_exts: - logger.warning(f"文件格式不支持,允许: {allowed_exts},实际: {file_ext_with_dot}") - return R.failed(msg=f"文件格式不支持,仅支持 {', '.join(allowed_exts)} 格式") - - # 步骤4.5:校验文件魔术字节,防止扩展名伪造 - is_valid, magic_msg = verify_file_magic(file, file_ext_with_dot) - if not is_valid: - logger.warning(f"文件魔术字节校验失败: {magic_msg},文件名: {original_name}") - return R.failed(msg=magic_msg) - - # 步骤5:获取文件的MIME类型 - mime_type = _get_mime_type(original_name, file_ext_with_dot) - - # 步骤6:生成新的文件名(避免文件名冲突) - generated_name = _generate_filename(file_ext_with_dot) - - # 步骤7:生成存储路径(按日期分目录存储) - date_dir = time.strftime('%Y%m%d') - save_dir = os.path.join(FLASK_TEMP_PATH, date_dir) - - # 步骤8:创建存放目录(如果目录不存在) - mkdir(save_dir) - - # 步骤9:生成完整的文件保存路径 - file_path = os.path.join(save_dir, generated_name) - - # 步骤10:保存文件 - file.save(file_path) - - # 步骤10.5:SVG文件清洗,移除可执行脚本防止XSS攻击 - if file_ext_with_dot == '.svg': - _sanitize_svg(file_path) - - # 步骤11:获取相对路径(用于存储和返回) - relative_path = file_path.replace(FLASK_UPLOAD_DIR, "").replace("\\", "/") - - # 步骤12:生成文件的访问URL - file_url = get_file_url(relative_path) - - # 步骤13:构建标准化的返回数据 - result = { - 'originalName': original_name, - 'fileExtension': file_ext, - 'fileType': mime_type, - 'fileSize': file_size, - 'fileName': generated_name, - 'filePath': relative_path, - 'fileUrl': file_url, - } - - return R.ok(msg="上传成功", data=result) - - except Exception as e: - logger.error(f"文件上传异常: {str(e)}") - return R.failed(msg=f"上传失败: {str(e)}") - - -# ============================================================ -# 内部辅助函数 -# ============================================================ - -def _parse_allowed_extensions(exts): - """ - 解析允许的扩展名参数(内部工具函数) - - 将各种格式的扩展名参数统一转换为带点的列表格式。 - 支持的输入格式: - - 列表:['.jpg', '.png'] 或 ['jpg', 'png'] - - 字符串:".jpg|.png|.gif" 或 "jpg,png,gif" 或 "jpg" - - Args: - exts: 扩展名参数,可以是字符串或列表 - - Returns: - list: 允许的扩展名列表,格式为 ['.jpg', '.png'] - """ - if not exts: - return [] - - if isinstance(exts, list): - return [ext if ext.startswith('.') else f'.{ext}' for ext in exts] - - if isinstance(exts, str): - if '|' in exts: - parts = exts.split('|') - elif ',' in exts: - parts = exts.split(',') - else: - parts = [exts] - - result = [] - for part in parts: - part = part.strip().lower() - if part: - if not part.startswith('.'): - part = f'.{part}' - result.append(part) - return result - - return [] - - -def _get_mime_type(filename, ext_with_dot): - """ - 获取文件的MIME类型(内部工具函数) - - Args: - filename: 原始文件名 - ext_with_dot: 带点的扩展名,如 '.jpg' - - Returns: - str: MIME类型 - """ - mime_type, _ = mimetypes.guess_type(filename) - if not mime_type: - mime_type = _guess_mime_type(ext_with_dot) - return mime_type - - -def _guess_mime_type(ext_with_dot): - """ - 根据扩展名推断MIME类型(内部工具函数) - - 当mimetypes.guess_type无法识别时,使用此函数手动推断MIME类型。 - - Args: - ext_with_dot: 带点的扩展名,如 '.jpg' - - Returns: - str: MIME类型,如果无法识别则返回默认的'application/octet-stream' - """ - mime_map = { - # 图片类型 - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.png': 'image/png', - '.gif': 'image/gif', - '.bmp': 'image/bmp', - '.webp': 'image/webp', - '.svg': 'image/svg+xml', - '.ico': 'image/x-icon', - - # 文本类型 - '.txt': 'text/plain', - '.html': 'text/html', - '.htm': 'text/html', - '.css': 'text/css', - '.js': 'application/javascript', - '.json': 'application/json', - '.xml': 'application/xml', - - # 文档类型 - '.pdf': 'application/pdf', - '.doc': 'application/msword', - '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - '.xls': 'application/vnd.ms-excel', - '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - '.ppt': 'application/vnd.ms-powerpoint', - '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - - # 音频视频类型 - '.mp3': 'audio/mpeg', - '.mp4': 'video/mp4', - '.avi': 'video/x-msvideo', - '.mov': 'video/quicktime', - - # 压缩包类型 - '.zip': 'application/zip', - '.rar': 'application/x-rar-compressed', - '.7z': 'application/x-7z-compressed', - '.tar': 'application/x-tar', - '.gz': 'application/gzip', - } - - return mime_map.get(ext_with_dot, 'application/octet-stream') - - -def _generate_filename(ext_with_dot): - """ - 生成唯一的文件名(内部工具函数) - - 格式:年月日时分秒 + 5位随机数 + 扩展名 - - Args: - ext_with_dot: 带点的扩展名,如 '.jpg' - - Returns: - str: 生成的文件名 - """ - timestamp = time.strftime('%Y%m%d%H%M%S') - random_num = '%05d' % random.randint(0, 99999) - return f"{timestamp}{random_num}{ext_with_dot}" - - -def _get_file_size(file): - """ - 获取文件大小(内部工具函数) - - Flask中可以通过多种方式获取文件大小: - - file.content_length: 内容长度 - - 保存后使用 os.path.getsize - - Args: - file: Flask文件对象 - - Returns: - int: 文件大小(字节),如果无法获取则返回0 - """ - try: - # 尝试获取 content_length - if hasattr(file, 'content_length') and file.content_length: - return file.content_length - - # 保存文件位置后获取大小 - if hasattr(file, 'filename') and file.filename: - # 临时保存到内存流中获取大小 - file.seek(0, 2) # 移动到文件末尾 - size = file.tell() # 获取当前位置(文件大小) - file.seek(0) # 重置到文件开头 - return size - except Exception as e: - logger.warning(f"获取文件大小异常: {str(e)}") - return 0 - - -# SVG中需要移除的危险标签 -_SVG_DANGEROUS_TAGS = re.compile( - r'<\s*/?(?:script|iframe|object|embed|applet|form|input|button|textarea|select|link|meta|base)' - r'(?:\s[^>]*)?\s*/?>', - re.IGNORECASE | re.DOTALL, -) - -# SVG中需要移除的危险事件属性(on*) -_SVG_EVENT_ATTRS = re.compile( - r'\s+on\w+\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s>]+)', - re.IGNORECASE, -) - -# SVG中需要移除的危险href(javascript: / data: 协议) -_SVG_DANGEROUS_HREF = re.compile( - r'(\s+(?:href|xlink:href)\s*=\s*)(?:"javascript:[^"]*"|\'javascript:[^\']*\'|javascript:[^\s>]+' - r'|"data:[^"]*"|\'data:[^\']*\'|data:[^\s>]+)', - re.IGNORECASE, -) - - -def _sanitize_svg(file_path): - """ - 清洗SVG文件,移除可执行脚本内容防止XSS攻击 - - SVG是文本格式的XML,可内嵌