"""证件附件授权:仅接受当前租户有数据权限的 MinIO 登记对象。""" from urllib.parse import unquote, urlsplit from fastapi import HTTPException from app.core.config import settings from app.models.crm_models import SysOss from app.services.pl_data_scope import assert_pl_row_in_scope def storage_base_url(): if settings.MINIO_BASE_URL: return settings.MINIO_BASE_URL.rstrip('/') scheme = 'https' if settings.MINIO_SECURE else 'http' return f'{scheme}://{settings.MINIO_ENDPOINT}/{settings.MINIO_BUCKET}' def object_key(value, base_url=None): raw = (value or '').strip() base = (base_url or storage_base_url()).rstrip('/') if raw.startswith(base + '/'): raw = raw[len(base) + 1:] parsed = urlsplit(raw) if (not raw or parsed.scheme or parsed.netloc or parsed.query or parsed.fragment or raw.startswith('/') or '\\' in raw or ',' in raw or any(part in ('', '.', '..') for part in unquote(raw).split('/')) or unquote(raw) != raw or any(ord(c) < 32 for c in raw)): raise HTTPException(400, '附件必须是本系统上传的MinIO对象') return raw def authorize_attachment(db, user, value, base_url=None): base = (base_url or storage_base_url()).rstrip('/') key = object_key(value, base) row = db.query(SysOss).filter( SysOss.tenant_no == user.tenant_no, SysOss.del_flag == 0, SysOss.service == 'minio', SysOss.url.in_((key, base + '/' + key)), ).first() if row is None: raise HTTPException(403, '附件不存在或不属于当前租户') # 统一上传的历史记录未写创建部门;本人上传是明确的对象授权依据。 # 其他人的文件仍需全角色数据范围,租户与有效状态已在查询中强制限定。 if not user.id or row.create_by != user.id: assert_pl_row_in_scope(db, row, SysOss, user, user.tenant_no) return key def read_image(db, user, value): from app.core.storage import MinIOStorage, get_storage from app.services.pl_quota_recognition import MAX_IMAGE_BYTES key = authorize_attachment(db, user, value) store = get_storage() if not isinstance(store, MinIOStorage): raise HTTPException(503, '证件识别需要MinIO存储') response = None try: response = store.client.get_object(store.bucket_name, key) content = response.read(MAX_IMAGE_BYTES + 1) except Exception: raise HTTPException(502, '读取证件图片失败,请重试或手工录入') from None finally: if response is not None: response.close() response.release_conn() if len(content) > MAX_IMAGE_BYTES: raise HTTPException(400, '证件图片过大') return content