"""把现站公开摘录写入「关于协会」相关模块,供测试环境查看。 数据来自仓库 docs/参考/现站/基础数据。重复执行会清掉关于协会单篇、专家委员会 与三类栏目文章后重写。口令只从环境变量读取。 """ from __future__ import annotations 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.about import ( AboutExpert, AboutExpertAttachment, AboutPage, AboutPageAttachment, create_expert, save_about_page, ) from app.models.content import AdminFile from app.models.home import ( HomeArticle, HomeArticleAttachment, create_article, ) ABOUT_COLUMN_CATEGORIES = ("annual_reports", "work_briefs", "qualifications") from app.services.storage import ensure_buckets, make_object_key, upload_object ROOT = Path(__file__).resolve().parents[3] SOURCE = ROOT / "docs" / "参考" / "现站" / "基础数据" ABOUT_DIR = SOURCE / "协会介绍" NEWS = SOURCE / "新闻动态" CONTACT = SOURCE / "联系我们" / "contact.json" def _paragraphs_from_lines(lines: list[str]) -> str: parts = [] for line in lines: line = line.strip() if not line: continue parts.append(f"

{escape(line)}

") return "".join(parts) def _paragraphs(path: Path) -> str: text = unescape(path.read_text(encoding="utf-8")) return _paragraphs_from_lines(text.splitlines()) 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" if suffix == ".pdf": return "application/pdf" return "image/jpeg" def _store_public(db, path: Path, filename: str | None = None) -> AdminFile: name = filename or f"about-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 _clear_about(db) -> None: article_ids = list( db.scalars( select(HomeArticle.id).where(HomeArticle.category.in_(ABOUT_COLUMN_CATEGORIES)) ).all() ) if article_ids: db.execute( delete(HomeArticleAttachment).where(HomeArticleAttachment.article_id.in_(article_ids)) ) db.execute(delete(HomeArticle).where(HomeArticle.id.in_(article_ids))) db.execute(delete(AboutExpertAttachment)) db.execute(delete(AboutExpert)) db.execute(delete(AboutPageAttachment)) db.execute(delete(AboutPage)) db.commit() def _intro_html() -> str: page = (ABOUT_DIR / "栏目页.txt").read_text(encoding="utf-8") lines = [] for line in page.splitlines(): line = line.strip() if not line: continue if line.startswith("来源:") or line in { "计时中", "首页", "协会介绍", "新闻中心", "通知公告", "新闻动态", "政策法规", "执业注册", "继续教育", "机构备案", "会员服务", "案例中心", "联系我们", "关于我们", "当前位置 :", "地址:", "电话:", "手机:", "邮箱:", "版权所有:", "甘肃省房地产价格评估行业协会", }: continue if line.startswith("|||") or line in {"넳", "넲", "끠", "搜索"}: continue if line.startswith("首页 ") or "甘肃省兰州市" in line and len(line) < 40: if line.startswith("甘肃省兰州市") or line.startswith("0931") or "@" in line: continue continue if line.startswith("甘肃省房地产价格评估行业协会(以下简称我会)"): lines.append(line) elif line.startswith("我会"): lines.append(line) lines.append("整理自现站公开页面:http://www.gsrea.org.cn/xhjs") return _paragraphs_from_lines(lines) def _org_html() -> str: lines = [ "我会日常办事机构为协会秘书处,秘书处下设综合办公室,在理事会领导下负责处理日常事务。", "理事会共有理事若干人,会员单位若干家(以现站公开信息为准)。", "协会下设五个专业委员会:", "一、专家委员会:负责重大疑难估价技术咨询与行业标准解读。", "二、自律委员会:围绕投诉争议、执业风险提示与行业诚信建设开展工作。", "三、技术委员会:组织估价技术研讨、典型案例交流与报告质量提升。", "四、培训交流委员会:衔接继续教育、培训计划与对外交流。", "五、信息化建设委员会:推进行业信息化与资料库、案例库建设。", "整理自现站协会介绍:http://www.gsrea.org.cn/xhjs", ] return _paragraphs_from_lines(lines) def _contact_html() -> str: import json contact = json.loads(CONTACT.read_text(encoding="utf-8")) if CONTACT.is_file() else {} address = contact.get("address") or "甘肃省兰州市城关区张掖路63号" phone = contact.get("phone") or "0931-8126195" email = contact.get("email") or "gsrea2018@163.com" lines = [ "来访路线 / 电话联系方式", f"办公地址:{address}", f"电话:{phone}", f"邮箱:{email}", "来访请先电话预约。公交与停车指引可现场或电话咨询秘书处。", "合作洽谈", "结构合作、学术合作、商务合作意向,请将单位介绍与合作方向发送至协会邮箱,秘书处将协调对接。", "加入协会指南", "我会的会员分为个人会员和单位会员。个人会员分为房地产估价师执业会员、房地产价格评估经纪人执业会员;单位会员分为团体会员、理事单位会员和会长(秘书长)单位会员。", "凡承认《甘肃省房地产价格评估行业协会章程》、具备会员条件的个人和单位均可申请入会。入会一般程序:提交入会申请书 → 理事会讨论通过 → 颁发会员证。", "会费标准与管理办法以协会现行文件为准,正式文本可在会员服务栏目或向秘书处索取。", f"整理自现站联系我们:{contact.get('url') or 'http://www.gsrea.org.cn/lxwm'}", ] return _paragraphs_from_lines(lines) def _news_article(article_id: int) -> tuple[str, str]: folder = NEWS / "文章" / str(article_id) meta = __import__("json").loads((folder / "meta.json").read_text(encoding="utf-8")) body = _paragraphs(folder / "content.txt") body += f"

整理自现站公开页面:{escape(meta.get('url') or '')}

" return meta.get("title") or str(article_id), body def seed() -> None: import app.models.about # noqa: F401 import app.models.content # noqa: F401 import app.models.home # noqa: F401 ensure_buckets() engine = get_engine() Base.metadata.create_all(bind=engine) db = SessionLocal() try: _clear_about(db) save_about_page( db, "intro", {"published": True, "bodyHtml": _intro_html(), "attachmentFileIds": []}, ) save_about_page( db, "org", {"published": True, "bodyHtml": _org_html(), "attachmentFileIds": []}, ) save_about_page( db, "contact", {"published": True, "bodyHtml": _contact_html(), "attachmentFileIds": []}, ) committees = [ ("专家委员会", "负责重大疑难估价技术咨询与行业标准解读,服务会员机构与估价师。", 1), ("自律委员会", "围绕投诉争议、执业风险提示与行业诚信建设,推动自律公约落地。", 2), ("技术委员会", "组织估价技术研讨、典型案例交流与报告质量提升活动。", 3), ("培训交流委员会", "衔接继续教育、培训计划与对外交流,提升从业人员专业水平。", 4), ("信息化建设委员会", "推进行业信息化、交易案例库与资料库建设。", 5), ] for name, blurb, order in committees: create_expert( db, { "name": name, "bodyHtml": _paragraphs_from_lines( [ blurb, "成员名单与正式任命以协会最新公示为准。", "整理自现站协会介绍公开信息。", ] ), "sortOrder": order, "published": True, "attachmentFileIds": [], }, ) # 年度报告 / 工作计划:理事会工作报告相关现站新闻 report_title, report_body = _news_article(8560786) create_article( db, { "category": "annual_reports", "title": report_title[:200], "publishedAt": "2025-07-24T16:00:00", "published": True, "bodyHtml": report_body, "imageFileId": None, "attachmentFileIds": [], }, ) create_article( db, { "category": "annual_reports", "title": "2018年度工作报告及2019年工作计划(会议公开摘要)", "publishedAt": "2019-04-26T16:00:00", "published": True, "bodyHtml": _paragraphs( ABOUT_DIR / "文章" / "1209332" / "content.txt" ) + "

整理自现站:http://www.gsrea.org.cn/newsinfo/1209332.html

", "imageFileId": None, "attachmentFileIds": [], }, ) # 工作简报:用现站新闻形态条目 for article_id, published_at in ( (8560786, "2025-07-24T16:00:00"), (7911765, "2024-12-31T10:00:00"), ): title, body = _news_article(article_id) create_article( db, { "category": "work_briefs", "title": f"工作简报 · {title[:180]}", "publishedAt": published_at, "published": True, "bodyHtml": body, "imageFileId": None, "attachmentFileIds": [], }, ) # 资质荣誉:喜报 + 表彰图片 honor_title, honor_body = _news_article(7911765) honor_images = [] for picture in sorted((ABOUT_DIR / "文章" / "1209332" / "images").glob("*.jpg"))[:2]: honor_images.append(_store_public(db, picture, f"about-honor-{picture.name}").id) image_tags = "".join( f'

协会荣誉示意

' for file_id in honor_images ) create_article( db, { "category": "qualifications", "title": honor_title[:200], "publishedAt": "2024-12-31T10:00:00", "published": True, "bodyHtml": honor_body + image_tags, "imageFileId": honor_images[0] if honor_images else None, "attachmentFileIds": honor_images[1:] if len(honor_images) > 1 else [], }, ) create_article( db, { "category": "qualifications", "title": "优秀报告评选表彰与行业交流(公开纪要)", "publishedAt": "2019-04-26T12:00:00", "published": True, "bodyHtml": _paragraphs(ABOUT_DIR / "文章" / "1209332" / "content.txt") + "

整理自现站表彰会议公开报道。

", "imageFileId": honor_images[0] if honor_images else None, "attachmentFileIds": [], }, ) print( "关于协会测试数据已写入:协会简介/组织机构/联系合作各 1 篇," f"专家委员会 {len(committees)} 条," "年度报告/工作计划 2 条,工作简报 2 条,资质荣誉 2 条。" ) finally: db.close() def _public_url(db, file_id: int) -> str: from app.services.storage import public_object_url row = db.get(AdminFile, file_id) if row is None or not row.object_key: return "" return public_object_url(row.object_key) if __name__ == "__main__": seed()