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='