2026-10-15 06:19:58 -04:00
|
|
|
|
"""系统设置接口(公司信息)。
|
|
|
|
|
|
|
|
|
|
|
|
`GET /public` 与 `GET /logo/public` **无鉴权**:登录页渲染公司名/logo 时
|
|
|
|
|
|
尚无令牌。响应经 `SettingPublicOut` 白名单收敛(见 schemas/setting.py),
|
|
|
|
|
|
仅暴露两个非敏感展示字段。
|
|
|
|
|
|
|
|
|
|
|
|
`/logo/public` 返回文件流而非统一信封——与参数导出同一例外口径
|
|
|
|
|
|
(二进制响应套 JSON 信封无意义)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Response, UploadFile
|
|
|
|
|
|
from fastapi.responses import FileResponse
|
|
|
|
|
|
|
|
|
|
|
|
from app.api.deps import AdminUser, DbSession, RequestMetaDep
|
2026-10-16 02:23:53 -04:00
|
|
|
|
from app.core.exceptions import BadRequestError
|
2026-10-15 06:19:58 -04:00
|
|
|
|
from app.core.response import ApiResponse, ok
|
|
|
|
|
|
from app.schemas.setting import SettingOut, SettingPublicOut, SettingUpdateIn
|
|
|
|
|
|
from app.services import system_setting_service as service
|
|
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/settings", tags=["系统管理 · 公司信息"])
|
|
|
|
|
|
|
|
|
|
|
|
_LOGO_MEDIA_TYPE = {"png": "image/png", "jpg": "image/jpeg"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("", response_model=ApiResponse[SettingOut], summary="公司信息")
|
|
|
|
|
|
def get_settings(session: DbSession, _: AdminUser) -> ApiResponse[SettingOut]:
|
|
|
|
|
|
"""取公司信息(管理端)。"""
|
|
|
|
|
|
return ok(service.to_out(service.get_setting(session)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("", response_model=ApiResponse[SettingOut], summary="更新公司名称")
|
|
|
|
|
|
def update_settings(
|
|
|
|
|
|
payload: SettingUpdateIn,
|
|
|
|
|
|
session: DbSession,
|
|
|
|
|
|
current_user: AdminUser,
|
|
|
|
|
|
meta: RequestMetaDep,
|
|
|
|
|
|
) -> ApiResponse[SettingOut]:
|
|
|
|
|
|
"""更新公司名称(审计留痕)。"""
|
|
|
|
|
|
return ok(
|
|
|
|
|
|
service.update_company(session, payload, operator=current_user, meta=meta),
|
|
|
|
|
|
message="已保存",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/logo", response_model=ApiResponse[SettingOut], summary="上传 logo")
|
2026-10-16 02:23:53 -04:00
|
|
|
|
def upload_logo(
|
2026-10-15 06:19:58 -04:00
|
|
|
|
file: UploadFile,
|
|
|
|
|
|
session: DbSession,
|
|
|
|
|
|
current_user: AdminUser,
|
|
|
|
|
|
meta: RequestMetaDep,
|
|
|
|
|
|
) -> ApiResponse[SettingOut]:
|
2026-10-16 02:23:53 -04:00
|
|
|
|
"""上传公司 logo(png/jpg ≤2MB;固定名覆盖,旧扩展名文件清理)。
|
|
|
|
|
|
|
|
|
|
|
|
同步 `def`:读文件、磁盘写与审计提交都是阻塞操作,async 路由里会让
|
|
|
|
|
|
它们卡死事件循环(项目红线:全栈同步路由,FastAPI 自动进线程池)。
|
|
|
|
|
|
大小先用 `file.size` 拦截,避免超限文件整体读进内存。
|
|
|
|
|
|
"""
|
|
|
|
|
|
if file.size is not None and file.size > service.LOGO_MAX_BYTES:
|
|
|
|
|
|
raise BadRequestError("logo 文件不能超过 2MB")
|
|
|
|
|
|
content = file.file.read()
|
2026-10-15 06:19:58 -04:00
|
|
|
|
ext = (file.filename or "").rsplit(".", 1)[-1]
|
|
|
|
|
|
return ok(
|
|
|
|
|
|
service.save_logo(session, content, ext, operator=current_user, meta=meta),
|
|
|
|
|
|
message="logo 已更新",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/public", response_model=ApiResponse[SettingPublicOut], summary="公司信息(匿名)")
|
|
|
|
|
|
def get_settings_public(session: DbSession, response: Response) -> ApiResponse[SettingPublicOut]:
|
|
|
|
|
|
"""公司信息(登录页消费,无鉴权)。短缓存减轻匿名端点压力。"""
|
|
|
|
|
|
response.headers["Cache-Control"] = "public, max-age=300"
|
|
|
|
|
|
return ok(service.to_public_out(service.get_setting(session)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/logo/public", summary="logo 图片(匿名)")
|
|
|
|
|
|
def get_logo_public(session: DbSession) -> FileResponse:
|
|
|
|
|
|
"""logo 文件流(无鉴权)。no-cache:图片替换后由版本参数破缓存兜底。"""
|
|
|
|
|
|
path, ext = service.read_logo(session)
|
|
|
|
|
|
return FileResponse(
|
|
|
|
|
|
path,
|
|
|
|
|
|
media_type=_LOGO_MEDIA_TYPE.get(ext, "application/octet-stream"),
|
|
|
|
|
|
headers={"Cache-Control": "no-cache"},
|
|
|
|
|
|
)
|