pipeline-opportunity/wwwroot/api/opp_demand_items.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

98 lines
4.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_demand_items.dspy - 某软件主题的众包需求分项列表(国内猪八戒/一品威客)
# 入参category=<主题名>。交互2026-09-08 用户定夺):
# 每条分项整行点击 → opp_item_detail.dspy 弹出具体内容(爬取平台数据+附件下载);
# 来源写进每条说明文字(来源:猪八戒/一品威客),不再渲染来源按钮。
import json as _json
uid = await get_user()
if not uid:
return {"widgettype": "Message", "options": {"title": "未登录", "message": "请先登录"}}
category = ((params_kw or {}).get("category") or "").strip()
days = int((params_kw or {}).get("days") or 30)
if not category:
return {"widgettype": "Message", "options": {"title": "缺少参数", "message": "缺少 category"}}
dbname = get_module_dbname('pipeline-opportunity')
detail_url = entire_url("/pipeline-opportunity/api/opp_item_detail.dspy")
async with DBPools().sqlorContext(dbname) as sor:
from pipeline_opportunity.opp_data_capability import category_references
from pipeline_opportunity.opp_common import source_text
ok, res = await category_references(sor, category, days=days, limit=500, record_type="demand")
await sor.sqlExe("COMMIT", {})
rows = []
if not ok:
rows.append({"widgettype": "Text", "options": {
"text": "查询失败:" + str(res)[:200],
"color": "#dc2626", "padding": "12px", "halign": "left"}})
else:
items = res.get("items") or []
if not items:
rows.append({"widgettype": "Text", "options": {
"text": "该主题近 %d 天无众包需求分项" % days, "color": "#64748b", "padding": "12px", "halign": "left"}})
for it in items:
item_id = str(it.get("id") or "")
budget = it.get("budget_wan") or 0
# 来源写进说明文字(不再做来源按钮)
src_name = source_text(str(it.get("source") or ""), str(it.get("source_url") or ""))
meta = "%s %s 来源:%s" % (
str(it.get("publish_time") or "")[:10],
(("赏金 %.2f 万" % budget) if budget else "赏金面议"),
src_name)
row_subs = [
{"widgettype": "Text", "options": {
"text": str(it.get("title") or ""), "cfontsize": 0.95,
"color": "#0f172a", "halign": "left", "wrap": True}},
{"widgettype": "HBox", "options": {"width": "100%", "gap": "10px",
"alignItems": "center"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": meta,
"cfontsize": 0.75, "color": "#94a3b8", "halign": "left",
"wrap": True}},
{"widgettype": "Filler"},
{"widgettype": "Text", "options": {"text": "",
"cfontsize": 1.0, "color": "#94a3b8"}}
]
}
]
row_opts = {"width": "100%", "padding": "8px 12px", "gap": "4px",
"border": "1px solid #e2e8f0", "borderRadius": "8px",
"margin": "0 0 6px 0", "bgcolor": "#ffffff"}
if item_id:
row_opts["cursor"] = "pointer"
script = ("var old=bricks.getWidgetById('opp_item_detail_pw',bricks.app);"
"if(old&&old.destroy){old.destroy();}"
"var rp=await fetch(" + _json.dumps(detail_url) + "+'?id='+encodeURIComponent(" + _json.dumps(item_id) + "));"
"var d=await rp.json();"
"if(d){bricks.widgetBuild(d,bricks.app);}")
rows.append({
"widgettype": "VBox", "options": row_opts,
"binds": [{"wid": "self", "event": "click", "actiontype": "script",
"target": "self", "script": script}],
"subwidgets": row_subs})
else:
rows.append({"widgettype": "VBox", "options": row_opts,
"subwidgets": row_subs})
total = res.get("count", len(rows)) if ok else 0
return {
"widgettype": "PopupWindow",
"id": "opp_demand_items_pw",
"options": {"title": "众包需求分项:%s%d 条)" % (category, total),
"width": "80%", "height": "82%", "auto_open": True, "resizable": True},
"subwidgets": [{
"widgettype": "VBox",
"options": {"css": "filler", "width": "100%", "height": "100%",
"padding": "12px 16px", "gap": "0px", "overflow": "auto"},
"subwidgets": [
{"widgettype": "Text", "options": {
"text": "点击任一条目弹出具体内容(数据来自数据爬取平台);每条说明中已标注来源平台。",
"cfontsize": 0.78, "color": "#64748b", "halign": "left", "margin": "0 0 8px 0"}}
] + rows
}]
}