feat: 商机产线能力包——招标数据(爬取平台内网HTTP)→热点推荐→研发报告→人工确认→研发审批
This commit is contained in:
commit
b5cf9599f4
110
build.sh
Normal file
110
build.sh
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# pipeline-opportunity 一键部署脚本(在宿主应用根目录执行或从本目录定位宿主)。
|
||||||
|
# 职责:建表(运行期禁止 schema 变更,建表只在部署期)+ 种子数据(appcodes + pipelines 产线记录)
|
||||||
|
# + wwwroot 软链 + pip install 本包。
|
||||||
|
# 参考模式:pipeline-bidding/build.sh。
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# 定位宿主应用根目录(含 wwwroot + py3)
|
||||||
|
APP_ROOT=""
|
||||||
|
for candidate in "$SCRIPT_DIR/../../pipeline-app" "$SCRIPT_DIR/../.." "$HOME/work/pipeline/pipeline-app"; do
|
||||||
|
if [ -d "$candidate/wwwroot" ] && [ -d "$candidate/py3" ]; then
|
||||||
|
APP_ROOT="$(cd "$candidate" && pwd)"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ -z "$APP_ROOT" ]; then
|
||||||
|
echo "ERROR: host app (pipeline-app) root not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
PYTHON="$APP_ROOT/py3/bin/python"
|
||||||
|
echo "Host app root: $APP_ROOT"
|
||||||
|
|
||||||
|
# ── 1. 建表(幂等:CREATE TABLE IF NOT EXISTS)──
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
"$PYTHON" - "$APP_ROOT" <<'PYEOF'
|
||||||
|
import re
|
||||||
|
ddl = open('mysql.ddl.sql', encoding='utf-8').read()
|
||||||
|
ddl = re.sub(r'drop table if exists (\w+);', '', ddl)
|
||||||
|
ddl = re.sub(r'CREATE TABLE (\w+)\s*\(', r'CREATE TABLE IF NOT EXISTS \1 (', ddl)
|
||||||
|
open('/tmp/pipeline_opportunity_ddl.sql', 'w', encoding='utf-8').write(ddl)
|
||||||
|
print('DDL prepared:', ddl.count('CREATE TABLE IF NOT EXISTS'), 'tables')
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
"$PYTHON" - "$APP_ROOT" <<'PYEOF'
|
||||||
|
import sys, os, subprocess
|
||||||
|
sys.path.insert(0, os.getcwd())
|
||||||
|
from appPublic.jsonConfig import getConfig
|
||||||
|
from appPublic.aes import aes_decode_b64
|
||||||
|
cfg = getConfig(sys.argv[1], {'workdir': sys.argv[1]})
|
||||||
|
kw = cfg.databases['pipeline'].kwargs
|
||||||
|
pwd = aes_decode_b64(cfg.password_key, kw.password)
|
||||||
|
with open('/tmp/pipeline_opportunity_ddl.sql', 'rb') as f:
|
||||||
|
r = subprocess.run(['mysql', '-h', str(kw.host), '-P', str(kw.port),
|
||||||
|
'-u', str(kw.user), '-p%s' % pwd, str(kw.db)],
|
||||||
|
stdin=f, capture_output=True)
|
||||||
|
if r.returncode == 0:
|
||||||
|
print('tables applied')
|
||||||
|
else:
|
||||||
|
print('ERROR:', r.stderr.decode('utf-8', 'replace').strip()[:300])
|
||||||
|
sys.exit(1)
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
# ── 2. 种子数据:appcodes 字典 + pipelines 产线记录(数据源 init/data.json,幂等)──
|
||||||
|
cd "$APP_ROOT"
|
||||||
|
"$PYTHON" - "$SCRIPT_DIR" "$APP_ROOT" <<'PYEOF'
|
||||||
|
import sys, os, json, asyncio
|
||||||
|
CDIR = sys.argv[1]
|
||||||
|
sys.path.insert(0, os.getcwd())
|
||||||
|
from appPublic.jsonConfig import getConfig
|
||||||
|
from sqlor.dbpools import DBPools
|
||||||
|
from appPublic.uniqueID import getID
|
||||||
|
|
||||||
|
data = json.load(open(os.path.join(CDIR, 'init', 'data.json')))
|
||||||
|
|
||||||
|
async def seed():
|
||||||
|
config = getConfig(sys.argv[2], {'workdir': sys.argv[2]})
|
||||||
|
db = DBPools(config.databases)
|
||||||
|
async with db.sqlorContext('pipeline') as sor:
|
||||||
|
for pl in data.get('pipelines', []):
|
||||||
|
recs = await sor.sqlExe("SELECT id FROM pipelines WHERE id=${i}$", {"i": pl['id']})
|
||||||
|
if not recs:
|
||||||
|
await sor.C('pipelines', {
|
||||||
|
'id': pl['id'], 'name': pl['name'],
|
||||||
|
'description': pl.get('description', ''),
|
||||||
|
'pipeline_type': pl.get('pipeline_type', 'opportunity'),
|
||||||
|
'version': '1.0.0', 'status': pl.get('status', 'published'),
|
||||||
|
'org_id': pl.get('org_id', '0'), 'created_by': 'system',
|
||||||
|
})
|
||||||
|
print('pipeline registered:', pl['id'])
|
||||||
|
for ac in data.get('appcodes', []):
|
||||||
|
pid = ac['parentid']
|
||||||
|
rows = await sor.R('appcodes', {'id': pid})
|
||||||
|
if not rows:
|
||||||
|
await sor.C('appcodes', {'id': pid, 'name': ac.get('parentname', pid),
|
||||||
|
'hierarchy_flg': '0'})
|
||||||
|
for item in ac.get('items', []):
|
||||||
|
chk = await sor.sqlExe(
|
||||||
|
"SELECT id FROM appcodes_kv WHERE parentid=${p}$ AND k=${k}$",
|
||||||
|
{'p': pid, 'k': item['k']})
|
||||||
|
if not chk:
|
||||||
|
await sor.C('appcodes_kv', {'id': getID(), 'parentid': pid,
|
||||||
|
'k': item['k'], 'v': item['v']})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
print('seeded %d code groups' % len(data.get('appcodes', [])))
|
||||||
|
|
||||||
|
asyncio.run(seed())
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
# ── 3. wwwroot 软链 + pip install ──
|
||||||
|
rm -f "$APP_ROOT/wwwroot/pipeline-opportunity"
|
||||||
|
ln -sf "$SCRIPT_DIR/wwwroot" "$APP_ROOT/wwwroot/pipeline-opportunity"
|
||||||
|
echo "wwwroot linked: pipeline-opportunity"
|
||||||
|
|
||||||
|
"$APP_ROOT/py3/bin/pip" install "$SCRIPT_DIR/" 2>&1 | tail -1
|
||||||
|
|
||||||
|
echo "pipeline-opportunity module deploy complete."
|
||||||
|
echo "后续(宿主负责):app 入口 import load_pipeline_opportunity;RBAC 用 scripts/load_path.py。"
|
||||||
|
echo "爬虫平台接入配置:appbase params 表设 tender_api_base / tender_api_token(默认 http://192.168.16.2:9085)。"
|
||||||
34
init/data.json
Normal file
34
init/data.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"pipelines": [
|
||||||
|
{
|
||||||
|
"id": "opportunity_general",
|
||||||
|
"name": "商机产线",
|
||||||
|
"pipeline_type": "opportunity",
|
||||||
|
"description": "招标市场情报(数据爬取平台)→热点软件推荐→每日AI/Agent招标→研发报告编写→人工确认→研发审批流程",
|
||||||
|
"status": "published",
|
||||||
|
"org_id": "0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"appcodes": [
|
||||||
|
{
|
||||||
|
"parentid": "opp_report_status",
|
||||||
|
"parentname": "研发报告状态",
|
||||||
|
"items": [
|
||||||
|
{"k": "draft", "v": "草稿"},
|
||||||
|
{"k": "confirmed", "v": "人工确认通过"},
|
||||||
|
{"k": "approval_initiated", "v": "已发起研发审批"},
|
||||||
|
{"k": "approved", "v": "审批通过"},
|
||||||
|
{"k": "rejected", "v": "审批驳回"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parentid": "opp_approval_status",
|
||||||
|
"parentname": "研发审批状态",
|
||||||
|
"items": [
|
||||||
|
{"k": "initiated", "v": "已发起"},
|
||||||
|
{"k": "approved", "v": "审批通过"},
|
||||||
|
{"k": "rejected", "v": "审批驳回"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
6
json/build.sh
Normal file
6
json/build.sh
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 由 models/*.json + json/*.json 生成 wwwroot 下 CRUD 页面
|
||||||
|
# 注意: py3/bin/xls2ui 的 shebang 可能是旧路径,直接用 python 调
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
PY=../../pipeline-app/py3/bin/python
|
||||||
|
$PY ../../pipeline-app/py3/bin/xls2ui -m ../models -o ../wwwroot pipeline_opportunity opp_reports.json opp_approvals.json
|
||||||
36
json/opp_approvals.json
Normal file
36
json/opp_approvals.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"tblname": "opp_approvals",
|
||||||
|
"title": "研发审批",
|
||||||
|
"params": {
|
||||||
|
"sortby": [
|
||||||
|
"created_at desc"
|
||||||
|
],
|
||||||
|
"confidential_fields": [],
|
||||||
|
"browserfields": {
|
||||||
|
"exclouded": [
|
||||||
|
"note"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editexclouded": [
|
||||||
|
"id",
|
||||||
|
"created_at",
|
||||||
|
"resolved_at",
|
||||||
|
"resolved_by",
|
||||||
|
"report_id"
|
||||||
|
],
|
||||||
|
"editable": {
|
||||||
|
"new_data_url": "{{entire_url('add_opp_approvals.dspy')}}",
|
||||||
|
"update_data_url": "{{entire_url('update_opp_approvals.dspy')}}",
|
||||||
|
"delete_data_url": "{{entire_url('delete_opp_approvals.dspy')}}"
|
||||||
|
},
|
||||||
|
"data_filter": {
|
||||||
|
"AND": [
|
||||||
|
{
|
||||||
|
"field": "status",
|
||||||
|
"op": "=",
|
||||||
|
"var": "status_input"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
json/opp_reports.json
Normal file
47
json/opp_reports.json
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"tblname": "opp_reports",
|
||||||
|
"title": "研发报告",
|
||||||
|
"params": {
|
||||||
|
"sortby": [
|
||||||
|
"created_at desc"
|
||||||
|
],
|
||||||
|
"confidential_fields": [],
|
||||||
|
"browserfields": {
|
||||||
|
"exclouded": [
|
||||||
|
"content",
|
||||||
|
"confirm_task_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editexclouded": [
|
||||||
|
"id",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"confirm_task_id",
|
||||||
|
"confirmed_by"
|
||||||
|
],
|
||||||
|
"editable": {
|
||||||
|
"new_data_url": "{{entire_url('add_opp_reports.dspy')}}",
|
||||||
|
"update_data_url": "{{entire_url('update_opp_reports.dspy')}}",
|
||||||
|
"delete_data_url": "{{entire_url('delete_opp_reports.dspy')}}"
|
||||||
|
},
|
||||||
|
"data_filter": {
|
||||||
|
"AND": [
|
||||||
|
{
|
||||||
|
"field": "title",
|
||||||
|
"op": "LIKE",
|
||||||
|
"var": "title_input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "software",
|
||||||
|
"op": "LIKE",
|
||||||
|
"var": "software_input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "status",
|
||||||
|
"op": "=",
|
||||||
|
"var": "status_input"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
models/opp_approvals.json
Normal file
70
models/opp_approvals.json
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "opp_approvals",
|
||||||
|
"title": "研发审批",
|
||||||
|
"primary": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"catelog": "entity"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"title": "主键ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"nullable": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "report_id",
|
||||||
|
"title": "报告ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"nullable": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "project_id",
|
||||||
|
"title": "项目ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"title": "审批状态",
|
||||||
|
"type": "str",
|
||||||
|
"length": 16,
|
||||||
|
"nullable": "no",
|
||||||
|
"default": "initiated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "note",
|
||||||
|
"title": "审批说明",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_by",
|
||||||
|
"title": "发起人",
|
||||||
|
"type": "str",
|
||||||
|
"length": 64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "resolved_by",
|
||||||
|
"title": "审批人",
|
||||||
|
"type": "str",
|
||||||
|
"length": 64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at",
|
||||||
|
"title": "创建时间",
|
||||||
|
"type": "timestamp",
|
||||||
|
"nullable": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "resolved_at",
|
||||||
|
"title": "结论时间",
|
||||||
|
"type": "timestamp"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
83
models/opp_reports.json
Normal file
83
models/opp_reports.json
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "opp_reports",
|
||||||
|
"title": "研发报告",
|
||||||
|
"primary": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"catelog": "entity"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"title": "主键ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"nullable": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "project_id",
|
||||||
|
"title": "项目ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "software",
|
||||||
|
"title": "软件/主题",
|
||||||
|
"type": "str",
|
||||||
|
"length": 128,
|
||||||
|
"nullable": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "title",
|
||||||
|
"title": "报告标题",
|
||||||
|
"type": "str",
|
||||||
|
"length": 255,
|
||||||
|
"nullable": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "content",
|
||||||
|
"title": "报告正文",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"title": "报告状态",
|
||||||
|
"type": "str",
|
||||||
|
"length": 24,
|
||||||
|
"nullable": "no",
|
||||||
|
"default": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "confirm_task_id",
|
||||||
|
"title": "确认任务ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "confirmed_by",
|
||||||
|
"title": "确认人",
|
||||||
|
"type": "str",
|
||||||
|
"length": 64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_by",
|
||||||
|
"title": "创建人",
|
||||||
|
"type": "str",
|
||||||
|
"length": 64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at",
|
||||||
|
"title": "创建时间",
|
||||||
|
"type": "timestamp",
|
||||||
|
"nullable": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updated_at",
|
||||||
|
"title": "更新时间",
|
||||||
|
"type": "timestamp"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
70
mysql.ddl.sql
Normal file
70
mysql.ddl.sql
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
|
||||||
|
-- /home/ymq/work/pipeline/pipeline-opportunity/models/opp_reports.json
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- 建库时请用以下语句,支持emoji字符
|
||||||
|
-- CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
drop table if exists opp_reports;
|
||||||
|
CREATE TABLE opp_reports
|
||||||
|
(
|
||||||
|
|
||||||
|
`id` VARCHAR(32) NOT NULL comment '主键ID',
|
||||||
|
`project_id` VARCHAR(32) comment '项目ID',
|
||||||
|
`software` VARCHAR(128) NOT NULL comment '软件/主题',
|
||||||
|
`title` VARCHAR(255) NOT NULL comment '报告标题',
|
||||||
|
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '报告正文',
|
||||||
|
`status` VARCHAR(24) NOT NULL DEFAULT 'draft' comment '报告状态',
|
||||||
|
`confirm_task_id` VARCHAR(32) comment '确认任务ID',
|
||||||
|
`confirmed_by` VARCHAR(64) comment '确认人',
|
||||||
|
`created_by` VARCHAR(64) comment '创建人',
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL comment '创建时间',
|
||||||
|
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP comment '更新时间'
|
||||||
|
|
||||||
|
|
||||||
|
,primary key(id)
|
||||||
|
|
||||||
|
|
||||||
|
)
|
||||||
|
CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_unicode_ci
|
||||||
|
engine=innodb
|
||||||
|
comment '研发报告'
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
|
-- /home/ymq/work/pipeline/pipeline-opportunity/models/opp_approvals.json
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- 建库时请用以下语句,支持emoji字符
|
||||||
|
-- CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
drop table if exists opp_approvals;
|
||||||
|
CREATE TABLE opp_approvals
|
||||||
|
(
|
||||||
|
|
||||||
|
`id` VARCHAR(32) NOT NULL comment '主键ID',
|
||||||
|
`report_id` VARCHAR(32) NOT NULL comment '报告ID',
|
||||||
|
`project_id` VARCHAR(32) comment '项目ID',
|
||||||
|
`status` VARCHAR(16) NOT NULL DEFAULT 'initiated' comment '审批状态',
|
||||||
|
`note` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '审批说明',
|
||||||
|
`created_by` VARCHAR(64) comment '发起人',
|
||||||
|
`resolved_by` VARCHAR(64) comment '审批人',
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL comment '创建时间',
|
||||||
|
`resolved_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP comment '结论时间'
|
||||||
|
|
||||||
|
|
||||||
|
,primary key(id)
|
||||||
|
|
||||||
|
|
||||||
|
)
|
||||||
|
CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_unicode_ci
|
||||||
|
engine=innodb
|
||||||
|
comment '研发审批'
|
||||||
|
;
|
||||||
|
|
||||||
1
pipeline_opportunity/__init__.py
Normal file
1
pipeline_opportunity/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .init import load_pipeline_opportunity # noqa: F401
|
||||||
BIN
pipeline_opportunity/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
pipeline_opportunity/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pipeline_opportunity/__pycache__/init.cpython-310.pyc
Normal file
BIN
pipeline_opportunity/__pycache__/init.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pipeline_opportunity/__pycache__/opp_ability.cpython-310.pyc
Normal file
BIN
pipeline_opportunity/__pycache__/opp_ability.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pipeline_opportunity/__pycache__/opp_common.cpython-310.pyc
Normal file
BIN
pipeline_opportunity/__pycache__/opp_common.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
35
pipeline_opportunity/init.py
Normal file
35
pipeline_opportunity/init.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
"""pipeline-opportunity 模块装载入口。
|
||||||
|
|
||||||
|
宿主应用(pipeline-app)在 init() 中调用 load_pipeline_opportunity() 即挂载商机产线:
|
||||||
|
能力包注册(opp_ability)。未装本模块的宿主完全零接触。
|
||||||
|
|
||||||
|
平台边界:本模块属于产线平台,只通过内网 HTTP 消费数据爬取平台的只读数据接口;
|
||||||
|
报告/确认/审批状态落产线自己的库(opp_* 表),绝不回写爬虫库。
|
||||||
|
|
||||||
|
表结构不在此处建(铁律:运行期不做 schema 变更),建表走部署期 build.sh。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger("pipeline.opportunity")
|
||||||
|
|
||||||
|
_loaded = False
|
||||||
|
|
||||||
|
|
||||||
|
def load_pipeline_opportunity():
|
||||||
|
"""挂载商机产线(幂等)。"""
|
||||||
|
global _loaded
|
||||||
|
if _loaded:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 主 agent 能力包 + slash 命令(import 即注册,与投标产线同机制)
|
||||||
|
try:
|
||||||
|
from . import opp_ability # noqa: F401
|
||||||
|
logger.info("[pipeline_opportunity] ability registered: opportunity_general")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("[pipeline_opportunity] ability 注册失败: %s", str(e)[:200])
|
||||||
|
return False
|
||||||
|
|
||||||
|
_loaded = True
|
||||||
|
logger.info("[pipeline_opportunity] v0.1.0 loaded")
|
||||||
|
return True
|
||||||
338
pipeline_opportunity/opp_ability.py
Normal file
338
pipeline_opportunity/opp_ability.py
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
"""商机产线能力包(opportunity_general):主 agent(驾驶舱)工具 + 角色定义 + slash 命令。
|
||||||
|
|
||||||
|
与开发/投标产线同构,通过 pipeline-core 的 PipelineAbility 注册表挂载,
|
||||||
|
零侵入 core / AgentExecutor:项目 sd_projects.pipeline_id='opportunity_general' 时自动生效。
|
||||||
|
|
||||||
|
数据边界:招标数据只从数据爬取平台(内网 HTTP)读取,本产线不爬取、不存招标原始数据;
|
||||||
|
本产线自己的状态(研发报告/审批)落产线库的 opp_reports / opp_approvals 表。
|
||||||
|
|
||||||
|
商机闭环:热点软件推荐 → 选定方向 → 编写研发报告 → 人工确认(门禁)→ 发起研发审批 → 审批结论。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pipeline_core import (
|
||||||
|
ToolDefinition, PipelineAbility, RoleSpec, register_ability,
|
||||||
|
SlashCommand, register_slash_command,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger("pipeline.opportunity.ability")
|
||||||
|
|
||||||
|
PIPELINE_ID = "opportunity_general"
|
||||||
|
|
||||||
|
# ══════════════════ 工具定义(主 agent / 驾驶舱)══════════════════
|
||||||
|
|
||||||
|
OPP_TOOLS = [
|
||||||
|
# ── 数据(来自数据爬取平台,只读)──
|
||||||
|
ToolDefinition(name="opp_hot_software", description="热点软件推荐:近N天招标市场按软件主题聚合排名(招标量/预算/热点地区/样例项目),用于判断哪些软件方向最值得做",
|
||||||
|
parameters={"days": "统计窗口天数(默认30)", "top": "返回前几名(默认10)"}, category="data"),
|
||||||
|
ToolDefinition(name="opp_daily_ai_tenders", description="每日AI/Agent软件类产品或项目招标信息展示:指定日期的AI相关招标清单+预算统计",
|
||||||
|
parameters={"date": "日期YYYY-MM-DD(默认今天)"}, category="data"),
|
||||||
|
ToolDefinition(name="opp_search_tenders", description="按关键词搜索招标信息(IT项目)",
|
||||||
|
parameters={"keyword": "标题关键词", "days": "回溯天数(默认7)",
|
||||||
|
"source": "来源(可选 ccgp/ggzy)", "limit": "条数(默认50)"}, category="data"),
|
||||||
|
ToolDefinition(name="opp_tender_detail", description="查看单条招标信息完整详情(含公告原文、预算、联系人、来源链接)",
|
||||||
|
parameters={"tender_id": "招标信息ID"}, category="data"),
|
||||||
|
ToolDefinition(name="opp_crawler_stats", description="查看数据爬取平台采集健康度(总量/24h新增/最近运行)",
|
||||||
|
parameters={}, category="data"),
|
||||||
|
|
||||||
|
# ── 报告与审批(商机闭环)──
|
||||||
|
ToolDefinition(name="opp_create_report", description="创建软件研发报告草稿。从热门软件中选定的方向写入报告,数据基础章节自动从爬取平台拉取",
|
||||||
|
parameters={"software": "软件/主题名", "title": "报告标题(可选)",
|
||||||
|
"analysis": "市场分析正文(可选,可先建骨架后补写)"}, category="report"),
|
||||||
|
ToolDefinition(name="opp_update_report", description="更新草稿报告内容(仅草稿状态可改)",
|
||||||
|
parameters={"report_id": "报告ID", "content": "新的报告全文"}, category="report"),
|
||||||
|
ToolDefinition(name="opp_list_reports", description="列出研发报告及状态(draft/confirmed/approval_initiated/approved/rejected)",
|
||||||
|
parameters={"status": "状态过滤(可选)"}, category="report"),
|
||||||
|
ToolDefinition(name="opp_get_report", description="查看报告全文",
|
||||||
|
parameters={"report_id": "报告ID"}, category="report"),
|
||||||
|
ToolDefinition(name="opp_submit_report", description="报告写完后提交人工确认(创建确认任务,等人工决策)",
|
||||||
|
parameters={"report_id": "报告ID", "note": "给确认人的说明(可选)"}, category="report"),
|
||||||
|
ToolDefinition(name="opp_list_approvals", description="列出研发审批单及状态",
|
||||||
|
parameters={"status": "状态过滤(可选)"}, category="report"),
|
||||||
|
ToolDefinition(name="opp_diagnose", description="诊断商机产线:报告/审批分布、待办人工任务、爬取平台连通性",
|
||||||
|
parameters={}, category="agent"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# ══════════════════ 产线 prompt 片段 ══════════════════
|
||||||
|
|
||||||
|
OPP_PROMPT = """你是「商机产线」的驾驶舱 agent,负责把招标市场情报转化为软件研发商机:
|
||||||
|
热点软件推荐 → 选定方向 → 编写软件研发报告 → 人工确认 → 发起研发审批 → 跟踪审批结论。
|
||||||
|
|
||||||
|
## 数据来源(重要边界)
|
||||||
|
招标数据全部来自「数据爬取平台」(内网 HTTP,已采集入库、已去重、每条带来源链接)。
|
||||||
|
你只消费数据(opp_hot_software / opp_daily_ai_tenders / opp_search_tenders / opp_tender_detail),
|
||||||
|
**不要**自己去爬网站,也不要怀疑数据新鲜度——先看 opp_crawler_stats 的采集时间。
|
||||||
|
|
||||||
|
## 商机闭环与门禁(不要绕过)
|
||||||
|
1. 用 opp_hot_software 看哪个软件方向招标多、预算大,给用户推荐。
|
||||||
|
2. 用户选定方向后:opp_create_report 建草稿 → 你基于真实招标数据补写分析
|
||||||
|
(市场空间/代表项目/竞争格局/研发建议)→ opp_update_report 更新全文。
|
||||||
|
3. 写完用 opp_submit_report 提交**人工确认**(门禁)。确认前不许发起审批。
|
||||||
|
4. 人工确认通过后,你才用(或由人工任务驱动)发起研发审批;审批结论由人工回流,
|
||||||
|
**禁止**自己批准自己的审批。
|
||||||
|
|
||||||
|
## 硬规则
|
||||||
|
- 报告里的招标数量、预算、项目案例必须来自工具返回的真实数据,**禁止编造**;
|
||||||
|
查不到就如实写"数据未覆盖"。
|
||||||
|
- 每条引用的招标信息带来源链接(爬虫平台已提供 url / source_url)。
|
||||||
|
- 报告没被人工确认,不许当作可发起审批;审批没过,不许当作可立项。
|
||||||
|
|
||||||
|
## 典型场景
|
||||||
|
- 用户问「最近什么软件最火/最值得做」→ opp_hot_software(days=30) 后给结论。
|
||||||
|
- 用户问「今天有什么AI招标」→ opp_daily_ai_tenders()。
|
||||||
|
- 用户说「给XX写个研发报告」→ opp_create_report → 拉数据补写 → opp_submit_report。
|
||||||
|
- 用户问进展 → opp_diagnose(报告/审批分布 + 待办人工任务)。"""
|
||||||
|
|
||||||
|
# ══════════════════ 角色集(商机产线轻量:分析+撰写两角色)══════════════════
|
||||||
|
|
||||||
|
OPP_ROLES = [
|
||||||
|
RoleSpec(
|
||||||
|
name="agent.opp_analyst",
|
||||||
|
description="商机分析师",
|
||||||
|
aliases=["opp_analyst", "analyst"],
|
||||||
|
system_prompt="你是商机分析师。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。",
|
||||||
|
next_role="",
|
||||||
|
task_title="商机分析",
|
||||||
|
),
|
||||||
|
RoleSpec(
|
||||||
|
name="agent.opp_writer",
|
||||||
|
description="研发报告撰写工程师",
|
||||||
|
aliases=["opp_writer", "writer"],
|
||||||
|
system_prompt="你是研发报告撰写工程师。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。",
|
||||||
|
next_role="",
|
||||||
|
task_title="研发报告编写",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# ══════════════════ handler(签名 async def handler(sor, params, ctx) -> str)══════════════════
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt(ok, msg):
|
||||||
|
return ("OK: " if ok else "ERROR: ") + str(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt_rows(rows, empty="(空)"):
|
||||||
|
if not rows:
|
||||||
|
return empty
|
||||||
|
if isinstance(rows, str):
|
||||||
|
return rows
|
||||||
|
return "\n".join(json.dumps(r, ensure_ascii=False, default=str) for r in rows[:80])
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_hot_software(sor, p, ctx):
|
||||||
|
from .opp_data_capability import hot_software
|
||||||
|
ok, res = await hot_software(sor, days=int(p.get("days") or 30),
|
||||||
|
top=int(p.get("top") or 10))
|
||||||
|
if not ok:
|
||||||
|
return _fmt(False, res)
|
||||||
|
out = {"窗口": "%d天" % res.get("window_days"),
|
||||||
|
"窗口内招标总数": res.get("tenders_in_window"),
|
||||||
|
"排名": res.get("ranking")}
|
||||||
|
return json.dumps(out, ensure_ascii=False, default=str)
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_daily_ai(sor, p, ctx):
|
||||||
|
from .opp_data_capability import daily_ai_tenders
|
||||||
|
ok, res = await daily_ai_tenders(sor, target_date=p.get("date") or "")
|
||||||
|
if not ok:
|
||||||
|
return _fmt(False, res)
|
||||||
|
return json.dumps({
|
||||||
|
"date": res.get("date"), "count": res.get("count"),
|
||||||
|
"有预算条数": res.get("with_budget"),
|
||||||
|
"预算合计(万元)": res.get("total_budget_wan"),
|
||||||
|
"items": res.get("items"),
|
||||||
|
}, ensure_ascii=False, default=str)
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_search(sor, p, ctx):
|
||||||
|
from .opp_data_capability import search_tenders
|
||||||
|
ok, res = await search_tenders(sor, keyword=p.get("keyword") or "",
|
||||||
|
days=int(p.get("days") or 7),
|
||||||
|
source=p.get("source") or "",
|
||||||
|
limit=int(p.get("limit") or 50))
|
||||||
|
if not ok:
|
||||||
|
return _fmt(False, res)
|
||||||
|
return json.dumps({"count": res.get("count"), "items": res.get("items")},
|
||||||
|
ensure_ascii=False, default=str)
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_detail(sor, p, ctx):
|
||||||
|
from .opp_data_capability import tender_detail
|
||||||
|
ok, res = await tender_detail(sor, p.get("tender_id") or "")
|
||||||
|
if not ok:
|
||||||
|
return _fmt(False, res)
|
||||||
|
return json.dumps(res, ensure_ascii=False, default=str)
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_crawler_stats(sor, p, ctx):
|
||||||
|
from .opp_data_capability import crawler_stats
|
||||||
|
ok, res = await crawler_stats(sor)
|
||||||
|
if not ok:
|
||||||
|
return _fmt(False, res)
|
||||||
|
return json.dumps(res, ensure_ascii=False, default=str)
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_create_report(sor, p, ctx):
|
||||||
|
from .opp_report_capability import create_report
|
||||||
|
ok, rid = await create_report(
|
||||||
|
ctx.get("project_id") or "", p.get("software") or "",
|
||||||
|
title=p.get("title") or "", analysis=p.get("analysis") or "",
|
||||||
|
created_by="agent.main_agent")
|
||||||
|
return _fmt(ok, ("report_id=%s(草稿,用 opp_update_report 补写,完成后 opp_submit_report)" % rid) if ok else rid)
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_update_report(sor, p, ctx):
|
||||||
|
from .opp_report_capability import update_report
|
||||||
|
ok, msg = await update_report(p.get("report_id") or "", p.get("content") or "")
|
||||||
|
return _fmt(ok, msg)
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_list_reports(sor, p, ctx):
|
||||||
|
from .opp_report_capability import list_reports
|
||||||
|
rows = await list_reports(project_id=ctx.get("project_id") or "",
|
||||||
|
status=p.get("status") or "")
|
||||||
|
return _fmt_rows(rows, "尚无研发报告")
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_get_report(sor, p, ctx):
|
||||||
|
from .opp_report_capability import get_report_full
|
||||||
|
rep = await get_report_full(p.get("report_id") or "")
|
||||||
|
if not rep:
|
||||||
|
return _fmt(False, "报告不存在")
|
||||||
|
return json.dumps(rep, ensure_ascii=False, default=str)
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_submit_report(sor, p, ctx):
|
||||||
|
from .opp_report_capability import submit_for_confirmation
|
||||||
|
ok, msg = await submit_for_confirmation(p.get("report_id") or "",
|
||||||
|
note=p.get("note") or "")
|
||||||
|
return _fmt(ok, msg)
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_list_approvals(sor, p, ctx):
|
||||||
|
from .opp_report_capability import list_approvals
|
||||||
|
rows = await list_approvals(project_id=ctx.get("project_id") or "",
|
||||||
|
status=p.get("status") or "")
|
||||||
|
return _fmt_rows(rows, "尚无审批单")
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_diagnose(sor, p, ctx):
|
||||||
|
from .opp_report_capability import list_reports, list_approvals
|
||||||
|
from .opp_data_capability import crawler_stats
|
||||||
|
from .opp_common import find_human_task, HT_REPORT_CONFIRM, HT_DEV_APPROVAL
|
||||||
|
reps = await list_reports()
|
||||||
|
apps = await list_approvals()
|
||||||
|
ok, cs = await crawler_stats(sor)
|
||||||
|
dist = {}
|
||||||
|
for r in reps:
|
||||||
|
dist[r.get("status")] = dist.get(r.get("status"), 0) + 1
|
||||||
|
ht_conf = await find_human_task(sor, ctx.get("project_id") or "",
|
||||||
|
HT_REPORT_CONFIRM, status="pending")
|
||||||
|
ht_appr = await find_human_task(sor, ctx.get("project_id") or "",
|
||||||
|
HT_DEV_APPROVAL, status="pending")
|
||||||
|
return json.dumps({
|
||||||
|
"报告状态分布": dist,
|
||||||
|
"审批单": len(apps),
|
||||||
|
"待确认报告任务": ht_conf.get("title") if ht_conf else None,
|
||||||
|
"待审批任务": ht_appr.get("title") if ht_appr else None,
|
||||||
|
"爬取平台": ("连通,共 %s 条,24h 新增 %s" % (
|
||||||
|
cs.get("total"), cs.get("collected_24h"))) if ok else "不可达: %s" % cs,
|
||||||
|
}, ensure_ascii=False, default=str)
|
||||||
|
|
||||||
|
|
||||||
|
OPP_HANDLERS = {
|
||||||
|
"opp_hot_software": _h_hot_software,
|
||||||
|
"opp_daily_ai_tenders": _h_daily_ai,
|
||||||
|
"opp_search_tenders": _h_search,
|
||||||
|
"opp_tender_detail": _h_detail,
|
||||||
|
"opp_crawler_stats": _h_crawler_stats,
|
||||||
|
"opp_create_report": _h_create_report,
|
||||||
|
"opp_update_report": _h_update_report,
|
||||||
|
"opp_list_reports": _h_list_reports,
|
||||||
|
"opp_get_report": _h_get_report,
|
||||||
|
"opp_submit_report": _h_submit_report,
|
||||||
|
"opp_list_approvals": _h_list_approvals,
|
||||||
|
"opp_diagnose": _h_diagnose,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_shared():
|
||||||
|
"""并入引擎级通用任务/交付件/问答/项目工具(不重复实现,见 pipeline_service.shared_ability)。"""
|
||||||
|
try:
|
||||||
|
from pipeline_service.shared_ability import SHARED_TOOLS, SHARED_HANDLERS
|
||||||
|
except Exception as e: # pipeline_service 未安装/版本旧 → 只用商机专属工具,诚实降级
|
||||||
|
logger.warning("shared_ability 不可用,商机产线仅挂载专属工具: %s", str(e)[:120])
|
||||||
|
return list(OPP_TOOLS), dict(OPP_HANDLERS)
|
||||||
|
names = {t.name for t in OPP_TOOLS}
|
||||||
|
tools = list(OPP_TOOLS) + [t for t in SHARED_TOOLS if t.name not in names]
|
||||||
|
handlers = dict(SHARED_HANDLERS)
|
||||||
|
handlers.update(OPP_HANDLERS) # 同名以商机产线实现为准
|
||||||
|
return tools, handlers
|
||||||
|
|
||||||
|
|
||||||
|
def register_opp_ability():
|
||||||
|
"""注册商机产线能力包(幂等)。"""
|
||||||
|
tools, handlers = _merge_shared()
|
||||||
|
ability = PipelineAbility(
|
||||||
|
pipeline_id=PIPELINE_ID,
|
||||||
|
name="商机产线",
|
||||||
|
tools=tools,
|
||||||
|
system_prompt=OPP_PROMPT,
|
||||||
|
handlers=handlers,
|
||||||
|
roles=OPP_ROLES,
|
||||||
|
menus=[
|
||||||
|
{"label": "📊 研发报告", "icon": "", "url": "/pipeline-opportunity/opp_reports/index.ui",
|
||||||
|
"type": "popup", "width": "88%", "height": "82%"},
|
||||||
|
{"label": "✅ 研发审批", "icon": "", "url": "/pipeline-opportunity/opp_approvals/index.ui",
|
||||||
|
"type": "popup", "width": "88%", "height": "82%"},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
register_ability(ability)
|
||||||
|
return ability
|
||||||
|
|
||||||
|
|
||||||
|
# ══════════════════ slash 命令(产线专属)══════════════════
|
||||||
|
|
||||||
|
async def _slash_hot(args, ctx):
|
||||||
|
ex = ctx.get("executor")
|
||||||
|
if not ex:
|
||||||
|
return "无执行器上下文"
|
||||||
|
return await ex._execute_ability_tool("opp_hot_software", {"days": args or 30})
|
||||||
|
|
||||||
|
|
||||||
|
async def _slash_ai(args, ctx):
|
||||||
|
ex = ctx.get("executor")
|
||||||
|
if not ex:
|
||||||
|
return "无执行器上下文"
|
||||||
|
return await ex._execute_ability_tool("opp_daily_ai_tenders", {"date": args or ""})
|
||||||
|
|
||||||
|
|
||||||
|
async def _slash_reports(args, ctx):
|
||||||
|
ex = ctx.get("executor")
|
||||||
|
if not ex:
|
||||||
|
return "无执行器上下文"
|
||||||
|
return await ex._execute_ability_tool("opp_list_reports", {})
|
||||||
|
|
||||||
|
|
||||||
|
async def _slash_oppdiag(args, ctx):
|
||||||
|
ex = ctx.get("executor")
|
||||||
|
if not ex:
|
||||||
|
return "无执行器上下文"
|
||||||
|
return await ex._execute_ability_tool("opp_diagnose", {})
|
||||||
|
|
||||||
|
|
||||||
|
def register_opp_slash_commands():
|
||||||
|
for cmd in [
|
||||||
|
SlashCommand("hot", "热点软件推荐(近30天招标排名)", _slash_hot, "pipeline", PIPELINE_ID),
|
||||||
|
SlashCommand("ai", "今日AI/Agent招标清单", _slash_ai, "pipeline", PIPELINE_ID),
|
||||||
|
SlashCommand("reports", "列出研发报告", _slash_reports, "pipeline", PIPELINE_ID),
|
||||||
|
SlashCommand("oppdiag", "商机产线诊断", _slash_oppdiag, "pipeline", PIPELINE_ID),
|
||||||
|
]:
|
||||||
|
register_slash_command(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
# import 即注册(与投标产线一致:宿主 import pipeline_opportunity 即生效)
|
||||||
|
register_opp_ability()
|
||||||
|
register_opp_slash_commands()
|
||||||
189
pipeline_opportunity/opp_common.py
Normal file
189
pipeline_opportunity/opp_common.py
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
"""商机产线公共层:DB 上下文、爬虫平台 HTTP 客户端、报告/审批状态、人工任务。
|
||||||
|
|
||||||
|
所有 opp_* capability 模块共用本模块,禁止各自重复实现。
|
||||||
|
|
||||||
|
爬虫平台接入:内网 HTTP(http_api.py),配置优先级
|
||||||
|
1. appbase params 表: tender_api_base / tender_api_token(系统级配置禁硬编码)
|
||||||
|
2. 兜底默认: http://192.168.16.2:9085(内网地址,仅默认值)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
from sqlor.dbpools import DBPools
|
||||||
|
from appPublic.uniqueID import getID
|
||||||
|
|
||||||
|
DBNAME = "pipeline"
|
||||||
|
PIPELINE_ID = "opportunity_general"
|
||||||
|
|
||||||
|
logger = logging.getLogger("pipeline.opportunity")
|
||||||
|
|
||||||
|
# 爬虫平台默认接入点(内网网卡地址,公网不可达);可被 params 表覆盖
|
||||||
|
DEFAULT_API_BASE = "http://192.168.16.2:9085"
|
||||||
|
|
||||||
|
# ── 报告状态 ──
|
||||||
|
RP_DRAFT = "draft" # 草稿(agent 编写中/待审阅)
|
||||||
|
RP_CONFIRMED = "confirmed" # 人工确认通过(门禁)
|
||||||
|
RP_APPROVAL_INITIATED = "approval_initiated" # 已发起研发审批
|
||||||
|
RP_APPROVED = "approved" # 审批通过
|
||||||
|
RP_REJECTED = "rejected" # 审批驳回
|
||||||
|
|
||||||
|
# ── 审批状态 ──
|
||||||
|
AP_INITIATED = "initiated"
|
||||||
|
AP_APPROVED = "approved"
|
||||||
|
AP_REJECTED = "rejected"
|
||||||
|
|
||||||
|
# ── 人工任务类型 ──
|
||||||
|
HT_REPORT_CONFIRM = "opp_report_confirm" # 报告人工确认门禁
|
||||||
|
HT_DEV_APPROVAL = "opp_dev_approval" # 研发审批人工决策
|
||||||
|
|
||||||
|
# 报告状态流转(合法迁移)
|
||||||
|
REPORT_TRANSITIONS = {
|
||||||
|
RP_DRAFT: (RP_CONFIRMED,),
|
||||||
|
RP_CONFIRMED: (RP_APPROVAL_INITIATED,),
|
||||||
|
RP_APPROVAL_INITIATED: (RP_APPROVED, RP_REJECTED),
|
||||||
|
RP_REJECTED: (RP_DRAFT,), # 驳回可改回草稿重来
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
"""取 DBPools(databases 为空时从 config 兜底注入,与引擎其他模块一致)。"""
|
||||||
|
db = DBPools()
|
||||||
|
if not db.databases:
|
||||||
|
from appPublic.jsonConfig import getConfig
|
||||||
|
config = getConfig()
|
||||||
|
if config and config.databases:
|
||||||
|
db.databases = config.databases
|
||||||
|
return db, DBNAME
|
||||||
|
|
||||||
|
|
||||||
|
def new_id():
|
||||||
|
return getID()
|
||||||
|
|
||||||
|
|
||||||
|
def rec_to_dict(rec):
|
||||||
|
"""sqlor 行对象 → dict(过滤方法名)。"""
|
||||||
|
if rec is None:
|
||||||
|
return {}
|
||||||
|
if isinstance(rec, dict):
|
||||||
|
return {k: v for k, v in rec.items() if not callable(v)}
|
||||||
|
d = {}
|
||||||
|
for k, v in vars(rec).items():
|
||||||
|
if k.startswith("_") or callable(v):
|
||||||
|
continue
|
||||||
|
d[k] = v
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def rows_to_dicts(recs, limit=200):
|
||||||
|
return [rec_to_dict(r) for r in (recs or [])[:limit]]
|
||||||
|
|
||||||
|
|
||||||
|
def json_loads(s, default=None):
|
||||||
|
if not s:
|
||||||
|
return default if default is not None else []
|
||||||
|
if isinstance(s, (list, dict)):
|
||||||
|
return s
|
||||||
|
try:
|
||||||
|
return json.loads(s)
|
||||||
|
except (json.JSONDecodeError, TypeError, ValueError):
|
||||||
|
return default if default is not None else []
|
||||||
|
|
||||||
|
|
||||||
|
async def get_param(sor, key, default=""):
|
||||||
|
"""读 appbase params 表配置 + 默认兜底(系统级配置禁硬编码)。"""
|
||||||
|
try:
|
||||||
|
from pipeline_service.workspace import get_param as _gp
|
||||||
|
v = await _gp(sor, key, "")
|
||||||
|
if v not in (None, ""):
|
||||||
|
return str(v)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
async def get_crawler_config(sor):
|
||||||
|
"""爬虫平台接入配置 (base, token)。params 表优先,默认内网地址兜底。"""
|
||||||
|
base = await get_param(sor, "tender_api_base", DEFAULT_API_BASE)
|
||||||
|
token = await get_param(sor, "tender_api_token", "")
|
||||||
|
return (base or DEFAULT_API_BASE).rstrip("/"), token
|
||||||
|
|
||||||
|
|
||||||
|
async def crawler_get(sor, path, params=None, timeout=20):
|
||||||
|
"""调爬虫平台只读接口。返回 (ok, payload|错误信息)。"""
|
||||||
|
base, token = await get_crawler_config(sor)
|
||||||
|
qs = ""
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
qs = "?" + urllib.parse.urlencode(clean)
|
||||||
|
url = base + path + qs
|
||||||
|
headers = {"X-API-Token": token} if token else {}
|
||||||
|
try:
|
||||||
|
import httpx
|
||||||
|
except ImportError:
|
||||||
|
httpx = None
|
||||||
|
try:
|
||||||
|
if httpx is not None:
|
||||||
|
async with httpx.AsyncClient(timeout=timeout) as cli:
|
||||||
|
r = await cli.get(url, headers=headers)
|
||||||
|
if r.status_code != 200:
|
||||||
|
return False, "爬虫平台返回 %d: %s" % (r.status_code, r.text[:200])
|
||||||
|
return True, r.json()
|
||||||
|
# 无 httpx 时退回 urllib(同步阻塞,仅在缺依赖时用)
|
||||||
|
import asyncio
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
def _blocking():
|
||||||
|
req = urllib.request.Request(url, headers=headers)
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||||
|
return json.loads(resp.read().decode("utf-8"))
|
||||||
|
|
||||||
|
return True, await asyncio.get_event_loop().run_in_executor(None, _blocking)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("crawler_get %s failed: %s", path, str(e)[:200])
|
||||||
|
return False, "爬虫平台调用失败: %s" % str(e)[:200]
|
||||||
|
|
||||||
|
|
||||||
|
async def find_human_task(sor, project_id, task_type, status=None):
|
||||||
|
"""查项目下指定类型的人工任务(供门禁判断与幂等创建)。"""
|
||||||
|
sql = ("SELECT id, title, status, task_type FROM pipeline_human_tasks "
|
||||||
|
"WHERE project_id=${pid}$ AND task_type=${tt}$")
|
||||||
|
args = {"pid": project_id, "tt": task_type}
|
||||||
|
if status:
|
||||||
|
sql += " AND status=${st}$"
|
||||||
|
args["st"] = status
|
||||||
|
sql += " ORDER BY created_at DESC LIMIT 1"
|
||||||
|
recs = await sor.sqlExe(sql, args)
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
return rec_to_dict(recs[0]) if recs else None
|
||||||
|
|
||||||
|
|
||||||
|
async def create_human_task(sor, project_id, task_type, title, description="",
|
||||||
|
assignee_role="owner"):
|
||||||
|
"""创建项目级人工任务(确认/审批门禁)。幂等由调用方先 find 保证。"""
|
||||||
|
hid = new_id()
|
||||||
|
await sor.C("pipeline_human_tasks", {
|
||||||
|
"id": hid,
|
||||||
|
"project_id": project_id,
|
||||||
|
"task_type": task_type,
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"status": "pending",
|
||||||
|
"assignee_role": assignee_role,
|
||||||
|
"created_by": "agent.opportunity",
|
||||||
|
})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
return hid
|
||||||
|
|
||||||
|
|
||||||
|
async def get_report(sor, report_id):
|
||||||
|
recs = await sor.sqlExe(
|
||||||
|
"SELECT * FROM opp_reports WHERE id=${i}$", {"i": report_id})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
return rec_to_dict(recs[0]) if recs else None
|
||||||
|
|
||||||
|
|
||||||
|
async def can_transition(cur_status, new_status):
|
||||||
|
return new_status in REPORT_TRANSITIONS.get(cur_status, ())
|
||||||
42
pipeline_opportunity/opp_data_capability.py
Normal file
42
pipeline_opportunity/opp_data_capability.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"""商机产线数据能力:从数据爬取平台拉取招标数据(只读)。
|
||||||
|
|
||||||
|
所有函数签名统一 (sor, **params),返回 (ok, payload|错误信息)。
|
||||||
|
薄封装:查询条件透传给爬虫平台,本层不做二次加工(热点排序由爬虫平台算)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .opp_common import crawler_get
|
||||||
|
|
||||||
|
|
||||||
|
async def search_tenders(sor, keyword="", days=7, source="", only_it=True, limit=50):
|
||||||
|
"""关键词检索招标信息。"""
|
||||||
|
params = {"days": days, "limit": limit}
|
||||||
|
if keyword:
|
||||||
|
params["keyword"] = keyword
|
||||||
|
if source:
|
||||||
|
params["source"] = source
|
||||||
|
params["only_it"] = "1" if only_it else "0"
|
||||||
|
return await crawler_get(sor, "/api/tenders", params)
|
||||||
|
|
||||||
|
|
||||||
|
async def tender_detail(sor, tender_id):
|
||||||
|
"""单条招标详情(含公告全文、来源 url)。"""
|
||||||
|
return await crawler_get(sor, "/api/tenders/%s" % tender_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def daily_ai_tenders(sor, target_date=""):
|
||||||
|
"""每日 AI/Agent 软件类招标展示数据。"""
|
||||||
|
params = {}
|
||||||
|
if target_date:
|
||||||
|
params["date"] = target_date
|
||||||
|
return await crawler_get(sor, "/api/ai_tenders", params)
|
||||||
|
|
||||||
|
|
||||||
|
async def hot_software(sor, days=30, top=10):
|
||||||
|
"""热点软件主题排名(数量+预算+热点地区+样例)。"""
|
||||||
|
return await crawler_get(sor, "/api/hot_software",
|
||||||
|
{"days": days, "top": top})
|
||||||
|
|
||||||
|
|
||||||
|
async def crawler_stats(sor):
|
||||||
|
"""爬虫平台采集健康度。"""
|
||||||
|
return await crawler_get(sor, "/api/stats")
|
||||||
212
pipeline_opportunity/opp_report_capability.py
Normal file
212
pipeline_opportunity/opp_report_capability.py
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
"""商机产线报告/审批能力:研发报告编写 → 人工确认 → 研发审批流程。
|
||||||
|
|
||||||
|
状态机(REPORT_TRANSITIONS,opp_common):
|
||||||
|
draft → confirmed → approval_initiated → approved / rejected
|
||||||
|
└ rejected → draft(可重来)
|
||||||
|
|
||||||
|
门禁纪律:
|
||||||
|
- confirm 只能 draft → confirmed,且是人工决策(由人工任务回流驱动)
|
||||||
|
- initiate_approval 只能 confirmed → approval_initiated(未经确认禁止发起审批)
|
||||||
|
- 审批结论由人工决策回流(approve/reject),agent 不得自批自审
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .opp_common import (
|
||||||
|
get_db, new_id, rec_to_dict, rows_to_dicts, can_transition,
|
||||||
|
RP_DRAFT, RP_CONFIRMED, RP_APPROVAL_INITIATED, RP_APPROVED, RP_REJECTED,
|
||||||
|
AP_INITIATED, AP_APPROVED, AP_REJECTED,
|
||||||
|
HT_REPORT_CONFIRM, HT_DEV_APPROVAL,
|
||||||
|
find_human_task, create_human_task, get_report,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger("pipeline.opportunity.report")
|
||||||
|
|
||||||
|
|
||||||
|
async def create_report(project_id, software, title="", analysis="",
|
||||||
|
created_by="agent.opportunity"):
|
||||||
|
"""创建研发报告草稿。返回 (True, report_id) 或 (False, 错误)。"""
|
||||||
|
if not software:
|
||||||
|
return False, "缺少 software(软件/主题名)"
|
||||||
|
if not title:
|
||||||
|
title = "%s 研发机会分析报告" % software
|
||||||
|
db, dbname = get_db()
|
||||||
|
rid = new_id()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
await sor.C("opp_reports", {
|
||||||
|
"id": rid,
|
||||||
|
"project_id": project_id or "",
|
||||||
|
"software": software,
|
||||||
|
"title": title,
|
||||||
|
"content": analysis or "",
|
||||||
|
"status": RP_DRAFT,
|
||||||
|
"created_by": created_by,
|
||||||
|
})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
return True, rid
|
||||||
|
|
||||||
|
|
||||||
|
async def update_report(report_id, content):
|
||||||
|
"""更新草稿报告内容(仅 draft 可改)。"""
|
||||||
|
db, dbname = get_db()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
rep = await get_report(sor, report_id)
|
||||||
|
if not rep:
|
||||||
|
return False, "报告不存在: %s" % report_id
|
||||||
|
if rep.get("status") != RP_DRAFT:
|
||||||
|
return False, "状态 %s 不可修改(仅草稿可编辑)" % rep.get("status")
|
||||||
|
await sor.U("opp_reports", {"id": report_id, "content": content})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
return True, "已更新"
|
||||||
|
|
||||||
|
|
||||||
|
async def list_reports(project_id="", status=""):
|
||||||
|
db, dbname = get_db()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
sql = ("SELECT id, project_id, software, title, status, created_by, "
|
||||||
|
"created_at, updated_at FROM opp_reports WHERE 1=1")
|
||||||
|
args = {}
|
||||||
|
if project_id:
|
||||||
|
sql += " AND project_id=${pid}$"
|
||||||
|
args["pid"] = project_id
|
||||||
|
if status:
|
||||||
|
sql += " AND status=${st}$"
|
||||||
|
args["st"] = status
|
||||||
|
sql += " ORDER BY created_at DESC LIMIT 50"
|
||||||
|
recs = await sor.sqlExe(sql, args)
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
return rows_to_dicts(recs)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_report_full(report_id):
|
||||||
|
db, dbname = get_db()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
rep = await get_report(sor, report_id)
|
||||||
|
return rep
|
||||||
|
|
||||||
|
|
||||||
|
async def submit_for_confirmation(report_id, note=""):
|
||||||
|
"""agent 完成报告后提交待人工确认:发人工确认任务(门禁)。
|
||||||
|
|
||||||
|
报告保持 draft,创建/复用 pending 人工任务。人工确认后才进 confirmed。
|
||||||
|
"""
|
||||||
|
db, dbname = get_db()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
rep = await get_report(sor, report_id)
|
||||||
|
if not rep:
|
||||||
|
return False, "报告不存在: %s" % report_id
|
||||||
|
if rep.get("status") != RP_DRAFT:
|
||||||
|
return False, "状态 %s 不可提交确认" % rep.get("status")
|
||||||
|
exist = await find_human_task(sor, rep.get("project_id", ""),
|
||||||
|
HT_REPORT_CONFIRM, status="pending")
|
||||||
|
if exist:
|
||||||
|
return True, "已有待确认任务: %s" % exist.get("id")
|
||||||
|
hid = await create_human_task(
|
||||||
|
sor, rep.get("project_id", ""), HT_REPORT_CONFIRM,
|
||||||
|
"确认研发报告:%s" % rep.get("title", ""),
|
||||||
|
note or "请审阅研发报告《%s》,确认后发起研发审批。" % rep.get("title", ""))
|
||||||
|
await sor.U("opp_reports", {
|
||||||
|
"id": report_id, "confirm_task_id": hid})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
return True, "已提交人工确认任务: %s" % hid
|
||||||
|
|
||||||
|
|
||||||
|
async def confirm_report(report_id, ok, operator=""):
|
||||||
|
"""人工确认回流(门禁)。ok=True → confirmed;ok=False → 退回草稿待改。"""
|
||||||
|
db, dbname = get_db()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
rep = await get_report(sor, report_id)
|
||||||
|
if not rep:
|
||||||
|
return False, "报告不存在: %s" % report_id
|
||||||
|
if rep.get("status") != RP_DRAFT:
|
||||||
|
return False, "状态 %s 无法确认(仅草稿可确认)" % rep.get("status")
|
||||||
|
new_status = RP_CONFIRMED if ok else RP_DRAFT
|
||||||
|
await sor.U("opp_reports", {
|
||||||
|
"id": report_id, "status": new_status,
|
||||||
|
"confirmed_by": operator or "human"})
|
||||||
|
# 完结对应的人工任务
|
||||||
|
hid = rep.get("confirm_task_id")
|
||||||
|
if hid:
|
||||||
|
await sor.U("pipeline_human_tasks", {
|
||||||
|
"id": hid, "status": "done" if ok else "rejected"})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
return True, "已确认" if ok else "已退回草稿"
|
||||||
|
|
||||||
|
|
||||||
|
async def initiate_approval(report_id, note="", created_by="agent.opportunity"):
|
||||||
|
"""发起研发审批:要求报告已 confirmed(人工确认门禁)。"""
|
||||||
|
db, dbname = get_db()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
rep = await get_report(sor, report_id)
|
||||||
|
if not rep:
|
||||||
|
return False, "报告不存在: %s" % report_id
|
||||||
|
if not await can_transition(rep.get("status"), RP_APPROVAL_INITIATED):
|
||||||
|
return False, "状态 %s 不可发起审批(须先人工确认)" % rep.get("status")
|
||||||
|
aid = new_id()
|
||||||
|
await sor.C("opp_approvals", {
|
||||||
|
"id": aid,
|
||||||
|
"report_id": report_id,
|
||||||
|
"project_id": rep.get("project_id", ""),
|
||||||
|
"status": AP_INITIATED,
|
||||||
|
"note": note,
|
||||||
|
"created_by": created_by,
|
||||||
|
})
|
||||||
|
await sor.U("opp_reports", {
|
||||||
|
"id": report_id, "status": RP_APPROVAL_INITIATED})
|
||||||
|
# 发人工审批任务
|
||||||
|
await create_human_task(
|
||||||
|
sor, rep.get("project_id", ""), HT_DEV_APPROVAL,
|
||||||
|
"研发审批:%s" % rep.get("title", ""),
|
||||||
|
note or "请审批《%s》的研发立项。" % rep.get("title", ""))
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
return True, aid
|
||||||
|
|
||||||
|
|
||||||
|
async def resolve_approval(approval_id, result, operator=""):
|
||||||
|
"""审批结论回流(人工决策)。result=approved/rejected。"""
|
||||||
|
if result not in (AP_APPROVED, AP_REJECTED):
|
||||||
|
return False, "result 必须是 approved 或 rejected"
|
||||||
|
db, dbname = get_db()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
recs = await sor.sqlExe(
|
||||||
|
"SELECT id, report_id, status FROM opp_approvals WHERE id=${i}$",
|
||||||
|
{"i": approval_id})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
if not recs:
|
||||||
|
return False, "审批单不存在: %s" % approval_id
|
||||||
|
rec = rec_to_dict(recs[0])
|
||||||
|
if rec.get("status") != AP_INITIATED:
|
||||||
|
return False, "审批单状态 %s 无法结论" % rec.get("status")
|
||||||
|
report_status = RP_APPROVED if result == AP_APPROVED else RP_REJECTED
|
||||||
|
await sor.U("opp_approvals", {
|
||||||
|
"id": approval_id, "status": result,
|
||||||
|
"resolved_by": operator or "human"})
|
||||||
|
await sor.U("opp_reports", {
|
||||||
|
"id": rec.get("report_id"), "status": report_status})
|
||||||
|
# 完结人工审批任务
|
||||||
|
ht = await find_human_task(sor, rec.get("project_id", ""),
|
||||||
|
HT_DEV_APPROVAL, status="pending")
|
||||||
|
if ht:
|
||||||
|
await sor.U("pipeline_human_tasks", {
|
||||||
|
"id": ht.get("id"),
|
||||||
|
"status": "done" if result == AP_APPROVED else "rejected"})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
return True, result
|
||||||
|
|
||||||
|
|
||||||
|
async def list_approvals(project_id="", status=""):
|
||||||
|
db, dbname = get_db()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
sql = ("SELECT id, report_id, project_id, status, note, created_by, "
|
||||||
|
"created_at, resolved_at FROM opp_approvals WHERE 1=1")
|
||||||
|
args = {}
|
||||||
|
if project_id:
|
||||||
|
sql += " AND project_id=${pid}$"
|
||||||
|
args["pid"] = project_id
|
||||||
|
if status:
|
||||||
|
sql += " AND status=${st}$"
|
||||||
|
args["st"] = status
|
||||||
|
sql += " ORDER BY created_at DESC LIMIT 50"
|
||||||
|
recs = await sor.sqlExe(sql, args)
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
return rows_to_dicts(recs)
|
||||||
9
pyproject.toml
Normal file
9
pyproject.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[project]
|
||||||
|
name = "pipeline_opportunity"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "商机产线:热点软件推荐→每日AI/Agent招标→研发报告→人工确认→研发审批。数据来自数据爬取平台(内网HTTP)。"
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["."]
|
||||||
|
include = ["pipeline_opportunity*"]
|
||||||
BIN
scripts/__pycache__/load_path.cpython-310.pyc
Normal file
BIN
scripts/__pycache__/load_path.cpython-310.pyc
Normal file
Binary file not shown.
6
scripts/gen_ddl.sh
Normal file
6
scripts/gen_ddl.sh
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 生成 pipeline-opportunity 的 MySQL DDL(json2ddl 需要 sqlor + appPublic 在 PYTHONPATH)
|
||||||
|
set -e
|
||||||
|
R=/home/ymq/work/repos
|
||||||
|
export PYTHONPATH="$R/xls2ddl:$R/sqlor:$R/apppublic"
|
||||||
|
python3 "$R/xls2ddl/gen_opportunity_ddl.py"
|
||||||
59
scripts/load_path.py
Normal file
59
scripts/load_path.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""RBAC path registration for pipeline-opportunity module.
|
||||||
|
|
||||||
|
与平台惯例一致:页面/CRUD/API → logined。
|
||||||
|
在宿主应用根目录执行(set_role_perm.py 位于宿主根):
|
||||||
|
cd <APP_ROOT> && py3/bin/python pkgs/pipeline-opportunity/scripts/load_path.py
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
MOD = "pipeline-opportunity"
|
||||||
|
# set_role_perm.py 在宿主应用根目录(本脚本位于 <APP_ROOT>/pkgs/pipeline-opportunity/scripts/)
|
||||||
|
APP_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
||||||
|
|
||||||
|
TABLES = [
|
||||||
|
"opp_reports", "opp_approvals",
|
||||||
|
]
|
||||||
|
|
||||||
|
PATHS_ANY = []
|
||||||
|
|
||||||
|
PATHS_LOGINED = [
|
||||||
|
"/%s" % MOD,
|
||||||
|
"/%s/index.ui" % MOD,
|
||||||
|
"/%s/agent" % MOD,
|
||||||
|
"/%s/agent/index.ui" % MOD,
|
||||||
|
]
|
||||||
|
for t in TABLES:
|
||||||
|
PATHS_LOGINED += [
|
||||||
|
"/%s/%s/index.ui" % (MOD, t),
|
||||||
|
"/%s/%s/get_%s.dspy" % (MOD, t, t),
|
||||||
|
"/%s/%s/add_%s.dspy" % (MOD, t, t),
|
||||||
|
"/%s/%s/update_%s.dspy" % (MOD, t, t),
|
||||||
|
"/%s/%s/delete_%s.dspy" % (MOD, t, t),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _run(role, path):
|
||||||
|
r = subprocess.run([sys.executable, os.path.join(APP_ROOT, "set_role_perm.py"), role, path],
|
||||||
|
capture_output=True, text=True, cwd=APP_ROOT)
|
||||||
|
if r.returncode != 0:
|
||||||
|
print(" FAIL [%s] %s: %s" % (role, path, (r.stderr or "").strip()[:120]))
|
||||||
|
return r.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=== %s RBAC registration ===" % MOD)
|
||||||
|
n = 0
|
||||||
|
for p in PATHS_ANY:
|
||||||
|
n += _run("any", p)
|
||||||
|
print(" any: %s" % p)
|
||||||
|
for p in PATHS_LOGINED:
|
||||||
|
n += _run("logined", p)
|
||||||
|
print(" logined: %s" % p)
|
||||||
|
print("Done. %d/%d paths registered" % (n, len(PATHS_ANY) + len(PATHS_LOGINED)))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
37
wwwroot/opp_approvals/add_opp_approvals.dspy
Normal file
37
wwwroot/opp_approvals/add_opp_approvals.dspy
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
ns = params_kw.copy()
|
||||||
|
for k,v in ns.items():
|
||||||
|
if v == 'NaN' or v == 'null':
|
||||||
|
ns[k] = None
|
||||||
|
id = params_kw.id
|
||||||
|
if not id or len(id) > 32:
|
||||||
|
id = uuid()
|
||||||
|
ns['id'] = id
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
db = DBPools()
|
||||||
|
dbname = get_module_dbname('pipeline_opportunity')
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
r = await sor.C('opp_approvals', ns.copy())
|
||||||
|
return {
|
||||||
|
"widgettype":"Message",
|
||||||
|
"options":{
|
||||||
|
"cwidth":16,
|
||||||
|
"cheight":9,
|
||||||
|
"title":"Add Success",
|
||||||
|
"timeout":3,
|
||||||
|
"message":"ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"widgettype":"Error",
|
||||||
|
"options":{
|
||||||
|
"title":"Add Error",
|
||||||
|
"cwidth":16,
|
||||||
|
"cheight":9,
|
||||||
|
"timeout":3,
|
||||||
|
"message":"failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
33
wwwroot/opp_approvals/delete_opp_approvals.dspy
Normal file
33
wwwroot/opp_approvals/delete_opp_approvals.dspy
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
ns = {
|
||||||
|
'id':params_kw['id'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
db = DBPools()
|
||||||
|
dbname = get_module_dbname('pipeline_opportunity')
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
r = await sor.D('opp_approvals', ns)
|
||||||
|
debug('delete success');
|
||||||
|
return {
|
||||||
|
"widgettype":"Message",
|
||||||
|
"options":{
|
||||||
|
"title":"Delete Success",
|
||||||
|
"timeout":3,
|
||||||
|
"cwidth":16,
|
||||||
|
"cheight":9,
|
||||||
|
"message":"ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('Delete failed');
|
||||||
|
return {
|
||||||
|
"widgettype":"Error",
|
||||||
|
"options":{
|
||||||
|
"title":"Delete Error",
|
||||||
|
"timeout":3,
|
||||||
|
"cwidth":16,
|
||||||
|
"cheight":9,
|
||||||
|
"message":"failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
112
wwwroot/opp_approvals/get_opp_approvals.dspy
Normal file
112
wwwroot/opp_approvals/get_opp_approvals.dspy
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
|
||||||
|
ns = params_kw.copy()
|
||||||
|
|
||||||
|
|
||||||
|
debug(f'get_opp_approvals.dspy:{ns=}')
|
||||||
|
if not ns.get('page'):
|
||||||
|
ns['page'] = 1
|
||||||
|
if not ns.get('sort'):
|
||||||
|
|
||||||
|
|
||||||
|
ns['sort'] = ["created_at desc"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
sql = '''select * from opp_approvals where 1=1 [[filterstr]]'''
|
||||||
|
|
||||||
|
filterjson = params_kw.get('data_filter')
|
||||||
|
if filterjson and isinstance(filterjson, str):
|
||||||
|
try:
|
||||||
|
filterjson = json.loads(filterjson)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
filterjson = None
|
||||||
|
fields_str=r'''[
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"title": "主键ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"nullable": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "report_id",
|
||||||
|
"title": "报告ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"nullable": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "project_id",
|
||||||
|
"title": "项目ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"title": "审批状态",
|
||||||
|
"type": "str",
|
||||||
|
"length": 16,
|
||||||
|
"nullable": "no",
|
||||||
|
"default": "initiated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "note",
|
||||||
|
"title": "审批说明",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_by",
|
||||||
|
"title": "发起人",
|
||||||
|
"type": "str",
|
||||||
|
"length": 64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "resolved_by",
|
||||||
|
"title": "审批人",
|
||||||
|
"type": "str",
|
||||||
|
"length": 64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at",
|
||||||
|
"title": "创建时间",
|
||||||
|
"type": "timestamp",
|
||||||
|
"nullable": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "resolved_at",
|
||||||
|
"title": "结论时间",
|
||||||
|
"type": "timestamp"
|
||||||
|
}
|
||||||
|
]'''
|
||||||
|
ori_fields = json.loads(fields_str)
|
||||||
|
if not filterjson:
|
||||||
|
fields = [ f['name'] for f in ori_fields ]
|
||||||
|
filterjson = default_filterjson(fields, ns)
|
||||||
|
|
||||||
|
filterdic = ns.copy()
|
||||||
|
filterdic['filterstr'] = ''
|
||||||
|
filterdic['userorgid'] = '${userorgid}$'
|
||||||
|
filterdic['userid'] = '${userid}$'
|
||||||
|
if filterjson:
|
||||||
|
dbf = DBFilter(filterjson)
|
||||||
|
conds = dbf.gen(ns)
|
||||||
|
if conds:
|
||||||
|
ns.update(dbf.consts)
|
||||||
|
conds = f' and {conds}'
|
||||||
|
filterdic['filterstr'] = conds
|
||||||
|
ac = ArgsConvert('[[', ']]')
|
||||||
|
vars = ac.findAllVariables(sql)
|
||||||
|
NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' }
|
||||||
|
filterdic.update(NameSpace)
|
||||||
|
sql = ac.convert(sql, filterdic)
|
||||||
|
|
||||||
|
debug(f'{sql=}')
|
||||||
|
db = DBPools()
|
||||||
|
dbname = get_module_dbname('pipeline_opportunity')
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
r = await sor.sqlPaging(sql, ns)
|
||||||
|
return r
|
||||||
|
return {
|
||||||
|
"total":0,
|
||||||
|
"rows":[]
|
||||||
|
}
|
||||||
176
wwwroot/opp_approvals/index.ui
Normal file
176
wwwroot/opp_approvals/index.ui
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
|
||||||
|
{
|
||||||
|
"widgettype":"VBox",
|
||||||
|
"options":{"cheight":40,"width":"100%"},
|
||||||
|
"subwidgets":[{
|
||||||
|
"id":"opp_approvals_tbl",
|
||||||
|
"widgettype":"Tabular",
|
||||||
|
"options":{
|
||||||
|
"width":"100%",
|
||||||
|
"height":"100%",
|
||||||
|
|
||||||
|
|
||||||
|
"title":"研发审批",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"css":"card",
|
||||||
|
|
||||||
|
|
||||||
|
"editable":{
|
||||||
|
|
||||||
|
"new_data_url":"{{entire_url('add_opp_approvals.dspy')}}",
|
||||||
|
|
||||||
|
|
||||||
|
"delete_data_url":"{{entire_url('delete_opp_approvals.dspy')}}",
|
||||||
|
|
||||||
|
|
||||||
|
"update_data_url":"{{entire_url('update_opp_approvals.dspy')}}"
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
"data_url":"{{entire_url('./get_opp_approvals.dspy')}}",
|
||||||
|
|
||||||
|
"data_method":"GET",
|
||||||
|
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
|
||||||
|
"row_options":{
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"browserfields": {
|
||||||
|
"exclouded": [
|
||||||
|
"note"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
"editexclouded":[
|
||||||
|
"id",
|
||||||
|
"created_at",
|
||||||
|
"resolved_at",
|
||||||
|
"resolved_by",
|
||||||
|
"report_id"
|
||||||
|
],
|
||||||
|
|
||||||
|
"fields":[
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"title": "主键ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"nullable": "no",
|
||||||
|
"cwidth": 18,
|
||||||
|
"uitype": "str",
|
||||||
|
"datatype": "str",
|
||||||
|
"label": "主键ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "report_id",
|
||||||
|
"title": "报告ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"nullable": "no",
|
||||||
|
"cwidth": 18,
|
||||||
|
"uitype": "str",
|
||||||
|
"datatype": "str",
|
||||||
|
"label": "报告ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "project_id",
|
||||||
|
"title": "项目ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"cwidth": 18,
|
||||||
|
"uitype": "str",
|
||||||
|
"datatype": "str",
|
||||||
|
"label": "项目ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"title": "审批状态",
|
||||||
|
"type": "str",
|
||||||
|
"length": 16,
|
||||||
|
"nullable": "no",
|
||||||
|
"default": "initiated",
|
||||||
|
"cwidth": 16,
|
||||||
|
"uitype": "str",
|
||||||
|
"datatype": "str",
|
||||||
|
"label": "审批状态"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "note",
|
||||||
|
"title": "审批说明",
|
||||||
|
"type": "text",
|
||||||
|
"length": 0,
|
||||||
|
"uitype": "text",
|
||||||
|
"datatype": "text",
|
||||||
|
"label": "审批说明"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_by",
|
||||||
|
"title": "发起人",
|
||||||
|
"type": "str",
|
||||||
|
"length": 64,
|
||||||
|
"cwidth": 18,
|
||||||
|
"uitype": "str",
|
||||||
|
"datatype": "str",
|
||||||
|
"label": "发起人"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "resolved_by",
|
||||||
|
"title": "审批人",
|
||||||
|
"type": "str",
|
||||||
|
"length": 64,
|
||||||
|
"cwidth": 18,
|
||||||
|
"uitype": "str",
|
||||||
|
"datatype": "str",
|
||||||
|
"label": "审批人"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at",
|
||||||
|
"title": "创建时间",
|
||||||
|
"type": "timestamp",
|
||||||
|
"nullable": "no",
|
||||||
|
"length": 0,
|
||||||
|
"uitype": "str",
|
||||||
|
"datatype": "timestamp",
|
||||||
|
"label": "创建时间"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "resolved_at",
|
||||||
|
"title": "结论时间",
|
||||||
|
"type": "timestamp",
|
||||||
|
"length": 0,
|
||||||
|
"uitype": "str",
|
||||||
|
"datatype": "timestamp",
|
||||||
|
"label": "结论时间"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"data_filter":{
|
||||||
|
"AND": [
|
||||||
|
{
|
||||||
|
"field": "status",
|
||||||
|
"op": "=",
|
||||||
|
"var": "status_input"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"page_rows":160,
|
||||||
|
"cache_limit":5
|
||||||
|
}
|
||||||
|
|
||||||
|
,"binds":[]
|
||||||
|
|
||||||
|
}]
|
||||||
|
}
|
||||||
36
wwwroot/opp_approvals/update_opp_approvals.dspy
Normal file
36
wwwroot/opp_approvals/update_opp_approvals.dspy
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
ns = params_kw.copy()
|
||||||
|
for k,v in ns.items():
|
||||||
|
if v == 'NaN' or v == 'null':
|
||||||
|
ns[k] = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
db = DBPools()
|
||||||
|
dbname = get_module_dbname('pipeline_opportunity')
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
|
r = await sor.U('opp_approvals', ns)
|
||||||
|
debug('update success');
|
||||||
|
return {
|
||||||
|
"widgettype":"Message",
|
||||||
|
"options":{
|
||||||
|
"title":"Update Success",
|
||||||
|
"cwidth":16,
|
||||||
|
"cheight":9,
|
||||||
|
"timeout":3,
|
||||||
|
"message":"ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"widgettype":"Error",
|
||||||
|
"options":{
|
||||||
|
"title":"Update Error",
|
||||||
|
"cwidth":16,
|
||||||
|
"cheight":9,
|
||||||
|
"timeout":3,
|
||||||
|
"message":"failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
37
wwwroot/opp_reports/add_opp_reports.dspy
Normal file
37
wwwroot/opp_reports/add_opp_reports.dspy
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
ns = params_kw.copy()
|
||||||
|
for k,v in ns.items():
|
||||||
|
if v == 'NaN' or v == 'null':
|
||||||
|
ns[k] = None
|
||||||
|
id = params_kw.id
|
||||||
|
if not id or len(id) > 32:
|
||||||
|
id = uuid()
|
||||||
|
ns['id'] = id
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
db = DBPools()
|
||||||
|
dbname = get_module_dbname('pipeline_opportunity')
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
r = await sor.C('opp_reports', ns.copy())
|
||||||
|
return {
|
||||||
|
"widgettype":"Message",
|
||||||
|
"options":{
|
||||||
|
"cwidth":16,
|
||||||
|
"cheight":9,
|
||||||
|
"title":"Add Success",
|
||||||
|
"timeout":3,
|
||||||
|
"message":"ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"widgettype":"Error",
|
||||||
|
"options":{
|
||||||
|
"title":"Add Error",
|
||||||
|
"cwidth":16,
|
||||||
|
"cheight":9,
|
||||||
|
"timeout":3,
|
||||||
|
"message":"failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
33
wwwroot/opp_reports/delete_opp_reports.dspy
Normal file
33
wwwroot/opp_reports/delete_opp_reports.dspy
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
ns = {
|
||||||
|
'id':params_kw['id'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
db = DBPools()
|
||||||
|
dbname = get_module_dbname('pipeline_opportunity')
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
r = await sor.D('opp_reports', ns)
|
||||||
|
debug('delete success');
|
||||||
|
return {
|
||||||
|
"widgettype":"Message",
|
||||||
|
"options":{
|
||||||
|
"title":"Delete Success",
|
||||||
|
"timeout":3,
|
||||||
|
"cwidth":16,
|
||||||
|
"cheight":9,
|
||||||
|
"message":"ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('Delete failed');
|
||||||
|
return {
|
||||||
|
"widgettype":"Error",
|
||||||
|
"options":{
|
||||||
|
"title":"Delete Error",
|
||||||
|
"timeout":3,
|
||||||
|
"cwidth":16,
|
||||||
|
"cheight":9,
|
||||||
|
"message":"failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
125
wwwroot/opp_reports/get_opp_reports.dspy
Normal file
125
wwwroot/opp_reports/get_opp_reports.dspy
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
|
||||||
|
ns = params_kw.copy()
|
||||||
|
|
||||||
|
|
||||||
|
debug(f'get_opp_reports.dspy:{ns=}')
|
||||||
|
if not ns.get('page'):
|
||||||
|
ns['page'] = 1
|
||||||
|
if not ns.get('sort'):
|
||||||
|
|
||||||
|
|
||||||
|
ns['sort'] = ["created_at desc"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
sql = '''select * from opp_reports where 1=1 [[filterstr]]'''
|
||||||
|
|
||||||
|
filterjson = params_kw.get('data_filter')
|
||||||
|
if filterjson and isinstance(filterjson, str):
|
||||||
|
try:
|
||||||
|
filterjson = json.loads(filterjson)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
filterjson = None
|
||||||
|
fields_str=r'''[
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"title": "主键ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"nullable": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "project_id",
|
||||||
|
"title": "项目ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "software",
|
||||||
|
"title": "软件/主题",
|
||||||
|
"type": "str",
|
||||||
|
"length": 128,
|
||||||
|
"nullable": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "title",
|
||||||
|
"title": "报告标题",
|
||||||
|
"type": "str",
|
||||||
|
"length": 255,
|
||||||
|
"nullable": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "content",
|
||||||
|
"title": "报告正文",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"title": "报告状态",
|
||||||
|
"type": "str",
|
||||||
|
"length": 24,
|
||||||
|
"nullable": "no",
|
||||||
|
"default": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "confirm_task_id",
|
||||||
|
"title": "确认任务ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "confirmed_by",
|
||||||
|
"title": "确认人",
|
||||||
|
"type": "str",
|
||||||
|
"length": 64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_by",
|
||||||
|
"title": "创建人",
|
||||||
|
"type": "str",
|
||||||
|
"length": 64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at",
|
||||||
|
"title": "创建时间",
|
||||||
|
"type": "timestamp",
|
||||||
|
"nullable": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updated_at",
|
||||||
|
"title": "更新时间",
|
||||||
|
"type": "timestamp"
|
||||||
|
}
|
||||||
|
]'''
|
||||||
|
ori_fields = json.loads(fields_str)
|
||||||
|
if not filterjson:
|
||||||
|
fields = [ f['name'] for f in ori_fields ]
|
||||||
|
filterjson = default_filterjson(fields, ns)
|
||||||
|
|
||||||
|
filterdic = ns.copy()
|
||||||
|
filterdic['filterstr'] = ''
|
||||||
|
filterdic['userorgid'] = '${userorgid}$'
|
||||||
|
filterdic['userid'] = '${userid}$'
|
||||||
|
if filterjson:
|
||||||
|
dbf = DBFilter(filterjson)
|
||||||
|
conds = dbf.gen(ns)
|
||||||
|
if conds:
|
||||||
|
ns.update(dbf.consts)
|
||||||
|
conds = f' and {conds}'
|
||||||
|
filterdic['filterstr'] = conds
|
||||||
|
ac = ArgsConvert('[[', ']]')
|
||||||
|
vars = ac.findAllVariables(sql)
|
||||||
|
NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' }
|
||||||
|
filterdic.update(NameSpace)
|
||||||
|
sql = ac.convert(sql, filterdic)
|
||||||
|
|
||||||
|
debug(f'{sql=}')
|
||||||
|
db = DBPools()
|
||||||
|
dbname = get_module_dbname('pipeline_opportunity')
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
r = await sor.sqlPaging(sql, ns)
|
||||||
|
return r
|
||||||
|
return {
|
||||||
|
"total":0,
|
||||||
|
"rows":[]
|
||||||
|
}
|
||||||
208
wwwroot/opp_reports/index.ui
Normal file
208
wwwroot/opp_reports/index.ui
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
|
||||||
|
{
|
||||||
|
"widgettype":"VBox",
|
||||||
|
"options":{"cheight":40,"width":"100%"},
|
||||||
|
"subwidgets":[{
|
||||||
|
"id":"opp_reports_tbl",
|
||||||
|
"widgettype":"Tabular",
|
||||||
|
"options":{
|
||||||
|
"width":"100%",
|
||||||
|
"height":"100%",
|
||||||
|
|
||||||
|
|
||||||
|
"title":"研发报告",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"css":"card",
|
||||||
|
|
||||||
|
|
||||||
|
"editable":{
|
||||||
|
|
||||||
|
"new_data_url":"{{entire_url('add_opp_reports.dspy')}}",
|
||||||
|
|
||||||
|
|
||||||
|
"delete_data_url":"{{entire_url('delete_opp_reports.dspy')}}",
|
||||||
|
|
||||||
|
|
||||||
|
"update_data_url":"{{entire_url('update_opp_reports.dspy')}}"
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
"data_url":"{{entire_url('./get_opp_reports.dspy')}}",
|
||||||
|
|
||||||
|
"data_method":"GET",
|
||||||
|
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
|
||||||
|
"row_options":{
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"browserfields": {
|
||||||
|
"exclouded": [
|
||||||
|
"content",
|
||||||
|
"confirm_task_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
"editexclouded":[
|
||||||
|
"id",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"confirm_task_id",
|
||||||
|
"confirmed_by"
|
||||||
|
],
|
||||||
|
|
||||||
|
"fields":[
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"title": "主键ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"nullable": "no",
|
||||||
|
"cwidth": 18,
|
||||||
|
"uitype": "str",
|
||||||
|
"datatype": "str",
|
||||||
|
"label": "主键ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "project_id",
|
||||||
|
"title": "项目ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"cwidth": 18,
|
||||||
|
"uitype": "str",
|
||||||
|
"datatype": "str",
|
||||||
|
"label": "项目ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "software",
|
||||||
|
"title": "软件/主题",
|
||||||
|
"type": "str",
|
||||||
|
"length": 128,
|
||||||
|
"nullable": "no",
|
||||||
|
"cwidth": 18,
|
||||||
|
"uitype": "str",
|
||||||
|
"datatype": "str",
|
||||||
|
"label": "软件/主题"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "title",
|
||||||
|
"title": "报告标题",
|
||||||
|
"type": "str",
|
||||||
|
"length": 255,
|
||||||
|
"nullable": "no",
|
||||||
|
"cwidth": 18,
|
||||||
|
"uitype": "str",
|
||||||
|
"datatype": "str",
|
||||||
|
"label": "报告标题"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "content",
|
||||||
|
"title": "报告正文",
|
||||||
|
"type": "text",
|
||||||
|
"length": 0,
|
||||||
|
"uitype": "text",
|
||||||
|
"datatype": "text",
|
||||||
|
"label": "报告正文"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"title": "报告状态",
|
||||||
|
"type": "str",
|
||||||
|
"length": 24,
|
||||||
|
"nullable": "no",
|
||||||
|
"default": "draft",
|
||||||
|
"cwidth": 18,
|
||||||
|
"uitype": "str",
|
||||||
|
"datatype": "str",
|
||||||
|
"label": "报告状态"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "confirm_task_id",
|
||||||
|
"title": "确认任务ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"cwidth": 18,
|
||||||
|
"uitype": "str",
|
||||||
|
"datatype": "str",
|
||||||
|
"label": "确认任务ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "confirmed_by",
|
||||||
|
"title": "确认人",
|
||||||
|
"type": "str",
|
||||||
|
"length": 64,
|
||||||
|
"cwidth": 18,
|
||||||
|
"uitype": "str",
|
||||||
|
"datatype": "str",
|
||||||
|
"label": "确认人"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_by",
|
||||||
|
"title": "创建人",
|
||||||
|
"type": "str",
|
||||||
|
"length": 64,
|
||||||
|
"cwidth": 18,
|
||||||
|
"uitype": "str",
|
||||||
|
"datatype": "str",
|
||||||
|
"label": "创建人"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at",
|
||||||
|
"title": "创建时间",
|
||||||
|
"type": "timestamp",
|
||||||
|
"nullable": "no",
|
||||||
|
"length": 0,
|
||||||
|
"uitype": "str",
|
||||||
|
"datatype": "timestamp",
|
||||||
|
"label": "创建时间"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updated_at",
|
||||||
|
"title": "更新时间",
|
||||||
|
"type": "timestamp",
|
||||||
|
"length": 0,
|
||||||
|
"uitype": "str",
|
||||||
|
"datatype": "timestamp",
|
||||||
|
"label": "更新时间"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"data_filter":{
|
||||||
|
"AND": [
|
||||||
|
{
|
||||||
|
"field": "title",
|
||||||
|
"op": "LIKE",
|
||||||
|
"var": "title_input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "software",
|
||||||
|
"op": "LIKE",
|
||||||
|
"var": "software_input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "status",
|
||||||
|
"op": "=",
|
||||||
|
"var": "status_input"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"page_rows":160,
|
||||||
|
"cache_limit":5
|
||||||
|
}
|
||||||
|
|
||||||
|
,"binds":[]
|
||||||
|
|
||||||
|
}]
|
||||||
|
}
|
||||||
36
wwwroot/opp_reports/update_opp_reports.dspy
Normal file
36
wwwroot/opp_reports/update_opp_reports.dspy
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
ns = params_kw.copy()
|
||||||
|
for k,v in ns.items():
|
||||||
|
if v == 'NaN' or v == 'null':
|
||||||
|
ns[k] = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
db = DBPools()
|
||||||
|
dbname = get_module_dbname('pipeline_opportunity')
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
|
r = await sor.U('opp_reports', ns)
|
||||||
|
debug('update success');
|
||||||
|
return {
|
||||||
|
"widgettype":"Message",
|
||||||
|
"options":{
|
||||||
|
"title":"Update Success",
|
||||||
|
"cwidth":16,
|
||||||
|
"cheight":9,
|
||||||
|
"timeout":3,
|
||||||
|
"message":"ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"widgettype":"Error",
|
||||||
|
"options":{
|
||||||
|
"title":"Update Error",
|
||||||
|
"cwidth":16,
|
||||||
|
"cheight":9,
|
||||||
|
"timeout":3,
|
||||||
|
"message":"failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user