pipeline-opportunity/wwwroot/api/opp_item_detail.dspy
yumoqing 45cec8987e feat(opp): 数据参考三级整行下钻+明细详情弹窗+附件下载代理
- 汇总主题/分项条目整行点击(cursor:pointer)弹下级弹窗,撤「查看分项」按钮
- 明细每条来源写进说明文字(source_text),删除 source_button_widget 来源按钮
- 新 opp_item_detail.dspy: 爬虫平台条目全量字段+公告全文+附件列表
- 新 opp_attachment_dl.dspy: 服务端附件下载代理,归属校验防SSRF,
  降级链 download_url→/api/attachments/{id}/download→原始url,失败如实报错
- opp_data_capability: tender_attachments/normalize_attachment 容错对接
  爬虫平台附件子系统(/api/tenders/{id}/attachments,另一会话开发中,未就绪时降级提示)
- opp_common: crawler_get_raw 原始字节下载辅助; load_path 注册两个新端点
- README/prompt 同步新交互描述
2026-09-08 23:39:16 +08:00

194 lines
8.1 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# opp_item_detail.dspy - 单条数据项的具体内容弹窗(数据来自数据爬取平台)
# 入参:id=<爬虫平台条目id>。展示公告/需求全量字段 + 公告全文 + 来源说明 + 附件列表。
# 附件点击下载:走 opp_attachment_dl.dspy 代理(平台副本优先,缺失回退原始地址)。
import json as _json
uid = await get_user()
if not uid:
return {"widgettype": "Message", "options": {"title": "未登录", "message": "请先登录"}}
item_id = str((params_kw or {}).get("id") or "").strip()
if not item_id:
return {"widgettype": "Message", "options": {"title": "缺少参数", "message": "缺少 id"}}
dbname = get_module_dbname('pipeline-opportunity')
async with DBPools().sqlorContext(dbname) as sor:
from pipeline_opportunity.opp_data_capability import tender_detail, tender_attachments
from pipeline_opportunity.opp_common import source_text
ok, det = await tender_detail(sor, item_id)
await sor.sqlExe("COMMIT", {})
att_ok, atts = await tender_attachments(sor, item_id)
await sor.sqlExe("COMMIT", {})
dl_base = entire_url("/pipeline-opportunity/api/opp_attachment_dl.dspy")
body = []
if not ok or not isinstance(det, dict):
body.append({"widgettype": "Text", "options": {
"text": "获取详情失败:" + str(det)[:200],
"color": "#dc2626", "padding": "12px", "halign": "left"}})
else:
# ── 标题 ──
body.append({"widgettype": "Text", "options": {
"text": str(det.get("title") or ""), "cfontsize": 1.15,
"color": "#0f172a", "halign": "left", "wrap": True,
"margin": "0 0 8px 0"}})
# ── 字段区 ──
rt = str(det.get("record_type") or "tender")
budget = det.get("budget_wan") or 0
fields = []
fields.append(("发布时间", str(det.get("publish_time") or "—")))
if rt == "tender":
fields.append(("公告类型", str(det.get("notice_type") or "—")))
fields.append(("地区", str(det.get("region") or "—")))
fields.append(("采购人", str(det.get("buyer") or "—")))
fields.append(("预算", ("%.2f 万元" % budget) if budget else "未公开"))
if det.get("item_category"):
fields.append(("品目", str(det.get("item_category"))))
if det.get("project_code"):
fields.append(("项目编号", str(det.get("project_code"))))
if det.get("agency"):
fields.append(("代理机构", str(det.get("agency"))))
if det.get("contact") or det.get("contact_phone"):
fields.append(("联系人", ("%s %s" % (str(det.get("contact") or ""),
str(det.get("contact_phone") or ""))).strip()))
else:
fields.append(("需求类型", str(det.get("notice_type") or "—")))
if det.get("item_category"):
fields.append(("站内类目", str(det.get("item_category"))))
fields.append(("赏金/预算", ("%.2f 万元" % budget) if budget else "未公开"))
if det.get("ai_score"):
fields.append(("AI相关度", str(det.get("ai_score"))))
field_rows = []
for k, v in fields:
field_rows.append({
"widgettype": "HBox",
"options": {"width": "100%", "gap": "8px", "padding": "2px 0"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": k, "cfontsize": 0.85,
"color": "#64748b", "halign": "left", "width": "90px"}},
{"widgettype": "Text", "options": {"text": v, "cfontsize": 0.9,
"color": "#0f172a", "halign": "left", "wrap": True}}
]
})
body.append({"widgettype": "VBox", "options": {
"width": "100%", "padding": "8px 12px", "gap": "2px",
"border": "1px solid #e2e8f0", "borderRadius": "8px",
"margin": "0 0 10px 0", "bgcolor": "#f8fafc"},
"subwidgets": field_rows})
# ── 来源说明(来源写进说明,不做来源按钮;2026-09-08 用户定夺)──
src_name = source_text(str(det.get("source") or ""), str(det.get("source_url") or ""))
src_lines = ["来源:" + src_name + "(数据爬取平台采集)"]
url = str(det.get("url") or "")
src_url = str(det.get("source_url") or "")
src_note = "|".join(src_lines)
link_btns = []
if url:
link_btns.append({"widgettype": "Button", "options": {"label": "打开原文页", "css": "small"},
"binds": [{"wid": "self", "event": "click", "actiontype": "script",
"target": "self",
"script": "window.open(" + _json.dumps(url) + ",'_blank','noopener');"}]})
if src_url:
link_btns.append({"widgettype": "Button", "options": {"label": "打开采集来源页", "css": "small"},
"binds": [{"wid": "self", "event": "click", "actiontype": "script",
"target": "self",
"script": "window.open(" + _json.dumps(src_url) + ",'_blank','noopener');"}]})
body.append({
"widgettype": "HBox",
"options": {"width": "100%", "gap": "10px", "alignItems": "center",
"margin": "0 0 10px 0"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": src_note,
"cfontsize": 0.8, "color": "#64748b", "halign": "left", "wrap": True}},
{"widgettype": "Filler"}
] + link_btns
})
# ── 公告/需求全文(爬取平台保存的正文)──
detail_text = str(det.get("detail_text") or "").strip()
body.append({"widgettype": "Text", "options": {
"text": "全文内容", "cfontsize": 0.95, "color": "#0f172a",
"halign": "left", "margin": "0 0 4px 0"}})
if detail_text:
shown = detail_text[:8000]
if len(detail_text) > 8000:
shown += "\n……(全文 %d 字,已截断显示,完整内容见原文页)" % len(detail_text)
body.append({"widgettype": "Text", "options": {
"text": shown, "cfontsize": 0.85, "color": "#334155",
"halign": "left", "wrap": True, "whiteSpace": "pre-wrap",
"padding": "8px 12px", "border": "1px solid #e2e8f0",
"borderRadius": "8px", "margin": "0 0 10px 0", "bgcolor": "#ffffff"}})
else:
body.append({"widgettype": "Text", "options": {
"text": "爬取平台未保存该条全文,可点「打开原文页」查看。",
"cfontsize": 0.82, "color": "#94a3b8", "halign": "left",
"margin": "0 0 10px 0"}})
# ── 附件区(有附件则逐个可点击下载)──
body.append({"widgettype": "Text", "options": {
"text": "附件(%d)" % (len(atts) if att_ok else 0),
"cfontsize": 0.95, "color": "#0f172a", "halign": "left",
"margin": "0 0 4px 0"}})
if not att_ok:
# 附件端点未就绪(爬虫平台附件子系统部署中)——如实提示,不静默
body.append({"widgettype": "Text", "options": {
"text": "附件服务暂不可用:" + str(atts)[:120],
"cfontsize": 0.8, "color": "#94a3b8", "halign": "left",
"margin": "0 0 10px 0"}})
elif not atts:
body.append({"widgettype": "Text", "options": {
"text": "该条暂无附件", "cfontsize": 0.8, "color": "#94a3b8",
"halign": "left", "margin": "0 0 10px 0"}})
else:
for a in atts:
a_name = str(a.get("name") or "附件")
a_url = str(a.get("url") or "")
a_id = str(a.get("id") or "")
a_size = int(a.get("size") or 0)
size_txt = ""
if a_size >= 1048576:
size_txt = "%.1f MB" % (a_size / 1048576.0)
elif a_size > 0:
size_txt = "%.0f KB" % (a_size / 1024.0)
dl_query = "?tender_id=" + item_id
if a_id:
dl_query += "&att_id=" + a_id
elif a_url:
dl_query += "&att_url=" + a_url
dl_href = dl_base + dl_query
body.append({
"widgettype": "HBox",
"options": {"width": "100%", "gap": "10px", "alignItems": "center",
"padding": "6px 12px", "border": "1px solid #e2e8f0",
"borderRadius": "8px", "margin": "0 0 6px 0", "bgcolor": "#ffffff"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "📎 " + a_name,
"cfontsize": 0.88, "color": "#0f172a", "halign": "left",
"wrap": True}},
{"widgettype": "Text", "options": {"text": size_txt,
"cfontsize": 0.75, "color": "#94a3b8"}},
{"widgettype": "Filler"},
{"widgettype": "Button", "options": {"label": "下载", "css": "small"},
"binds": [{"wid": "self", "event": "click", "actiontype": "script",
"target": "self",
"script": "window.location.href=" + _json.dumps(dl_href) + ";"}]}
]
})
return {
"widgettype": "PopupWindow",
"id": "opp_item_detail_pw",
"options": {"title": "数据项详情", "width": "70%", "height": "85%",
"auto_open": True, "resizable": True},
"subwidgets": [{
"widgettype": "VBox",
"options": {"css": "filler", "width": "100%", "height": "100%",
"padding": "12px 16px", "gap": "0px", "overflow": "auto"},
"subwidgets": body
}]
}