- system_settings 单行 CRUD:GET/PUT(AdminUser)+ GET /public(无鉴权, SettingPublicOut 白名单仅 company_name/logo_url 两字段,短缓存 300s) - logo 上传:后缀+magic bytes+2MB 三重校验,固定名覆盖落盘 uploads/logo/ (不使用上传方文件名,杜绝路径拼接);旧扩展名文件清理;文件流端点 no-cache,URL 带 update_time 版本参数破缓存 - logo 存目录不存库:图片入 DB 让每次读取与 mysqldump 放大传输且无法 独立替换;DB 只持一行相对路径 - 新增依赖 python-multipart(FastAPI UploadFile 必备传输层);.gitignore 增 backend/uploads/ 与 db/backups/ - 集成 11 例:单行语义/白名单字段/三重校验/越权 403/匿名 401/审计留痕 Co-Authored-By: Claude Code <noreply@anthropic.com>
76 lines
2.9 KiB
Python
76 lines
2.9 KiB
Python
"""系统设置接口(公司信息)。
|
||
|
||
`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
|
||
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")
|
||
async def upload_logo(
|
||
file: UploadFile,
|
||
session: DbSession,
|
||
current_user: AdminUser,
|
||
meta: RequestMetaDep,
|
||
) -> ApiResponse[SettingOut]:
|
||
"""上传公司 logo(png/jpg ≤2MB;固定名覆盖,旧扩展名文件清理)。"""
|
||
content = await file.read()
|
||
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"},
|
||
)
|