-
-
-
-
-
-
-
-
-
-
-
- 删除
-
-
-
-
- 添加链接
-
-
-
-
@@ -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,
)