refactor: P3c 服务层迁入 src/modules/ → service.py(29 服务)

- 22 个单服务 + 3 组合并(config+config_item、dict+dict_item、login_log+operation_log)
- 特殊命名:job_executor→executor、scheduler→scheduler_service、
  mail_svc→mail_service、sms_svc→sms_service(含使用处标识符改写)
- 重写引用:跨服务(module 别名/子模块)、models/forms→src.modules、
  db/extensions→src.core、utils→src.common|core、R→src.core.response
- application/services/ 全部替换为 shim

【验收】全部服务文件编译通过;应用启动;pytest 5 passed;核心服务导入正常

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
DjangoAdmin Dev
2026-08-07 14:18:40 +08:00
co-authored by Claude
parent 2586c0aace
commit 87aec4e26e
53 changed files with 10586 additions and 11055 deletions
+234
View File
@@ -0,0 +1,234 @@
# +======================================================================
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
# +----------------------------------------------------------------------
# | Copyright (c) 2021-2026 一米阳光团队 版权所有
# +----------------------------------------------------------------------
# | 开源协议: Apache-2.0
# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。
# +----------------------------------------------------------------------
# | 官方网站: https://www.djangoadmin.cn
# +----------------------------------------------------------------------
# | 研发团队: 一米阳光团队 荣誉出品
# +======================================================================
# | 版权和免责声明:
# | 一、知识产权声明
# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、
# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权,
# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。
# | 二、使用限制与禁止条款
# | 您理解并同意,禁止将本产品用于以下任何行为:
# | 1. 任何违反中华人民共和国法律法规的用途;
# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为;
# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。
# | 三、责任豁免
# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失
# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三
# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。
# | 四、违约与法律责任
# | 如发现任何违反本声明的行为,本团队有权:
# | 1. 无条件配合公安机关及其他有权机关的调查取证工作;
# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失;
# | 3. 保留进一步采取刑事控告等法律措施的权利。
# | 五、许可范围说明
# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包
# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通
# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、
# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造
# | 成的一切经济损失及商誉损害。
# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件
# | 许可协议》附件为准。
# +======================================================================
from flask import request, g
from sqlalchemy import and_
from src.modules.auth.schema import LoginForm
from src.modules.log.models import LoginLog
from src.modules.system.user.models import User
from src.modules.log import service as login_log
from config import CAPTCHA_BYPASS_CODE, FLASK_DEMO, LOGIN_FAIL_MAX, LOGIN_LOCK_SECONDS
from src.common.utils import regular
from src.core import response as R
from src.core.password import verify_password
from src.common.utils.request import parse_request_body
from src.core.jwt import create_token
from src.core.redis_client import redis
from src.common.utils.ip import get_client_ip
from src.common.utils.ip2region import get_ip_location
from src.common.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)}")