pipeline-opportunity/wwwroot/api/opp_report_detail.dspy
yumoqing 4092304726 feat(feasibility): P3可行性研究——opp_feasibility引擎(fp_estimate规则引擎FP/draft_feasibility八节报告/score_candidate评分/promote_to_project立项门禁)+report_type列(models/DDL/CRUD重生成)+详情页与PPT按类型选四节或八节模板(sections_for_type单一规则源)+列表类型徽标+prompt链路C+params/demand_mining字典种子+i18n词条
- fp_calc.calc()返回键实锤核对: total_fp/counts/errors(原total_ufp/by_type兜底猜错,已修)
- promote门禁修正: resolve_approval会把status置approved,confirmed只是中间态→门禁接受confirmed/approval_initiated/approved(原只认confirmed会导致审批通过后反而不能立项)
- _cluster_ctx补项目隔离(对齐_check_batch_visible口径)+全personal类别如实拒绝可行性样本不足
- 八节模板兜底节=需求规格;四节存量解析口径零变化
- opp_approvals CRUD手改(项目过滤)被xls2ui重生成吃掉→已checkout还原,只保留opp_reports新列部分
2026-09-14 09:03:58 +08:00

129 lines
5.6 KiB
Plaintext
Raw Permalink 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_report_detail.dspy - 研发报告详情弹窗(2026-09-09 用户要求)
# 入参:report_id。展示四节(研发场景/输入/输出/成果,MdWidget 渲染)+ 成果文件列表(逐个下载)。
# 权限:check_report_visible(与列表同一可见性规则——owner 过滤语义不漂移)。
# 文件下载走 opp_report_file_dl.dspy(只传 report_id+文件名,服务端回查清单命中才给文件,防路径穿越)。
import json as _json
from urllib.parse import quote as _quote
uid = await get_user()
if not uid:
return {"widgettype": "Message", "options": {"title": "未登录", "message": "请先登录"}}
report_id = ((params_kw or {}).get("report_id") or "").strip()
if not report_id:
return {"widgettype": "Message", "options": {"title": "缺少参数", "message": "缺少 report_id"}}
dbname = get_module_dbname('pipeline-opportunity')
async with DBPools().sqlorContext(dbname) as sor:
from pipeline_opportunity.opp_report_capability import (
check_report_visible, parse_report_sections, list_report_files,
sections_for_type)
visible, rep = await check_report_visible(sor, report_id, uid)
await sor.sqlExe("COMMIT", {})
if not visible:
return {"widgettype": "Message", "options": {
"title": "无权查看", "message": "报告不存在,或你不是该报告所属项目的 owner"}}
files = await list_report_files(sor, rep)
dl_url = entire_url("/pipeline-opportunity/api/opp_report_file_dl.dspy")
STATUS_TXT = {
"draft": "草稿", "confirmed": "已确认", "approval_initiated": "审批中",
"approved": "审批通过", "rejected": "已驳回",
}
TYPE_TXT = {"research": "调研", "feasibility": "可行性研究"}
body = []
# ── 标题 ──
body.append({"widgettype": "Text", "options": {
"text": str(rep.get("title") or rep.get("software") or "研发报告"),
"cfontsize": 1.15, "color": "#0f172a", "halign": "left", "wrap": True,
"margin": "0 0 6px 0"}})
# ── 元信息 ──
st = str(rep.get("status") or "")
rt = str(rep.get("report_type") or "research")
proj = str(rep.get("project_name") or "")
meta = "%s | %s | %s | %s | 创建人 %s" % (
str(rep.get("created_at") or "")[:16],
("项目:" + proj) if proj else "平台级调研(未挂项目)",
TYPE_TXT.get(rt, rt or "调研"),
STATUS_TXT.get(st, st or "—"),
str(rep.get("created_by") or "—"))
body.append({"widgettype": "Text", "options": {
"text": meta, "cfontsize": 0.78, "color": "#64748b", "halign": "left",
"wrap": True, "margin": "0 0 10px 0"}})
# ── 分节渲染:research 四节 / feasibility 八节(按 report_type 选模板;
# 空节如实标注,不编造)──
sections = parse_report_sections(str(rep.get("content") or ""), rt)
for name in sections_for_type(rt):
md = (sections.get(name) or "").strip()
body.append({"widgettype": "Text", "options": {
"text": name, "cfontsize": 0.95, "color": "#0f172a", "halign": "left",
"margin": "0 0 4px 0"}})
if md:
shown = md[:20000]
if len(md) > 20000:
shown += "\n\n……(本节 %d 字,已截断显示)" % len(md)
body.append({"widgettype": "MdWidget", "options": {
"mdtext": shown, "width": "100%", "padding": "8px 12px",
"border": "1px solid #e2e8f0", "borderRadius": "8px",
"margin": "0 0 10px 0", "bgcolor": "#ffffff"}})
else:
body.append({"widgettype": "Text", "options": {
"otext": "(本报告未填写此节)", "text": "(本报告未填写此节)", "i18n": True, "cfontsize": 0.82, "color": "#94a3b8",
"halign": "left", "margin": "0 0 10px 0"}})
# ── 成果文件(多个列出,逐个下载)──
body.append({"widgettype": "Text", "options": {
"text": "成果文件(%d)" % len(files), "cfontsize": 0.95, "color": "#0f172a",
"halign": "left", "margin": "0 0 4px 0"}})
if not files:
body.append({"widgettype": "Text", "options": {
"otext": "暂无成果文件(报告提交人工确认时会自动生成 PPT 存入项目 deliverables/)", "text": "暂无成果文件(报告提交人工确认时会自动生成 PPT 存入项目 deliverables/)", "i18n": True,
"cfontsize": 0.82, "color": "#94a3b8", "halign": "left", "margin": "0 0 10px 0"}})
for f in files:
size = int(f.get("size") or 0)
if size >= 1048576:
size_txt = "%.1f MB" % (size / 1048576.0)
elif size > 0:
size_txt = "%.0f KB" % (size / 1024.0)
else:
size_txt = ""
# 下载:只传 report_id + 文件名,服务端回查清单命中才返回文件(防路径穿越)
href = dl_url + "?report_id=" + _quote(report_id) + "&name=" + _quote(str(f.get("name") or ""))
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": "📄 " + str(f.get("name") or "文件"),
"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(href) + ";"}]}]
})
return {
"widgettype": "PopupWindow",
"id": "opp_report_detail_pw",
"options": {"title": "研发报告详情", "width": "72%", "height": "86%",
"auto_open": True, "resizable": True},
"subwidgets": [{
"widgettype": "VBox",
"options": {"css": "filler", "width": "100%", "height": "100%",
"padding": "12px 16px", "gap": "0px", "overflow": "auto"},
"subwidgets": body
}]
}