feat(settings): 公司信息配置与 logo 管理(public 端点供登录页)
- 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>
This commit is contained in:
1 parent
e95dc4b2de
commit
efed29647c
9 files changed
+433
No files matched your search
@@ -0,0 +1,75 @@
|
||||
"""系统设置接口(公司信息)。
|
||||
|
||||
`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"},
|
||||
)
|
||||
Reference in new issue
Block a user