feat(user): 昵称上限放宽到 20 字(与客户端/原型一致) (#63)
Co-authored-by: zzhyyyyy <2685922758@qq.com> Reviewed-on: #63 Co-authored-by: zhuzihao <zhuzihao@wonderable.ai> Co-committed-by: zhuzihao <zhuzihao@wonderable.ai>
This commit was merged in pull request #63.
This commit is contained in:
+26
-6
@@ -37,7 +37,7 @@ def _sniff_ext(data: bytes) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _save_image(subdir: str, user_id: int, data: bytes) -> str:
|
||||
def _save_named(subdir: str, name_prefix: str, data: bytes) -> str:
|
||||
"""通用图片落盘:校验非空 / ≤上限 / 魔数为图片,随机文件名,返回相对 URL。"""
|
||||
if not data:
|
||||
raise MediaError("空文件")
|
||||
@@ -47,11 +47,16 @@ def _save_image(subdir: str, user_id: int, data: bytes) -> str:
|
||||
if ext is None:
|
||||
raise MediaError("仅支持 JPEG / PNG / WebP 图片")
|
||||
|
||||
fname = f"u{user_id}_{secrets.token_hex(8)}{ext}"
|
||||
fname = f"{name_prefix}_{secrets.token_hex(8)}{ext}"
|
||||
(_media_dir(subdir) / fname).write_bytes(data)
|
||||
return f"{settings.MEDIA_URL_PREFIX}/{subdir}/{fname}"
|
||||
|
||||
|
||||
def _save_image(subdir: str, user_id: int, data: bytes) -> str:
|
||||
"""用户上传图片落盘(文件名带 user_id 前缀)。"""
|
||||
return _save_named(subdir, f"u{user_id}", data)
|
||||
|
||||
|
||||
def save_avatar(user_id: int, data: bytes) -> str:
|
||||
"""保存头像,返回相对 URL(`/media/avatars/<file>`)。"""
|
||||
return _save_image("avatars", user_id, data)
|
||||
@@ -67,6 +72,11 @@ def save_report_image(user_id: int, data: bytes) -> str:
|
||||
return _save_image("price_report", user_id, data)
|
||||
|
||||
|
||||
def save_feedback_qr(data: bytes) -> str:
|
||||
"""保存反馈页二维码(运营后台上传的运营素材,非用户文件),返回相对 URL(`/media/feedback_qr/<file>`)。"""
|
||||
return _save_named("feedback_qr", "qr", data)
|
||||
|
||||
|
||||
def save_cps_image(admin_id: int, data: bytes) -> str:
|
||||
"""保存 CPS 活动落地页图,返回相对 URL(`/media/cps/<file>`)。admin_id 入文件名便于追溯。"""
|
||||
return _save_image("cps", admin_id, data)
|
||||
@@ -84,15 +94,25 @@ def to_abs_media_url(rel_url: str | None) -> str | None:
|
||||
return f"{base}{rel_url}" if base else rel_url
|
||||
|
||||
|
||||
def delete_avatar(url: str | None) -> None:
|
||||
"""删除本服务托管的旧头像文件;外部 URL(如微信头像)或空值不处理。"""
|
||||
prefix = f"{settings.MEDIA_URL_PREFIX}/avatars/"
|
||||
def _delete_managed(subdir: str, url: str | None) -> None:
|
||||
"""删除本服务托管的某子目录下文件;外部 URL / 空值 / 非本子目录的不处理。"""
|
||||
prefix = f"{settings.MEDIA_URL_PREFIX}/{subdir}/"
|
||||
if not url or not url.startswith(prefix):
|
||||
return
|
||||
fname = url[len(prefix):]
|
||||
if not fname or "/" in fname or "\\" in fname or ".." in fname:
|
||||
return # 防路径穿越
|
||||
try:
|
||||
(_media_dir("avatars") / fname).unlink(missing_ok=True)
|
||||
(_media_dir(subdir) / fname).unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def delete_avatar(url: str | None) -> None:
|
||||
"""删除本服务托管的旧头像文件;外部 URL(如微信头像)或空值不处理。"""
|
||||
_delete_managed("avatars", url)
|
||||
|
||||
|
||||
def delete_feedback_qr(url: str | None) -> None:
|
||||
"""删除本服务托管的旧反馈页二维码文件;外部 URL 或空值不处理。"""
|
||||
_delete_managed("feedback_qr", url)
|
||||
|
||||
Reference in New Issue
Block a user