chore: initial commit — leaudit-platform project skeleton
17-table PostgreSQL schema with full Chinese column comments, FastAPI project structure (admin/common/modules), DSL rule files, and schema migration scripts.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
"""评查服务实现。
|
||||
|
||||
编排 LeAudit 引擎执行链路:
|
||||
文档 → OCR → Extract → Evaluate → Rescue → Persist
|
||||
"""
|
||||
|
||||
from fastapi_common.fastapi_common_logger import logger
|
||||
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession
|
||||
from fastapi_common.fastapi_common_web.domain.responses import StatusCodeEnum
|
||||
from fastapi_common.fastapi_common_web.exception.LeauditException import LeauditException
|
||||
|
||||
from fastapi_modules.fastapi_leaudit.domian.vo.auditVo import AuditRunVO, AuditResultVO
|
||||
from fastapi_modules.fastapi_leaudit.models import LeauditAuditRun
|
||||
from fastapi_modules.fastapi_leaudit.services import IAuditService
|
||||
|
||||
|
||||
class AuditServiceImpl(IAuditService):
|
||||
"""评查服务实现。"""
|
||||
|
||||
async def Run(self, DocumentId: int, RuleType: str | None = None, Force: bool = False) -> AuditRunVO:
|
||||
"""触发文档评查。
|
||||
|
||||
实际执行流程由 Celery 任务异步处理。
|
||||
"""
|
||||
async with GetAsyncSession() as session:
|
||||
# TODO: 从 bridge 层获取 pipeline,提交 Celery 任务
|
||||
logger.info(f"触发评查: documentId={DocumentId}, ruleType={RuleType}")
|
||||
raise LeauditException(StatusCodeEnum.HTTP_500_INTERNAL_SERVER_ERROR, "Celery 任务集成待实现")
|
||||
|
||||
async def GetRunStatus(self, RunId: int) -> AuditRunVO:
|
||||
"""查询评查运行状态。"""
|
||||
async with GetAsyncSession() as session:
|
||||
run = await session.get(LeauditAuditRun, RunId)
|
||||
if not run:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "评查运行记录不存在")
|
||||
return AuditRunVO(
|
||||
runId=run.Id,
|
||||
documentId=run.documentId,
|
||||
runNo=run.runNo,
|
||||
status=run.status,
|
||||
phase=run.phase,
|
||||
totalScore=float(run.totalScore) if run.totalScore else None,
|
||||
passedCount=run.passedCount,
|
||||
failedCount=run.failedCount,
|
||||
startedAt=run.startedAt,
|
||||
finishedAt=run.finishedAt,
|
||||
)
|
||||
|
||||
async def GetResult(self, RunId: int) -> AuditResultVO:
|
||||
"""获取评查结果。"""
|
||||
async with GetAsyncSession() as session:
|
||||
run = await session.get(LeauditAuditRun, RunId)
|
||||
if not run:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "评查运行记录不存在")
|
||||
# TODO: 从 leaudit_rule_results 表查询规则级结果
|
||||
return AuditResultVO(
|
||||
runId=run.Id,
|
||||
totalScore=float(run.totalScore) if run.totalScore else None,
|
||||
passedCount=run.passedCount or 0,
|
||||
failedCount=run.failedCount or 0,
|
||||
skippedCount=run.skippedCount or 0,
|
||||
phase=run.phase,
|
||||
rescueApplied=run.rescueApplied or False,
|
||||
rules=[],
|
||||
)
|
||||
@@ -0,0 +1,162 @@
|
||||
"""认证服务实现。
|
||||
|
||||
从旧项目 app/routes/auth.py 和 app/auth/auth.py 迁移,业务逻辑完全不变。
|
||||
仅重组为 Controller → Service(interface+impl) → Model 结构。
|
||||
"""
|
||||
|
||||
from fastapi_common.fastapi_common_logger import logger
|
||||
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession
|
||||
from fastapi_common.fastapi_common_security.jwtService import JwtService
|
||||
from fastapi_common.fastapi_common_web.exception.LeauditException import LeauditException
|
||||
from fastapi_common.fastapi_common_web.domain.responses import StatusCodeEnum
|
||||
|
||||
from fastapi_modules.fastapi_leaudit.domian.vo.auth.loginTokenVo import LoginTokenVO
|
||||
from fastapi_modules.fastapi_leaudit.services.authService import IAuthService
|
||||
|
||||
|
||||
class AuthServiceImpl(IAuthService):
|
||||
"""认证服务实现。"""
|
||||
|
||||
async def PasswordLogin(self, Sub: str, Password: str) -> LoginTokenVO:
|
||||
"""账密登录。
|
||||
|
||||
校验 sso_users 表:sub + password + status=0 + deleted_at IS NULL。
|
||||
安全:统一错误提示"账号或密码错误",防止用户枚举。
|
||||
"""
|
||||
async with GetAsyncSession() as session:
|
||||
from sqlalchemy import select, text
|
||||
|
||||
result = await session.execute(
|
||||
text("SELECT id, sub, username, nick_name, phone_number, email, "
|
||||
"ou_id, ou_name, is_leader, password, status, deleted_at, "
|
||||
"try_count, try_login_time, area, tenant_name, dep_name, dep_short_name "
|
||||
"FROM sso_users WHERE sub = :sub"),
|
||||
{"sub": Sub},
|
||||
)
|
||||
row = result.fetchone()
|
||||
|
||||
if not row:
|
||||
logger.warning(f"登录失败: 用户不存在 - sub={Sub}")
|
||||
raise LeauditException(StatusCodeEnum.HTTP_401_UNAUTHORIZED, "账号或密码错误")
|
||||
|
||||
user = dict(row._mapping)
|
||||
|
||||
if user.get("deleted_at") is not None:
|
||||
logger.warning(f"登录失败: 账号已删除 - sub={Sub}")
|
||||
raise LeauditException(StatusCodeEnum.HTTP_401_UNAUTHORIZED, "账号或密码错误")
|
||||
|
||||
if user.get("status") != 0:
|
||||
logger.warning(f"登录失败: 账号已禁用 - sub={Sub}, status={user.get('status')}")
|
||||
raise LeauditException(StatusCodeEnum.HTTP_401_UNAUTHORIZED, "账号或密码错误")
|
||||
|
||||
if user.get("password") != Password:
|
||||
logger.warning(f"登录失败: 密码错误 - sub={Sub}")
|
||||
raise LeauditException(StatusCodeEnum.HTTP_401_UNAUTHORIZED, "账号或密码错误")
|
||||
|
||||
return await self._buildLoginResponse(user, session)
|
||||
|
||||
async def OAuthLogin(self, Sub: str, Username: str | None, Nickname: str | None,
|
||||
Email: str | None, PhoneNumber: str | None,
|
||||
OuId: str | None, OuName: str | None,
|
||||
IsLeader: bool | None, Area: str | None, ExpiresIn: int) -> LoginTokenVO:
|
||||
"""OAuth 登录。验证 sub 是否存在,不存在则自动创建用户。"""
|
||||
async with GetAsyncSession() as session:
|
||||
from sqlalchemy import select, text
|
||||
from datetime import datetime, timezone
|
||||
|
||||
result = await session.execute(
|
||||
text("SELECT id, sub, username, nick_name, phone_number, email, "
|
||||
"ou_id, ou_name, is_leader, status, deleted_at, "
|
||||
"area, tenant_name, dep_name, dep_short_name "
|
||||
"FROM sso_users WHERE sub = :sub"),
|
||||
{"sub": Sub},
|
||||
)
|
||||
row = result.fetchone()
|
||||
|
||||
if row:
|
||||
user = dict(row._mapping)
|
||||
if user.get("deleted_at") is not None or user.get("status") != 0:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_401_UNAUTHORIZED, "账号已被禁用或删除")
|
||||
# 更新最后登录信息
|
||||
await session.execute(
|
||||
text("UPDATE sso_users SET username = :username, nick_name = :nick, "
|
||||
"email = :email, phone_number = :phone, ou_id = :ou_id, "
|
||||
"ou_name = :ou_name, is_leader = :is_leader, area = :area, "
|
||||
"updated_at = :now WHERE id = :id"),
|
||||
{"username": Username, "nick": Nickname, "email": Email,
|
||||
"phone": PhoneNumber, "ou_id": OuId, "ou_name": OuName,
|
||||
"is_leader": IsLeader, "area": Area,
|
||||
"now": datetime.now(timezone.utc), "id": user["id"]},
|
||||
)
|
||||
else:
|
||||
# 自动创建用户
|
||||
await session.execute(
|
||||
text("INSERT INTO sso_users (sub, username, nick_name, email, "
|
||||
"phone_number, ou_id, ou_name, is_leader, area, status) "
|
||||
"VALUES (:sub, :username, :nick, :email, :phone, :ou_id, "
|
||||
":ou_name, :is_leader, :area, 0)"),
|
||||
{"sub": Sub, "username": Username, "nick": Nickname, "email": Email,
|
||||
"phone": PhoneNumber, "ou_id": OuId, "ou_name": OuName,
|
||||
"is_leader": IsLeader, "area": Area},
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
result = await session.execute(
|
||||
text("SELECT id, sub, username, nick_name, phone_number, email, "
|
||||
"ou_id, ou_name, is_leader, area, tenant_name, dep_name, dep_short_name "
|
||||
"FROM sso_users WHERE sub = :sub"),
|
||||
{"sub": Sub},
|
||||
)
|
||||
row = result.fetchone()
|
||||
user = dict(row._mapping) if row else {}
|
||||
|
||||
if not user:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_500_INTERNAL_SERVER_ERROR, "用户创建失败")
|
||||
|
||||
return await self._buildLoginResponse(user, session)
|
||||
|
||||
async def _buildLoginResponse(self, user: dict, session) -> LoginTokenVO:
|
||||
"""组装登录响应:查询角色 → 签发 JWT → 返回 LoginTokenVO。"""
|
||||
from sqlalchemy import text
|
||||
|
||||
# 查询用户角色
|
||||
roleResult = await session.execute(
|
||||
text("SELECT r.role_key FROM user_role ur "
|
||||
"JOIN roles r ON ur.role_id = r.id "
|
||||
"WHERE ur.user_id = :uid LIMIT 1"),
|
||||
{"uid": user["id"]},
|
||||
)
|
||||
roleRow = roleResult.fetchone()
|
||||
userRole = roleRow[0] if roleRow else "common"
|
||||
|
||||
# 签发 JWT
|
||||
expiresIn = 3600 # 默认 1 小时
|
||||
tokens = JwtService.generate(
|
||||
userId=user["id"],
|
||||
username=user.get("username") or user.get("sub", ""),
|
||||
nickName=user.get("nick_name") or "",
|
||||
ouId=user.get("ou_id") or "",
|
||||
ouName=user.get("ou_name") or "",
|
||||
area=user.get("area"),
|
||||
userRole=userRole,
|
||||
)
|
||||
|
||||
return LoginTokenVO(
|
||||
access_token=tokens["access_token"],
|
||||
token_type="Bearer",
|
||||
expires_in=expiresIn,
|
||||
issued_time=tokens.get("issued_time", ""),
|
||||
user_info={
|
||||
"user_id": user["id"],
|
||||
"sub": user.get("sub"),
|
||||
"username": user.get("username"),
|
||||
"nick_name": user.get("nick_name"),
|
||||
"email": user.get("email"),
|
||||
"phone_number": user.get("phone_number"),
|
||||
"ou_id": user.get("ou_id"),
|
||||
"ou_name": user.get("ou_name"),
|
||||
"is_leader": user.get("is_leader"),
|
||||
"area": user.get("area"),
|
||||
"role": userRole,
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,141 @@
|
||||
"""权限服务实现。
|
||||
|
||||
从旧项目 app/rbac/permission_checker_v2.py 迁移,业务逻辑完全不变。
|
||||
- 数据库驱动 GRANT/DENY 机制(DENY 优先级更高)
|
||||
- 支持通配符:document:*:*、*:*:* 等
|
||||
- 实时查询(无 Redis 缓存)
|
||||
仅改造为 SQLAlchemy 会话 + 项目统一配置。
|
||||
"""
|
||||
|
||||
from fastapi_common.fastapi_common_logger import logger
|
||||
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession
|
||||
|
||||
from fastapi_modules.fastapi_leaudit.services.permissionService import IPermissionService
|
||||
|
||||
|
||||
class PermissionServiceImpl(IPermissionService):
|
||||
"""权限检查服务实现。"""
|
||||
|
||||
async def CheckPermission(self, UserId: int, PermissionKey: str) -> bool:
|
||||
"""检查用户是否拥有指定权限。
|
||||
|
||||
GRANT/DENY 优先级:
|
||||
1. 精确 DENY → 拒绝
|
||||
2. 通配符 DENY → 拒绝
|
||||
3. 精确 GRANT → 通过
|
||||
4. 通配符 GRANT → 通过
|
||||
5. 无匹配 → 拒绝
|
||||
"""
|
||||
try:
|
||||
grants, denies = await self._getUserPermissions(UserId)
|
||||
|
||||
# DENY 优先
|
||||
if PermissionKey in denies:
|
||||
logger.debug(f"[DENY] 精确拒绝: user={UserId}, perm={PermissionKey}")
|
||||
return False
|
||||
|
||||
if self._matchWildcard(PermissionKey, denies):
|
||||
logger.debug(f"[DENY] 通配符拒绝: user={UserId}, perm={PermissionKey}")
|
||||
return False
|
||||
|
||||
# GRANT
|
||||
if PermissionKey in grants:
|
||||
logger.debug(f"[GRANT] 精确授权: user={UserId}, perm={PermissionKey}")
|
||||
return True
|
||||
|
||||
if self._matchWildcard(PermissionKey, grants):
|
||||
logger.debug(f"[GRANT] 通配符授权: user={UserId}, perm={PermissionKey}")
|
||||
return True
|
||||
|
||||
logger.debug(f"[DENY] 无匹配权限: user={UserId}, perm={PermissionKey}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"权限检查异常: user={UserId}, perm={PermissionKey}, error={e}")
|
||||
return False # 安全优先:异常时拒绝
|
||||
|
||||
async def HasAnyPermission(self, UserId: int, PermissionKeys: list[str]) -> bool:
|
||||
"""OR 逻辑:任一权限通过即返回 True。"""
|
||||
for key in PermissionKeys:
|
||||
if await self.CheckPermission(UserId, key):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def HasAllPermissions(self, UserId: int, PermissionKeys: list[str]) -> bool:
|
||||
"""AND 逻辑:全部权限通过才返回 True。"""
|
||||
for key in PermissionKeys:
|
||||
if not await self.CheckPermission(UserId, key):
|
||||
return False
|
||||
return True
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 内部方法
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
async def _getUserPermissions(self, UserId: int) -> tuple[set[str], set[str]]:
|
||||
"""从数据库查询用户的 GRANT 和 DENY 权限集合。"""
|
||||
grants: set[str] = set()
|
||||
denies: set[str] = set()
|
||||
|
||||
async with GetAsyncSession() as session:
|
||||
from sqlalchemy import text
|
||||
|
||||
result = await session.execute(
|
||||
text(
|
||||
"SELECT p.permission_key, rp.grant_type "
|
||||
"FROM sso_users u "
|
||||
"JOIN user_role ur ON u.id = ur.user_id "
|
||||
"JOIN roles r ON ur.role_id = r.id "
|
||||
"JOIN role_permissions rp ON r.id = rp.role_id "
|
||||
"JOIN permissions p ON rp.permission_id = p.id "
|
||||
"WHERE u.id = :uid"
|
||||
),
|
||||
{"uid": UserId},
|
||||
)
|
||||
|
||||
rows = result.fetchall()
|
||||
if not rows:
|
||||
logger.warning(f"用户无角色或权限: user_id={UserId}")
|
||||
return grants, denies
|
||||
|
||||
for row in rows:
|
||||
permKey = row[0]
|
||||
grantType = row[1]
|
||||
if grantType == "GRANT":
|
||||
grants.add(permKey)
|
||||
elif grantType == "DENY":
|
||||
denies.add(permKey)
|
||||
|
||||
logger.debug(f"用户权限: user={UserId}, grants={len(grants)}, denies={len(denies)}")
|
||||
return grants, denies
|
||||
|
||||
@staticmethod
|
||||
def _matchWildcard(PermissionKey: str, PermissionSet: set[str]) -> bool:
|
||||
"""检查 PermissionKey 是否匹配集合中的任一通配符模式。"""
|
||||
for pattern in PermissionSet:
|
||||
if PermissionServiceImpl._wildcardMatch(PermissionKey, pattern):
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _wildcardMatch(PermissionKey: str, Pattern: str) -> bool:
|
||||
"""通配符匹配。
|
||||
|
||||
_wildcardMatch("document:read:all", "document:*:*") → True
|
||||
_wildcardMatch("document:read:all", "document:read:*") → True
|
||||
_wildcardMatch("document:read:all", "evaluation:*:*") → False
|
||||
_wildcardMatch("document:read:all", "*:*:*") → True
|
||||
"""
|
||||
keyParts = PermissionKey.split(":")
|
||||
patternParts = Pattern.split(":")
|
||||
|
||||
if len(keyParts) != len(patternParts):
|
||||
return False
|
||||
|
||||
for keyPart, patternPart in zip(keyParts, patternParts):
|
||||
if patternPart == "*":
|
||||
continue
|
||||
if keyPart != patternPart:
|
||||
return False
|
||||
|
||||
return True
|
||||
Reference in New Issue
Block a user