From 460cfdc78a44ea8250136f2e56c77da357cacb3f Mon Sep 17 00:00:00 2001 From: wuzilong <704541015@qq.com> Date: Tue, 10 Nov 2026 17:55:47 +0800 Subject: [PATCH] =?UTF-8?q?fix(site):=20=E7=B2=BE=E7=AE=80=E7=AB=99?= =?UTF-8?q?=E7=82=B9=E9=85=8D=E7=BD=AE=EF=BC=8C=E7=A7=BB=E9=99=A4=E5=90=8D?= =?UTF-8?q?=E7=A7=B0/Banner/=E7=9B=B8=E5=85=B3=E9=93=BE=E6=8E=A5=20(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- admin/src/types/admin.ts | 8 -- admin/src/views/system/site/index.vue | 147 +------------------------- docs/分析/管理端权限基础功能.md | 13 +-- server/app/api/admin/content.py | 43 +------- server/app/api/site/settings.py | 3 - server/app/models/content.py | 15 --- server/scripts/live_acceptance.py | 7 +- server/tests/test_content.py | 26 ++--- 8 files changed, 24 insertions(+), 238 deletions(-) diff --git a/admin/src/types/admin.ts b/admin/src/types/admin.ts index 1b05097..275053c 100644 --- a/admin/src/types/admin.ts +++ b/admin/src/types/admin.ts @@ -173,18 +173,10 @@ export interface AdminFileQuery extends PageQuery { q?: string } -export interface FriendLink { - name: string - url: string -} - export interface SiteSettings { - site_name?: string | null logo_file_id?: number | null - banner_file_ids?: number[] | null icp_text?: string | null police_text?: string | null - friend_links?: FriendLink[] | null link_cirea?: string | null link_housing?: string | null complaint_workdays?: number | null diff --git a/admin/src/views/system/site/index.vue b/admin/src/views/system/site/index.vue index 84de6f7..16b4c75 100644 --- a/admin/src/views/system/site/index.vue +++ b/admin/src/views/system/site/index.vue @@ -3,9 +3,6 @@
- - -
- -
-
- - - × - -
- 上传 -
- - -
- -
-
- - - - 删除 - -
-
- - 添加链接 - -
-
-
@@ -127,7 +74,7 @@ fetchUpdateSiteSettings, uploadAdminFile } from '@/api/system-manage' - import type { FriendLink, SiteSettings } from '@/types/admin' + import type { SiteSettings } from '@/types/admin' defineOptions({ name: 'SiteSetting' }) @@ -136,21 +83,14 @@ const loading = ref(false) const saving = ref(false) const logoUploading = ref(false) - const bannerUploading = ref(false) const logoPercent = ref(0) - const bannerPercent = ref(0) const logoUrl = ref('') - const banners = ref<{ id: number; url: string }[]>([]) const logoInput = ref() - const bannerInput = ref() const form = reactive({ - site_name: '', logo_file_id: null as number | null, - banner_file_ids: [] as number[], icp_text: '', police_text: '', - friend_links: [] as FriendLink[], link_cirea: '', link_housing: '', complaint_workdays: null as number | null, @@ -166,38 +106,6 @@ return null } - const asIdList = (value: unknown): number[] => { - let source = value - if (typeof value === 'string' && value.trim()) { - try { - source = JSON.parse(value) - } catch { - source = [] - } - } - if (!Array.isArray(source)) return [] - return source.map((item) => asNumber(item)).filter((item): item is number => item !== null) - } - - const asLinks = (value: unknown): FriendLink[] => { - let source = value - if (typeof value === 'string' && value.trim()) { - try { - source = JSON.parse(value) - } catch { - source = [] - } - } - if (!Array.isArray(source)) return [] - return source.map((item) => { - const row = item && typeof item === 'object' ? (item as Record) : {} - return { - name: asText(row.name), - url: asText(row.url || row.address) - } - }) - } - const asSettings = (payload: unknown): Record => { if (Array.isArray(payload)) return listToMap(payload) if (!payload || typeof payload !== 'object') return {} @@ -232,29 +140,15 @@ loading.value = true try { const raw = asSettings(await fetchGetSiteSettings()) - form.site_name = asText(raw.site_name) form.logo_file_id = asNumber(raw.logo_file_id) - form.banner_file_ids = asIdList(raw.banner_file_ids) form.icp_text = asText(raw.icp_text) form.police_text = asText(raw.police_text) - form.friend_links = asLinks(raw.friend_links) form.link_cirea = asText(raw.link_cirea) form.link_housing = asText(raw.link_housing) form.complaint_workdays = asNumber(raw.complaint_workdays) form.expert_reply_workdays = asNumber(raw.expert_reply_workdays) const directLogo = asText(raw.logoUrl) logoUrl.value = directLogo || (form.logo_file_id ? await fileUrl(form.logo_file_id) : '') - const bannerUrls = Array.isArray(raw.bannerUrls) - ? raw.bannerUrls.map((item) => asText(item)) - : [] - if (bannerUrls.length === form.banner_file_ids.length) { - banners.value = form.banner_file_ids.map((id, index) => ({ id, url: bannerUrls[index] })) - } else { - banners.value = [] - for (const id of form.banner_file_ids) { - banners.value.push({ id, url: await fileUrl(id) }) - } - } } finally { loading.value = false } @@ -263,7 +157,6 @@ onMounted(load) const pickLogo = () => logoInput.value?.click() - const pickBanner = () => bannerInput.value?.click() const onLogo = async (event: Event) => { const input = event.target as HTMLInputElement @@ -285,54 +178,16 @@ } } - const onBanner = async (event: Event) => { - const input = event.target as HTMLInputElement - const files = input.files ? Array.from(input.files) : [] - input.value = '' - if (!files.length) return - bannerUploading.value = true - try { - for (const file of files) { - bannerPercent.value = 0 - const saved = await uploadAdminFile(file, 'public', (percent) => { - bannerPercent.value = percent - }) - form.banner_file_ids.push(saved.id) - banners.value.push({ id: saved.id, url: saved.url || '' }) - } - } catch { - // 失败文案由接口层展示 - } finally { - bannerUploading.value = false - } - } - const clearLogo = () => { form.logo_file_id = null logoUrl.value = '' } - const removeBanner = (id: number) => { - form.banner_file_ids = form.banner_file_ids.filter((item) => item !== id) - banners.value = banners.value.filter((item) => item.id !== id) - } - const save = async () => { - const links = form.friend_links.map((item) => ({ - name: item.name.trim(), - url: item.url.trim() - })) - if (links.some((item) => (item.name && !item.url) || (!item.name && item.url))) { - ElMessage.warning('相关链接需要同时填写名称和地址') - return - } const payload: Partial = { - site_name: form.site_name, logo_file_id: form.logo_file_id, - banner_file_ids: form.banner_file_ids, icp_text: form.icp_text, police_text: form.police_text, - friend_links: links.filter((item) => item.name && item.url), link_cirea: form.link_cirea.trim(), link_housing: form.link_housing.trim(), complaint_workdays: form.complaint_workdays, diff --git a/docs/分析/管理端权限基础功能.md b/docs/分析/管理端权限基础功能.md index da6c071..559fe81 100644 --- a/docs/分析/管理端权限基础功能.md +++ b/docs/分析/管理端权限基础功能.md @@ -35,8 +35,8 @@ ### 站点配置 -16. 可维护站点名称、LOGO、Banner、页脚备案号、相关链接。 -17. 中房学、住建厅只保存外链地址,不做数据对接。 +16. 可维护 LOGO、页脚备案号。 +17. 中房学、住建厅只保存外链地址,不做数据对接。站点名称、Banner、相关链接不在本模块维护(首页轮播与页脚相关链接另管)。 18. 投诉处理时限、专家回复时限做成配置项,单位为工作日,不写死在代码里。 19. 门户以后通过公开接口读取已保存的配置。本批不建设门户页面。 @@ -73,7 +73,7 @@ | 操作日志 | `views/system/audit-log` | 按时间、用户检索。只读 | | 登录安全 | `views/system/login-security` | 失败次数、锁定分钟数、IP 黑名单 | | 附件 | `views/system/file` | 选择公开或私有、上传、删除、下载。富文本上传走同一套接口 | -| 站点配置 | `views/system/site` | 名称、LOGO、Banner、备案号、外链、时限 | +| 站点配置 | `views/system/site` | LOGO、备案号、外链、时限 | | 敏感词 | `views/system/sensitive-word` | 词库维护,以及一段 HTML 的过滤预览 | 用户弹窗里的角色下拉改为接口数据,不再读 `ROLE_LIST_DATA`。角色权限树改为保存到后端,去掉「获取选中数据」这类演示按钮。个人中心只保留修改自己的密码。 @@ -175,12 +175,9 @@ | key | 含义 | |---|---| -| site_name | 站点名称 | | logo_file_id | LOGO,对应公开附件 | -| banner_file_ids | Banner 图片 id 列表 | | icp_text | 陇ICP备19001227号 | | police_text | 甘公网安备62010202001984号 | -| friend_links | 相关链接,每项含名称和地址 | | link_cirea | 中房学外链 | | link_housing | 住建厅外链 | | complaint_workdays | 投诉处理时限,工作日 | @@ -271,7 +268,7 @@ | 方法 | 路径 | 作用 | |---|---|---| -| GET | `/api/site/settings` | 返回站点名称、LOGO、Banner、备案号、相关链接、中房学外链、住建厅外链。不返回上传上限和登录锁定参数 | +| GET | `/api/site/settings` | 返回 LOGO、备案号、中房学外链、住建厅外链。不返回上传上限和登录锁定参数 | 登录失败、验证码错误、账号停用或注销、账号锁定,都返回「用户名或密码错误」,不说明具体原因。验证码错误时同样增加失败次数,避免用验证码接口探测账号。 @@ -304,5 +301,5 @@ 附件、配置、过滤: 13. 上传超过上限的文件被拒绝。公开图片能直接访问。私有文件的链接过期后不能访问。 -14. 修改站点名称、备案号和一条外链后,`/api/site/settings` 返回新值。响应里没有登录锁定参数。 +14. 修改备案号和一条外链后,`/api/site/settings` 返回新值。响应里没有登录锁定参数,也不再包含站点名称、Banner、相关链接。 15. 过滤预览会去掉脚本和事件属性,并标出已录入的敏感词。预览结果不产生可被门户读取的文章。 diff --git a/server/app/api/admin/content.py b/server/app/api/admin/content.py index cecafb8..d274040 100644 --- a/server/app/api/admin/content.py +++ b/server/app/api/admin/content.py @@ -1,5 +1,3 @@ -import json - from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, Request, UploadFile, status from pydantic import BaseModel from sqlalchemy import func, select @@ -28,7 +26,7 @@ from app.services.storage import ( router = APIRouter(dependencies=[Depends(require_staff)]) -_TEXT_KEYS = {"site_name", "icp_text", "police_text", "link_cirea", "link_housing"} +_TEXT_KEYS = {"icp_text", "police_text", "link_cirea", "link_housing"} _LINK_KEYS = {"link_cirea", "link_housing"} _POSITIVE = { "upload_max_bytes": "上传大小上限必须是正整数", @@ -97,41 +95,6 @@ def _logo_id(value) -> str: return str(int(text)) -def _banner_ids(value) -> str: - if not isinstance(value, list): - _bad("banner_file_ids 必须是数组") - numbers: list[int] = [] - for item in value: - if isinstance(item, bool) or not isinstance(item, (int, str)): - _bad("banner_file_ids 必须是附件编号") - text = str(item).strip() - if not text.isdigit() or int(text) <= 0: - _bad("banner_file_ids 必须是附件编号") - numbers.append(int(text)) - return json.dumps(numbers, separators=(",", ":")) - - -def _friend_links(value) -> str: - if not isinstance(value, list): - _bad("friend_links 必须是数组") - cleaned: list[dict[str, str]] = [] - for item in value: - if not isinstance(item, dict): - _bad("相关链接必须包含名称和地址") - name = item.get("name") - url = item.get("url") - if not isinstance(name, str) or not isinstance(url, str): - _bad("相关链接必须包含名称和地址") - name = name.strip() - url = url.strip() - if not name or not url: - _bad("相关链接必须包含名称和地址") - if is_javascript_url(url): - _bad("链接地址不正确") - cleaned.append({"name": name, "url": url}) - return json.dumps(cleaned, ensure_ascii=False, separators=(",", ":")) - - def _positive(value, message: str) -> str: if isinstance(value, bool) or not isinstance(value, int) or value <= 0 or value > _INT_MAX: _bad(message) @@ -155,10 +118,6 @@ def _normalize_settings(payload: dict) -> dict[str, str]: stored[key] = _text_setting(key, value) elif key == "logo_file_id": stored[key] = _logo_id(value) - elif key == "banner_file_ids": - stored[key] = _banner_ids(value) - elif key == "friend_links": - stored[key] = _friend_links(value) elif key in _POSITIVE: stored[key] = _positive(value, _POSITIVE[key]) elif key in _WORKDAYS: diff --git a/server/app/api/site/settings.py b/server/app/api/site/settings.py index 5ffe510..a5621f7 100644 --- a/server/app/api/site/settings.py +++ b/server/app/api/site/settings.py @@ -12,12 +12,9 @@ def read_public_settings(db: Session = Depends(get_db)) -> dict: # 只返回门户展示字段。上传上限和登录锁定参数留在管理端。 data = settings_view(db) return { - "siteName": data["site_name"], "logoUrl": data["logoUrl"], - "bannerUrls": data["bannerUrls"], "icpText": data["icp_text"], "policeText": data["police_text"], - "friendLinks": data["friend_links"], "linkCirea": data["link_cirea"], "linkHousing": data["link_housing"], } diff --git a/server/app/models/content.py b/server/app/models/content.py index 429b138..123ad0e 100644 --- a/server/app/models/content.py +++ b/server/app/models/content.py @@ -1,4 +1,3 @@ -import json from datetime import datetime, timezone from sqlalchemy import DateTime, Integer, String, Text, select @@ -11,12 +10,9 @@ def _mysql_args() -> dict: return {"mysql_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"} DEFAULTS: dict[str, str] = { - "site_name": "甘肃省房地产价格评估行业协会", "logo_file_id": "", - "banner_file_ids": "[]", "icp_text": "陇ICP备19001227号", "police_text": "甘公网安备62010202001984号", - "friend_links": "[]", "link_cirea": "", "link_housing": "", "complaint_workdays": "", @@ -27,7 +23,6 @@ DEFAULTS: dict[str, str] = { } SETTING_KEYS = tuple(DEFAULTS) -_JSON_KEYS = {"banner_file_ids", "friend_links"} _NUMBER_KEYS = {"upload_max_bytes", "login_max_failures", "login_lock_minutes"} _OPTIONAL_NUMBER_KEYS = {"complaint_workdays", "expert_reply_workdays"} @@ -109,14 +104,6 @@ def save_settings(db, values: dict[str, str]) -> None: def _present(key: str, raw: str): - if key in _JSON_KEYS: - if not raw: - return [] - try: - parsed = json.loads(raw) - except json.JSONDecodeError: - return [] - return parsed if isinstance(parsed, list) else [] if key in _NUMBER_KEYS: text = str(raw).strip() if text.isdigit(): @@ -160,6 +147,4 @@ def settings_view(db) -> dict: logo_id = raw.get("logo_file_id", "") logo_urls = _public_urls(db, [logo_id]) if str(logo_id).strip() else [] view["logoUrl"] = logo_urls[0] if logo_urls else "" - banners = view["banner_file_ids"] if isinstance(view["banner_file_ids"], list) else [] - view["bannerUrls"] = _public_urls(db, banners) return view diff --git a/server/scripts/live_acceptance.py b/server/scripts/live_acceptance.py index c2eb11f..f718d84 100644 --- a/server/scripts/live_acceptance.py +++ b/server/scripts/live_acceptance.py @@ -301,7 +301,6 @@ def main() -> None: "/api/admin/site-settings", headers=headers, json={ - "site_name": "验收站点", "icp_text": "陇ICP备19001227号", "link_cirea": "https://www.cirea.org.cn/", }, @@ -310,10 +309,12 @@ def main() -> None: fail(f"site {site.status_code} {site.text[:200]}") public_site = client.get("/api/site/settings") public_body = public_site.json() - if public_body.get("siteName") != "验收站点" or public_body.get("linkCirea") != "https://www.cirea.org.cn/": + if public_body.get("icpText") != "陇ICP备19001227号" or public_body.get("linkCirea") != "https://www.cirea.org.cn/": fail(f"public site {public_body}") if "loginMaxFailures" in public_body or "login_max_failures" in public_body: fail("public site leaked lock settings") + if "siteName" in public_body or "bannerUrls" in public_body or "friendLinks" in public_body: + fail(f"public site kept removed keys {public_body}") probe_word = f"验收敏感词{uuid.uuid4().hex[:6]}" word = client.post("/api/admin/sensitive-words", headers=headers, json={"word": probe_word}) @@ -340,7 +341,7 @@ def main() -> None: client.put( "/api/admin/site-settings", headers=headers, - json={"site_name": "甘肃省房地产价格评估行业协会", "link_cirea": ""}, + json={"link_cirea": ""}, ) db = SessionLocal() diff --git a/server/tests/test_content.py b/server/tests/test_content.py index 4467029..d9cd35f 100644 --- a/server/tests/test_content.py +++ b/server/tests/test_content.py @@ -13,12 +13,9 @@ from app.core.config import get_settings from app.services.html_filter import filter_html _PUBLIC_KEYS = { - "siteName", "logoUrl", - "bannerUrls", "icpText", "policeText", - "friendLinks", "linkCirea", "linkHousing", } @@ -224,7 +221,7 @@ def test_admin_content_requires_login(api_client): ("get", "/api/admin/files/1", None), ("get", "/api/admin/site-settings", None), ("get", "/api/admin/sensitive-words", None), - ("put", "/api/admin/site-settings", {"site_name": "x"}), + ("put", "/api/admin/site-settings", {"icp_text": "x"}), ("post", "/api/admin/sensitive-words", {"word": "x"}), ("post", "/api/admin/content/preview-filter", {"html": "

a

"}), ("delete", "/api/admin/sensitive-words/1", None), @@ -309,8 +306,6 @@ def test_public_settings_hide_login_lock(ready_client): assert isinstance(original["login_max_failures"], int) assert isinstance(original["login_lock_minutes"], int) assert isinstance(original["logoUrl"], str) - assert isinstance(original["bannerUrls"], list) - link = {"name": "示例链接", "url": "https://example.com/gsrea"} try: rejected = ready_client.put( "/api/admin/site-settings", @@ -318,12 +313,17 @@ def test_public_settings_hide_login_lock(ready_client): headers=headers, ) assert rejected.status_code == 400 + unknown = ready_client.put( + "/api/admin/site-settings", + json={"site_name": "甘肃价协测试站点"}, + headers=headers, + ) + assert unknown.status_code == 400 saved = ready_client.put( "/api/admin/site-settings", json={ - "site_name": "甘肃价协测试站点", "icp_text": "陇ICP备测试号", - "friend_links": [link], + "link_cirea": "https://example.com/cirea", }, headers=headers, ) @@ -332,11 +332,9 @@ def test_public_settings_hide_login_lock(ready_client): assert response.status_code == 200, response.text body = response.json() assert set(body) == _PUBLIC_KEYS - assert body["siteName"] == "甘肃价协测试站点" assert body["icpText"] == "陇ICP备测试号" - assert body["friendLinks"] == [link] + assert body["linkCirea"] == "https://example.com/cirea" assert isinstance(body["logoUrl"], str) - assert isinstance(body["bannerUrls"], list) text = response.text assert "login_max_failures" not in text assert "login_lock_minutes" not in text @@ -344,13 +342,15 @@ def test_public_settings_hide_login_lock(ready_client): assert "loginLockMinutes" not in text assert "upload_max_bytes" not in text assert "uploadMaxBytes" not in text + assert "siteName" not in text + assert "bannerUrls" not in text + assert "friendLinks" not in text finally: ready_client.put( "/api/admin/site-settings", json={ - "site_name": original["site_name"], "icp_text": original["icp_text"], - "friend_links": original["friend_links"], + "link_cirea": original["link_cirea"], }, headers=headers, )