437 lines
16 KiB
Python
437 lines
16 KiB
Python
"""把现站公开摘录写入首页各模块,供本地查看。
|
||
|
||
数据来自仓库 docs/参考/现站/基础数据。重复执行会清掉首页模块后按同一批摘录重写,
|
||
不改站点备案号,也不改当前启用的模板。口令只从环境变量读取。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import tempfile
|
||
from html import escape, unescape
|
||
from pathlib import Path
|
||
|
||
from sqlalchemy import delete, select
|
||
|
||
from app.db import Base, SessionLocal, get_engine
|
||
from app.models.content import AdminFile, SiteSetting, ensure_site_defaults, save_settings
|
||
from app.models.home import (
|
||
BRAND,
|
||
TECH,
|
||
HomeArticle,
|
||
HomeArticleAttachment,
|
||
HomeAudienceLink,
|
||
HomeComplianceLink,
|
||
HomeContact,
|
||
HomeHonor,
|
||
HomeHotLink,
|
||
HomeMarquee,
|
||
HomeMember,
|
||
HomeMenuItem,
|
||
HomeRelatedLink,
|
||
HomeSlide,
|
||
HomeSlideAttachment,
|
||
HomeTemplate,
|
||
create_article,
|
||
create_slide,
|
||
ensure_home_templates,
|
||
save_audiences,
|
||
save_compliance,
|
||
save_contact,
|
||
save_honors,
|
||
save_hot_links,
|
||
save_members,
|
||
save_menu,
|
||
save_related_links,
|
||
set_article_display,
|
||
)
|
||
from app.services.storage import ensure_buckets, make_object_key, public_object_url, upload_object
|
||
|
||
ROOT = Path(__file__).resolve().parents[3]
|
||
SOURCE = ROOT / "docs" / "参考" / "现站" / "基础数据"
|
||
CAROUSEL = SOURCE / "首页轮播"
|
||
NEWS = SOURCE / "新闻动态"
|
||
NOTICES = SOURCE / "通知公告"
|
||
POLICIES = SOURCE / "政策法规"
|
||
|
||
SLOGAN = "服务政府、服务行业、服务会员"
|
||
VALUE = "独立、客观、公正执业"
|
||
|
||
# 现站轮播图对应的新闻编号,见首页解码 HTML。
|
||
SLIDES = [
|
||
("58612080.jpg", 8560786),
|
||
("52398194.png", 7519867),
|
||
("50933127.jpg", 7282421),
|
||
("49922210.jpg", 7108127),
|
||
("45355362.png", 6272136),
|
||
]
|
||
|
||
# 有正文、且不与前 6 条新闻重复的市场类转载,用作数据看板文章。
|
||
DASHBOARD_IDS = (7544540, 7519901, 6888657)
|
||
|
||
# 张掖路 63 号所在城关区的大致坐标,只用于地图测试。
|
||
LATITUDE = 36.0609
|
||
LONGITUDE = 103.8234
|
||
|
||
|
||
def _load_index(folder: Path) -> list[dict]:
|
||
return json.loads((folder / "index.json").read_text(encoding="utf-8"))
|
||
|
||
|
||
def _article_dir(folder: Path, article_id: int) -> Path:
|
||
return folder / "文章" / str(article_id)
|
||
|
||
|
||
def _paragraphs(path: Path) -> str:
|
||
text = unescape(path.read_text(encoding="utf-8"))
|
||
parts = []
|
||
for line in text.splitlines():
|
||
line = line.strip()
|
||
if not line:
|
||
continue
|
||
parts.append(f"<p>{escape(line)}</p>")
|
||
return "".join(parts)
|
||
|
||
|
||
def _content_type(path: Path) -> str:
|
||
suffix = path.suffix.lower()
|
||
if suffix == ".png":
|
||
return "image/png"
|
||
if suffix == ".webp":
|
||
return "image/webp"
|
||
if suffix == ".gif":
|
||
return "image/gif"
|
||
return "image/jpeg"
|
||
|
||
|
||
def _store_public(db, path: Path, filename: str | None = None) -> AdminFile:
|
||
name = filename or f"home-sample-{path.name}"
|
||
row = db.scalar(
|
||
select(AdminFile).where(AdminFile.bucket == "public", AdminFile.filename == name)
|
||
)
|
||
if row is not None:
|
||
return row
|
||
data = path.read_bytes()
|
||
key = make_object_key(name)
|
||
upload_object("public", key, data, _content_type(path))
|
||
row = AdminFile(
|
||
bucket="public",
|
||
object_key=key,
|
||
filename=name,
|
||
content_type=_content_type(path),
|
||
size=len(data),
|
||
created_by=None,
|
||
)
|
||
db.add(row)
|
||
db.commit()
|
||
db.refresh(row)
|
||
return row
|
||
|
||
|
||
def _nameplate(name: str, dest: Path) -> None:
|
||
from PIL import Image, ImageDraw, ImageFont
|
||
|
||
image = Image.new("RGB", (360, 120), (15, 76, 129))
|
||
draw = ImageDraw.Draw(image)
|
||
font_path = "/System/Library/Fonts/Hiragino Sans GB.ttc"
|
||
font = ImageFont.truetype(font_path, 22)
|
||
# 两行排开,避免会员名称超出标志宽度。
|
||
midpoint = max(8, len(name) // 2)
|
||
split = name.rfind("有限", 0, midpoint + 6)
|
||
if split < 4:
|
||
split = midpoint
|
||
lines = [name[:split], name[split:]]
|
||
draw.multiline_text((18, 28), "\n".join(lines), fill=(255, 255, 255), font=font, spacing=6)
|
||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||
image.save(dest, "PNG")
|
||
|
||
|
||
def _clear_home(db) -> None:
|
||
for model in (
|
||
HomeSlideAttachment,
|
||
HomeSlide,
|
||
HomeArticleAttachment,
|
||
HomeArticle,
|
||
HomeAudienceLink,
|
||
HomeHotLink,
|
||
HomeHonor,
|
||
HomeMember,
|
||
HomeRelatedLink,
|
||
HomeComplianceLink,
|
||
HomeContact,
|
||
HomeMarquee,
|
||
):
|
||
db.execute(delete(model))
|
||
db.execute(delete(HomeMenuItem))
|
||
db.commit()
|
||
|
||
|
||
def _import_articles(
|
||
db, folder: Path, category: str, limit: int, skip: set[int], only: set[int] | None = None
|
||
) -> dict[int, int]:
|
||
created: dict[int, int] = {}
|
||
count = 0
|
||
for item in _load_index(folder):
|
||
article_id = int(item["id"])
|
||
if only is not None and article_id not in only:
|
||
continue
|
||
if article_id in skip or not item.get("bodySaved"):
|
||
continue
|
||
body_path = _article_dir(folder, article_id) / "content.txt"
|
||
if not body_path.is_file():
|
||
continue
|
||
image_id = None
|
||
meta_path = _article_dir(folder, article_id) / "meta.json"
|
||
if meta_path.is_file():
|
||
meta = json.loads(meta_path.read_text(encoding="utf-8"))
|
||
images = meta.get("images") or []
|
||
if images:
|
||
picture = _article_dir(folder, article_id) / images[0]["file"]
|
||
if picture.is_file():
|
||
image_id = _store_public(db, picture).id
|
||
html = _paragraphs(body_path)
|
||
html += f"<p>整理自现站公开页面:{escape(item['url'])}</p>"
|
||
created[article_id] = create_article(
|
||
db,
|
||
{
|
||
"category": category,
|
||
"title": item["title"][:200],
|
||
"bodyHtml": html,
|
||
"imageFileId": image_id,
|
||
"published": True,
|
||
"publishedAt": f"{item['date']}T08:00:00",
|
||
"attachmentFileIds": [],
|
||
},
|
||
)
|
||
count += 1
|
||
if count >= limit:
|
||
break
|
||
return created
|
||
|
||
|
||
def _related(name: str) -> list[dict]:
|
||
groups = {
|
||
"主管部门": [
|
||
("甘肃省住房和城乡建设厅", "http://www.gsjs.gansu.gov.cn/?tabid=1"),
|
||
("中华人民共和国住房和城乡建设部", "https://www.mohurd.gov.cn/"),
|
||
],
|
||
"相关部门": [
|
||
("甘肃省人力资源和社会保障厅", "http://www.rst.gansu.gov.cn/"),
|
||
("国家税务总局甘肃省税务局", "http://www.gstax.gov.cn/"),
|
||
],
|
||
"合作协会": [
|
||
("中国房地产估价师与房地产经纪人学会", "http://www.cirea.org.cn/"),
|
||
("英国皇家特许测量师学会(RICS)", "https://www.rics.org/cn/"),
|
||
("香港测量师学会(HKIS)", "https://www.hkis.org.hk/en/index.html"),
|
||
],
|
||
}
|
||
return [
|
||
{"label": label, "url": url, "visible": True, "sortOrder": index}
|
||
for index, (label, url) in enumerate(groups[name])
|
||
]
|
||
|
||
|
||
def seed() -> None:
|
||
if not SOURCE.is_dir():
|
||
raise SystemExit(f"找不到现站摘录:{SOURCE}")
|
||
engine = get_engine()
|
||
Base.metadata.create_all(bind=engine)
|
||
SessionLocal.configure(bind=engine)
|
||
db = SessionLocal()
|
||
try:
|
||
ensure_buckets()
|
||
ensure_site_defaults(db)
|
||
ensure_home_templates(db)
|
||
news_index = {int(item["id"]): item for item in _load_index(NEWS)}
|
||
|
||
logo = SOURCE / "logo" / "logo.jpg"
|
||
if logo.is_file():
|
||
logo_row = _store_public(db, logo, "home-sample-logo.jpg")
|
||
current = db.scalar(select(SiteSetting).where(SiteSetting.key == "logo_file_id"))
|
||
if current is None or not str(current.value).strip():
|
||
save_settings(db, {"logo_file_id": str(logo_row.id)})
|
||
|
||
_clear_home(db)
|
||
active = db.scalar(select(HomeTemplate.name).where(HomeTemplate.is_active.is_(True))) or TECH
|
||
for row in db.scalars(select(HomeTemplate)).all():
|
||
row.slogan = SLOGAN
|
||
row.value_proposition = VALUE
|
||
row.is_active = row.name == active
|
||
db.commit()
|
||
|
||
for index, (filename, source_id) in enumerate(SLIDES):
|
||
picture = CAROUSEL / filename
|
||
meta = news_index.get(source_id, {})
|
||
title = meta.get("title") or "协会活动"
|
||
body_path = _article_dir(NEWS, source_id) / "content.txt"
|
||
html = _paragraphs(body_path) if body_path.is_file() else f"<p>{escape(title)}</p>"
|
||
if meta.get("url"):
|
||
html += f"<p>整理自现站公开页面:{escape(meta['url'])}</p>"
|
||
image = _store_public(db, picture)
|
||
create_slide(
|
||
db,
|
||
{
|
||
"title": title[:200],
|
||
"imageFileId": image.id,
|
||
"sortOrder": index,
|
||
"enabled": True,
|
||
"intervalSeconds": 6,
|
||
"published": True,
|
||
"bodyHtml": html,
|
||
"attachmentFileIds": [],
|
||
},
|
||
)
|
||
|
||
set_article_display(db, {"news": 6, "notices": 6, "policies": 6, "dashboard": 6})
|
||
_import_articles(db, NEWS, "news", 6, set(DASHBOARD_IDS))
|
||
_import_articles(db, NOTICES, "notices", 6, set())
|
||
policy_ids = _import_articles(db, POLICIES, "policies", 6, set())
|
||
policy_ids.update(_import_articles(db, POLICIES, "policies", 1, set(), only={1209303}))
|
||
dashboard_ids = _import_articles(db, NEWS, "dashboard", 3, set(), only=set(DASHBOARD_IDS))
|
||
|
||
privacy_id = create_article(
|
||
db,
|
||
{
|
||
"category": "policies",
|
||
"title": "隐私政策(测试占位)",
|
||
"bodyHtml": (
|
||
"<p>现站没有单独发布隐私政策。本条只用于首页合规链接测试,正式文本以协会后续发布为准。</p>"
|
||
"<p>协会公开介绍写明,协会遵守法律、法规和国家政策。</p>"
|
||
),
|
||
"published": True,
|
||
"publishedAt": "2019-01-01T08:00:00",
|
||
"attachmentFileIds": [],
|
||
},
|
||
)
|
||
charter = db.scalar(select(HomeArticle).where(HomeArticle.title.contains("章程")))
|
||
covenant = db.scalar(select(HomeArticle).where(HomeArticle.title.contains("自律公约")))
|
||
compliance = [
|
||
{"label": "隐私政策", "url": f"/articles/{privacy_id}", "visible": True, "sortOrder": 0},
|
||
{
|
||
"label": "使用条款",
|
||
"url": f"/articles/{covenant.id}" if covenant else f"/articles/{privacy_id}",
|
||
"visible": True,
|
||
"sortOrder": 1,
|
||
},
|
||
{
|
||
"label": "网站声明",
|
||
"url": f"/articles/{charter.id}" if charter else f"/articles/{privacy_id}",
|
||
"visible": True,
|
||
"sortOrder": 2,
|
||
},
|
||
]
|
||
|
||
save_audiences(
|
||
db,
|
||
[
|
||
{
|
||
"name": "估价师",
|
||
"links": [
|
||
{"label": "估价师查询", "url": "http://xhzhglxt.cirea.org.cn/website/gjs_Iframe.asp", "visible": True, "sortOrder": 0},
|
||
{"label": "注册系统", "url": "http://gjszcxt.cirea.org.cn/", "visible": True, "sortOrder": 1},
|
||
{"label": "信用档案", "url": "http://gjxydaxt.cirea.org.cn/", "visible": True, "sortOrder": 2},
|
||
],
|
||
},
|
||
{
|
||
"name": "估价机构",
|
||
"links": [
|
||
{"label": "估价机构查询", "url": "http://xhzhglxt.cirea.org.cn/website/gjjg_iframe.asp", "visible": True, "sortOrder": 0},
|
||
{"label": "备案系统", "url": "http://gjjgzzxt.cirea.org.cn/", "visible": True, "sortOrder": 1},
|
||
],
|
||
},
|
||
{
|
||
"name": "公众",
|
||
"links": [
|
||
{"label": "联系我们", "url": "/contact", "visible": True, "sortOrder": 0},
|
||
{"label": "机构名录", "url": "/institutions", "visible": True, "sortOrder": 1},
|
||
{"label": "投诉举报", "url": "/discipline", "visible": True, "sortOrder": 2},
|
||
],
|
||
},
|
||
],
|
||
)
|
||
save_hot_links(
|
||
db,
|
||
[
|
||
{"key": "reportQuery", "label": "报告查询", "url": "/reports/query", "visible": True, "sortOrder": 0},
|
||
{"key": "reportReview", "label": "报告审核", "url": "/reports/review", "visible": True, "sortOrder": 1},
|
||
{"key": "complaint", "label": "投诉举报", "url": "/discipline", "visible": True, "sortOrder": 2},
|
||
],
|
||
)
|
||
save_menu(
|
||
db,
|
||
[
|
||
{"key": "member", "label": "会员服务", "url": "http://www.gsrea.org.cn/hyfw", "visible": True, "sortOrder": 1},
|
||
],
|
||
)
|
||
|
||
honor_dir = _article_dir(NEWS, 7911765)
|
||
honor_meta = json.loads((honor_dir / "meta.json").read_text(encoding="utf-8"))
|
||
honor_items = []
|
||
for index, image in enumerate(honor_meta.get("images") or []):
|
||
picture = honor_dir / image["file"]
|
||
if not picture.is_file():
|
||
continue
|
||
honor_items.append({"imageFileId": _store_public(db, picture).id, "sortOrder": index})
|
||
if len(honor_items) >= 4:
|
||
break
|
||
save_honors(db, 8, honor_items)
|
||
|
||
# 现站首页滚动的单位,网址来自首页外链;没有网址的保持空链接。
|
||
members = [
|
||
("甘肃中瑞房地产资产评估有限公司", "http://www.lzzr.org.cn"),
|
||
("甘肃信诺房地产土地资产评估有限公司", ""),
|
||
("兰州三信房地产评估有限公司", ""),
|
||
("甘肃华澳房地产估价有限公司", ""),
|
||
("甘肃宏信房地资产评估有限公司", ""),
|
||
("天水中恒房地产土地资产评估咨询有限公司", ""),
|
||
("兰州中信房地产土地评估事务有限公司", "http://www.8479514.com"),
|
||
("甘肃方家房地产评估有限公司", "http://www.gstdpg.com"),
|
||
("甘肃智信房地产咨询评估有限公司", "http://www.gansuzhixin.com"),
|
||
]
|
||
member_items = []
|
||
plate_dir = Path(tempfile.gettempdir()) / "gsrea-member-plates"
|
||
for index, (name, url) in enumerate(members):
|
||
plate = plate_dir / f"{index}.png"
|
||
_nameplate(name, plate)
|
||
member_items.append(
|
||
{
|
||
"name": name,
|
||
"url": url,
|
||
"imageFileId": _store_public(db, plate, f"home-sample-member-{index}.png").id,
|
||
"sortOrder": index,
|
||
}
|
||
)
|
||
save_members(db, 8, member_items)
|
||
|
||
contact = {
|
||
"address": "甘肃省兰州市城关区张掖路63号",
|
||
"phone": "0931-8126195",
|
||
"email": "gsrea2018@163.com",
|
||
"latitude": LATITUDE,
|
||
"longitude": LONGITUDE,
|
||
}
|
||
save_contact(db, [{"name": TECH, **contact}, {"name": BRAND, **contact}])
|
||
for template in (TECH, BRAND):
|
||
save_related_links(
|
||
db,
|
||
template,
|
||
[
|
||
{"name": "主管部门", "links": _related("主管部门")},
|
||
{"name": "相关部门", "links": _related("相关部门")},
|
||
{"name": "合作协会", "links": _related("合作协会")},
|
||
],
|
||
)
|
||
save_compliance(db, template, compliance)
|
||
|
||
print(
|
||
"首页测试数据已写入:"
|
||
f"轮播 {len(SLIDES)},新闻 6,通知 6,政策 {len(policy_ids)},"
|
||
f"数据看板 {len(dashboard_ids)},荣誉 {len(honor_items)},会员 {len(member_items)}"
|
||
)
|
||
finally:
|
||
db.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
seed()
|