fix(portal): 补齐写操作的必填 body(风控升级/解决、场外六个写接口)
- 风控:escalations 要 reason、resolutions 要 resolution(字段名不同), 且真实顺序是 先确认接收 才能升级/解决(否则 409 请先确认接收预警) - 场外:六个写接口都必填 operator_id(防伪校验,须等于当前登录用户), confirmations 还要 decision(中文枚举)、notifications 还要 notification_type、 recalculate 要 fund_code+application_date;门户自动带当前 user_id - 实测:recalculate 200 code=0;不传 operator_id 必得 422(证明该字段必须) - 清单 §3/§4 更新为实测结果并列出各写接口的必填字段表
This commit is contained in:
+58
-17
@@ -724,7 +724,9 @@ function renderStaff(box) {
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h2>预警列表</h2>
|
||||
<p class="hint">点「处置」可执行确认 / 升级 / 解决 —— 这些是真实写操作,会在审计里留痕。</p>
|
||||
<p class="hint">操作有<b>业务顺序</b>:必须先点「确认」接收预警,才能「升级」或「解决」——
|
||||
顺序不对会返回 <code>409 请先确认接收预警</code>。
|
||||
这三个都是真实写操作,会在审计留痕;升级要填原因、解决要填处理结论(各 1-500 字)。</p>
|
||||
<div id="alerts"><div class="muted">加载中…</div></div>
|
||||
</div>
|
||||
<div class="panel" id="staff-extra"></div>`;
|
||||
@@ -786,14 +788,26 @@ async function dailyReport() {
|
||||
showExtra2('日报结果', r);
|
||||
}
|
||||
|
||||
async function ack(no) { await act_(no, 'acknowledgements', '确认'); }
|
||||
async function esc_(no) { await act_(no, 'escalations', '升级'); }
|
||||
async function resolve(no) { await act_(no, 'resolutions', '解决'); }
|
||||
async function ack(no) { await act_(no, 'acknowledgements', '确认'); }
|
||||
|
||||
async function act_(no, action, label) {
|
||||
async function esc_(no) { // 升级:接口要求 reason(1-500 字)
|
||||
const reason = prompt('升级原因(必填,最多 500 字):', '客户风险等级需人工复核');
|
||||
if (reason === null) return;
|
||||
if (!reason.trim()) return alert('升级原因不能为空');
|
||||
await act_(no, 'escalations', '升级', { reason: reason.slice(0, 500) });
|
||||
}
|
||||
|
||||
async function resolve(no) { // 解决:字段名是 resolution,不是 reason
|
||||
const resolution = prompt('处理结论(必填,最多 500 字):', '已联系客户核实,风险已排除');
|
||||
if (resolution === null) return;
|
||||
if (!resolution.trim()) return alert('处理结论不能为空');
|
||||
await act_(no, 'resolutions', '解决', { resolution: resolution.slice(0, 500) });
|
||||
}
|
||||
|
||||
async function act_(no, action, label, body) {
|
||||
if (!confirm(`对预警 ${no} 执行「${label}」?这会写审计。`)) return;
|
||||
const r = await jpost('/api/call', { method:'POST',
|
||||
path: `/api/v1/risk/alerts/${no}/${action}`, body: {} });
|
||||
path: `/api/v1/risk/alerts/${no}/${action}`, body: body || {} });
|
||||
showExtra2(`${label} ${no}`, r);
|
||||
loadAlerts();
|
||||
}
|
||||
@@ -810,8 +824,9 @@ function renderOffsite(box) {
|
||||
<div class="panel">
|
||||
<h2>运营工作台 · 场外基金</h2>
|
||||
<p class="hint">面向 <code>operator</code>。场外线的服务层用**角色门槛**
|
||||
<code>{"operator","risk_operator","admin","super_admin"}</code> 判断,所以主体功能靠角色就通;
|
||||
另外给了 <code>financial:nl2sql:read</code>,用于单据字段识别。</p>
|
||||
<code>{"operator","risk_operator","admin","super_admin"}</code> 判断,所以主体功能靠角色就通。
|
||||
另外:场外的**写接口必须带 <code>operator_id</code>**,而且是**防伪校验** —— 平台会核对
|
||||
它是否等于当前登录用户,所以门户一律自动带本次登录的 user_id,不让你手填。</p>
|
||||
<div class="grid" id="mailbox"><div class="stat"><div class="n">…</div><div class="l">邮箱状态加载中</div></div></div>
|
||||
<div class="row" style="margin-top:14px">
|
||||
<button class="act primary" onclick="loadMailbox()">刷新邮箱状态</button>
|
||||
@@ -868,19 +883,25 @@ async function loadMails() {
|
||||
: `<div class="muted">HTTP ${r.status}${r.status === 403 ? ' —— 权限不足' : ''}</div><pre>${esc(pretty(r.body))}</pre>`;
|
||||
}
|
||||
|
||||
// 场外线的写接口**必须带 operator_id**,而且是防伪校验:平台会核对它是否等于当前
|
||||
// 登录用户。所以这里一律取本次登录的 user_id,不硬编码、也不让用户随便填。
|
||||
function myId() { return (ME && ME.user_id) || ''; }
|
||||
|
||||
async function mailFields(id) {
|
||||
showOffsite('邮件识别字段 ' + id, await GET(`/api/v1/offsite-fund/mails/${id}/recognition-fields`));
|
||||
}
|
||||
async function mailDelete(id) {
|
||||
if (!confirm('删除邮件 ' + id + '?这是写操作。')) return;
|
||||
showOffsite('删除邮件 ' + id, await jpost('/api/call',
|
||||
{ method:'POST', path:`/api/v1/offsite-fund/mails/${id}/deletions`, body:{} }));
|
||||
{ method:'POST', path:`/api/v1/offsite-fund/mails/${id}/deletions`,
|
||||
body:{ operator_id: myId() } }));
|
||||
loadMails();
|
||||
}
|
||||
async function recoverMailbox() {
|
||||
if (!confirm('触发邮箱恢复?这是写操作。')) return;
|
||||
showOffsite('邮箱恢复', await jpost('/api/call',
|
||||
{ method:'POST', path:'/api/v1/offsite-fund/mailbox-status/recoveries', body:{} }));
|
||||
{ method:'POST', path:'/api/v1/offsite-fund/mailbox-status/recoveries',
|
||||
body:{ operator_id: myId() } }));
|
||||
}
|
||||
async function docFields() {
|
||||
const t = $('task').value.trim(); if (!t) return alert('请先填单据号');
|
||||
@@ -892,25 +913,45 @@ async function docRules() {
|
||||
}
|
||||
async function docConfirm() {
|
||||
const t = $('task').value.trim(); if (!t) return alert('请先填单据号');
|
||||
if (!confirm('确认单据 ' + t + '?这是写操作,会进审计。')) return;
|
||||
// decision 是**中文枚举**:确认无误 / 确认异常 / 未处理
|
||||
const decision = prompt('确认结论(确认无误 / 确认异常 / 未处理):', '确认无误');
|
||||
if (decision === null) return;
|
||||
if (['确认无误', '确认异常', '未处理'].indexOf(decision) < 0) {
|
||||
return alert('只能是:确认无误 / 确认异常 / 未处理');
|
||||
}
|
||||
if (!confirm(`对单据 ${t} 提交「${decision}」?这是写操作,会进审计。`)) return;
|
||||
showOffsite('确认单据 ' + t, await jpost('/api/call',
|
||||
{ method:'POST', path:`/api/v1/offsite-fund/documents/${t}/confirmations`, body:{} }));
|
||||
{ method:'POST', path:`/api/v1/offsite-fund/documents/${t}/confirmations`,
|
||||
body:{ decision, operator_id: myId() } }));
|
||||
}
|
||||
async function docRetry() {
|
||||
const t = $('task').value.trim(); if (!t) return alert('请先填单据号');
|
||||
showOffsite('重试识别 ' + t, await jpost('/api/call',
|
||||
{ method:'POST', path:`/api/v1/offsite-fund/documents/${t}/recognition-retries`, body:{} }));
|
||||
{ method:'POST', path:`/api/v1/offsite-fund/documents/${t}/recognition-retries`,
|
||||
body:{ operator_id: myId() } }));
|
||||
}
|
||||
async function docNotify() {
|
||||
const t = $('task').value.trim(); if (!t) return alert('请先填单据号');
|
||||
if (!confirm('为单据 ' + t + ' 创建通知?')) return;
|
||||
// notification_type 取值:risk / settlement / mail_return / normal_return / exception_return…
|
||||
const type = prompt('通知类型(risk / settlement / mail_return / normal_return / exception_return):',
|
||||
'normal_return');
|
||||
if (type === null) return;
|
||||
if (!confirm(`为单据 ${t} 创建「${type}」通知?`)) return;
|
||||
showOffsite('创建通知 ' + t, await jpost('/api/call',
|
||||
{ method:'POST', path:`/api/v1/offsite-fund/documents/${t}/notifications`, body:{} }));
|
||||
{ method:'POST', path:`/api/v1/offsite-fund/documents/${t}/notifications`,
|
||||
body:{ notification_type: type, operator_id: myId() } }));
|
||||
}
|
||||
async function settle() {
|
||||
if (!confirm('重算结算统计?这是写操作。')) return;
|
||||
// 这个接口要的是 fund_code + application_date,没有 operator_id
|
||||
const fund = prompt('基金代码 fund_code:', '');
|
||||
if (fund === null) return;
|
||||
const date = prompt('申请日期 application_date(YYYY-MM-DD):', '');
|
||||
if (date === null) return;
|
||||
if (!fund.trim() || !date.trim()) return alert('基金代码与申请日期都必填');
|
||||
if (!confirm(`重算 ${fund} 在 ${date} 的结算统计?这是写操作。`)) return;
|
||||
showOffsite('结算重算', await jpost('/api/call',
|
||||
{ method:'POST', path:'/api/v1/offsite-fund/settlement-statistics/recalculate', body:{} }));
|
||||
{ method:'POST', path:'/api/v1/offsite-fund/settlement-statistics/recalculate',
|
||||
body:{ fund_code: fund.trim(), application_date: date.trim() } }));
|
||||
}
|
||||
function showOffsite(title, r) {
|
||||
$('offsite-extra').innerHTML = `<h2>${esc(title)}</h2>
|
||||
|
||||
Reference in New Issue
Block a user