## 问题 `svc_handover_ticket` 的 DDL 与状态机在 `docs/02` §7.2 早就定好了 (pending → assigned → processing → resolved → closed,未解决可 cancelled), 但平台**只有 handover:read(只读队列)**:没有任何入口能改状态、assigned_to / accepted_at / resolved_at / closed_at / resolution 五列**全库 0 非空**, 于是 40 张工单永远停在 pending —— 用户看到的就是"工单全都长一样"。 ## 改了什么 后端: - 新增 `app/service/customer_service_handover_action_service.py`:五个动作 (分配/接单/解决/关闭/取消),`SELECT ... FOR UPDATE` 锁单后判状态; 接单允许从 pending 自助接管(同时记受理人);取消不写 closed_at(该列属 closed 状态); 每次流转写一条 interaction_audit(handover.assigned/accepted/resolved/closed/cancelled); 非法流转 409、坐席不存在 422、工单不存在 404;回包不含 customer_id/session_id。 - 只读服务保持只读(读侧与写侧是两条边界,单测守着"读侧不许长出写方法"), 但列表支持 `?status=` 六态筛选、详情补上受理人与流转时间(坐席侧路由信息,非客户数据)。 - `app/api/controllers/admin.py`:五个 action 端点 A049–A053 (assignments / acceptances / resolutions / closures / cancellations), 走 `ApiTransactionService.execute_in` —— 幂等记录与业务写入同事务、重复键回放。 - 权限:新增 `handover:write`(9069,只授 admin),已并进种子 `tools/seed_test_rbac.py`;配套幂等脚本 `tools/grant_handover_write_permission.py`。 前端(管理员工作台 · 转人工工单页): - 按状态给按钮(待处理→分配/直接接单、已分配→接单、处理中→解决、已解决→关闭、 未解决都可取消),加了状态筛选与"刷新";摘要弹窗补上受理人与四个时间点、处置结论。 - api-client 注册五个端点;workspace.js 的 api-client 引用与页面自身的 ?v= 一并升版, 避免浏览器拿旧缓存(旧缓存里没有这些端点)。 冒烟与测试: - `tools/e2e_smoke_test.py`:B 段建的测试工单由 F 段走完 分配→接单→解决→关闭 收尾 —— 既不再把测试件堆在 pending 队列里(此前每次冒烟攒一张),又让每次冒烟都覆盖一遍状态机。 总数 40 → 44 项,实测 44/44 全绿。 - 新增单测 24 条(状态机合法/非法路径、越权、坐席不存在、审计、视图不泄漏客户标识) 与一条真机集成用例(HTTP 十步 + 数据库侧审计证据 + 自动清理)。 - 读侧那条"详情不得返回 assigned_to"的旧断言按新口径更新,并写清为什么。 ## 验证 - `pytest tests/unit tests/contract` → 1489 passed, 2 skipped, 0 failed - 新增集成用例通过;`tests/integration` 全量跑时 `test_memory_extraction` / `test_run_cancellation_mysql` 两条偶发红 —— 单独跑都通过, 是 AGENTS.md 已登记的"常驻 Worker 抢队列"(跑验收前须先停 Worker) - `tools/portal_api_check.py` → 41 项通过 39、失败 0 - `tools/e2e_smoke_test.py` → 44/44 全通过 - `python tools/check_rbac_seed_consistency.py` → 通过(种子 63 条权限) - 真机 HTTP 实测:分配→接单→解决→关闭四步 200 且时间戳齐全;取消路径 200 且 closed_at 为空; 同键重发回放不二次推进;对已关闭工单再分配 409;风控账号处置 403 ## 文档 `docs/44-演示流程.md`(场景 4/8 + 命令 + 44 项)、`docs/演示用/后端接口文档`(新增 §11.4b 与 A049–A053)、`docs/演示用/全功能流程-大白话版.md`(工单页签改"读写"+ 已知偏差)、 `AGENTS.md`(9066-9069 号段演进 + 冒烟 44 项)
50 KiB
全平台功能流程 · 大白话版(2026-09-14)
读者:第一次接触这个平台的人(演示听众 / 接手的人 / 业务方)。 写法:不用"链路""编排""收敛""双写"这类词,讲的是"点一下之后到底发生了什么"。 每一句说法都能在代码或数据库里查到,关键处标了文件路径,方便你反查。 配套:想照着演一遍看
docs/44-演示流程.md;只想看「今日盈亏」看今日盈亏实现说明-2026-09-14.md。
0. 三十秒版:这平台到底是什么
- 它是一个基金业务的"通用 Agent 平台":把"会话、登录鉴权、记忆、模型调用、工具调用、 合规校验、审计留痕、配置发布"这些每个业务都要用的东西做成底座, 上面挂着 7 个业务 Agent 和一条不做 AI 的模拟交易服务。
- 不做真实交易:场内基金是模拟盘(钱是假钱),但行情是真行情(取自公开数据源)。 场外基金那条线只做"运营流程"(收邮件、认单据、发通知),同样不下真实单。
- 对外一共六个门(门户),每个角色一个:
| 门户 | 地址 | 谁能进 | 一句话作用 |
|---|---|---|---|
| 访客 | /portal/guest/home/ |
任何人(不用登录) | 看产品、问客服 |
| 客户 | /portal/customer/login/ |
客户 | 看资产、下单、看流水、做风险测评 |
| 风控 | /portal/employee-risk/dashboard/ |
风控专员 | 处置预警、出日报 |
| 投顾 | /portal/employee-advisor/dashboard/ |
投顾 | 给自己服务的客户出方案 |
| 运营 | /portal/employee-operations/offsite/ |
运营 | 场外收件、推广材料、问数(NL2SQL) |
| 管理员 | /portal/employee-console/workspace/ |
管理员 | 权限、配置发布、审计、工单 |
员工侧只有一个登录页:
/portal/employee-console/login/。 登录后系统按你的角色把你送到上面四个工作台里的某一个(见 §3)。 客户侧有自己的登录页/portal/customer/login/,员工账号在客户登录页会被拒绝,反之亦然。
1. 跑起来一共三个"人":浏览器、API、Worker
看懂这三者的分工,后面所有的"为什么没反应"都能自己判断。
| 角色 | 是什么 | 少了它会怎样 |
|---|---|---|
| 浏览器 | 六个门户的页面(原生 JS,没有前端框架)。它只做两件事:调接口、把结果画出来 | 没有界面(但接口还能用 curl 调) |
| API | python -m uvicorn app.main:app --port 8000。所有接口 + 所有页面都从它出 |
什么都没有 |
| Worker | python -m app.worker。后台慢慢干的活都归它 |
客服对话永远"繁忙/超时";新知识搜不到;记忆不更新;而且它不跑通常一声不响 |
Worker 具体干哪些活(都在 app/worker/runtime.py 的 run_once() 和
app/worker/__main__.py 里,一轮一轮轮询):
- Agent 运行:把"排队中"的 Agent 请求领出来真正执行(这一条最关键,见下);
- 领域事件投递(
domain_event_outbox):事件先落库、再由它一条条消费,失败就重试、重试到头进死信; - 知识向量同步:把知识库内容写进 Milvus(不写进去,客服就检索不到);
- 记忆抽取与画像重建:从对话里提炼长期记忆 → 重建画像 → 投到 Neo4j(关系)与 Milvus(向量);
- 会话片段聚合(把若干轮对话合成一个"事件片段",供长期记忆用);
- 清理派生数据:记忆失效/销户时删掉图库与向量库里的副本,删不掉就如实记"跳过",不假装删了;
- 场外收件:
OffsiteMailWorker在同一个进程、同一个入口里跑(邮箱收信、认单据)。
⚠️ "客服一直转圈/超时" 十有八九是 Worker 没在跑。 因为 Agent 请求是三段式:API 只负责受理并返回
202("收到了")→ 请求进队列 (agent_run.status='queued')→ Worker 领单才真正执行,结果再落库。 没有 Worker,队列里那行就一直停着,而页面只会显示"客服繁忙/响应超时"—— 看起来像慢,其实是没人处理。一句话排查:查agent_run最新那行是不是queued。
数据库和中间件(都在本机):
| 组件 | 端口 | 干什么 |
|---|---|---|
| MySQL | 3306 | 唯一权威数据源,90 张表 |
| Redis | 6379 | 配置缓存、客服会话短期上下文 |
| Milvus | 19530 | 向量检索(知识库片段、长期记忆向量) |
| Neo4j | 7687 | 客户画像的关系(标签、产品、多跳),是派生视图,一律以 MySQL 为准 |
一键启动:双击桌面的 启动金融Agent平台.bat(或 powershell -File start.ps1)——
它按顺序做六件事:找解释器 → 检查 MySQL/Redis/Milvus → 刷行情 → 起 API 与 Worker 两个窗口
→ 等 API 真正应答 → 自动打开浏览器。重复双击是安全的,不会起第二份。
⚠️ 那个 bat 必须同时满足"GBK 编码 + CRLF 换行 + 无 BOM",所以别手写,
改完 start.ps1 后跑 python tools/make_launcher_bat.py 重新生成。
2. 第一眼:访客门户(不用登录就能看)
打开 http://127.0.0.1:8000/portal/,它会跳到 /portal/guest/home/。三个页面:
| 页面 | 看到什么 | 数据从哪来 |
|---|---|---|
| 首页 | 平台的宣传页 + 推荐位(前 3 只基金) | 推荐位来自 GET /api/v1/products,其余静态 |
| 产品列表 | 场内基金清单、最新价、"最近涨跌";还能切"基金排行"视图 | GET /api/v1/products(真实接口,不是写死的假数据) |
| 产品详情 | 产品要素 + 净值走势图 | 净值来自 fin_nav_history(当前库里 26 只产品、3253 个净值日) |
"最近涨跌"有个刻意的设计:这个数只显示行情源直接给的涨跌幅。 源没给、或这只产品走的是净值降级路径时,页面显示"暂无"而不是 0 —— 因为 0 会被读成"今天平盘",那是编数据。
右下角客服浮窗(本平台最值得看的 AI 能力,因为它"会拒答"):
- 你发一句话 → 平台先判断意图(是问费率?问规则?还是问账户?);
- 命知识类意图 → 去已发布的知识里做向量检索;
- 有把握才答:命中分 ≥ 0.75 直接答;0.55~0.75 之间还要求"领先第二名 ≥ 0.07";否则不答;
- 答的时候带引用来源,你可以点开看出自哪份材料;
- 答不了 → 引导你转人工(点"转人工客服"才建工单); 问"我的账户里有多少钱"这类要登录才能查的问题 → 它会明确说"需要登录后查询",不编数字。
它是"异步 + 轮询",不是"一问一答的同步调用":前端把问题交上去,接口立刻返回 202(收到了),
然后每 0.5 秒问一次"好了吗",最多问 40 次(约 20 秒)。所以Worker 没在跑时,
你看到的就是那句"客服繁忙,暂时没能给出答复"——不是模型慢,是没人处理。
(平台也提供了更省事的 SSE 推送端点,但浮窗目前用的是轮询。)
访客没有账号也能用,是因为平台给了它一张范围极小的临时通行证 (
POST /api/v1/visitor-tokens,只带knowledge:query等最小权限)。 客户/员工登录后走的是另一套令牌,权限大得多。两个"知道就好"的前端细节:产品详情页如果
?code=写错了,页面不会报错, 而是静默展示第一只基金;「资金流水」页因为导航配置里active是空串, 顶部导航不会高亮它(功能正常,只是不好看)。
3. 登录:我该去哪个门、进去之后能看到什么
3.1 一次登录到底做了什么
- 你在登录页填用户名 + 密码,前端调
POST /api/v1/auth/tokens(端点代号 A034); - 接口返回你的角色列表(roles);
- 前端做一次门禁检查:客户登录页只接受
customer;员工登录页只接受risk_operator / operator / advisor / admin / super_admin。角色的门走错了会被拦下, 并明确提示"该账号不是客户账号,请使用后台登录入口"(app/static/portal/common/login-controller.js); - 登录成功 → 令牌写进 cookie(
auth_token)+ sessionStorage 缓存, 跨标签页以 cookie 为准; - 按角色送到不同的工作台(下表);客户如果还没做风险测评,会被强制先跳到问卷页
(
GET /api/v1/onboarding/risk-questionnaire返回required=true时); - 令牌失效/换账号 → 自动跳回登录页并带
?reason=session-expired之类的提示。
3.2 角色 → 落地页(app/static/portal/common/auth.js 的 staffHomeForRoles)
| 角色 | 登录后落到 |
|---|---|
customer(客户) |
/portal/customer/dashboard/(未测评则先到 /portal/customer/risk-questionnaire/) |
risk_operator(风控专员) |
/portal/employee-risk/dashboard/ |
advisor(投顾) |
/portal/employee-advisor/dashboard/ |
operator(运营) |
/portal/employee-operations/offsite/ |
admin / super_admin(管理员) |
/portal/employee-console/workspace/ |
3.3 为什么"令牌里不带权限"
令牌里只有"你是谁",没有"你能干什么"。 每次请求到达服务端时, 平台重新查一遍你的角色、权限码和数据范围。这么做换来两件事:
- 管理员改了权限,下一次请求立刻生效,不用等你重新登录;
- 令牌被别人拿到,也不能靠它越权——服务端照样按当前权限判。
3.4 越权有三道防线,但只有后两道是真的在拦
| 防线 | 在哪 | 实际强度 |
|---|---|---|
| 第一道:前端隐藏/禁用入口 | 各门户页面(员工工作台会调 A038 拿服务端角色与权限后再刷按钮) |
体验层,不是安全层。而且客户门户这一道目前是装饰性的:登录响应里不含权限清单,前端对客户角色直接按"全量权限"渲染,所以 data-permission-required 恒为"已授权"。别把它当防线。 |
| 第二道:权限码 | 服务端每个接口都要一个权限码,比如下单要 trade:order:create |
真拦。没有就 403(AGENT_PERMISSION_DENIED 一类) |
| 第三道:数据范围 | data_scope + 客户归属关系(sys_customer_assignment) |
真拦。有"看客户"的权限,也只能看分配给自己的客户 |
换句话说:能不能点,看前端;能不能成,看后端。 判断"某人到底能不能做某件事", 一律以服务端为准;前端只是提前把不该看到的藏起来。
第三道是最容易被忽略、也最关键的一道:员工号和客户号是同号段的 (演示数据里客户 9001-9020、员工 9002/9020 并存)。 如果只按"号"去查,风控/投顾就会读到陌生客户的长期记忆,而且不会报错。 所以平台把"能读哪些客户"收敛到一个文件(
app/core/memory_scope.py): 客户只读自己;员工只读分配给自己的客户,且单次最多 10 个;归属没维护就失败关闭。
4. 客户门户:八个页面,一页一句
| 页面 | 地址 | 干什么 | 主要接口 |
|---|---|---|---|
| 登录 | /portal/customer/login/ |
登录 | A034 |
| 资产总览 | /portal/customer/dashboard/ |
总资产、持仓市值、累计盈亏、今日盈亏 | T001 |
| 我的持仓 | /portal/customer/holdings/ |
每只基金的份额、成本、最新价、市值、持有盈亏、今日盈亏 | T006 |
| 委托 | /portal/customer/orders/ |
我下过的委托及状态(页面上没有撤单按钮:市价单下单即成交,能撤的极少) | T003 / T004(撤单接口 T005 已就绪但页面未接) |
| 成交 | /portal/customer/transactions/ |
已经成交的每一笔(价格、数量、费用) | T007 / T008 |
| 资金流水 | /portal/customer/cash-ledger/ |
每一笔钱的进出与余额变化 | T009 |
| 盈亏分析 | /portal/customer/profit-loss/ |
累计盈亏 / 今日盈亏 + 按持仓拆分 | T001(复用看板数据) |
| 风险测评 | /portal/customer/risk-questionnaire/ |
填问卷,决定你的风险承受等级(C1–C5) | ONB001 / ONB002 |
4.1 下单一笔,中间发生了六件事(这是全平台最该讲清楚的一条)
- 你点了买入 → 前端调
POST /api/v1/users/me/orders(T002), 并且必须带一个幂等键(Idempotency-Key,16–128 个可打印 ASCII 字符)。 它的作用是:同一次请求被重发(超时重试、网络抖动)时只成交一笔。 ⚠️ 注意边界:它防的是"同一次请求的重发",不防"你又点了一次"—— 前端目前每提交一次就生成一个新键,所以手动连点两下确实会产生两笔委托。 要防这个得让前端为"一次下单意图"复用同一个键,属于已知的前端缺口。 - 服务端先看行情:取这只基金的最新行情快照,要求它在 15 分钟内。
超过 15 分钟直接拒绝(
503 行情已过期)——这是演示最容易翻的一环, 因为系统没有任何自动刷新,补刷命令是python tools/sync_market_prices.py(立即生效,不用重启)。 - 适当性校验:你的风险等级(C1–C5)和这只基金的风险等级(R1–R5)必须匹配, 不匹配直接拒单。这就是"风险测评不是形式"的落地处。
- 算钱:按
fin_fee_rule里优先级最高且生效的费率规则算手续费, 成交时把费率固化进这笔成交记录(以后改费率不影响历史)。 - 落账(同一个数据库事务里同时写四处):委托、成交、资金流水、持仓。 任何一处失败,整笔回滚。所以这三页(资产/持仓/流水)看到的数字永远对得上。
- 成交价来自外部行情,不是随机数;订单状态直接是"已成交"—— 因为首版是市价委托立即全额成交,没有撮合排队。
顺带解释一个常被当 bug 的现象:撤单几乎总是失败。 因为市价单下单即成交,只有停在"待风控"的委托才能撤,所以页面点撤单大概率提示"该委托已成交"。
4.2 「今日盈亏」是怎么算的(2026-09-14 起是真实数据)
一句话:今天收盘时值多少钱 − 昨天收盘时值多少钱 − 今天买入花掉的钱 + 今天卖出收回的钱 (不含手续费,和旁边的"持有盈亏"同口径)。
两个值得在演示时说清的细节:
- 当天买的份额只算"买入价 → 今收",不享受昨天到今天那段涨幅(昨天它还不是你的);
- 行情只有一天的产品会回退用净值算,所以不会出现"某只永远是 0"。
完整口径、三个数据坑、逐只复算命令见 今日盈亏实现说明-2026-09-14.md。
5. 风控专员工作台(预警的"从哪来 → 怎么处置 → 怎么闭环")
进门:从员工登录页登录后自动落到 /portal/employee-risk/dashboard/;
页面加载时先调 A038 把服务端真正解析出来的角色、权限、数据范围拿回来,
再据此决定哪些按钮可点(这就是 §3.4 说的"第一道只是体验层")。
5.1 上半屏:概览四张卡
| 卡片 | 含义 |
|---|---|
| 未闭环预警 | 还没走完处置流程的预警总数 |
| 高风险 | 其中等级为"高风险"的数量 |
| 待处理 | 状态还是"待处理"的数量 |
| 已超时 | 超过处置时限还没动的(大于 0 会标红) |
5.2 预警是从哪来的
两条路:页面上点"手动扫描"(RK006,同步执行、最长 60 秒),
或定时调度(app/worker/risk_scan_scheduler.py,是个独立的可选进程)。
扫描会按规则(RW-xxx)核对交易、资金、持仓、登录等数据,命中就落一条预警。
演示数据里那些"异常交易"记录是刻意的:客户 12001–12005 名下有 6 行
RISKDEMO-*记录(大额申购/赎回),就是为了造出"大额赎回""频繁申购"这类预警场景 才写进去的演示触发材料,有意保留,不是脏数据、也不是漏删。 它们不代表真实场内成交(不改持仓表,份额也和持仓对不上)。 副作用只有一个:算「今日盈亏」时必须只认本平台的场内成交(买入/卖出), 否则这些虚拟份额会把"昨日持仓"算到几十万份 —— 详见今日盈亏实现说明-2026-09-14.md。
5.3 预警队列与筛选
游标分页(不是页码),每页固定 10 条。筛选项:关键词、客户编号、产品代码、
风险等级(高/中/低)、规则编号(必须形如 RW-001)。
后端还支持"产品名 + 时间范围"两个筛选,页面暂未提供输入框(属可用性缺口,不是故障)。
5.4 预警详情:为什么说它"有据可查"
一条预警点开,能看到四层东西:
- 基本信息:预警编号、风险等级、客户、产品、处置状态、确认状态、命中规则、是否已升级;
- 证据摘要:触发这条预警的那几条关键事实;
- 七个关联业务区段:把这个客户在这条线索上的交易、资金、持仓等上下文摆在一起;
- 证据快照 / 证据归档:当时的字段原样留存,事后可复查。
另外"证据查询"标签页提供 8 类只读实体的直查:客户、产品、交易、资金流水、 持仓、登录记录、预警、通知。全部是只读的,改不了任何数据。
5.5 处置是"五步状态机",不是改个字段
| 动作 | 效果 |
|---|---|
| 确认接收 | 只记录"谁在什么时候看到了",状态不变 |
| 进入调查 | 状态:待处理 → 调查中 |
| 关闭误报 | 状态 → 已排除,必须填理由 |
| 完成结案 | 只有"调查中"的才能结案;必须填结论,并且按风险等级扣客户行为分:低 3 / 中 5 / 高 20(行为分 0–20 封顶) |
| 升级处理 | 只打"已升级"标记,不改状态,可以和上面几步并行 |
每一步都写审计(谁、何时、做了什么)。所以"这条预警谁处理的、依据是什么"事后能查清。
5.6 日报与助手
- 日报:点生成后逐段出现(SSE 推流),一共 9 段(当日数量、等级分布、重点事件、 未闭环、误报情况、类型分布、处置结果、规则效果、优化建议)。 末尾会标来源:模型可用时是"大模型生成",不可用时自动降级为规则模板并如实标注—— 降级不是报错。生成完可以填多个邮箱发送(逗号/分号/空格分隔)。
- 风控助手:预设问法 6 个(通用的 3 个 + 绑定当前预警的 3 个)。
它只会调三个只读工具:
get_risk_overview、search_risk_alerts、get_alert_evidence, 输出的是"研判与话术草案",并明确声明不构成任何处置结论,处置一律由人工做。 - 通知记录:可以看到每条通知的渠道、发送状态、失败原因。顶部铃铛只统计"站内提醒"。 ⚠️ 邮件通道依赖 SMTP 配置,授权码失效时会显示"发送失败"(详见 §12)。
6. 投顾工作台(给自己服务的客户出方案)
进门:员工登录页登录 → 落到 /portal/employee-advisor/dashboard/。
投顾能操作的客户范围由服务端按归属关系限定(sys_customer_assignment:本人 + 分配给我的客户),
拿不到全量客户——这是数据范围,不是页面缺陷。(页面上那份"我的客户"名单目前是内置的演示数据,
真正的范围以服务端为准,见 §6.1。)
6.1 页面上有什么
| 区域 | 作用 |
|---|---|
| 概览四张卡 | 已发布交付物数量、名下客户数、风评熔断数、会话来源 |
| 客户区 | 客户列表 + 参数 + 7 个动作按钮 + 一句自然语言输入框。⚠️ 这份客户名单目前是页面内置的演示数据(张总 9101 / 李阿姨 9102 / 王工 9103 / 陈医生 9104);真正的客户范围在服务端按归属关系(sys_customer_assignment)判定,并且有一个灰度开关:没开放的账号直接 403「当前投顾功能尚未对该账号开放」 |
| 操作区 | 7 个动作:组合分析 / 资产配置 / 生成推荐方案 / 录入客户目标 / 目标确认与方案书 / 画像评分 / 调仓建议 |
| 结果区 | 动作结果 + 推荐流水线六步进度(① 鉴权 ② 画像 ③ 筛选 ④ 方案 ⑤ 合规 ⑥ 审核留痕) |
| 已发布交付物 | 已发布给客户的方案书/推荐方案(只列"我自己 + 归属我的客户"的,且已发布、倒序 20 条) |
| 助手对话框 | 用自然语言下指令。⚠️ 它走的是前端正则意图路由(识别"组合分析/配置/推荐"等词后复用同一套按钮动作),刻意没有走 Agent 异步链路——因为异步 run 需要常驻 Worker 与发布配置,投顾工作台要保证离线也能演示 |
6.2 三种动作,真实的只有三种(这点要讲清楚)
| 动作 | 走哪里 |
|---|---|
| 组合分析、资产配置、生成推荐方案 | 走真实后端(/api/v1/advisor/portfolio-analysis、/asset-allocation、/recommendations),并且会带上所选客户的 customer_id |
| 画像评分、调仓建议 | 页面本地演示引擎算(后端暂无对应接口),数据取自页面内置的演示产品/客户表 |
| 录入客户目标、目标确认与方案书 | 走真实后端(投资目标与方案书接口) |
这是刻意标注的边界:页面内置了一张演示产品与客户表,只服务"离线演示"和那两个后端没有的动作。 演示时如果被问"这个评分是怎么来的",照实说"这一步是前端演示引擎,后端暂未提供该接口"即可。
6.3 生成推荐方案:为什么可能"一个产品都没有"
这是最容易被误判成故障的一步。系统不是"随机挑几只",而是逐道闸门筛,全过了才进候选:
- 可交易:产品状态是"上市"、在沪深交易所、当前在开放窗口内;
- 适当性证据:该产品在治理表里有
verified(已核验)的适当性记录,机构、来源链接、文档摘要都要齐; - 合同证据:同样要有已核验的合同快照;
- 不在治理待办里(有未决事项的产品先排除);
- 规模(如果设了门槛);
- 流动性:日均成交额要达到门槛(日频 1000 万、7 日内 100 万、30 日内 10 万);
- 硬性适当性过滤:按客户风险等级(C1–C5)挡掉不匹配的产品;
- 剩下的按规则排序取前列。
所以"候选池为空"时,页面会把原因讲清楚(证据缺失/流动性不够/适当性不匹配/没有客户目标), 并给出两条路(补证据材料,或走管理员审核的既有交付物)——不是一句"没有通过校验的产品"。 实测:客户 9101(C4)出的方案里有 3 只 R4 产品,9102(C1)只有 1 只 R1。
6.4 审核与发布:哪一步投顾自己能做
| 交付物 | 投顾能做什么 | 谁来做审核/发布 |
|---|---|---|
| 产品推荐方案 | 生成、自助审核 / 驳回 / 发布给客户 | 投顾自己(2026-09-14 起开放),管理员复核队列同样能做 |
| 投资目标方案书 | 录入目标 → 确认目标 → 查看方案书 | 仍归管理员(接口上是 admin=True 的复核环节,故意的) |
方案书与推荐方案都有状态:目标 待确认 → 已确认;方案书 待审核 → 审核通过 → 已发布(或已退回)。
6.5 合规熔断(FM-03)
客户的风险测评超过 12 个月即视为失效:页面会冻结购买类动作 (生成推荐方案、资产配置、画像评分、调仓建议),只留赎回方向,并明确显示熔断原因。 ⚠️ 这个拦截目前发生在前端闸门——后端只判断"测评有没有有效期字段", 不比较是否已过期。所以不要绕过页面直接调接口来"演示"这件事,那里拦不住。
6.6 一个专门的"离线演示模式":网址后面加 ?demo=1
投顾工作台有一条唯一能绕过登录守卫的路径:/portal/employee-advisor/dashboard/?demo=1。
进去之后:
- 不持有令牌(所有真实接口请求都不发),页面顶部明确标注「演示模式 · 本地引擎」;
- 数据范围文案改成"本地演示引擎",指标卡的数据源也标成"本地引擎";
- 生成的方案带「规则模拟,不构成真实推荐」字样;
- 没有退出按钮(本来就是离线)。
它是给"没有后端 / 没有 Worker 也要讲一遍投顾流程"准备的,演示时务必主动说明这是演示模式, 别让人以为真实链路就是这么跑的。
7. 运营门户(三条线:场外收件 / 推广材料 / 问数)
进门:员工登录页登录后落到 /portal/employee-operations/offsite/。
顶部导航四个入口:概览、场外基金、推广材料、NL2SQL。
7.1 线一 · 场外基金受理:把"收到的邮件"变成"核对过的单据 + 发出的通知"
这是一条确定性流程(不靠模型自由发挥),完整走一遍是这样:
| 步 | 操作 | 背后发生了什么 |
|---|---|---|
| 1 | 看收件状态 | ⚠️ 页面上没有"上传邮件/投递"入口:邮件是 Worker 通过 IMAP 从真实邮箱收进来的(Worker 不在跑 = 永远不收信),而且发件人必须在白名单里,否则回 403「发件人不在场外业务白名单」。状态显示"运行中/未启用";游标卡住时可以点"恢复收件游标"(⚠️ 它只把状态从"阻塞"重置为"空闲",不会跳过那封失败的邮件) |
| 2 | 打开一封邮件 | 左侧列表 → 右边显示正文、附件原件(可预览/下载)、OCR 识别字段、关联单据 |
| 3 | 人工修正 OCR 字段并保存 | 识别不可能 100% 准,所以运营可以逐字段改。保存后系统会自动重新核对一遍(触发 NL2SQL → 拉回字段 → 重判规则),不用再手动点一次 |
| 4 | (必要时)识别重试 | 附件识别失败可以提交重试 |
| 5 | 看单据要素 | 申请日期、申购金额 / 赎回份额、机构、基金代码 |
| 6 | 看规则结果表 | 每条规则一行:规则名 / 结果(满足·不满足·无法判断)/ 单据值 / 规则判断。可点"重新核对并判定规则"(这一步只重跑计算与判定,不会重新识别、也不会重新生成自然语言) |
| 7 | 人工下结论 | "确认正常" 或 "确认异常" —— 这是人的决策,系统只给依据。⚠️ 识别或核对没走完就点确认,会回 422「识别或数据核对尚未完成,不能进行业务确认」 |
| 8 | 创建并发送通知 | 按结论选通知类型(邮件回执 / 正常回执 / 异常回执 / 风险通知 / 清算通知),正文可编辑,然后发送 |
| 9 | 收尾 | 结清统计可重算(只统计"已确认正常且已成功回执"的单据);也可以彻底删除一封邮件(附件、OCR、规则、通知一并删除,删除审计保留) |
三个"知道就好、别被问倒"的点:
- 后端对通知类型与确认结论的一致性有硬校验:风险通知 / 异常回执只允许"已确认异常", 清算通知 / 正常回执只允许"已确认正常",邮件回执必须先完成人工确认——对不上直接 422。
- ⚠️ 发送通知默认是"演练"(dry-run):本机配置里 SMTP 发送是关着的、dry-run 是开着的, 所以点"发送通知"会返回发送结果与状态,但不会真的把邮件发出去。要真发得先配 SMTP。
- ⚠️「收件状态」显示的是配置,不是 Worker 心跳:Worker 假死时它仍然显示"运行中"。 判断 Worker 死活要看进程,别只看这一格。
⚠️ 这一步是演示最容易尴尬的地方:邮件列表可能是 0 条,因为收件依赖真实可用的邮箱配置; 发通知还额外依赖 SMTP 授权码。没有真实邮件源时,这页就是空的——是"已知空数据",不是坏了。
7.2 线二 · 推广材料:把产品事实编排成"可审核、可交付"的材料
| 步 | 操作 | 要点 |
|---|---|---|
| 1 | 创建任务 | 任务编号、产品名称、产品代码、材料标题、视觉风格(均衡配置 / 稳健专业 / 成长研究)。进页面不会发任何请求,任务号由后端按 PM-日期-序号 生成 |
| 2 | 填结构化资料 | 产品信息、管理人信息、团队与策略、费用/业绩/风险四组。⚠️ 页面上带 * 的必填其实不会触发浏览器校验(页面没有 <form>),靠后端兜底 |
| 3 | 上传附件 | 基金经理照片(jpg/png/webp)+ 业绩数据文件(csv/xlsx/xlsm)。上传时解析就是上传的一部分,没有单独的"解析"按钮 |
| 4 | 生成材料 | 产出 PPTX、宣传长图、PDF 和若干图表。这一步是同步的(前端单独给了 120 秒超时),不依赖 Worker;只有背景图会调外部图像服务,不可用时自动降级为程序化背景 |
| 5 | 看合规检查 | 每条命中显示规则名 + 建议,分"阻断"与"提示"两类 |
| 6 | 审核 | 审核通过 / 要求修改 / 审核驳回,都要写意见 |
| 7 | 交付 | 审核通过后填投顾编号(逗号分隔)。⚠️ 首版交付只写内部记录,还没接外部投顾端 |
合规检查是真会拦人的(以下都是"阻断"级,不是提示):
- 勾了"展示排名"就必须同时给出评价期间 ≥3 年 + 评价机构 + 公开来源;
- 勾了"展示业绩"就必须已上传业绩数据文件,且历史月份 > 6;
- 出现收益 / 回撤 / 波动率 / 夏普比率里任意一项,三项风险指标必须齐;
- 七项费用必须填全;
- 材料里不许出现"保本、稳赚、无风险、保证收益、抢购"这类词;
- 必须有法定风险声明;不许把"从业年限"和"投资管理经验"混为一谈。
两个"为什么这么设计"的点,被问到很值得讲:
- 业绩数字不是手填的:数据截止日期、历史月份、产品收益、最大回撤这几个字段是只读的, 由后端从你上传的业绩数据文件里算出来;波动率与夏普比率才需要手工填。 换句话说,编不出来——要么有数据文件,要么填不上。
- "排名"必须给出来源:要单独填"评价期间(年,需 ≥3)/ 评价机构 / 公开来源 / 排名文本"四项。 因为排名属于公开可核查表述,没有来源就不让写进去(这是合规要求,不是表单啰嗦)。
- 上传的幂等键用"文件字节数 + 修改时间"生成,不用文件名: 中文文件名会让浏览器在构造请求头时直接抛错,表现为"网络连接失败"、而服务端一条记录都没有(已修)。
7.3 线三 · NL2SQL:用中文问数,但只能读、只能查白名单表
页面顶部自己写着三条边界:只读查询 / 白名单表 / 权限审计。 它有两个模式:
| 模式 | 怎么用 |
|---|---|
| 场外单据核对 | 输入单据 task_id → 点"生成自然语言"(生成后可人工修改)→ 勾选"我已人工确认原始文件内容" → "执行只读核对" → 返回每张表核对规则的状态、返回行数、错误信息。这一段是同步的,不排队 |
| 通用自然语言查询 | 直接输入业务问题(如"查询某基金最近一个交易日的净值")→ 提交后页面每 1.2 秒轮询一次运行状态(依赖常驻 Worker,否则会一直停在"排队中")→ 展示 AI 生成的 SQL + 最终查询结果(没有图表) |
关键点:SQL 由后端的 Agent 链路生成,并且只能碰发布配置里白名单允许的表; 前端不拼 SQL,也不能绕过白名单。查询、权限、审计与最终状态都在后端。 (操作界面里连"生成 SQL"按钮都没有——这是刻意的。)
后端会拦下这些写法,报错文案就写在那里,可以直接念:
仅支持 SELECT 查询、检测到非只读数据库操作、禁止使用 SELECT *、
SQL 使用的数据表与查询计划不一致、查询字段不在白名单内、
查询涉及的表缺少合法关联路径、查询参数过多。
单次查询结果最多 50 行;表最多跨 3 个业务域。
演示时如果问"它会不会乱查":讲这条链路是"自然语言 → 意图识别 → 白名单工具 → 生成 SQL → 只读执行", 越界的表在工具层就被挡掉。这里也踩过一个真实缺陷:只识别 6 位数字代码, 而演示产品里有 5 位代码(如
15911),结果"查的是别的产品的数据、状态还显示成功"—— 已修正为 5–6 位并加了单位校验。
8. 管理员工作台(平台的"总控台")
进门:员工登录页登录 → staffHomeForRoles() 判为管理员 → 落到 /portal/employee-console/workspace/。
上半屏四张卡:有效角色数、生效配置版本数、可用模型端点、待人工复核(工单 + 画像候选 + 画像漂移之和)。
下面是八个标签页,逐个一句话说清"它管什么、能写还是只读":
| 标签页 | 能做什么 | 读写 |
|---|---|---|
| 角色与权限 | 看角色清单(角色名/代码/权限数/用户数/状态);输入一个用户 ID,看服务端实际解析出来的角色、权限、数据范围 | 只读 |
| 配置与模型 | 平台里唯一能改配置的地方:新建发布版本 → 校验 → 审核 → 激活;管平台配置项(命名空间:工具白名单 agent_tools、记忆 memory、关系 relationship、运行时 runtime、行情 fund_market)与模型路由规则 |
读写 |
| 审计记录 | 最近 20 条审计(动作、主体、入口、时间);没有"看敏感详情"的权限时,详情由服务端脱敏 | 只读 |
| 转人工工单 | 看客服转人工的工单与二次脱敏的会话摘要;并且能处置:分配 → 接单 → 解决 → 关闭(未解决可取消),可按状态筛,每一步都写审计 | 读写 |
| 画像候选 | 客户对话里提炼出的画像候选,批准后才进正式记忆(批准/驳回都要填意见) | 读写 |
| 投顾复核 | 待审的投顾交付物(推荐方案 + 投资方案书)→ 审核通过 / 驳回 / 发布给客户 | 读写 |
| 知识库 | 上传文档(faq / product / policy)→ 自动切分入库并投向量同步;客服就是靠它作答;也能让某份知识失效 | 读写 |
| 画像漂移复核 | 客户重新测评导致画像变化时,人工确认是否采纳 | 读写 |
配置发布的状态机(config_release 表里能直接看到这些取值):
草稿 draft → 待审核 pending_review → 已审核 approved → 生效 active
↘ 驳回 rejected
生效版本被新版本取代 → superseded(当前库里绝大多数历史版本都是这个状态)
为什么管理员也不能"直接改权限":平台的立场是权限与提示词的变更必须留痕、可复核、可回滚, 所以后端根本没提供"直接改"的接口,只能走上面这条状态机。这不是没做,是刻意不做。
已知的三个小缺口(后端有接口、页面没接):页面没有"驳回 / 回滚"按钮(审核只有"通过")、 模型端点页是只读的(包含激活入口)、审计页没有筛选框(只能看最近 20 条)。 演示被问到时按"前端未接、后端已有"回答即可。
9. 底座里那几件"看不见、但决定一切"的事
这九件事是跨所有业务的,任何一个业务页面背后都会用到其中几件。
9.1 鉴权与数据范围
令牌只回答"你是谁";权限与数据范围每次请求重新算。
数据范围只有三个有效取值:self(只看自己)、own_customers(只看分配给我的客户)、all(全部);
写成别的值会被静默丢弃(等于没给范围)。员工能看到哪些客户由 sys_customer_assignment 决定。
9.2 幂等:为什么"重发"不会重复扣钱
所有会产生数据的操作(下单、建会话、转人工、配置发布、场外确认……)都要求带 Idempotency-Key,
服务端把"键 + 结果"记在 api_request_receipt 里。同一个键再来一次,直接把上次的结果还给你,
不会重复执行。键的规则是 16–128 个可打印 ASCII 字符——
这条规则是真踩过坑:曾经前端把中文文件名当成键,浏览器在发请求前就抛 UnicodeEncodeError,
页面只显示"网络连接失败",服务端一条记录都没有,非常难查。
⚠️ 它的边界要说清:幂等键保护的是"同一次请求的重发",不是"同一个操作意图"。 前端目前每次提交都用新键,所以手动连点两下会真的下两笔单。 要彻底防重,需要前端把"一次下单意图"的键稳定下来(把键与表单实例绑定),这是已知缺口。
9.3 三个 Agent、七个业务 Agent
- 所有业务 Agent 都必须继承公共的
BaseAgent,并且只能由AgentFactory创建 —— 为的是不许绕过鉴权、记忆、模型路由、工具、合规、审计这条公共流程。 (规矩写在AGENTS.md,有测试守着。) - 目前 7 个:客服、风控、投顾、场外基金、推广材料、平台探针、基金查询示例。
- 工具能不能用,是两个集合的交集:代码里注册的 ∩ 当前生效配置里发布的。 缺发布配置 → 失败关闭(宁可不能用,也不放开)。这就是为什么"代码明明写了这个工具, 调用却报没有权限"——去管理员工作台看当前生效的发布版本里有没有它。
9.4 记忆:值在 MySQL、关系在 Neo4j、向量在 Milvus
一条"记住客户说过什么"的完整过程:
- 对话发生 → 若干轮之后 Worker 把这几轮聚合成一个片段;
- 从片段里抽取长期记忆(只抽明确的业务事实;客服和访客不写长期记忆);
- 记忆入库(MySQL,唯一权威)→ 触发画像重建 → 生成画像快照;
- 画像与记忆再投影出去:关系进 Neo4j、向量进 Milvus(集合
user_long_term_memory_v1); - 下次这个客户再来,就能"召回"到相关记忆,注入给模型。
为什么投影要绕一层"事件":抽取发生时那条记忆还在没提交的事务里, 另开一个连接去重建画像根本看不到它。走事件就一定是"提交之后"才消费,数据一定可见。
谁能读到谁的记忆只有一个判定口径(app/core/memory_scope.py):
客户只读自己;员工只读分配给自己的客户,单次上限 10 个;归属没维护 → 失败关闭。
不这么收敛的后果是越权陷阱:员工号和客户号同号段,按号查会读到陌生客户的记忆且不报错。
9.5 模型路由
所有模型调用都从统一的网关走,按"任务类型"选端点(意图分类 / 向量化 / 生成各不相同),
端点在数据库里配置、可切换。密钥放在 .env(不进仓库)。
演示环境当前用 deepseek-flash 做意图与生成、qwen 做向量化。
9.6 配置发布:为什么管理员也"不能直接改权限"
平台故意不提供"直接改权限/直接改提示词"的接口,所有变更必须走状态机:
草稿(draft) → 待审核(pending_review) → 生效(active) → 被新版本取代(superseded)
一次发布是整版本替换:生效版本里包含哪些工具白名单、提示词、模型路由规则,一目了然、可回滚。 受管的内容只有三张表:平台配置项、提示词版本、模型路由规则。 生效后平台会发一个事件去清缓存,让新配置立刻对所有请求生效。
9.7 事件与 Outbox:先落库,再干活
"要慢慢做的事"(投递记忆、同步向量、重建画像、发通知)都不在请求里硬等, 而是先往事件表写一行,再由 Worker 一条条消费:失败了记重试次数、退避重试, 重试到头进"死信",永远不丢、可查、可重放。
一个真实踩过的坑:事件里表示"投到哪个存储"的字段必须是小写英文 (
milvus/neo4j)。文档里曾写成大写MILVUS,消费端按小写分派, 结果事件谁都领不到、永久滞留、且不报错。
9.8 审计:每一次判断都留痕
权限判定、工具调用、预警处置、投影清理、转人工……都往 interaction_audit 写一条:
谁、什么身份、对哪个客户、在哪个门户、做了什么、结果如何。
管理面的工单接口不返回客户标识与原始对话(脱敏),这是刻意的。
9.9 降级:宁可说"没做成",也不假装成功
Milvus/Neo4j 没配或连不上时,平台的取向是显式降级 + 留痕: 事件保持"待处理"、审计里写"跳过 + 原因",绝不写一条"已同步"。 所以看到"待处理堆积"先怀疑依赖没起来,而不是怀疑业务代码。
10. 把上面全部串起来:一条主线的时间线(从登录到一笔成交)
| # | 你做的事 | 平台内部发生什么 | 落在哪 |
|---|---|---|---|
| 1 | 打开 http://127.0.0.1:8000/portal/ |
307 跳到访客首页 | — |
| 2 | 打开客户登录页,填 cust_t / 123456 |
前端调 A034 换令牌;核对"这是不是客户账号";写 cookie |
sys_user + 令牌 |
| 3 | 自动跳资产总览 | 前端带令牌调 T001;服务端重新解析身份/权限/数据范围 → 查账户、持仓、最新行情、昨日基准 → 算市值、持有盈亏、今日盈亏 |
读 fin_sim_account / fin_holding / fin_market_price / fin_nav_history / fin_transaction |
| 4 | 切到"我的持仓" | 调 T006,同一套计算(保证两页口径完全一致) |
同上 |
| 5 | 点买入 100 份 510300 | 前端生成幂等键 → 调 T002 |
— |
| 6 | — | 服务端:查幂等表(重复键直接返回上次结果)→ 校验行情是否在 15 分钟内 → 适当性校验 → 按费率规则算钱 → 一个事务里写委托、成交、资金流水、持仓 | api_request_receipt / fin_sim_order / fin_transaction / fin_cash_ledger / fin_holding |
| 7 | 刷新页面 | 持仓、成交、流水三页同时出现这笔;今日盈亏把"今天买的这部分"按成交价计入 | 同上 |
| 8 | 顺手在浮窗问客服"场内基金管理费率是多少" | 前端建会话 C001 → 发问 R001,立即返回 202(受理)→ 事件落库 → Worker 领单执行 → 检索已发布知识 → 有把握才答、附引用 → 结果落库 → 前端取回 |
conversation* / agent_run / domain_event_outbox / interaction_audit |
| 9 | 对话结束、过一会儿再问同样的问题 | Worker 聚合会话片段 → 抽取长期记忆 → 重建画像 → 投影到 Neo4j(关系)与 Milvus(向量)→ 下次召回用得到 | memory_unit / profile_snapshots / memory_sync_outbox |
| 10 | (若触发风控)风控专员在队列里处置 | 扫描 → 生成预警 → 通知;确认/调查/结案每一步写审计,结案按等级扣客户行为分 | fin_risk_alert / fin_risk_notification / interaction_audit |
11. 常见疑问(大白话问答)
Q:下单报"行情已过期"(503)怎么办?
A:行情只认 15 分钟内的快照,且不会自动刷新。跑一次
python tools/sync_market_prices.py 即可,立即生效、不用重启任何服务。
Q:客服一直"繁忙/响应超时"?
A:先看 Worker 在不在跑(python -m app.worker)。Agent 请求是"受理 → Worker 执行 → 落结果"
三段式,没有 Worker,队列里那行会一直停在 queued,页面只会显示超时。
Q:整个平台突然都很卡(客服、看板、风控同时迟钝)? A:多半是 Milvus 抖动:检索层用的是同步客户端,一次卡住会阻塞整个 API 进程。 确认 Docker Desktop / Milvus 在跑即可,恢复后自动正常,不用改代码也不用重启。
Q:为什么"今日盈亏"显示的是最近一个交易日的数? A:因为它用的行情/净值都是日终数据,语义就是"最近一个交易日相对前一交易日的盈亏", 不是盘中实时。当天数据还没同步时,它会等于"昨日"。
Q:为什么投顾看不到某个客户?
A:投顾只能看分配给自己的客户(sys_customer_assignment)。没分配就看不到,这是数据范围,不是故障。
Q:为什么运营那边的邮件是 0 条? A:场外收件需要真实可用的邮箱(收件依赖邮箱配置;发件还依赖 SMTP 授权码)。 没有真实邮箱源时,这一页就是空的——这是"已知空数据",不是坏了。
Q:为什么管理员不能直接改权限? A:金融场景要求权限变更留痕、可追溯、可回滚,所以统一走"草稿→审核→生效"的发布流程。
Q:为什么有些问题客服答不上来? A:知识是人工审核后入库的。检索不到、或把握不够(分数不够高/候选并列)时就转人工—— 宁可转人工也不硬答,这是刻意的取舍。
Q:为什么撤单总是失败? A:首版是市价委托立即全额成交,只有停在"待风控"的委托能撤,所以基本都会提示"该委托已成交"。
Q:点了"发送通知",为什么对方没收到邮件? A:本机 SMTP 默认是演练模式(发送开关关着、dry-run 开着),所以流程会走完、状态会显示, 但不会真的发出去。要真发得先配好 SMTP。另外发件还依赖邮箱的 SMTP 授权码,授权码失效会显示"发送失败"。
Q:场外的"收件状态"显示运行中,为什么就是收不到邮件? A:那一格反映的是配置,不是 Worker 心跳——Worker 卡死时它仍然显示"运行中"。 另外发件人必须在白名单里,不在白名单的邮件会被拒(403)。
Q:为什么有的接口报错却还是 HTTP 200?
A:场外这条线的业务错误用的是 HTTP 200 + {code, message} 的信封(前端把 code ≥ 400 当错误处理),
参数格式错误才会是真正的 HTTP 422。看错误要看 code 与 message,别只看状态码。
12. 速查
六个门户
访客 http://127.0.0.1:8000/portal/guest/home/
客户 http://127.0.0.1:8000/portal/customer/login/
风控 http://127.0.0.1:8000/portal/employee-console/login/ → 自动进风控工作台
投顾 http://127.0.0.1:8000/portal/employee-console/login/ → 自动进投顾工作台
运营 http://127.0.0.1:8000/portal/employee-console/login/ → 自动进运营门户
管理员 http://127.0.0.1:8000/portal/employee-console/login/ → 自动进管理员工作台
演示账号
| 角色 | 用户名 | 密码 |
|---|---|---|
| 客户 | cust_t |
123456 |
| 风控专员 | risk_t |
666666 |
| 管理员 | admin_t |
88888888 |
| 投顾 | advisor_t |
abc12345 |
| 运营 | offsite_t |
offsite123 |
| 风控演示客户 | 12001–12005 |
risk12345 |
| 投顾演示客户 | advc_zhang / advc_li / advc_chen |
(见投顾线脚本) |
常用命令
powershell -ExecutionPolicy Bypass -File start.ps1 # 起 API + Worker(自动刷行情、开浏览器)
python -m app.worker # 只起 Worker
python tools/seed_demo_data.py # 准备演示数据(10 步,首次/换机器)
python tools/sync_market_prices.py # 刷行情(下单报 503 时补跑)
python tools/check_today_profit_loss.py # 复算并核对「今日盈亏」
python tools/e2e_smoke_test.py --read-only # 业务链路体检(不动数据)
python tools/portal_api_check.py # 接口契约体检(按前端的方式调)
端口:API 8000 / MySQL 3306 / Redis 6379 / Milvus 19530 / Neo4j 7687
已知空数据(演示时别点进去尴尬)
| 位置 | 现状 | 原因 |
|---|---|---|
| 运营 · 场外邮件 | 0 条 | 需要真实可用的收件邮箱 |
| 运营 · 通知发送 | 只"演练",不真发 | SMTP 开关默认关闭 + dry-run 默认开启 |
| 管理员 · 画像候选 | 0 条 | 需要客户对话触发画像抽取 |
| 投顾 · 客户列表 | 页面内置 4 位演示客户 | 前端暂无"查我的客户"接口,真实范围在服务端按归属判定 |
已知小偏差(不影响主流程;被问到就照实说,别硬解释)
| 现象 | 实情 |
|---|---|
| 场外通知类型的下拉"切了没用" | 那个下拉没接事件处理,页面实际只能创建"正常回执/异常回执";风险通知、清算通知、邮件回执在这一页点不出来(接口是有的) |
NL2SQL 场外核对结果里"错误信息"列永远是 -- |
后端返回的记录里没这个字段,前端取了个不存在的键 |
| 场外部分写操作偶尔报"请求超时" | 这些接口用的是默认 8 秒超时,而同步核对与统计重算本身可能跑到 10 秒以上(接口能跑完,只是前端先放弃了) |
| 识别重试"提交了"但状态没变 | 后端失败时也返回 code:0 + "已保留异常状态",前端一律提示"已提交识别重试" |
| 推广材料页的必填校验不弹提示 | 页面没有 <form>,required 不生效,必填靠后端返回错误 |
| 投顾"已发布交付物"空态写"需管理员审核" | 文案没跟进"投顾可自助审核发布"这次改动 |
| 管理员配置发布没有"驳回/回滚"按钮、模型端点页是只读的、审计页没有筛选框 | 后端接口都有,前端未接 |
| 客户「资金流水」页顶部导航不高亮 | 导航配置里该页的 active 是空串 |
| 转人工工单队列里一堆一模一样的"端到端冒烟"单子 | 冒烟脚本每跑一次建一张。2026-09-14 起它自己会走完处置闭环收尾(分配→接单→解决→关闭),不再堆积;历史遗留的那些可以手动处置掉 |